diff --git a/src/kernel/fileSystem.cpp b/src/kernel/fileSystem.cpp index d0e10909b..6f73b53b8 100644 --- a/src/kernel/fileSystem.cpp +++ b/src/kernel/fileSystem.cpp @@ -1197,6 +1197,67 @@ int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb) { return OK; } +int KYTY_SYSV_ABI KernelTruncate(const char* path, int64_t length) { + PRINT_NAME(); + if (path == nullptr) { + return KERNEL_ERROR_EFAULT; + } + if (path[0] != '/' || length < 0) { + return KERNEL_ERROR_EINVAL; + } + size_t component_length = 0; + for (size_t i = 0; path[i] != '\0'; ++i) { + if (i >= 1023) { + return KERNEL_ERROR_ENAMETOOLONG; + } + if (path[i] == '/') { + component_length = 0; + } else if (++component_length > 255) { + return KERNEL_ERROR_ENAMETOOLONG; + } + } + if (IsRandomDevice(path)) { + return OK; + } + const auto real_name = g_mount_points->ResolvePath(path); + if (real_name.empty()) { + return KERNEL_ERROR_ENOENT; + } + if (Common::IsArchivePath(real_name)) { + return KERNEL_ERROR_EROFS; + } + + std::error_code error; + const auto status = std::filesystem::status(real_name, error); + if (!error) { + if (!std::filesystem::exists(status)) { + return KERNEL_ERROR_ENOENT; + } + if (std::filesystem::is_directory(status)) { + return KERNEL_ERROR_EISDIR; + } + if (std::filesystem::is_regular_file(status)) { + std::filesystem::resize_file(real_name, static_cast(length), error); + } + } + if (error) { + switch (static_cast(error.default_error_condition().value())) { + case std::errc::no_such_file_or_directory: return KERNEL_ERROR_ENOENT; + case std::errc::not_a_directory: return KERNEL_ERROR_ENOTDIR; + case std::errc::filename_too_long: return KERNEL_ERROR_ENAMETOOLONG; + case std::errc::permission_denied: return KERNEL_ERROR_EACCES; + case std::errc::read_only_file_system: return KERNEL_ERROR_EROFS; + case std::errc::file_too_large: return KERNEL_ERROR_EFBIG; + case std::errc::invalid_argument: return KERNEL_ERROR_EINVAL; + case std::errc::no_space_on_device: return KERNEL_ERROR_ENOSPC; + default: return KERNEL_ERROR_EIO; + } + } + LOGF("\tTruncate (size = %" PRId64 ") file: %s\n", length, + Common::PathToString(real_name).c_str()); + return OK; +} + int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length) { PRINT_NAME(); diff --git a/src/kernel/fileSystem.h b/src/kernel/fileSystem.h index ef6237239..4875197fd 100644 --- a/src/kernel/fileSystem.h +++ b/src/kernel/fileSystem.h @@ -69,6 +69,7 @@ int64_t KYTY_SYSV_ABI KernelPwritev(int d, const KernelIovec* iov, int iovcnt, i int64_t KYTY_SYSV_ABI KernelLseek(int d, int64_t offset, int whence); int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb); int KYTY_SYSV_ABI KernelFstat(int d, FileStat* sb); +int KYTY_SYSV_ABI KernelTruncate(const char* path, int64_t length); int KYTY_SYSV_ABI KernelFtruncate(int d, int64_t length); int KYTY_SYSV_ABI KernelUnlink(const char* path); int KYTY_SYSV_ABI KernelRename(const char* from, const char* to); diff --git a/src/libs/libKernel.cpp b/src/libs/libKernel.cpp index 5d95b5f2e..1ba48fb88 100644 --- a/src/libs/libKernel.cpp +++ b/src/libs/libKernel.cpp @@ -3118,6 +3118,7 @@ LIB_DEFINE(InitLibKernel_1_FS) { LIB_FUNC("eV9wAD2riIA", FileSystem::KernelStat); LIB_FUNC("kBwCPsYX-m4", FileSystem::KernelFstat); LIB_FUNC("VW3TVZiM4-E", FileSystem::KernelFtruncate); + LIB_FUNC("WlyEA-sLDf0", FileSystem::KernelTruncate); LIB_FUNC("AUXVxWeJU-A", FileSystem::KernelUnlink); LIB_FUNC("52NcYU9+lEo", FileSystem::KernelRename); LIB_FUNC("taRWhTJFTgE", FileSystem::KernelGetdirentries); diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index f7675d4e3..5262a25d0 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -332,6 +332,60 @@ void TestSaveOpenVisibility() { Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file"); } +void TestSaveTruncate() { + namespace Kernel = Libs::LibKernel; + Loader::SymbolDatabase symbols; + Libs::InitLibKernel_1(&symbols); + const auto* symbol = symbols.Find( + {"WlyEA-sLDf0", "libkernel", 1, "libkernel", 1, 1, Loader::SymbolType::Func}); + Check(symbol != nullptr, "sceKernelTruncate export resolves"); + const auto truncate = + reinterpret_cast(symbol->vaddr); + constexpr char Path[] = "/savedata0/truncate-save.dat"; + constexpr char Payload[] = "profile data"; + const int fd = FileSystem::KernelOpen(Path, 0x602, 0777); + Check(fd >= 3 && FileSystem::KernelWrite(fd, Payload, sizeof(Payload) - 1) == + sizeof(Payload) - 1, + "populate path truncation fixture"); + FileSystem::FileStat stat {}; + std::array bytes {}; + Check(truncate(Path, 3) == OK && + FileSystem::KernelFstat(fd, &stat) == OK && stat.st_size == 3 && + FileSystem::KernelLseek(fd, 0, 1) == sizeof(Payload) - 1 && + FileSystem::KernelPread(fd, bytes.data(), bytes.size(), 0) == 3 && + std::memcmp(bytes.data(), Payload, 3) == 0, + "path truncate discards the tail without moving an open descriptor"); + bytes.fill('x'); + Check(truncate(Path, bytes.size()) == OK && + FileSystem::KernelFstat(fd, &stat) == OK && stat.st_size == bytes.size() && + FileSystem::KernelPread(fd, bytes.data(), bytes.size(), 0) == bytes.size() && + std::memcmp(bytes.data(), Payload, 3) == 0 && + std::all_of(bytes.begin() + 3, bytes.end(), [](char byte) { return byte == 0; }) && + FileSystem::KernelLseek(fd, 0, 1) == sizeof(Payload) - 1, + "path truncate extends with zero bytes and preserves the descriptor position"); + Check(truncate(Path, -1) == Kernel::KERNEL_ERROR_EINVAL && + truncate(nullptr, 0) == Kernel::KERNEL_ERROR_EFAULT && + truncate("truncate-save.dat", 0) == Kernel::KERNEL_ERROR_EINVAL && + truncate("/savedata0/missing-save.dat", 0) == Kernel::KERNEL_ERROR_ENOENT && + truncate("/savedata0", 0) == Kernel::KERNEL_ERROR_EISDIR && + truncate("/savedata0/truncate-save.dat/child", 0) == Kernel::KERNEL_ERROR_ENOTDIR && + FileSystem::KernelFstat(fd, &stat) == OK && stat.st_size == bytes.size() && + FileSystem::KernelLseek(fd, 0, 1) == sizeof(Payload) - 1, + "path truncate errors preserve file size and descriptor position"); + Check(truncate("/dev/urandom", 0) == OK, + "path truncate is a no-op for a nonregular nondirectory file"); + std::string long_path = "/savedata0"; + long_path.resize(1024, '/'); + Check(truncate(long_path.c_str(), 0) == Kernel::KERNEL_ERROR_ENAMETOOLONG, + "path truncate rejects a pathname beyond the guest length limit"); + long_path.pop_back(); + Check(truncate(long_path.c_str(), 0) == Kernel::KERNEL_ERROR_EISDIR && + truncate((std::string("/savedata0/") + std::string(256, 'a')).c_str(), 0) == + Kernel::KERNEL_ERROR_ENAMETOOLONG, + "path truncate permits the pathname length limit and bounds each component"); + Check(FileSystem::KernelClose(fd) == OK, "close path truncation fixture"); +} + void TestAioBatches() { namespace Kernel = Libs::LibKernel; Loader::SymbolDatabase symbols; @@ -699,6 +753,12 @@ void CheckArchiveMount(const std::filesystem::path &root) { FileSystem::KernelFtruncate(fd, 0) == Libs::LibKernel::KERNEL_ERROR_EBADF, "archive read-only descriptors reject write and truncate"); + Check(FileSystem::KernelTruncate(GuestMember, 0) == + Libs::LibKernel::KERNEL_ERROR_EROFS && + FileSystem::KernelFstat(fd, &descriptor_stat) == OK && + descriptor_stat.st_size == payload.size() && + FileSystem::KernelLseek(fd, 0, 1) == payload.size(), + "path truncate rejects a read-only archive and preserves its member"); const auto unicode_guest = std::string("/app0/") + std::string(ArchiveTests::UnicodeFilename); const int unicode = FileSystem::KernelOpen(unicode_guest.c_str(), 0, 0); @@ -1908,6 +1968,7 @@ int main(int, char**) { CheckAprPaths(temporary.Path()); FileSystem::Mount(temporary.Path(), "/savedata0"); TestSaveOpenVisibility(); + TestSaveTruncate(); TestAioBatches(); CheckSaveRename(temporary.Path(), "first-save"); CheckSaveRename(temporary.Path(), "replacement-save");