fix: ESM-from-CJS import when CJK is in path (#48862)

Upstream fix: https://github.com/nodejs/node/pull/60575

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Fedor Indutny <indutny@signal.org>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
This commit is contained in:
trop[bot]
2025-11-11 09:47:03 +01:00
committed by GitHub
parent 45cc8dd600
commit 8bb0f146ea
2 changed files with 313 additions and 0 deletions

View File

@@ -41,4 +41,5 @@ lib_check_sharedarraybuffer_existence_in_fast-utf8-stream.patch
chore_handle_support_for_import_defer_as_ns_and_import_defer.patch
api_delete_deprecated_fields_on_v8_isolate.patch
api_promote_deprecation_of_v8_context_and_v8_object_api_methods.patch
src_use_cp_utf8_for_wide_file_names_on_win32.patch
reland_temporal_unflag_temporal.patch

View File

@@ -0,0 +1,312 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Fedor Indutny <238531+indutny@users.noreply.github.com>
Date: Fri, 7 Nov 2025 19:41:44 -0800
Subject: src: use CP_UTF8 for wide file names on win32
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`src/node_modules.cc` needs to be consistent with `src/node_file.cc` in
how it translates the utf8 strings to `std::wstring` otherwise we might
end up in situation where we can read the source code of imported
package from disk, but fail to recognize that it is an ESM (or CJS) and
cause runtime errors. This type of error is possible on Windows when the
path contains unicode characters and "Language for non-Unicode programs"
is set to "Chinese (Traditional, Taiwan)".
See: #58768
PR-URL: https://github.com/nodejs/node/pull/60575
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Darshan Sen <raisinten@gmail.com>
Reviewed-By: Stefan Stojanovic <stefan.stojanovic@janeasystems.com>
Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
diff --git a/src/node_file.cc b/src/node_file.cc
index 969e7d08086f8442bed476feaf15599b8c79db7c..e7459654401c275dfb86207831016ed71060bcc9 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -3175,42 +3175,6 @@ static void GetFormatOfExtensionlessFile(
return args.GetReturnValue().Set(EXTENSIONLESS_FORMAT_JAVASCRIPT);
}
-#ifdef _WIN32
-#define BufferValueToPath(str) \
- std::filesystem::path(ConvertToWideString(str.ToString(), CP_UTF8))
-
-std::string ConvertWideToUTF8(const std::wstring& wstr) {
- if (wstr.empty()) return std::string();
-
- int size_needed = WideCharToMultiByte(CP_UTF8,
- 0,
- &wstr[0],
- static_cast<int>(wstr.size()),
- nullptr,
- 0,
- nullptr,
- nullptr);
- std::string strTo(size_needed, 0);
- WideCharToMultiByte(CP_UTF8,
- 0,
- &wstr[0],
- static_cast<int>(wstr.size()),
- &strTo[0],
- size_needed,
- nullptr,
- nullptr);
- return strTo;
-}
-
-#define PathToString(path) ConvertWideToUTF8(path.wstring());
-
-#else // _WIN32
-
-#define BufferValueToPath(str) std::filesystem::path(str.ToStringView());
-#define PathToString(path) path.native();
-
-#endif // _WIN32
-
static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
@@ -3223,7 +3187,7 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemRead, src.ToStringView());
- auto src_path = BufferValueToPath(src);
+ auto src_path = src.ToPath();
BufferValue dest(isolate, args[1]);
CHECK_NOT_NULL(*dest);
@@ -3231,7 +3195,7 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemWrite, dest.ToStringView());
- auto dest_path = BufferValueToPath(dest);
+ auto dest_path = dest.ToPath();
bool dereference = args[2]->IsTrue();
bool recursive = args[3]->IsTrue();
@@ -3260,8 +3224,8 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo<Value>& args) {
(src_status.type() == std::filesystem::file_type::directory) ||
(dereference && src_status.type() == std::filesystem::file_type::symlink);
- auto src_path_str = PathToString(src_path);
- auto dest_path_str = PathToString(dest_path);
+ auto src_path_str = ConvertPathToUTF8(src_path);
+ auto dest_path_str = ConvertPathToUTF8(dest_path);
if (!error_code) {
// Check if src and dest are identical.
@@ -3356,7 +3320,7 @@ static bool CopyUtimes(const std::filesystem::path& src,
uv_fs_t req;
auto cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
- auto src_path_str = PathToString(src);
+ auto src_path_str = ConvertPathToUTF8(src);
int result = uv_fs_stat(nullptr, &req, src_path_str.c_str(), nullptr);
if (is_uv_error(result)) {
env->ThrowUVException(result, "stat", nullptr, src_path_str.c_str());
@@ -3367,7 +3331,7 @@ static bool CopyUtimes(const std::filesystem::path& src,
const double source_atime = s->st_atim.tv_sec + s->st_atim.tv_nsec / 1e9;
const double source_mtime = s->st_mtim.tv_sec + s->st_mtim.tv_nsec / 1e9;
- auto dest_file_path_str = PathToString(dest);
+ auto dest_file_path_str = ConvertPathToUTF8(dest);
int utime_result = uv_fs_utime(nullptr,
&req,
dest_file_path_str.c_str(),
@@ -3502,7 +3466,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
std::error_code error;
for (auto dir_entry : std::filesystem::directory_iterator(src)) {
auto dest_file_path = dest / dir_entry.path().filename();
- auto dest_str = PathToString(dest);
+ auto dest_str = ConvertPathToUTF8(dest);
if (dir_entry.is_symlink()) {
if (verbatim_symlinks) {
@@ -3565,7 +3529,7 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
}
} else if (std::filesystem::is_regular_file(dest_file_path)) {
if (!dereference || (!force && error_on_exist)) {
- auto dest_file_path_str = PathToString(dest_file_path);
+ auto dest_file_path_str = ConvertPathToUTF8(dest_file_path);
env->ThrowStdErrException(
std::make_error_code(std::errc::file_exists),
"cp",
diff --git a/src/node_modules.cc b/src/node_modules.cc
index d8477191efafba3c41c06d765f4b03bd00b8573c..5444e556b89ba5b71739753eaafa706cd9727013 100644
--- a/src/node_modules.cc
+++ b/src/node_modules.cc
@@ -365,12 +365,13 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
// Stop the search when the process doesn't have permissions
// to walk upwards
- if (is_permissions_enabled &&
- !env->permission()->is_granted(
- env,
- permission::PermissionScope::kFileSystemRead,
- current_path.generic_string())) [[unlikely]] {
- return nullptr;
+ if (is_permissions_enabled) {
+ if (!env->permission()->is_granted(
+ env,
+ permission::PermissionScope::kFileSystemRead,
+ ConvertGenericPathToUTF8(current_path))) [[unlikely]] {
+ return nullptr;
+ }
}
// If current path is outside the resources path, bail.
@@ -380,13 +381,14 @@ const BindingData::PackageConfig* BindingData::TraverseParent(
}
// Check if the path ends with `/node_modules`
- if (current_path.generic_string().ends_with("/node_modules")) {
+ if (current_path.filename() == "node_modules") {
return nullptr;
}
auto package_json_path = current_path / "package.json";
+
auto package_json =
- GetPackageJSON(realm, package_json_path.string(), nullptr);
+ GetPackageJSON(realm, ConvertPathToUTF8(package_json_path), nullptr);
if (package_json != nullptr) {
return package_json;
}
@@ -408,20 +410,12 @@ void BindingData::GetNearestParentPackageJSONType(
ToNamespacedPath(realm->env(), &path_value);
- std::string path_value_str = path_value.ToString();
+ auto path = path_value.ToPath();
+
if (slashCheck) {
- path_value_str.push_back(kPathSeparator);
+ path /= "";
}
- std::filesystem::path path;
-
-#ifdef _WIN32
- std::wstring wide_path = ConvertToWideString(path_value_str, GetACP());
- path = std::filesystem::path(wide_path);
-#else
- path = std::filesystem::path(path_value_str);
-#endif
-
auto package_json = TraverseParent(realm, path);
if (package_json == nullptr) {
diff --git a/src/util-inl.h b/src/util-inl.h
index da9268dcf2ff432ddeec7c0f61a147b73f3130e2..82b2760f535345d126bc3dcf3890573d36dbb497 100644
--- a/src/util-inl.h
+++ b/src/util-inl.h
@@ -698,12 +698,11 @@ inline bool IsWindowsBatchFile(const char* filename) {
return !extension.empty() && (extension == "cmd" || extension == "bat");
}
-inline std::wstring ConvertToWideString(const std::string& str,
- UINT code_page) {
+inline std::wstring ConvertUTF8ToWideString(const std::string& str) {
int size_needed = MultiByteToWideChar(
- code_page, 0, &str[0], static_cast<int>(str.size()), nullptr, 0);
+ CP_UTF8, 0, &str[0], static_cast<int>(str.size()), nullptr, 0);
std::wstring wstrTo(size_needed, 0);
- MultiByteToWideChar(code_page,
+ MultiByteToWideChar(CP_UTF8,
0,
&str[0],
static_cast<int>(str.size()),
@@ -711,6 +710,59 @@ inline std::wstring ConvertToWideString(const std::string& str,
size_needed);
return wstrTo;
}
+
+std::string ConvertWideStringToUTF8(const std::wstring& wstr) {
+ if (wstr.empty()) return std::string();
+
+ int size_needed = WideCharToMultiByte(CP_UTF8,
+ 0,
+ &wstr[0],
+ static_cast<int>(wstr.size()),
+ nullptr,
+ 0,
+ nullptr,
+ nullptr);
+ std::string strTo(size_needed, 0);
+ WideCharToMultiByte(CP_UTF8,
+ 0,
+ &wstr[0],
+ static_cast<int>(wstr.size()),
+ &strTo[0],
+ size_needed,
+ nullptr,
+ nullptr);
+ return strTo;
+}
+
+template <typename T, size_t kStackStorageSize>
+std::filesystem::path MaybeStackBuffer<T, kStackStorageSize>::ToPath() const {
+ std::wstring wide_path = ConvertUTF8ToWideString(ToString());
+ return std::filesystem::path(wide_path);
+}
+
+std::string ConvertPathToUTF8(const std::filesystem::path& path) {
+ return ConvertWideStringToUTF8(path.wstring());
+}
+
+std::string ConvertGenericPathToUTF8(const std::filesystem::path& path) {
+ return ConvertWideStringToUTF8(path.generic_wstring());
+}
+
+#else // _WIN32
+
+template <typename T, size_t kStackStorageSize>
+std::filesystem::path MaybeStackBuffer<T, kStackStorageSize>::ToPath() const {
+ return std::filesystem::path(ToStringView());
+}
+
+std::string ConvertPathToUTF8(const std::filesystem::path& path) {
+ return path.native();
+}
+
+std::string ConvertGenericPathToUTF8(const std::filesystem::path& path) {
+ return path.generic_string();
+}
+
#endif // _WIN32
inline v8::MaybeLocal<v8::Object> NewDictionaryInstance(
diff --git a/src/util.h b/src/util.h
index 6da57f95165bbdedb65dab6eaae8c39b815ee4e5..e65d6dbf359f61d051efe4e129c9035ed90cc8f6 100644
--- a/src/util.h
+++ b/src/util.h
@@ -507,6 +507,8 @@ class MaybeStackBuffer {
inline std::basic_string_view<T> ToStringView() const {
return {out(), length()};
}
+ // This can only be used if the buffer contains path data in UTF8
+ inline std::filesystem::path ToPath() const;
private:
size_t length_;
@@ -1026,9 +1028,15 @@ class JSONOutputStream final : public v8::OutputStream {
// Returns true if OS==Windows and filename ends in .bat or .cmd,
// case insensitive.
inline bool IsWindowsBatchFile(const char* filename);
-inline std::wstring ConvertToWideString(const std::string& str, UINT code_page);
+inline std::wstring ConvertUTF8ToWideString(const std::string& str);
+inline std::string ConvertWideStringToUTF8(const std::wstring& wstr);
+
#endif // _WIN32
+inline std::filesystem::path ConvertUTF8ToPath(const std::string& str);
+inline std::string ConvertPathToUTF8(const std::filesystem::path& path);
+inline std::string ConvertGenericPathToUTF8(const std::filesystem::path& path);
+
// A helper to create a new instance of the dictionary template.
// Unlike v8::DictionaryTemplate::NewInstance, this method will
// check that all properties have been set (are not empty MaybeLocals)