Merge pull request #56 from oneandonlydean/feat/linux-pread-getdents

feat(libkernel): pread, pwrite, getdents and getdirentries on linux
- **pread/pwrite:** use the existing `NativePread`/`NativePwrite` and don't move the file offset. A null buffer with a non-zero size returns `EFAULT`, a negative offset `EINVAL`, and host failures go through `SceErrorFromErrno`.
- **getdirentries:**
  - reads host `getdents64` records and converts them to the FreeBSD `struct dirent` layout the guest expects (`d_fileno` u32, `d_reclen` u16, `d_type` u8, `d_namlen` u8, NUL-terminated name, record padded to 4 bytes);
  - `*basep` gets the directory position from before the read;
  - when a record doesn't fit in the guest buffer, the descriptor is moved with `lseek` back to that entry's cookie, so the next call resumes there;
  - if not even the first record fits, the call returns `EINVAL` without consuming anything, and a name longer than 255 bytes returns `ENAMETOOLONG`.
- **getdents** is `getdirentries` without `basep`.
- Adds the guest `EFAULT`, `EINVAL` and `ENAMETOOLONG` constants.
This commit is contained in:
boykopovar 2026-09-28 09:38:47 +03:00 committed by GitHub
commit 73d014ed83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 205 additions and 0 deletions

View File

@ -373,6 +373,12 @@ if(BUILD_TESTING)
set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
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)

View File

@ -8,6 +8,7 @@
#include "prx/libkernel/File/include/NativeStat.hpp"
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
#include <cerrno>
#include <cstring>
#include <cstdarg>
#include <filesystem>
#include <stdexcept>
@ -78,6 +79,9 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb
#include <unistd.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <dirent.h>
#include <sys/syscall.h>
#include <vector>
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<int64_t>(base);
std::vector<char> host(static_cast<std::size_t>(nbytes) < 4096 ? 4096 : static_cast<std::size_t>(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<const struct dirent64*>(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<std::size_t>(nbytes)) {
if (::lseek(fd, resume, SEEK_SET) < 0) return SceErrorFromErrno(errno);
if (written != 0) return static_cast<int>(written);
return nameLength > GuestMaxName ? static_cast<int>(0x80020000u | GUEST_ENAMETOOLONG) : SceErrorFromErrno(GUEST_EINVAL);
}
char* out = buf + written;
std::memset(out, 0, record);
const auto fileNumber = static_cast<std::uint32_t>(entry->d_ino);
const auto recordLength = static_cast<std::uint16_t>(record);
const auto nameBytes = static_cast<std::uint8_t>(nameLength);
std::memcpy(out, &fileNumber, sizeof(fileNumber));
std::memcpy(out + 4, &recordLength, sizeof(recordLength));
out[6] = static_cast<char>(entry->d_type);
out[7] = static_cast<char>(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<int>(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);

View File

@ -0,0 +1,122 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/File/include/FileFlags.hpp"
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <string>
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<int>(0x80020016u);
static constexpr int ErrorEfault = static_cast<int>(0x8002000Eu);
static constexpr int ErrorEbadf = static_cast<int>(0x80020009u);
struct Entry {
std::uint8_t type;
std::uint16_t recordLength;
};
static int Collect(const char* buffer, int bytes, std::map<std::string, Entry>& 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<std::uint8_t>(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<std::uint8_t>(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<char, 48> small{};
std::map<std::string, Entry> entries;
int calls = 0;
std::int64_t previousBase = -1;
for (;;) {
std::int64_t base = -1;
const int read = sceKernelGetdirentries(directory, small.data(), static_cast<int>(small.size()), &base);
Require(read >= 0 && read <= static_cast<int>(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<char, 8> tooSmall{};
Require(sceKernelGetdents(directory, tooSmall.data(), static_cast<int>(tooSmall.size())) == ErrorEinval);
Require(sceKernelGetdents(directory, nullptr, 64) == ErrorEfault);
Require(sceKernelGetdents(directory, small.data(), 0) == ErrorEinval);
std::array<char, 4096> large{};
std::map<std::string, Entry> all;
const int readAll = sceKernelGetdents(directory, large.data(), static_cast<int>(large.size()));
Require(readAll > 0 && Collect(large.data(), readAll, all) == 6);
Require(sceKernelGetdents(directory, large.data(), static_cast<int>(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);
}