From f7e1bdf0b95e7da121912f48f46aae703edc5e5d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 28 Sep 2026 16:40:00 +1300 Subject: [PATCH] feat(libkernel): pread, pwrite, getdents and getdirentries on linux Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01ButHENCbXXt8h4EZuWUabJ --- core/libs/CMakeLists.txt | 6 + core/libs/prx/libkernel/File/src/Stdio.cpp | 77 +++++++++++++ core/libs/tests/GuestDirectoryEntries.cpp | 122 +++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 core/libs/tests/GuestDirectoryEntries.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index b0572b5bf..f5425dfef 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -328,6 +328,12 @@ if(BUILD_TESTING) set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT WIN32) + add_executable(guest_directory_entry_tests tests/GuestDirectoryEntries.cpp) + target_include_directories(guest_directory_entry_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + endif() add_executable(guest_search_tests tests/GuestSearch.cpp) target_include_directories(guest_search_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_search_tests PRIVATE libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 30ee4ccf7..ece3fae6d 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -8,6 +8,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include +#include #include #include #include @@ -78,6 +79,9 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb #include #include #include +#include +#include +#include static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); } @@ -103,7 +107,10 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb static constexpr int GUEST_ENOENT = 2; static constexpr int GUEST_EIO = 5; +static constexpr int GUEST_EFAULT = 14; static constexpr int GUEST_EEXIST = 17; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ENAMETOOLONG = 63; static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_ENOTEMPTY = 66; @@ -283,6 +290,8 @@ int APS5_VABI sceKernelFsync(int fd) { return 0; } +#ifdef _WIN32 + int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { (void)fd; (void)buf; @@ -300,6 +309,54 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas return 0; } +#else + +int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + constexpr std::size_t GuestHeaderBytes = 8; + constexpr std::size_t GuestMaxName = 255; + if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); + if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + const off_t base = ::lseek(fd, 0, SEEK_CUR); + if (base < 0) return SceErrorFromErrno(errno); + if (basep != nullptr) *basep = static_cast(base); + std::vector host(static_cast(nbytes) < 4096 ? 4096 : static_cast(nbytes)); + const auto read = ::syscall(SYS_getdents64, fd, host.data(), host.size()); + if (read < 0) return SceErrorFromErrno(errno); + std::size_t written = 0; + off_t resume = base; + for (long offset = 0; offset < read;) { + const auto* entry = reinterpret_cast(host.data() + offset); + const auto nameLength = std::strlen(entry->d_name); + const auto record = (GuestHeaderBytes + nameLength + 1 + 3) & ~std::size_t{3}; + if (nameLength > GuestMaxName || written + record > static_cast(nbytes)) { + if (::lseek(fd, resume, SEEK_SET) < 0) return SceErrorFromErrno(errno); + if (written != 0) return static_cast(written); + return nameLength > GuestMaxName ? static_cast(0x80020000u | GUEST_ENAMETOOLONG) : SceErrorFromErrno(GUEST_EINVAL); + } + char* out = buf + written; + std::memset(out, 0, record); + const auto fileNumber = static_cast(entry->d_ino); + const auto recordLength = static_cast(record); + const auto nameBytes = static_cast(nameLength); + std::memcpy(out, &fileNumber, sizeof(fileNumber)); + std::memcpy(out + 4, &recordLength, sizeof(recordLength)); + out[6] = static_cast(entry->d_type); + out[7] = static_cast(nameBytes); + std::memcpy(out + GuestHeaderBytes, entry->d_name, nameLength); + written += record; + resume = entry->d_off; + offset += entry->d_reclen; + } + if (::lseek(fd, resume, SEEK_SET) < 0) return SceErrorFromErrno(errno); + return static_cast(written); +} + +int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { + return sceKernelGetdirentries(fd, buf, nbytes, nullptr); +} + +#endif + int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; if (path == nullptr) throw std::invalid_argument("sceKernelMkdir: path is null"); @@ -311,6 +368,8 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { return 0; } +#ifdef _WIN32 + int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { (void)d; (void)buf; @@ -329,6 +388,24 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return 0; } +#else + +int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { + if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + const auto result = NativePread(d, buf, nbytes, offset); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { + if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + const auto result = NativePwrite(d, buf, nbytes, offset); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +#endif + int APS5_VABI sceKernelRename(const char* from, const char* to) { if (from == nullptr || to == nullptr) throw std::invalid_argument("sceKernelRename: path is null"); const auto source = ResolvePath_nid_no_patch(from); diff --git a/core/libs/tests/GuestDirectoryEntries.cpp b/core/libs/tests/GuestDirectoryEntries.cpp new file mode 100644 index 000000000..912247d44 --- /dev/null +++ b/core/libs/tests/GuestDirectoryEntries.cpp @@ -0,0 +1,122 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI sceKernelGetdents(int, char*, int); +int APS5_VABI sceKernelGetdirentries(int, char*, int, std::int64_t*); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI sceKernelPwrite(int, const void*, std::size_t, std::int64_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory entry check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr int ErrorEinval = static_cast(0x80020016u); +static constexpr int ErrorEfault = static_cast(0x8002000Eu); +static constexpr int ErrorEbadf = static_cast(0x80020009u); + +struct Entry { + std::uint8_t type; + std::uint16_t recordLength; +}; + +static int Collect(const char* buffer, int bytes, std::map& entries) { + int records = 0; + for (int offset = 0; offset < bytes; ++records) { + std::uint16_t recordLength = 0; + std::memcpy(&recordLength, buffer + offset + 4, sizeof(recordLength)); + const auto nameLength = static_cast(buffer[offset + 7]); + const std::string name(buffer + offset + 8); + Require(name.size() == nameLength); + Require(recordLength == ((8 + nameLength + 1 + 3) & ~3)); + Require(offset + recordLength <= bytes); + Require(entries.emplace(name, Entry{static_cast(buffer[offset + 6]), recordLength}).second); + offset += recordLength; + } + return records; +} + +int main() { + const auto root = std::filesystem::path("anyps5-directory-entries-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const std::string longName = "a-much-longer-entry-name-0123456789"; + { std::ofstream stream(root / "data.bin", std::ios::binary); stream << "0123456789"; } + { std::ofstream stream(root / "b"); } + { std::ofstream stream(root / longName); } + Require(std::filesystem::create_directory(root / "sub")); + + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + std::array small{}; + std::map entries; + int calls = 0; + std::int64_t previousBase = -1; + for (;;) { + std::int64_t base = -1; + const int read = sceKernelGetdirentries(directory, small.data(), static_cast(small.size()), &base); + Require(read >= 0 && read <= static_cast(small.size())); + Require(base >= 0 && base != previousBase); + if (calls == 0) Require(base == 0); + previousBase = base; + if (read == 0) break; + Require(Collect(small.data(), read, entries) > 0); + ++calls; + } + Require(calls >= 3); + Require(entries.size() == 6); + for (const char* name : {".", "..", "data.bin", "b", "sub"}) Require(entries.contains(name)); + Require(entries.contains(longName) && entries[longName].recordLength == 44); + Require(entries["b"].recordLength == 12); + Require(entries["sub"].type == 4 || entries["sub"].type == 0); + Require(entries["data.bin"].type == 8 || entries["data.bin"].type == 0); + + Require(sceKernelLseek(directory, 0, 0) == 0); + std::array tooSmall{}; + Require(sceKernelGetdents(directory, tooSmall.data(), static_cast(tooSmall.size())) == ErrorEinval); + Require(sceKernelGetdents(directory, nullptr, 64) == ErrorEfault); + Require(sceKernelGetdents(directory, small.data(), 0) == ErrorEinval); + std::array large{}; + std::map all; + const int readAll = sceKernelGetdents(directory, large.data(), static_cast(large.size())); + Require(readAll > 0 && Collect(large.data(), readAll, all) == 6); + Require(sceKernelGetdents(directory, large.data(), static_cast(large.size())) == 0); + Require(sceKernelClose(directory) == 0); + + const int file = sceKernelOpen((root / "data.bin").string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(file >= 0); + Require(sceKernelLseek(file, 2, 0) == 2); + char bytes[16] = {}; + Require(sceKernelPread(file, bytes, 4, 5) == 4 && std::memcmp(bytes, "5678", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 2); + Require(sceKernelPwrite(file, "XY", 2, 8) == 2); + Require(sceKernelLseek(file, 0, 1) == 2); + Require(sceKernelRead(file, bytes, 3) == 3 && std::memcmp(bytes, "234", 3) == 0); + Require(sceKernelPread(file, bytes, sizeof(bytes), 0) == 10 && std::memcmp(bytes, "01234567XY", 10) == 0); + Require(sceKernelPread(file, bytes, sizeof(bytes), 10) == 0); + Require(sceKernelPread(file, bytes, 1, -1) == ErrorEinval); + Require(sceKernelPwrite(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelClose(file) == 0); + Require(sceKernelPread(file, bytes, 1, 0) == ErrorEbadf); + + std::filesystem::remove_all(root); +}