Merge pull request #4653 from Lorkydey/feat/libc-statvfs

feat(libc): query filesystem statistics by guest path
This commit is contained in:
Adrià 2026-10-12 23:08:17 +02:00 committed by GitHub
commit 5efd09afc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 292 additions and 2 deletions

View File

@ -3,9 +3,19 @@ target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS})
target_sources(libSceLibcInternal PRIVATE Directory.cpp)
configure_windows_unwind(libSceLibcInternal)
target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp)
target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp)
if(BUILD_TESTING)
add_test_executable(guest_statvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestStatvfs.cpp)
target_include_directories(guest_statvfs_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_statvfs_tests PRIVATE libSceLibcInternal libc)
configure_windows_unwind(guest_statvfs_tests)
add_test(NAME guest_statvfs COMMAND guest_statvfs_tests)
set_tests_properties(guest_statvfs PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_statvfs PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>")
endif()
add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp)
target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR})
configure_windows_unwind(guest_fstatvfs_conversion_tests)

View File

@ -0,0 +1,122 @@
#include "prx/libc/include/FilesystemError.hpp"
#include <cerrno>
#include <cstdint>
#include <fcntl.h>
#include <string_view>
#include <system_error>
#ifdef _WIN32
#include <windows.h>
#include <io.h>
#else
#include <unistd.h>
#endif
struct LibcStatvfs;
extern "C" int APS5_VABI fstatvfs_nid_postfix(int, LibcStatvfs*);
namespace {
int Fail(int error) {
errno = error;
return -1;
}
int HostError(int error) {
switch (error) {
case ENOENT: return 2;
case EINTR: return 4;
case EIO: return 5;
case ENOMEM: return 12;
case EACCES: return 13;
case ENOTDIR: return 20;
case EINVAL: return 22;
case ENFILE: return 23;
case EMFILE: return 24;
case EAGAIN: return 35;
case ELOOP: return 62;
case ENAMETOOLONG: return 63;
default: throw std::system_error(error, std::generic_category(), "statvfs: unsupported host error");
}
}
struct Descriptor {
int value;
~Descriptor() {
const int saved = errno;
#ifdef _WIN32
::_close(value);
#else
::close(value);
#endif
errno = saved;
}
};
int OpenMetadata(const std::filesystem::path& path, bool directory) {
#ifdef _WIN32
const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
const DWORD error = GetLastError();
if (error == ERROR_PATH_NOT_FOUND || error == ERROR_INVALID_NAME || error == ERROR_DIRECTORY) {
for (auto prefix = path.parent_path(); !prefix.empty();) {
const DWORD attributes = GetFileAttributesW(prefix.c_str());
if (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) return Fail(20);
const auto parent = prefix.parent_path();
if (parent == prefix) break;
prefix = parent;
}
}
switch (error) {
case ERROR_FILE_NOT_FOUND:
case ERROR_PATH_NOT_FOUND:
case ERROR_INVALID_DRIVE: return Fail(2);
case ERROR_ACCESS_DENIED:
case ERROR_SHARING_VIOLATION: return Fail(13);
case ERROR_DIRECTORY: return Fail(20);
case ERROR_INVALID_NAME:
case ERROR_INVALID_PARAMETER: return Fail(22);
case ERROR_CANT_RESOLVE_FILENAME: return Fail(62);
case ERROR_FILENAME_EXCED_RANGE: return Fail(63);
default: throw std::system_error(error, std::system_category(), "statvfs: CreateFileW");
}
}
if (directory) {
BY_HANDLE_FILE_INFORMATION info{};
if (!GetFileInformationByHandle(handle, &info)) {
const DWORD error = GetLastError();
CloseHandle(handle);
throw std::system_error(error, std::system_category(), "statvfs: GetFileInformationByHandle");
}
if (!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
CloseHandle(handle);
return Fail(20);
}
}
const int descriptor = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), _O_RDONLY | _O_BINARY | _O_NOINHERIT);
if (descriptor < 0) {
const int error = errno;
CloseHandle(handle);
return Fail(HostError(error));
}
return descriptor;
#else
const int descriptor = ::open(path.c_str(), O_PATH | O_CLOEXEC | (directory ? O_DIRECTORY : 0));
return descriptor < 0 ? Fail(HostError(errno)) : descriptor;
#endif
}
}
extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) {
if (path == nullptr) return Fail(14);
if (*path == '\0') return Fail(2);
const int saved = errno;
const auto last = std::string_view(path).back();
const bool directory = last == '/' || last == '\\' || FinalPathComponentIsDot_nid_no_patch(path);
std::filesystem::path native;
if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error);
const int descriptor = OpenMetadata(native, directory);
if (descriptor < 0) return -1;
const Descriptor owner{descriptor};
errno = saved;
return fstatvfs_nid_postfix(descriptor, output);
}

View File

