Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions core/libs/prx/libc/src/General.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,32 @@ struct NameMatch {
std::string name;
std::optional<std::filesystem::path> path;
bool ambiguous = false;
bool stem = false;
};

std::string_view NameStem(std::string_view name) {
const auto dot = name.rfind('.');
return dot == std::string_view::npos || dot == 0 ? name : name.substr(0, dot);
}

bool SameRelinkedStem(std::string_view filename, std::string_view name) {
constexpr std::string_view suffix = ".guest.prx";
if (filename.size() <= suffix.size() || !SameName(filename.substr(filename.size() - suffix.size()), suffix)) return false;
return SameName(NameStem(filename.substr(0, filename.size() - suffix.size())), NameStem(name));
}

std::error_code MatchNames(const std::filesystem::path& directory, std::span<NameMatch> names) {
std::error_code error;
std::filesystem::directory_iterator entry(directory, error), end;
for (; !error && entry != end; entry.increment(error)) {
const auto filename = entry->path().filename().string();
for (auto& name : names) {
if (name.stem) {
std::error_code status;
if (!SameRelinkedStem(filename, name.name) || !entry->is_regular_file(status) || status) continue;
if (!name.path || filename.size() < name.path->filename().string().size()) name.path = entry->path();
continue;
}
if (!SameName(filename, name.name)) continue;
name.ambiguous = name.ambiguous || name.path.has_value();
name.path = entry->path();
Expand Down Expand Up @@ -615,9 +633,10 @@ extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::file
const auto relinked = RelinkedModuleName(path);
std::error_code error;
if (std::filesystem::is_regular_file(relinked, error)) return relinked;
NameMatch names[] = {{relinked.filename().string()}};
NameMatch names[] = {{relinked.filename().string()}, {path.filename().string(), std::nullopt, false, true}};
MatchNames(relinked.parent_path().empty() ? "." : relinked.parent_path(), names);
return MatchedModuleOrPath(path, relinked, names[0]);
if (names[0].ambiguous || names[0].path) return MatchedModuleOrPath(path, relinked, names[0]);
return names[1].path ? *names[1].path : path;
}

extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) {
Expand Down
24 changes: 24 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,30 @@ if(BUILD_TESTING)
set_tests_properties(guest_dynamic_loader_fixed_base PROPERTIES
WORKING_DIRECTORY "$<TARGET_FILE_DIR:guest_module_fixture>" RESOURCE_LOCK guest_dynamic_loader_files)
endif()
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/variant_names")
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.sprx.elf.GUEST.PRX" "not a module\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture no longer exercises the shortest-match or dangling-symlink-before-valid-candidate paths after the last-extension change. For the request guest_module_fixture.prx, SameRelinkedStem strips .GUEST.PRX and then only the last extension: guest_module_fixture.sprx.elf.GUEST.PRX yields stem guest_module_fixture.sprx, and the symlink name ending in .zprx.elf.GUEST.PRX yields guest_module_fixture.zprx. Neither matches guest_module_fixture; only the copied .elf.GUEST.PRX file matches. Could the decoy and symlink instead be named e.g. guest_module_fixture.sprx.GUEST.PRX and guest_module_fixture.zprx.GUEST.PRX? Those share the requested stem under this rule and would make the selection/error-path coverage real.

add_custom_command(TARGET guest_module_fixture POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:guest_module_fixture>"
"${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.elf.GUEST.PRX"
VERBATIM)
if(NOT WIN32)
add_custom_command(TARGET guest_module_fixture POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink missing.guest.prx
"${CMAKE_CURRENT_BINARY_DIR}/variant_names/guest_module_fixture.zprx.elf.GUEST.PRX"
VERBATIM)
endif()
add_test_executable(guest_dynamic_loader_variants_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoaderVariants.cpp)
target_include_directories(guest_dynamic_loader_variants_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_dynamic_loader_variants_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_dynamic_loader_variants_tests)
add_dependencies(guest_dynamic_loader_variants_tests guest_module_fixture)
add_test(NAME guest_dynamic_loader_variant_names COMMAND guest_dynamic_loader_variants_tests)
set_tests_properties(guest_dynamic_loader_variant_names PROPERTIES TIMEOUT 15
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/variant_names")
if(WIN32)
set_tests_properties(guest_dynamic_loader_variant_names PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp)
target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_module_info_tests PRIVATE libkernel libc)
Expand Down
24 changes: 24 additions & 0 deletions core/libs/tests/GuestDynamicLoaderVariants.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cstdio>
#include <cstdlib>
#include <source_location>
extern "C" {
void* APS5_VABI dlopen_nid_postfix(const char*, int);
void* APS5_VABI dlsym_nid_postfix(void*, const char*);
int APS5_VABI dlclose_nid_postfix(void*);
}
static void Require(bool value, const std::source_location& location = std::source_location::current()) {
if (value) return;
std::fprintf(stderr, "guest_dynamic_loader_variants: failed at %s:%u\n", location.file_name(), location.line());
std::abort();
}
int main() {
using Add = int (APS5_VABI *)(int, int);
void* extension = dlopen_nid_postfix("guest_module_fixture.prx", 2);
Require(extension != nullptr);
auto add = reinterpret_cast<Add>(dlsym_nid_postfix(extension, "GuestModuleAdd"));
Require(add && add(19, 23) == 42);
Require(dlclose_nid_postfix(extension) == 0);
Require(dlopen_nid_postfix("guest_module_fixture.other.prx", 2) == nullptr);
return 0;
}
2 changes: 1 addition & 1 deletion docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
- [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`, and fail to load on main with unresolved import (0xC0000139).
- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits
- [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e`
- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `<name>.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`
- [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `<name>.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `<name>.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/<name>.prx` and `Media/Plugins/<name>.prx`, and the relinker names the converted module after the ELF it was built from, `<name>.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console

Check warning on line 113 in docs/dev/debt/libc.md

View workflow job for this annotation

GitHub Actions / conventions

debt-length

Technical debt entry over 1000 characters: state the gap first (what differs from the console, what is unverified, what throws) and keep only the behaviour it needs: 1754 characters
- [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount
- [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known
- [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias
Expand Down
Loading