diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 636687c8a..28578aab4 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -86,6 +86,9 @@ set(SPDLOG_BUILD_WARNINGS OFF CACHE BOOL "" FORCE) set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE) set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE) set(SPDLOG_NO_EXCEPTIONS ON CACHE BOOL "" FORCE) +if(WIN32) + set(SPDLOG_WCHAR_FILENAMES ON CACHE BOOL "" FORCE) +endif() add_subdirectory(spdlog EXCLUDE_FROM_ALL) if (NOT TARGET FFmpeg::ffmpeg) diff --git a/src/common/file.cpp b/src/common/file.cpp index 086effbd1..90839a88e 100644 --- a/src/common/file.cpp +++ b/src/common/file.cpp @@ -245,88 +245,78 @@ 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) { - 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( @@ -460,13 +450,7 @@ std::vector File::FindFiles(const std::filesystem::path& path) { SysFileFindFiles(path, files); - auto path_str = PathToGenericString(path); - uint32_t len = static_cast(path_str.size()); - - if (!path_str.ends_with("/") && !path_str.ends_with("\\")) { - len++; - } - + const auto normalized_path = path.lexically_normal(); std::vector ret; ret.reserve(files.size()); @@ -474,7 +458,7 @@ std::vector File::FindFiles(const std::filesystem::path& path) { File::FindInfo r {}; r.path_with_name = f.path_with_name; - r.rel_path_with_name = Common::Mid(PathToGenericString(f.path_with_name), len); + r.rel_path_with_name = f.path_with_name.lexically_normal().lexically_relative(normalized_path); r.size = f.size; SysTimeStruct at {}; diff --git a/src/common/logging/log.cpp b/src/common/logging/log.cpp index 2777801c9..4a1a95d68 100644 --- a/src/common/logging/log.cpp +++ b/src/common/logging/log.cpp @@ -3,7 +3,6 @@ #include "common/assert.h" #include "common/emulatorConfig.h" -#include "common/stringUtils.h" #include #include @@ -47,7 +46,7 @@ std::shared_ptr MakeFileLogger(std::string name } auto sink = - std::make_shared(Common::PathToString(path), true); + std::make_shared(path.native(), true); return MakeLogger(std::move(name), std::move(sink)); } 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); diff --git a/src/common/stringUtils.h b/src/common/stringUtils.h index 3e8386170..89f7217e5 100644 --- a/src/common/stringUtils.h +++ b/src/common/stringUtils.h @@ -34,6 +34,14 @@ inline std::string PathToGenericString(const std::filesystem::path& path) { #endif } +inline std::filesystem::path PathFromUtf8(std::string_view text) { +#if defined(__cpp_char8_t) + return std::filesystem::path(std::u8string(text.begin(), text.end())); +#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/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/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index 9f2d5aab5..3a7158f16 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) { @@ -270,7 +269,7 @@ void MountPoints::Umount(const std::string& folder_or_point) { const auto it = std::find_if( m_mount_pairs.begin(), m_mount_pairs.end(), [&folder_or_point_str](const MountPair& p) { - return Common::PathToGenericString(p.dir) == folder_or_point_str || + return Common::FixDirectorySlash(Common::PathToGenericString(p.dir)) == folder_or_point_str || p.point == folder_or_point_str; }); if (it != m_mount_pairs.end()) { @@ -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/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 diff --git a/src/main.cpp b/src/main.cpp index 81f379805..0eb3a2e37 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,11 +232,13 @@ 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; } - 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") { @@ -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 diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index faf89dc60..290041416 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -4,12 +4,14 @@ #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" #include "libs/network.h" #include "loader/symbolDatabase.h" +#include #include #include @@ -153,6 +155,94 @@ void CheckMountRoot(const std::filesystem::path &root) { } } FileSystem::Umount("/app0"); + Check(FileSystem::GetRealFilename("/app0/rpf.cache") == "/app0/rpf.cache", + "unmount by guest path"); + for (const auto &folder : {root, root / ""}) { + for (const auto &host : {root, root / ""}) { + FileSystem::Mount(folder, "/app0"); + FileSystem::Umount(Common::PathToGenericString(host)); + Check(FileSystem::GetRealFilename("/app0/rpf.cache") == "/app0/rpf.cache", + "unmount by host path with or without trailing separator"); + } + } +} + +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); + const auto nested_root = unicode_root / "nested"; + + Check(Common::File::CreateDirectories(nested_root), + "create nested Unicode host directory"); + + CheckMountRoot(nested_root); + + const auto native_file = + nested_root / Common::PathFromUtf8(GuestFilename); + + Common::File fixture; + Check(fixture.Create(native_file), "create Unicode filename"); + fixture.Close(); + + for (const auto &directory : {unicode_root, std::filesystem::relative(unicode_root)}) { + // A Windows temporary directory may be on a different drive from the working directory. + if (directory.empty()) { + continue; + } + const auto files = Common::File::FindFiles(directory); + const auto relative_file = + std::filesystem::path(u8"nested/asset-\u00e9-\u65e5\u672c\u8a9e.bin"); + Check(std::any_of(files.begin(), files.end(), [&](const auto &file) { + return file.path_with_name.lexically_normal() == + (directory / relative_file).lexically_normal() && + file.rel_path_with_name == relative_file; + }), "recursive enumeration preserves native and relative Unicode paths"); + } + const auto entries = Common::File::GetDirEntries(nested_root); + Check(std::any_of(entries.begin(), entries.end(), [&](const auto &entry) { + return entry.is_file && entry.name == GuestFilename; + }), "directory enumeration returns UTF-8 filenames"); + + FileSystem::Mount(nested_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 CheckUnicodeLogPath(const std::filesystem::path &root) { + Config::ConfigOptions options; + options.printf_direction = Config::LogDirection::File; + options.printf_output_file = root / u8"logs-\u65e5\u672c\u8a9e-\U0001f600" / u8"log-\u00e9.txt"; + Config::Load(options); + Log::Initialize(); + constexpr std::string_view Payload = "Unicode log path\n"; + Log::Write(Payload); + Log::Shutdown(); + + Common::File result(options.printf_output_file, Common::File::Mode::Read); + Check(!result.IsInvalid(), "open Unicode log file"); + const auto data = result.ReadWholeBuffer(); + Check(data.size() == Payload.size() && + std::memcmp(data.data(), Payload.data(), data.size()) == 0, + "Unicode log file contains output"); + options.printf_direction = Config::LogDirection::Silent; + Config::Load(options); + Log::Initialize(); } void CheckDirectoryStream(const std::filesystem::path &root) { @@ -414,6 +504,8 @@ int main() { TempDirectory temporary; FileSystem::Initialize(); CheckMountRoot(temporary.Path()); + CheckUnicodePaths(temporary.Path()); + CheckUnicodeLogPath(temporary.Path()); CheckDirectoryStream(temporary.Path()); CheckAprPaths(temporary.Path()); FileSystem::Mount(temporary.Path(), "/savedata0");