@ -226,6 +226,7 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb
}
#else
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/time.h>
@ -390,7 +391,7 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) {
constexpr long PipeFilesystem = 0x50495045;
struct stat file{};
if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff;
if (S_ISSOCK(file.st_mode)) return GUEST_EINVAL;
if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL;
if (S_ISFIFO(file.st_mode)) {
struct statfs kind{};
if (::fstatfs(descriptor, &kind) == 0 && static_cast<long>(kind.f_type) == PipeFilesystem) return GUEST_EINVAL;

View File

@ -0,0 +1,157 @@
#include "prx/libc/include/General.hpp"
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>
#endif
extern "C" int APS5_VABI statvfs_nid_postfix(const char*, void*);
extern "C" int* APS5_VABI __error_nid_postfix();
static void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "statvfs check failed at line %d\n", line);
std::abort();
}
}
#define Require(value) Check((value), __LINE__)
static void RequireVolume(const char* path, const std::filesystem::path& native) {
std::array<std::uint64_t, 13> output;
output.fill(0xA5A5A5A5A5A5A5A5ull);
*__error_nid_postfix() = 12345;
Require(statvfs_nid_postfix(path, output.data() + 1) == 0);
Require(*__error_nid_postfix() == 12345);
Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front());
const auto* fields = output.data() + 1;
Require(fields[6] > 0 && fields[8] > 0);
Require(fields[2] == std::filesystem::space(native).capacity / fields[8]);
Require(fields[0] <= fields[1] && fields[1] <= fields[2]);
Require((fields[7] & ~std::uint64_t{3}) == 0);
Require(fields[9] == 0 && fields[10] >= 255);
}
static void RequireError(const char* path, int error) {
std::array<std::uint64_t, 11> output;
output.fill(0x5A5A5A5A5A5A5A5Aull);
const auto saved = output;
*__error_nid_postfix() = 0;
const int result = statvfs_nid_postfix(path, output.data());
if (result != -1)
std::fprintf(stderr, "path %s: expected failure, got %d\n", path ? path : "(null)", result);
Require(result == -1);
if (*__error_nid_postfix() != error)
std::fprintf(stderr, "path %s: expected errno %d, got %d\n", path ? path : "(null)", error, *__error_nid_postfix());
Require(*__error_nid_postfix() == error && output == saved);
}
static std::size_t OpenHandles() {
#ifdef _WIN32
DWORD count = 0;
Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0);
return count;
#else
return static_cast<std::size_t>(std::distance(std::filesystem::directory_iterator("/proc/self/fd"),
std::filesystem::directory_iterator{}));
#endif
}
int main() {
const auto root = std::filesystem::temp_directory_path() /
("anyps5-statvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
const auto file = root / "file.txt";
{
std::ofstream stream(file, std::ios::binary);
stream << "unchanged";
Require(static_cast<bool>(stream));
}
AddPathAlias_nid_no_patch("/anyps5-statvfs-test", root.string().c_str());
RequireVolume("/anyps5-statvfs-test", root);
RequireVolume("/anyps5-statvfs-test/file.txt", file);
RequireVolume("/anyps5-statvfs-test/", root);
RequireError(nullptr, 14);
RequireError("", 2);
RequireError("/anyps5-statvfs-test/missing", 2);
RequireError("/anyps5-statvfs-test/file.txt/child", 20);
RequireError("/anyps5-statvfs-test/file.txt/", 20);
RequireError("/anyps5-statvfs-test/file.txt/.", 20);
RequireError("/anyps5-statvfs-test/file.txt/../file.txt", 20);
RequireError("/anyps5-statvfs-test/missing/../file.txt", 2);
BlockPathAlias_nid_no_patch("/anyps5-statvfs-unavailable");
RequireError("/anyps5-statvfs-unavailable", 2);
AddPathAlias_nid_no_patch("/anyps5-statvfs-unavailable", root.string().c_str());
RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable");
Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1);
Require(*__error_nid_postfix() == 14);
const auto unicodeFile = root / std::filesystem::path(u8"\u65e5.txt");
{
std::ofstream stream(unicodeFile);
stream << "unicode path";
Require(static_cast<bool>(stream));
}
RequireVolume("/anyps5-statvfs-test/\xE6\x97\xA5.txt", unicodeFile);
Require(std::filesystem::remove(unicodeFile));
#ifdef _WIN32
const HANDLE exclusive = CreateFileW(file.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
Require(exclusive != INVALID_HANDLE_VALUE);
RequireVolume("/anyps5-statvfs-test/file.txt", file);
Require(CloseHandle(exclusive) != 0);
#else
Require(::chmod(file.c_str(), 0) == 0);
RequireVolume("/anyps5-statvfs-test/file.txt", file);
Require(::chmod(file.c_str(), 0600) == 0);
const auto fifo = root / "fifo";
Require(::mkfifo(fifo.c_str(), 0600) == 0);
RequireVolume("/anyps5-statvfs-test/fifo", fifo);
const auto socketPath = root / "socket";
const int socket = ::socket(AF_UNIX, SOCK_STREAM, 0);
Require(socket >= 0);
sockaddr_un address{};
address.sun_family = AF_UNIX;
const auto name = socketPath.string();
Require(name.size() < sizeof(address.sun_path));
std::memcpy(address.sun_path, name.c_str(), name.size() + 1);
Require(::bind(socket, reinterpret_cast<const sockaddr*>(&address), sizeof(address)) == 0);
RequireVolume("/anyps5-statvfs-test/socket", socketPath);
Require(::close(socket) == 0);
Require(std::filesystem::remove(socketPath));
Require(std::filesystem::remove(fifo));
std::filesystem::create_symlink(file.filename(), root / "link");
RequireVolume("/anyps5-statvfs-test/link", file);
Require(std::filesystem::remove(root / "link"));
std::filesystem::create_symlink("missing", root / "dangling");
RequireError("/anyps5-statvfs-test/dangling", 2);
Require(std::filesystem::remove(root / "dangling"));
#endif
const auto before = OpenHandles();
for (int i = 0; i != 32; ++i) {
RequireVolume("/anyps5-statvfs-test/file.txt", file);
Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1);
Require(*__error_nid_postfix() == 14);
}
Require(OpenHandles() == before);
{
std::ifstream stream(file, std::ios::binary);
std::string text((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
Require(text == "unchanged");
}
RemovePathAlias_nid_no_patch("/anyps5-statvfs-test");
Require(std::filesystem::remove(file));
Require(std::filesystem::remove(root));
}