From 8bb0f146ea7c46fa29302e4d9bcdef97324bad7e Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 09:47:03 +0100 Subject: [PATCH] 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 Co-authored-by: John Kleinschmidt --- patches/node/.patches | 1 + ...cp_utf8_for_wide_file_names_on_win32.patch | 312 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 patches/node/src_use_cp_utf8_for_wide_file_names_on_win32.patch diff --git a/patches/node/.patches b/patches/node/.patches index cee04df51e..172ea7c7e3 100644 --- a/patches/node/.patches +++ b/patches/node/.patches @@ -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 diff --git a/patches/node/src_use_cp_utf8_for_wide_file_names_on_win32.patch b/patches/node/src_use_cp_utf8_for_wide_file_names_on_win32.patch new file mode 100644 index 0000000000..b3e93de8af --- /dev/null +++ b/patches/node/src_use_cp_utf8_for_wide_file_names_on_win32.patch @@ -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 +Reviewed-By: Darshan Sen +Reviewed-By: Stefan Stojanovic +Reviewed-By: Juan José Arboleda +Reviewed-By: Joyee Cheung +Reviewed-By: Rafael Gonzaga + +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(wstr.size()), +- nullptr, +- 0, +- nullptr, +- nullptr); +- std::string strTo(size_needed, 0); +- WideCharToMultiByte(CP_UTF8, +- 0, +- &wstr[0], +- static_cast(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& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); +@@ -3223,7 +3187,7 @@ static void CpSyncCheckPaths(const FunctionCallbackInfo& 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& 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& 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& 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& 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(str.size()), nullptr, 0); ++ CP_UTF8, 0, &str[0], static_cast(str.size()), nullptr, 0); + std::wstring wstrTo(size_needed, 0); +- MultiByteToWideChar(code_page, ++ MultiByteToWideChar(CP_UTF8, + 0, + &str[0], + static_cast(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(wstr.size()), ++ nullptr, ++ 0, ++ nullptr, ++ nullptr); ++ std::string strTo(size_needed, 0); ++ WideCharToMultiByte(CP_UTF8, ++ 0, ++ &wstr[0], ++ static_cast(wstr.size()), ++ &strTo[0], ++ size_needed, ++ nullptr, ++ nullptr); ++ return strTo; ++} ++ ++template ++std::filesystem::path MaybeStackBuffer::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 ++std::filesystem::path MaybeStackBuffer::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 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 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)