fix(nid): preserve Unicode paths on Windows

This commit is contained in:
MiniRML 2026-10-12 08:43:02 -07:00
parent d592d54e11
commit bf6a054c52
5 changed files with 105 additions and 5 deletions

View File

@ -25,7 +25,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i
target_compile_features(nid_patcher PRIVATE cxx_std_20)
if(MINGW)
target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++)
target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables)
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
@ -165,6 +165,9 @@ if(BUILD_TESTING)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $<TARGET_FILE:nid_patcher>)
add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $<TARGET_FILE:nid_patcher>)
set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20)
endif()
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
add_dependencies(patched_libraries_tests libs)

View File

@ -0,0 +1,15 @@
#ifndef NID_NATIVEPATH_HPP
#define NID_NATIVEPATH_HPP
#include <filesystem>
#include <string>
namespace Nid::Internal {
inline std::filesystem::path NativePath(const std::string& path) {
return std::filesystem::path(std::u8string(path.begin(), path.end()));
}
}
#endif

View File

@ -3,6 +3,7 @@
#include <nid/PeNidPatcher.hpp>
#include <nid/MachONidPatcher.hpp>
#include <nid/NidPatcherUtils.hpp>
#include <nid/NativePath.hpp>
#include <bit>
#include <cstring>
#include <fstream>
@ -110,7 +111,7 @@ std::string NormalizeExportName(const std::string& name) {
std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host");
std::ifstream input(path, std::ios::binary | std::ios::ate);
std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate);
if (!input) throw std::runtime_error("cannot open export reference: " + path);
const auto size = input.tellg();
if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path);

View File

@ -1,5 +1,6 @@
#include <nid/BinaryPatcherFactory.hpp>
#include <nid/ExportExclusions.hpp>
#include <nid/NativePath.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
@ -9,20 +10,20 @@
namespace {
std::vector<std::uint8_t> _readFile(const std::string& path) {
std::ifstream f(path, std::ios::binary);
std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary);
if (!f) throw std::runtime_error("cannot open: " + path);
return {std::istreambuf_iterator<char>(f), {}};
}
void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) {
std::ofstream f(path, std::ios::binary);
std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary);
if (!f) throw std::runtime_error("cannot write: " + path);
f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
}
}
int main(int argc, char* argv[]) {
int Run(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n";
return 1;
@ -65,3 +66,27 @@ int main(int argc, char* argv[]) {
return 0;
}
#ifdef _WIN32
int wmain(int argc, wchar_t* argv[]) {
try {
std::vector<std::string> values;
values.reserve(static_cast<std::size_t>(argc));
for (int index = 0; index < argc; ++index) {
const auto utf8 = std::filesystem::path(argv[index]).u8string();
values.emplace_back(utf8.begin(), utf8.end());
}
std::vector<char*> arguments;
arguments.reserve(values.size());
for (auto& value : values) arguments.push_back(value.data());
return Run(argc, arguments.data());
} catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 2;
}
}
#else
int main(int argc, char* argv[]) {
return Run(argc, argv);
}
#endif

View File

@ -0,0 +1,56 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from test_macho_nids import build_dylib, exports_of, nid
PATCHER = Path(sys.argv.pop(1)).resolve()
class NativePathTests(unittest.TestCase):
def invoke(self, *arguments):
return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)],
capture_output=True, encoding="utf-8", errors="replace", timeout=10)
def test_non_ascii_directory_and_filename_patch_the_intended_file(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary) / "\u6e2c\u8a66"
root.mkdir()
file = root / "\u043c\u043e\u0434\u0443\u043b\u044c.prx"
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
result = self.invoke(file)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(file.read_bytes()), {"_" + nid("puts"): (0, 0x100)})
self.assertIn(str(file), result.stdout)
self.assertEqual(list(root.iterdir()), [file])
def test_non_ascii_export_reference_preserves_the_named_export(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
reference = root / "\u6e2c\u8a66-reference.prx"
original = build_dylib([("_puts", 0, 0x100)], [])
reference.write_bytes(original)
file = root / "target.prx"
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
result = self.invoke("--preserve-exports", reference, file)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(exports_of(file.read_bytes()), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
self.assertEqual(reference.read_bytes(), original)
def test_missing_unicode_input_does_not_modify_another_file(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
existing = root / "existing.prx"
existing.write_bytes(b"unchanged")
missing = root / "\u6e2c\u8a66-missing.prx"
result = self.invoke(missing)
self.assertEqual(result.returncode, 2)
self.assertIn(str(missing), result.stderr)
self.assertFalse(missing.exists())
self.assertEqual(existing.read_bytes(), b"unchanged")
if __name__ == "__main__":
unittest.main()