From f3dcf6d8bef1cf4a27258aef2ffe11e75dec8056 Mon Sep 17 00:00:00 2001 From: Adnan Khan Date: Thu, 22 Oct 2026 14:02:37 +0530 Subject: [PATCH] fix(relinker): skip unreadable candidates and fold case for --windows in the needed-module report --- core/relinker/cli/include/NativeModules.hpp | 2 +- core/relinker/cli/src/NativeModules.cpp | 34 +++++++++++++------ core/relinker/main.cpp | 2 +- .../tests/test_guest_needed_modules.py | 18 ++++++++++ docs/user/USAGE.md | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index 31d54f5477..862acfcf0c 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -16,6 +16,6 @@ bool SameDirectory(const std::filesystem::path& first, const std::filesystem::pa NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); -void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out); +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, bool foldCase, std::ostream& out); } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index a55ea22605..d10056fa28 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -21,12 +22,12 @@ struct CandidateHeader { std::uint16_t Type = 0; }; -CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { +std::optional TryReadCandidateHeader(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + if (!stream) return std::nullopt; char header[18]{}; stream.read(header, sizeof header); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + Io::Utf8Path(path)); + if (stream.bad()) return std::nullopt; CandidateHeader result; if (stream.gcount() < 4) return result; const auto byte = [&](const std::size_t index) { return static_cast(header[index]); }; @@ -36,6 +37,17 @@ CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { return result; } +CandidateHeader ReadCandidateHeader(const std::filesystem::path& path) { + const auto header = TryReadCandidateHeader(path); + if (!header) throw Domain::RelinkerException("Cannot read guest candidate: " + Io::Utf8Path(path)); + return *header; +} + +std::string FoldedName(std::string name, const bool foldCase) { + if (foldCase) for (auto& character : name) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + return name; +} + std::string_view ModuleStem(const std::string_view name) { return name.substr(0, name.size() - Domain::ModuleSuffix(name).size()); } @@ -130,10 +142,12 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil return result; } -void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, std::ostream& out) { +void ReportUnconvertedNeeded(const NativeModules& modules, const std::vector& neededLibraries, const bool foldCase, std::ostream& out) { std::map> stems; - for (const auto& name : neededLibraries) - if (!Domain::ModuleSuffix(name).empty()) stems[std::string(ModuleStem(name))].push_back(name); + for (const auto& name : neededLibraries) { + const auto folded = FoldedName(name, foldCase); + if (!Domain::ModuleSuffix(folded).empty()) stems[std::string(ModuleStem(folded))].push_back(name); + } if (stems.empty()) return; struct Found { std::filesystem::path Relative; @@ -147,13 +161,13 @@ void ReportUnconvertedNeeded(const NativeModules& modules, const std::vectoris_regular_file()) continue; - const auto filename = Io::Utf8Path(entry->path().filename()); + const auto filename = FoldedName(Io::Utf8Path(entry->path().filename()), foldCase); if (Domain::ModuleSuffix(filename).empty()) continue; const auto stem = stems.find(std::string(ModuleStem(filename))); if (stem == stems.end()) continue; - const auto header = ReadCandidateHeader(entry->path()); - if (!header.Self && !(header.Elf && header.Type == 0xfe18)) continue; - for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header.Self}); + const auto header = TryReadCandidateHeader(entry->path()); + if (!header || (!header->Self && !(header->Elf && header->Type == 0xfe18))) continue; + for (const auto& name : stem->second) found.push_back({entry->path().lexically_relative(modules.Root), name, header->Self}); } std::sort(found.begin(), found.end(), [](const auto& left, const auto& right) { return std::tie(left.Relative, left.Name) < std::tie(right.Relative, right.Name); }); for (const auto& [relative, name, self] : found) { diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 49ae7b38ac..904f129ad8 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -76,7 +76,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, progress.IncludeFile(Io::Utf8Path(input.OutputPath)); for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path)); auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); - if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, std::cout); + if (!args.skipSceModule) Cli::ReportUnconvertedNeeded(modules, converted.Analysis.DynamicSection.NeededLibraries, args.toWindows, std::cout); return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase}; } diff --git a/core/relinker/relinker/tests/test_guest_needed_modules.py b/core/relinker/relinker/tests/test_guest_needed_modules.py index 99295fd68c..c6b04179dc 100644 --- a/core/relinker/relinker/tests/test_guest_needed_modules.py +++ b/core/relinker/relinker/tests/test_guest_needed_modules.py @@ -143,6 +143,24 @@ def check_needed_outside(relinker, work): (case / "needed.prx").write_bytes(sce_module) assert convert(case, ["--skip-sce-module"]) == [] + case = work / "outside-case" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "NEEDED.PRX").write_bytes(sce_module) + assert convert(case) == [] + assert convert(case, ["--windows"]) == [message + "needed.prx at Media/NEEDED.PRX; add --module-dir Media"] + + if os.name != "nt" and os.geteuid() != 0: + case = work / "outside-unreadable" + (case / "sce_module").mkdir(parents=True) + (case / "Media").mkdir() + (case / "Media" / "needed.prx").write_bytes(sce_module) + (case / "needed.prx").write_bytes(self_container) + (case / "needed.prx").chmod(0) + try: + assert convert(case) == [message + "needed.prx at Media/needed.prx; add --module-dir Media"] + finally: + (case / "needed.prx").chmod(0o644) def main(): diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 20e78e83e2..c781c7ee49 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -27,7 +27,7 @@ relinker [options] An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. -A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix, is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. +A module the executable needs that is not converted is looked for under the module parent directory, outside the module directories. Each SELF container or `ET_SCE_DYNAMIC` ELF found there whose name matches, up to the `.prx`, `.sprx`, `.suprx` or `.debug_prx` suffix (ignoring case with `--windows`, as the guest module match does), is reported after the conversion with the `--module-dir` that would include it; the output still refers to a host library of that name. A file that cannot be read is not reported. Linux output: