KytyPS5/tests/KernelFileSystemTests.cpp

403 lines
18 KiB
C++

#include "SDL.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/subsystems.h"
#include "common/threads.h"
#include "graphics/presentation/window/windowInternal.h"
#include "kernel/fileSystem.h"
#include "libs/errno.h"
#include "libs/network.h"
#include "loader/symbolDatabase.h"
#include <array>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace Libs::LibKernelApr {
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
}
namespace {
namespace FileSystem = Libs::LibKernel::FileSystem;
void Check(bool value, const char *text) {
if (!value) {
std::fprintf(stderr, "KernelFileSystemTests: failed: %s\n", text);
std::abort();
}
}
class TempDirectory {
public:
TempDirectory() {
const auto unique =
std::chrono::steady_clock::now().time_since_epoch().count();
m_path = std::filesystem::temp_directory_path() /
("kyty_kernel_file_system_" + std::to_string(unique));
Check(std::filesystem::create_directories(m_path),
"create temporary directory");
}
~TempDirectory() {
std::error_code error;
std::filesystem::remove_all(m_path, error);
}
[[nodiscard]] const std::filesystem::path &Path() const { return m_path; }
KYTY_CLASS_NO_COPY(TempDirectory);
private:
std::filesystem::path m_path;
};
void CheckSaveRename(const std::filesystem::path &root,
std::string_view payload) {
constexpr char Source[] = "/savedata0/STEMP000.DAT";
constexpr char Target[] = "/savedata0/SDATA000.DAT";
constexpr char Suffix[] = "-after-rename";
const int fd = FileSystem::KernelOpen(Source, 0x601, 0777);
Check(fd >= 3, "open temporary save file");
Check(FileSystem::KernelWrite(fd, payload.data(), payload.size()) ==
payload.size(),
"write save payload");
Check(FileSystem::KernelRename(Source, Target) == OK,
"rename open save file");
Check(FileSystem::KernelWrite(fd, Suffix, sizeof(Suffix) - 1) ==
sizeof(Suffix) - 1,
"write through renamed descriptor");
Check(FileSystem::KernelClose(fd) == OK, "close renamed descriptor");
Common::File result(root / "SDATA000.DAT", Common::File::Mode::Read);
Check(!result.IsInvalid(), "open renamed save file");
const auto data = result.ReadWholeBuffer();
const std::string expected = std::string(payload) + Suffix;
Check(data.Size() == expected.size(), "renamed save size");
Check(std::memcmp(data.GetData(), expected.data(), expected.size()) == 0,
"renamed save contents");
}
void CheckMountRoot(const std::filesystem::path &root) {
Common::File cache;
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
cache.Close();
FileSystem::Mount(root, "/app0");
Check(FileSystem::GetRealFilename("/app0/rpf.cache") == root / "rpf.cache",
"resolve mount descendant");
Check(FileSystem::GetRealFilename("/app01/rpf.cache") == "/app01/rpf.cache",
"mount prefix must end at a path component");
for (const char *path : {"/app0", "/app0/"}) {
for (const int flags : {0, 0x00020000}) {
const int fd = FileSystem::KernelOpen(path, flags, 0);
Check(fd >= 3, "open mounted root with O_RDONLY or O_DIRECTORY");
std::array<char, 512> entries {};
const int size = FileSystem::KernelGetdents(fd, entries.data(), entries.size());
Check(size > 0 && size <= entries.size(), "enumerate mounted root");
bool found = false;
for (int offset = 0; offset < size;) {
// Directory record: inode, record length, type, name length, name.
Check(size - offset >= 8, "directory record header fits");
uint16_t length = 0;
std::memcpy(&length, entries.data() + offset + 4, sizeof(length));
const auto name_length = static_cast<uint8_t>(entries[offset + 7]);
Check(length >= 8 + name_length + 1 && length <= size - offset,
"directory record and name fit");
if (std::string_view(entries.data() + offset + 8, name_length) == "rpf.cache") {
Check(entries[offset + 6] == 8, "cache directory entry is a regular file");
found = true;
}
offset += length;
}
Check(found, "mounted root listing contains rpf.cache");
Check(FileSystem::KernelClose(fd) == OK, "close mounted root");
}
}
FileSystem::Umount("/app0");
}
void CheckDirectoryStream(const std::filesystem::path &root) {
const auto directory = root / "directory-stream";
Check(std::filesystem::create_directory(directory),
"create seek fixture directory");
for (int i = 0; i < 48; ++i) {
Common::File fixture;
Check(fixture.Create(directory / ("directory-entry-" + std::to_string(i))),
"create enough entries to cross directory blocks");
fixture.Close();
}
FileSystem::Mount(directory, "/app0");
const int fd = FileSystem::KernelOpen("/app0/", 0, 0);
Check(fd >= 3, "open directory as read-only asset");
const auto end = FileSystem::KernelLseek(fd, 0, 2);
Check(end > 512 && end % 512 == 0,
"directory SEEK_END uses padded stream size");
FileSystem::FileStat stat{};
Check(FileSystem::KernelFstat(fd, &stat) == OK && stat.st_size == end &&
stat.st_blksize == 512,
"directory stat agrees with seek and enumeration");
Check(FileSystem::KernelStat("/app0/", &stat) == OK && stat.st_size == end &&
stat.st_blocks == end / 512,
"path and descriptor directory stat agree");
Check(FileSystem::KernelLseek(fd, 0, 0) == 0,
"rewind directory for asset read");
std::vector<char> raw(static_cast<size_t>(end));
Check(FileSystem::KernelRead(fd, raw.data(), raw.size()) == end &&
FileSystem::KernelRead(fd, raw.data(), 1) == 0,
"raw directory read reaches EOF");
Check(FileSystem::KernelLseek(fd, -end, 1) == 0,
"directory SEEK_CUR uses the position advanced by read");
std::array<char, 512> block{};
int64_t base = -1;
for (int64_t offset = 0; offset < end; offset += block.size()) {
Check(FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
&base) == block.size() &&
base == offset &&
std::memcmp(block.data(), raw.data() + offset, block.size()) == 0,
"directory enumeration shares raw bytes and reports each block "
"position");
for (size_t pos = 0; pos < block.size();) {
Check(block.size() - pos >= 8, "directory record header fits");
uint16_t length = 0;
std::memcpy(&length, block.data() + pos + 4, sizeof(length));
const auto name_length = static_cast<uint8_t>(block[pos + 7]);
Check(length >= 9 + name_length && length <= block.size() - pos &&
block[pos + 8 + name_length] == '\0',
"directory entries remain complete within every block");
pos += length;
}
}
Check(FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
&base) == 0 &&
base == end,
"directory enumeration reports EOF position");
Check(FileSystem::KernelLseek(fd, 512, 0) == 512 &&
FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
&base) == block.size() &&
base == 512 &&
std::memcmp(block.data(), raw.data() + 512, block.size()) == 0,
"restore and reread a directory enumeration position");
const auto position = FileSystem::KernelLseek(fd, 0, 1);
Check(
FileSystem::KernelLseek(fd, 0, 9) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
FileSystem::KernelLseek(fd, -1, 0) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
FileSystem::KernelLseek(fd, std::numeric_limits<int64_t>::min(), 1) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
FileSystem::KernelLseek(fd, std::numeric_limits<int64_t>::max(), 1) ==
Libs::LibKernel::KERNEL_ERROR_EOVERFLOW &&
FileSystem::KernelLseek(fd, 0, 1) == position,
"invalid and overflowing directory seeks preserve the position");
Check(FileSystem::KernelLseek(fd, -19, 2) == end - 19 &&
FileSystem::KernelRead(fd, block.data(), block.size()) == 19 &&
std::memcmp(block.data(), raw.data() + end - 19, 19) == 0,
"raw directory reads support byte positions and stop at EOF");
Check(FileSystem::KernelLseek(fd, 1, 0) == 1 &&
FileSystem::KernelGetdents(fd, block.data(), block.size()) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
FileSystem::KernelLseek(fd, 0, 1) == 1,
"directory enumeration rejects an incomplete record position");
Check(FileSystem::KernelLseek(fd, 512, 2) == end + 512 &&
FileSystem::KernelGetdirentries(fd, block.data(), block.size(),
&base) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
FileSystem::KernelLseek(fd, 0, 1) == end + 512,
"directory enumeration rejects a position beyond EOF");
Check(FileSystem::KernelRead(
fd, block.data(),
static_cast<size_t>(std::numeric_limits<int>::max()) + 1) ==
Libs::LibKernel::KERNEL_ERROR_EINVAL &&
FileSystem::KernelLseek(fd, 0, 1) == end + 512,
"oversized read fails without changing the directory position");
Check(FileSystem::KernelClose(fd) == OK, "close directory stream");
Check(FileSystem::KernelLseek(fd, 0, 0) ==
Libs::LibKernel::KERNEL_ERROR_EBADF,
"seek rejects a closed descriptor");
FileSystem::Umount("/app0");
}
void CheckAprPaths(const std::filesystem::path &root) {
Loader::SymbolDatabase symbols;
Libs::LibKernelApr::InitLibKernel_1_Apr(&symbols);
const auto *resolve_symbol = symbols.FindByNid("w5fcCG+t31g", Loader::SymbolType::Func);
const auto *each_symbol = symbols.FindByNid("C+Khtbbx2g8", Loader::SymbolType::Func);
Check(resolve_symbol && each_symbol, "APR path exports are registered");
using Resolve = int (KYTY_SYSV_ABI *)(const char *, const char *const *, uint32_t,
uint32_t *, uint64_t *, uint32_t *);
using ResolveEach = int (KYTY_SYSV_ABI *)(const char *, const char *const *, uint32_t,
uint32_t *, uint64_t *, int *);
const auto resolve = reinterpret_cast<Resolve>(resolve_symbol->vaddr);
const auto resolve_each = reinterpret_cast<ResolveEach>(each_symbol->vaddr);
Common::File fixture;
Check(fixture.Create(root / "apr.dat"), "create APR fixture");
fixture.Write("APR", 3);
fixture.Close();
FileSystem::Mount(root, "/app0");
uint32_t expected_id = 0xffffffffu;
for (const auto &parts : {std::array{"", "/app0/apr.dat"},
std::array{"/app0/", "apr.dat"},
std::array{"/", "app0/apr.dat"},
std::array{"/app", "0/apr.dat"}}) {
uint32_t id = 0xffffffffu, error_index = 0xffffffffu;
uint64_t size = 0;
Check(resolve(parts[0], &parts[1], 1, &id, &size, &error_index) == OK &&
id != 0xffffffffu && size == 3,
"APR concatenates empty, one-character and partial-component prefixes");
if (expected_id == 0xffffffffu) {
expected_id = id;
}
Check(id == expected_id, "equivalent APR paths return the same ID");
}
const char *paths[] = {"/app0/missing.dat", "/app0/apr.dat"};
uint32_t ids[2] = {}, error_index = 0xffffffffu;
uint64_t sizes[2] = {1, 1};
int results[2] = {};
Check(resolve_each("", paths, 2, ids, sizes, results) == 1 &&
results[0] == Libs::LibKernel::KERNEL_ERROR_ENOENT && results[1] == OK &&
ids[0] == 0xffffffffu && ids[1] == expected_id && sizes[0] == 0 && sizes[1] == 3,
"APR foreach reports a missing path and continues to the valid file");
// PATH_MAX includes NUL; all components remain below NAME_MAX (255).
std::string longest = "/app0/";
for (int i = 0; i < 3; ++i) {
longest += std::string(254, 'a') + '/';
}
longest += std::string(1023 - longest.size(), 'b');
paths[0] = longest.c_str();
Check(resolve("", paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENOENT && error_index == 0,
"APR accepts a pathname whose final NUL is at PATH_MAX minus one");
Check(resolve("/", paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects concatenated paths exceeding PATH_MAX");
std::array<char, 1024> unterminated;
unterminated.fill('/');
paths[0] = unterminated.data();
Check(resolve("", paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects an unterminated pathname");
paths[0] = "apr.dat";
Check(resolve(unterminated.data(), paths, 1, ids, sizes, &error_index) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_ENAMETOOLONG,
"APR rejects an unterminated prefix");
FileSystem::Umount("/app0");
}
void CheckSocketWakeup() {
namespace Net = Libs::Network::Net;
// Guest sockaddr_in: length, family, network-order port/address, padding.
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
const int listener = Net::Socket(2, 1, 0);
Check(listener >= 0, "create loopback listener");
Check(Net::Bind(listener, address.data(), address.size()) == 0, "bind loopback");
Check(Net::Listen(listener, 1) == 0, "listen on loopback");
uint32_t address_size = address.size();
Check(Net::Getsockname(listener, address.data(), &address_size) == 0,
"get assigned loopback port");
const int writer = Net::Socket(2, 1, 0);
Check(writer >= 0 && Net::Connect(writer, address.data(), address_size) == 0,
"connect wake socket");
const int reader = Net::Accept(listener, nullptr, nullptr);
Check(reader >= 0, "accept wake socket");
Check(Net::SocketClose(listener) == 0, "close listener");
const int enabled = 1;
Check(Net::Setsockopt(writer, 6, 1, &enabled, sizeof(enabled)) == 0,
"enable TCP_NODELAY");
int socket_error = -1;
uint32_t error_size = sizeof(socket_error);
*Libs::Posix::GetErrorAddr() = Libs::Posix::POSIX_EINVAL;
Check(Net::Getsockopt(writer, 0xffff, 0x1007, &socket_error, &error_size) == 0 &&
socket_error == 0 && error_size == sizeof(socket_error) &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EINVAL,
"SO_ERROR reports socket status without changing guest errno");
std::array<uint64_t, 16> readable {};
const auto bit = uint64_t {1} << (reader % 64);
readable[reader / 64] = bit;
const std::array<int64_t, 2> immediate {0, 0};
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
immediate.data()) == 0 && readable[reader / 64] == 0,
"empty socket is not readable");
const char payload[] = "wake";
Check(Net::Send(writer, payload, sizeof(payload), 0x20000) == sizeof(payload),
"send wake bytes with guest MSG_NOSIGNAL");
readable[reader / 64] = bit;
const std::array<int64_t, 2> deadline {1, 0};
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
deadline.data()) == 1 && readable[reader / 64] == bit,
"select reports the guest descriptor after wake");
std::array<char, sizeof(payload)> received {};
Check(Net::Recv(reader, received.data(), received.size(), 0x42) == sizeof(payload) &&
std::memcmp(received.data(), payload, sizeof(payload)) == 0,
"guest PEEK and WAITALL preserve the wake bytes");
Check(Net::Recv(reader, received.data(), received.size(), 0x40) == sizeof(payload),
"consume wake bytes with guest WAITALL");
#if !defined(_WIN32)
Check(Net::Recv(reader, received.data(), received.size(), 0x80) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
"empty nonblocking receive translates guest errno");
#endif
Check(Net::SocketClose(reader) == 0 && Net::SocketClose(writer) == 0,
"close wake sockets");
readable[reader / 64] = bit;
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
immediate.data()) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EBADF &&
readable[reader / 64] == bit,
"closed descriptor fails without clearing input fd_set");
}
} // namespace
int main() {
Common::InitializeThreads();
Common::Subsystems subsystems;
subsystems.Initialize<Config::Lifecycle>();
Config::ConfigOptions options;
options.printf_direction = Config::LogDirection::Silent;
Config::Load(options);
subsystems.Initialize<Log::Lifecycle>();
Check(SDL_InitSubSystem(SDL_INIT_VIDEO) == 0, "initialize Vulkan test video");
auto graphics = std::make_unique<Libs::Graphics::WindowContext>();
graphics->graphic_ctx.screen_width = 64;
graphics->graphic_ctx.screen_height = 64;
graphics->window = SDL_CreateWindow("KernelFileSystemTests", 0, 0, 64, 64,
SDL_WINDOW_VULKAN | SDL_WINDOW_HIDDEN);
Check(graphics->window != nullptr, "create hidden Vulkan test window");
graphics->CreateVulkan();
TempDirectory temporary;
FileSystem::Initialize();
CheckMountRoot(temporary.Path());
CheckDirectoryStream(temporary.Path());
CheckAprPaths(temporary.Path());
FileSystem::Mount(temporary.Path(), "/savedata0");
CheckSaveRename(temporary.Path(), "first-save");
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
CheckSocketWakeup();
graphics.reset();
subsystems.Destroy();
std::printf("KernelFileSystemTests: all cases passed\n");
return 0;
}