diff --git a/include/psprecomp/runtime.hpp b/include/psprecomp/runtime.hpp index 6c426e0..5403d93 100644 --- a/include/psprecomp/runtime.hpp +++ b/include/psprecomp/runtime.hpp @@ -322,6 +322,8 @@ private: std::unordered_map native_fast_paths_; std::vector import_bindings_; std::filesystem::path game_root_; + // Cached when the extract case differs. + mutable std::unordered_map folded_paths_; bool stopped_{}; std::string stop_reason_; bool hle_histogram_enabled_{}; diff --git a/lcs/host/lcs_profile.cpp b/lcs/host/lcs_profile.cpp index 5140bef..69ec734 100644 --- a/lcs/host/lcs_profile.cpp +++ b/lcs/host/lcs_profile.cpp @@ -200,6 +200,15 @@ struct DirectoryHandle { std::size_t index{}; }; +// ELF directory names are uppercase. +std::string disc_directory_name(const std::filesystem::path &path) { + std::string name = path.filename().string(); + for (char &ch : name) { + if (ch >= 'a' && ch <= 'z') ch = static_cast(ch - 'a' + 'A'); + } + return name; +} + struct RawSectorFile { std::uint64_t base{}; std::uint64_t size{}; @@ -3747,7 +3756,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start DirectoryHandle handle; for (const auto &entry : std::filesystem::directory_iterator(native)) handle.entries.push_back(entry); std::sort(handle.entries.begin(), handle.entries.end(), [](const auto &a, const auto &b) { - return a.path().filename().string() < b.path().filename().string(); + return disc_directory_name(a.path()) < disc_directory_name(b.path()); }); const auto fd = file_table.next_fd++; file_table.directories.emplace(fd, std::move(handle)); @@ -3781,13 +3790,13 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start rt.memory().store32(dirent + 12u, static_cast(disc_file->size >> 32u)); rt.memory().store32(dirent + 0x40u, disc_file->start_sector); if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) { - std::cerr << "[io] sceIoDread file=\"" << entry.path().filename().string() + std::cerr << "[io] sceIoDread file=\"" << disc_directory_name(entry.path()) << "\" sector=" << disc_file->start_sector << " size=" << disc_file->size << "\n"; } } } - const std::string name = entry.path().filename().string(); + const std::string name = disc_directory_name(entry.path()); std::vector bytes(name.begin(), name.end()); bytes.push_back(0u); if (bytes.size() > 256u) bytes.resize(256u); diff --git a/src/runtime.cpp b/src/runtime.cpp index c4c9d99..6976df0 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -492,9 +493,27 @@ void Runtime::register_hle(std::string library, std::uint32_t nid, HleFunction f bool Runtime::has_function(std::uint32_t address) const { return lookup_function(address) != nullptr; } std::size_t Runtime::function_count() const noexcept { return functions_.size(); } -void Runtime::set_game_root(std::filesystem::path root) { game_root_ = std::filesystem::weakly_canonical(std::move(root)); } +void Runtime::set_game_root(std::filesystem::path root) { + game_root_ = std::filesystem::weakly_canonical(std::move(root)); + folded_paths_.clear(); +} const std::filesystem::path &Runtime::game_root() const noexcept { return game_root_; } +namespace { +bool ascii_iequal(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + const auto fold = [](unsigned char value) { + return value >= 'A' && value <= 'Z' ? static_cast(value - 'A' + 'a') : value; + }; + for (std::size_t index = 0; index < left.size(); ++index) { + if (fold(static_cast(left[index])) != + fold(static_cast(right[index]))) + return false; + } + return true; +} +} + std::filesystem::path Runtime::translate_path(const std::string &psp_path) const { std::string relative = psp_path; const auto colon = relative.find(':'); @@ -506,7 +525,53 @@ std::filesystem::path Runtime::translate_path(const std::string &psp_path) const if (part == "..") throw Error("Rejected PSP path traversal: " + psp_path); if (part != ".") clean /= part; } - return game_root_ / clean; + const std::string key = clean.generic_string(); + if (const auto cached = folded_paths_.find(key); cached != folded_paths_.end()) + return cached->second; + + std::error_code error; + const std::filesystem::path direct = game_root_ / clean; + if (std::filesystem::exists(direct, error) && !error) return direct; + + // Disc paths are case-insensitive. + std::filesystem::path resolved = game_root_; + bool folded = false; + bool missing = false; + for (const auto &part : clean) { + if (missing) { + resolved /= part; + continue; + } + error.clear(); + const std::filesystem::path exact = resolved / part; + if (std::filesystem::exists(exact, error) && !error) { + resolved = exact; + continue; + } + error.clear(); + if (!std::filesystem::is_directory(resolved, error) || error) { + missing = true; + resolved /= part; + continue; + } + bool matched = false; + error.clear(); + for (std::filesystem::directory_iterator it(resolved, error), end; + !error && it != end; it.increment(error)) { + const std::filesystem::path name = it->path().filename(); + if (!ascii_iequal(name.string(), part.string())) continue; + resolved = it->path(); + matched = true; + if (name != part) folded = true; + break; + } + if (!matched) { + missing = true; + resolved /= part; + } + } + if (folded) folded_paths_.emplace(key, resolved); + return resolved; } void Runtime::run(std::uint32_t entry, std::uint64_t max_dispatches) { diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 79d190a..0d051f9 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -1767,6 +1767,40 @@ int main() { try { (void)runtime.translate_path("disc0:/../secret"); } catch (...) { traversal_rejected = true; } require(traversal_rejected, "Path traversal was not rejected"); + const auto case_root = std::filesystem::temp_directory_path() / "psprecomp_case_test"; + std::filesystem::remove_all(case_root); + const auto movie = case_root / "psp_game" / "usrdir" / "movies"; + std::filesystem::create_directories(movie); + { + std::ofstream out(movie / "logo.pmf", std::ios::binary); + out << "pmf"; + } + runtime.set_game_root(case_root); + const auto folded = runtime.translate_path("disc0:/PSP_GAME/USRDIR/MOVIES/LOGO.PMF"); + require(std::filesystem::equivalent(folded, movie / "logo.pmf"), + "Case-insensitive disc path failed"); + const auto folded_again = runtime.translate_path("disc0:/PSP_GAME/USRDIR/MOVIES/LOGO.PMF"); + require(folded_again == folded, "Folded disc path was not stable"); + const auto missing = runtime.translate_path("disc0:/PSP_GAME/USRDIR/MOVIES/NO_SUCH.BIN"); + require(missing.filename() == "NO_SUCH.BIN", "Missing disc file kept the guest name"); + require(std::filesystem::equivalent(missing.parent_path(), movie), + "Missing disc file lost the folded parent"); + std::filesystem::remove_all(case_root); + + const auto exact_root = std::filesystem::temp_directory_path() / "psprecomp_case_exact"; + std::filesystem::remove_all(exact_root); + const auto exact_file = exact_root / "PSP_GAME" / "USRDIR" / "data.bin"; + std::filesystem::create_directories(exact_file.parent_path()); + { + std::ofstream out(exact_file, std::ios::binary); + out << "x"; + } + runtime.set_game_root(exact_root); + const auto exact = runtime.translate_path("disc0:/PSP_GAME/USRDIR/data.bin"); + require(std::filesystem::equivalent(exact, exact_file), "Exact disc path failed"); + require(exact.filename() == "data.bin", "Exact disc path changed case"); + std::filesystem::remove_all(exact_root); + const auto temp = std::filesystem::temp_directory_path() / "psprecomp_sha_test.txt"; { std::ofstream out(temp, std::ios::binary); out << "abc"; } require(psprecomp::sha256_file(temp) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",