From 001dfea501dc7d19dafcfb5003c87ca127597d67 Mon Sep 17 00:00:00 2001 From: Augusto Domingos Date: Sat, 19 Sep 2026 13:11:55 +0200 Subject: [PATCH 1/5] Preserve native windows paths in filesystem operations --- src/common/platform/sysWindowsFileIO.cpp | 41 ++++++++---------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/common/platform/sysWindowsFileIO.cpp b/src/common/platform/sysWindowsFileIO.cpp index cbe1c2693..bbdc73d4e 100644 --- a/src/common/platform/sysWindowsFileIO.cpp +++ b/src/common/platform/sysWindowsFileIO.cpp @@ -477,18 +477,10 @@ bool SysFileSetLastAccessAndWriteTimeUtc(const std::filesystem::path& name, } void SysFileFindFiles(const std::filesystem::path& path, std::vector& out) { - std::string real_path = Common::ReplaceChar(Common::PathToGenericString(path), '\\', '/'); - if (!real_path.ends_with("/")) { - real_path += "/"; - } + const auto pattern = path / L"*"; - std::string pattern = real_path + "*"; - - HANDLE h = nullptr; - WIN32_FIND_DATAW data; - - auto wide_pattern = std::filesystem::path(pattern).wstring(); - h = FindFirstFileW(wide_pattern.c_str(), &data); + WIN32_FIND_DATAW data {}; + HANDLE h = FindFirstFileW(pattern.c_str(), &data); if (h == INVALID_HANDLE_VALUE) { return; @@ -496,18 +488,19 @@ void SysFileFindFiles(const std::filesystem::path& path, std::vector(FILE_ATTRIBUTE_DIRECTORY)) != 0u) { - SysFileFindFiles(std::filesystem::path(real_path) / file_name, out); + SysFileFindFiles(full_path, out); } else { sys_file_find_t r {}; - r.path_with_name = std::filesystem::path(real_path) / file_name; + r.path_with_name = full_path; r.size = (static_cast(data.nFileSizeHigh) << 32u) + static_cast(data.nFileSizeLow); r.last_access_time.is_invalid = false; @@ -515,7 +508,7 @@ void SysFileFindFiles(const std::filesystem::path& path, std::vector& out) { - std::string real_path = Common::ReplaceChar(Common::PathToGenericString(path), '\\', '/'); - if (!real_path.ends_with("/")) { - real_path += "/"; - } + const auto pattern = path / L"*"; - std::string pattern = real_path + "*"; - - HANDLE h = nullptr; - WIN32_FIND_DATAW data; - - auto wide_pattern = std::filesystem::path(pattern).wstring(); - h = FindFirstFileW(wide_pattern.c_str(), &data); + WIN32_FIND_DATAW data {}; + HANDLE h = FindFirstFileW(pattern.c_str(), &data); if (h == INVALID_HANDLE_VALUE) { return; @@ -549,7 +534,7 @@ void SysFileGetDents(const std::filesystem::path& path, std::vector(FILE_ATTRIBUTE_DIRECTORY)) == 0u); r.name = Common::PathToString(file_name); - out.push_back(r); + out.push_back(std::move(r)); } while (FindNextFileW(h, &data) != 0); From b61b3f2a404f994c029f12588581661ad608e163 Mon Sep 17 00:00:00 2001 From: Augusto Domingos Date: Sat, 19 Sep 2026 14:12:35 +0200 Subject: [PATCH 2/5] handle UTF-8 command-line paths --- src/common/file.cpp | 29 +++++++++++-------- src/common/stringUtils.h | 17 +++++++++++ src/kernel/fileSystem.cpp | 12 ++++---- src/main.cpp | 59 ++++++++++++++++++++++++++++++++------- 4 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/common/file.cpp b/src/common/file.cpp index 086effbd1..90bc6db6c 100644 --- a/src/common/file.cpp +++ b/src/common/file.cpp @@ -245,28 +245,33 @@ void File::Printf(const char* format, ...) { Write(s.data(), static_cast(s.size())); } +static std::filesystem::path WithoutTrailingSeparator( + const std::filesystem::path& path) { + if (!path.empty() && !path.has_filename() && + path != path.root_path()) { + return path.parent_path(); + } + + return path; +} + + bool File::IsDirectoryExisting(const std::filesystem::path& path) { - auto path_str = PathToGenericString(path); - return SysFileIsDirectoryExisting(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1) - : path_str); + return SysFileIsDirectoryExisting(WithoutTrailingSeparator(path)); } bool File::IsFileExisting(const std::filesystem::path& name) { return SysFileIsFileExisting(name); } -bool File::CreateDirectory( - const std::filesystem::path& path) // @suppress("Member declaration not found") -{ - auto path_str = PathToGenericString(path); - return SysFileCreateDirectory(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1) - : path_str); +bool File::CreateDirectory(const std::filesystem::path& path) { + return SysFileCreateDirectory( + WithoutTrailingSeparator(path)); } bool File::DeleteDirectory(const std::filesystem::path& path) { - auto path_str = PathToGenericString(path); - return SysFileDeleteDirectory(path_str.ends_with("/") ? Common::RemoveLast(path_str, 1) - : path_str); + return SysFileDeleteDirectory( + WithoutTrailingSeparator(path)); } bool File::CreateDirectories(const std::filesystem::path& path) { diff --git a/src/common/stringUtils.h b/src/common/stringUtils.h index 3e8386170..3f071e9ee 100644 --- a/src/common/stringUtils.h +++ b/src/common/stringUtils.h @@ -34,6 +34,23 @@ inline std::string PathToGenericString(const std::filesystem::path& path) { #endif } +inline std::filesystem::path PathFromUtf8(std::string_view text) { +#if defined(__cpp_char8_t) + std::u8string utf8; + utf8.reserve(text.size()); + + for (const char ch: text) { + utf8.push_back( + static_cast(static_cast(ch)) + ); + } + + return std::filesystem::path(utf8); +#else + return std::filesystem::u8path(text.begin(), text.end()); +#endif +} + inline bool EqualNoCase(std::string_view lhs, std::string_view rhs) { if (lhs.size() != rhs.size()) { return false; diff --git a/src/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index 9f2d5aab5..e0a446b80 100644 --- a/src/kernel/fileSystem.cpp +++ b/src/kernel/fileSystem.cpp @@ -251,16 +251,15 @@ void FileDescriptors::CloseAll() { void MountPoints::Mount(const std::filesystem::path& folder, const std::string& point) { Common::LockGuard lock(m_mutex); - auto folder_str = Common::FixDirectorySlash(Common::PathToGenericString(folder)); auto point_str = Common::FixDirectorySlash(point); Umount(point_str); MountPair p; - p.dir = folder_str; + p.dir = folder; p.point = point_str; - m_mount_pairs.push_back(p); + m_mount_pairs.push_back(std::move(p)); } void MountPoints::Umount(const std::string& folder_or_point) { @@ -343,14 +342,17 @@ std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name) while (rel_path.starts_with('/')) { rel_path = Common::RemoveFirst(rel_path, 1); } + + const auto native_rel_path = Common::PathFromUtf8(rel_path); + #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (HasWindowsForbiddenFilenameCharacter(rel_path)) { ::printf("FileSystem: Windows-incompatible guest filename: %s\n", mounted_name.c_str()); } - return p.dir / rel_path; + return p.dir / native_rel_path; #else - return ResolvePathIgnoringCase(p.dir / rel_path); + return ResolvePathIgnoringCase(p.dir / native_rel_path); #endif } diff --git a/src/main.cpp b/src/main.cpp index 81f379805..34af5582a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include @@ -207,15 +209,19 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he } value = Common::FixFilenameSlash(value); - if (Common::File::IsDirectoryExisting(value)) { - options.app0_dir = value; + const auto path = Common::PathFromUtf8(value); + + if (Common::File::IsDirectoryExisting(path)) { + options.app0_dir = path; options.elf = "/app0/eboot.bin"; - } else if (Common::File::IsFileExisting(value)) { - options.app0_dir = Common::DirectoryWithoutFilename(value); + } else if (Common::File::IsFileExisting(path)) { + options.app0_dir = path.parent_path(); + if (options.app0_dir.empty()) { options.app0_dir = "."; } - options.elf = "/app0/" + Common::FilenameWithoutDirectory(value); + + options.elf = std::filesystem::path("/app0") / path.filename(); } else { ::printf("--game must point to an existing directory or ELF: %s\n", value.c_str()); return false; @@ -226,7 +232,9 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he return false; } value = Common::FixFilenameSlash(value); - if (!Common::File::IsFileExisting(value)) { + const auto path = Common::PathFromUtf8(value); + + if (!Common::File::IsFileExisting(path)) { ::printf("--game-patch must point to an existing file: %s\n", value.c_str()); return false; } @@ -291,14 +299,14 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he return false; } } else if (arg == "--shader-log-folder") { - options.config.shader_log_folder = value; + options.config.shader_log_folder = Common::PathFromUtf8(value); } else if (arg == "--command-buffer-dump") { if (!ParseBool(value, options.config.command_buffer_dump_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); return false; } } else if (arg == "--command-buffer-dump-folder") { - options.config.command_buffer_dump_folder = value; + options.config.command_buffer_dump_folder = Common::PathFromUtf8(value); } else if (arg == "--graphics-debug-dump") { if (!ParseBool(value, options.config.graphics_debug_dump_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); @@ -310,7 +318,7 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he return false; } } else if (arg == "--printf-output-file") { - options.config.printf_output_file = value; + options.config.printf_output_file = Common::PathFromUtf8(value); } else if (arg == "--spirv-debug-printf") { if (!ParseBool(value, options.config.spirv_debug_printf_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); @@ -341,7 +349,7 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he return show_help || (!options.app0_dir.empty() && !options.elf.empty()); } -int main(int argc, char* argv[]) { +static int Main(int argc, char* argv[]) { VirtualMemory::Init(); InitializeThreads(); @@ -367,3 +375,34 @@ int main(int argc, char* argv[]) { return 0; } + +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + +int wmain(int argc, wchar_t* argv[]) { + std::vector utf8_args; + utf8_args.reserve(static_cast(argc)); + + for (int index = 0; index < argc; index++) { + const std::wstring_view wide(argv[index]); + const std::u16string utf16(wide.begin(), wide.end()); + + utf8_args.push_back(Common::Utf16ToUtf8(utf16)); + } + + std::vector utf8_argv; + utf8_argv.reserve(utf8_args.size()); + + for (auto& argument: utf8_args) { + utf8_argv.push_back(argument.data()); + } + + return Main(argc, utf8_argv.data()); +} + +#else + +int main(int argc, char* argv[]) { + return Main(argc, argv); +} + +#endif From 07c9978ac67eeaac535d13778d039a85d6aef712 Mon Sep 17 00:00:00 2001 From: Augusto Domingos Date: Sat, 19 Sep 2026 14:45:24 +0200 Subject: [PATCH 3/5] keep game icon paths native in windows --- src/graphics/presentation/window/window.cpp | 8 +++++--- src/loader/systemContent.cpp | 20 ++++++++++---------- src/loader/systemContent.h | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 593dc71e6..f4899e132 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -27,6 +27,7 @@ #include "common/systemInfo.h" #include "common/threads.h" #include "common/timer.h" +#include "common/stringUtils.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" @@ -45,6 +46,7 @@ #include #include #include +#include #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_SIMD @@ -945,10 +947,10 @@ struct WindowIcon { } }; -static void WindowLoadPngIcon(const std::string& path, WindowIcon* icon) { +static void WindowLoadPngIcon(const std::filesystem::path& path, WindowIcon* icon) { Common::File f; if (!f.Open(path, Common::File::Mode::Read)) { - EXIT("Can't open icon file %s\n", path.c_str()); + EXIT("Can't open icon file %s\n", Common::PathToString(path).c_str()); } int width = 0; @@ -974,7 +976,7 @@ void WindowContext::UpdateIcon() { static bool icon_loaded = false; if (!icon_loaded) { - std::string icon_path; + std::filesystem::path icon_path; if (Loader::SystemContentGetIconPath(&icon_path)) { WindowLoadPngIcon(icon_path, &icon); } diff --git a/src/loader/systemContent.cpp b/src/loader/systemContent.cpp index 2ce0a2e89..f4a4c27fc 100644 --- a/src/loader/systemContent.cpp +++ b/src/loader/systemContent.cpp @@ -647,20 +647,20 @@ uint64_t SystemContentGetFlexibleMemorySize() { return sc->psf.GetFlexibleMemorySize(); } -bool SystemContentGetIconPath(std::string* path) { - if (path == nullptr) { - return false; - } +bool SystemContentGetIconPath(std::filesystem::path* path) { + if (path == nullptr) { + return false; + } - auto* sc = Common::Singleton::Instance(); + auto* sc = Common::Singleton::Instance(); - if (sc->icon_path.empty()) { - return false; - } + if (sc->icon_path.empty()) { + return false; + } - *path = Common::PathToString(sc->icon_path); + *path = sc->icon_path; - return true; + return true; } bool SystemContentGetChunksNum(uint32_t* num) { diff --git a/src/loader/systemContent.h b/src/loader/systemContent.h index f3216f1f6..778847641 100644 --- a/src/loader/systemContent.h +++ b/src/loader/systemContent.h @@ -15,7 +15,7 @@ bool SystemContentParamSfoGetInt(const char* name, int32_t* value); bool SystemContentParamSfoGetString(const char* name, std::string* value); bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_size); uint64_t SystemContentGetFlexibleMemorySize(); -bool SystemContentGetIconPath(std::string* path); +bool SystemContentGetIconPath(std::filesystem::path* path); bool SystemContentGetChunksNum(uint32_t* num); } // namespace Loader From 8b294eb5b86ebd9036a73dce7dfbf7d8fc94b7b4 Mon Sep 17 00:00:00 2001 From: Augusto Domingos Date: Sat, 19 Sep 2026 15:29:30 +0200 Subject: [PATCH 4/5] Add unicode path regression coverage --- src/main.cpp | 2 +- tests/KernelFileSystemTests.cpp | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 34af5582a..0eb3a2e37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -238,7 +238,7 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he ::printf("--game-patch must point to an existing file: %s\n", value.c_str()); return false; } - options.game_patch = value; + options.game_patch = path; } else if (arg == "--screen-width") { options.config.screen_width = static_cast(Common::ToInt32(value)); } else if (arg == "--screen-height") { diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index a673bc39f..9b4bf1422 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -4,6 +4,7 @@ #include "common/logging/log.h" #include "common/subsystems.h" #include "common/threads.h" +#include "common/stringUtils.h" #include "graphics/presentation/window/windowInternal.h" #include "kernel/fileSystem.h" #include "libs/errno.h" @@ -128,6 +129,43 @@ void CheckMountRoot(const std::filesystem::path &root) { FileSystem::Umount("/app0"); } +void CheckUnicodePaths(const std::filesystem::path &root) { + constexpr std::string_view HostDirectory = + "Test\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; + constexpr std::string_view GuestFilename = + "asset-\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e.bin"; + + const auto unicode_root = + root / Common::PathFromUtf8(HostDirectory); + + Check(std::filesystem::create_directories(unicode_root), + "create Unicode host directory"); + + CheckMountRoot(unicode_root); + + const auto native_file = + unicode_root / Common::PathFromUtf8(GuestFilename); + + Common::File fixture; + Check(fixture.Create(native_file), "create Unicode filename"); + fixture.Close(); + + FileSystem::Mount(unicode_root, "/app0"); + + const auto guest_file = + std::string("/app0/") + std::string(GuestFilename); + + Check(FileSystem::GetRealFilename(guest_file) == native_file, + "resolve Unicode guest path"); + + const int fd = FileSystem::KernelOpen(guest_file.c_str(), 0, 0); + Check(fd >= 3, "open Unicode guest path"); + Check(FileSystem::KernelClose(fd) == OK, + "close Unicode guest path"); + + FileSystem::Umount("/app0"); +} + void CheckDirectoryStream(const std::filesystem::path &root) { const auto directory = root / "directory-stream"; Check(std::filesystem::create_directory(directory), @@ -387,6 +425,7 @@ int main() { TempDirectory temporary; FileSystem::Initialize(); CheckMountRoot(temporary.Path()); + CheckUnicodePaths(temporary.Path()); CheckDirectoryStream(temporary.Path()); CheckAprPaths(temporary.Path()); FileSystem::Mount(temporary.Path(), "/savedata0"); From fe45265fcd7c74809fbeb8dd4b060f94f7bdfd88 Mon Sep 17 00:00:00 2001 From: Augusto Domingos Date: Sat, 19 Sep 2026 17:53:15 +0200 Subject: [PATCH 5/5] preserve native paths when creating directories --- src/common/file.cpp | 75 +++++++++++++-------------------- tests/KernelFileSystemTests.cpp | 11 ++--- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/src/common/file.cpp b/src/common/file.cpp index 90bc6db6c..4dfec8af3 100644 --- a/src/common/file.cpp +++ b/src/common/file.cpp @@ -275,63 +275,48 @@ bool File::DeleteDirectory(const std::filesystem::path& path) { } bool File::CreateDirectories(const std::filesystem::path& path) { - std::string real_path = Common::ReplaceChar(PathToGenericString(path), '\\', '/'); + const auto normalized_path = + WithoutTrailingSeparator(path); - std::vector list = Common::Split(real_path, "/"); + auto current_path = normalized_path.root_path(); - std::string p; + for (const auto& component: + normalized_path.relative_path()) { + current_path /= component; - for (uint32_t si = 0; si < list.size(); si++) { - const std::string& s = list[si]; + if (IsDirectoryExisting(current_path)) { + continue; + } - if (si != 0 || real_path.starts_with("/")) { - p += "/"; - } + if (!CreateDirectory(current_path)) { + return false; + } + } - p += s; - - if (IsDirectoryExisting(p)) { - continue; - } - - if (!CreateDirectory(p)) // @suppress("Invalid arguments") - { - return false; - } - } - - return true; + return true; } bool File::DeleteDirectories(const std::filesystem::path& path) { - std::string real_path = Common::ReplaceChar(PathToGenericString(path), '\\', '/'); + const auto normalized_path = + WithoutTrailingSeparator(path); - std::vector list = Common::Split(real_path, "/"); + std::vector directories; + auto current_path = normalized_path.root_path(); - std::string p; - std::vector list2; + for (const auto& component: + normalized_path.relative_path()) { + current_path /= component; + directories.push_back(current_path); + } - for (uint32_t si = 0; si < list.size(); si++) { - const std::string& s = list[si]; + for (auto it = directories.rbegin(); + it != directories.rend(); ++it) { + if (!DeleteDirectory(*it)) { + return false; + } + } - if (si != 0 || real_path.starts_with("/")) { - p += "/"; - } - - p += s; - - list2.push_back(p); - } - - uint32_t num = list2.size(); - - for (uint32_t si = num - 1; si < num; si--) { - if (!DeleteDirectory(list2[si])) { - return false; - } - } - - return true; + return true; } bool File::DeleteFile( diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index 9b4bf1422..b7cf5a26b 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -137,20 +137,21 @@ void CheckUnicodePaths(const std::filesystem::path &root) { const auto unicode_root = root / Common::PathFromUtf8(HostDirectory); + const auto nested_root = unicode_root / "nested"; - Check(std::filesystem::create_directories(unicode_root), - "create Unicode host directory"); + Check(Common::File::CreateDirectories(nested_root), + "create nested Unicode host directory"); - CheckMountRoot(unicode_root); + CheckMountRoot(nested_root); const auto native_file = - unicode_root / Common::PathFromUtf8(GuestFilename); + nested_root / Common::PathFromUtf8(GuestFilename); Common::File fixture; Check(fixture.Create(native_file), "create Unicode filename"); fixture.Close(); - FileSystem::Mount(unicode_root, "/app0"); + FileSystem::Mount(nested_root, "/app0"); const auto guest_file = std::string("/app0/") + std::string(GuestFilename);