diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index d094f39cf..545380b12 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -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 $) + add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $) + 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) diff --git a/core/libs/nid/include/nid/NativePath.hpp b/core/libs/nid/include/nid/NativePath.hpp new file mode 100644 index 000000000..994be15df --- /dev/null +++ b/core/libs/nid/include/nid/NativePath.hpp @@ -0,0 +1,15 @@ +#ifndef NID_NATIVEPATH_HPP +#define NID_NATIVEPATH_HPP + +#include +#include + +namespace Nid::Internal { + +inline std::filesystem::path NativePath(const std::string& path) { + return std::filesystem::path(std::u8string(path.begin(), path.end())); +} + +} + +#endif diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 9c00d6b3e..79cf293d1 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -110,7 +111,7 @@ std::string NormalizeExportName(const std::string& name) { std::unordered_set 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); diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index 2f91413fd..c07a4e8bf 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -9,20 +10,20 @@ namespace { std::vector _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(f), {}}; } void _writeFile(const std::string& path, const std::vector& 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(data.data()), static_cast(data.size())); } } -int main(int argc, char* argv[]) { +int Run(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage: nid_patcher [--preserve-exports ] [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 values; + values.reserve(static_cast(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 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 diff --git a/core/libs/nid/tests/test_native_paths.py b/core/libs/nid/tests/test_native_paths.py new file mode 100644 index 000000000..5a08185bc --- /dev/null +++ b/core/libs/nid/tests/test_native_paths.py @@ -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()