From c584f749faf43c4801eeff63220a66d51aeb9514 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: [PATCH] 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 --- src/node_file.cc | 52 ++++++--------------------------------- src/node_modules.cc | 32 ++++++++++-------------- src/util-inl.h | 60 ++++++++++++++++++++++++++++++++++++++++++--- src/util.h | 10 +++++++- 4 files changed, 86 insertions(+), 68 deletions(-) diff --git a/src/node_file.cc b/src/node_file.cc index 77f8f1bd4e8294..25d2a22061b7ab 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -3056,42 +3056,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(); @@ -3104,7 +3068,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); @@ -3112,7 +3076,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(); @@ -3141,8 +3105,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. @@ -3237,7 +3201,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()); @@ -3248,7 +3212,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(), @@ -3383,7 +3347,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) { @@ -3446,7 +3410,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 60b03b1563b230..e63a23d94d7823 100644 --- a/src/node_modules.cc +++ b/src/node_modules.cc @@ -291,22 +291,24 @@ 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; + } } // 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; } @@ -328,20 +330,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 17b870e2dd91ab..9ca2ff66d812b5 100644 --- a/src/util-inl.h +++ b/src/util-inl.h @@ -678,12 +678,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()), @@ -691,6 +690,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 } // namespace node diff --git a/src/util.h b/src/util.h index 8f27afbb9e4e45..ed558de64aaaba 100644 --- a/src/util.h +++ b/src/util.h @@ -506,6 +506,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_; @@ -1011,9 +1013,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); + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS