Files
Jan ŠléglandClaude Fable 5.1 e1f168a24d feat(libc): implement realpath
Resolves a guest path the way FreeBSD 11's realpath does: a relative
path starts from the guest working directory, "." and empty components
are skipped, ".." drops the last component and never climbs above "/",
every other component must exist, and one that is not a directory with
anything after it is ENOTDIR. Backslashes separate components as in the
rest of the path layer. The result is the guest path; the host is
consulted only for existence and type, so host symlinks are followed but
not expanded, and ".." is lexical, as in the rest of the path layer. A
null buffer is PATH_MAX bytes from the application heap, a null path is
EINVAL, an empty one ENOENT, and 1024 bytes or more in the input or the
result is ENAMETOOLONG. On failure the caller's buffer holds the path
that failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq
2026-10-11 01:11:33 +02:00

107 lines
4.6 KiB
C++

#include "prx/libc/include/ApplicationHeap.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
extern "C" {
char* APS5_VABI realpath_nid_postfix(const char*, char*);
int APS5_VABI chdir_nid_postfix(const char*);
void APS5_VABI free_nid_postfix(void*);
int* APS5_VABI __error_nid_postfix();
}
static void Require(bool value) { if (!value) std::abort(); }
static bool Resolves(const std::string& path, const std::string& expected) {
char out[1024];
std::memset(out, 'x', sizeof(out));
return realpath_nid_postfix(path.c_str(), out) == out && expected == out;
}
static bool Fails(const char* path, int error, const std::string& trouble = {}) {
char out[1024];
std::memset(out, 'x', sizeof(out));
*__error_nid_postfix() = 0;
if (realpath_nid_postfix(path, out) != nullptr || *__error_nid_postfix() != error) return false;
return trouble.empty() || trouble == out;
}
int main() {
std::array<void*, 10> api{};
ApplicationHeapRegister_nid_no_patch(api.data());
const auto host = std::filesystem::canonical(std::filesystem::current_path());
const auto name = "anyps5-realpath-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const auto directory = host / name;
Require(std::filesystem::create_directories(directory / "sub"));
{ std::ofstream file(directory / "file.txt"); file << "file"; }
{ std::ofstream file(directory / "sub" / "inner.txt"); file << "inner"; }
const std::string guest = "/" + name;
Require(Resolves("/", "/"));
Require(Resolves(guest, guest));
Require(Resolves(guest + "/", guest));
Require(Resolves(guest + "/sub/../file.txt", guest + "/file.txt"));
Require(Resolves(guest + "/./sub//inner.txt", guest + "/sub/inner.txt"));
Require(Resolves("/../.." + guest + "/sub/", guest + "/sub"));
Require(Resolves(guest + "/sub/..", guest));
Require(Resolves("/..", "/"));
Require(Resolves(guest + "\\sub\\..\\file.txt", guest + "/file.txt"));
Require(Resolves("\\" + name + "\\sub\\", guest + "/sub"));
std::string upper = guest;
for (auto& character : upper) character = static_cast<char>(std::toupper(static_cast<unsigned char>(character)));
Require(Resolves(upper + "/file.txt", upper + "/file.txt"));
Require(chdir_nid_postfix(guest.c_str()) == 0);
Require(Resolves("sub/inner.txt", guest + "/sub/inner.txt"));
Require(Resolves("../" + name + "/file.txt", guest + "/file.txt"));
Require(Resolves(".", guest));
Require(Resolves("sub/./../sub", guest + "/sub"));
Require(Resolves("..", "/"));
Require(chdir_nid_postfix("/") == 0);
AddPathAlias_nid_no_patch("realpath-alias", directory.string().c_str());
Require(Resolves("/realpath-alias/sub/../file.txt", "/realpath-alias/file.txt"));
Require(Fails("/realpath-alias/missing", 2, "/realpath-alias/missing"));
RemovePathAlias_nid_no_patch("realpath-alias");
BlockPathAlias_nid_no_patch("realpath-blocked");
Require(Fails("/realpath-blocked/x", 2, "/realpath-blocked"));
char* allocated = realpath_nid_postfix((guest + "/file.txt").c_str(), nullptr);
Require(allocated != nullptr && guest + "/file.txt" == allocated);
free_nid_postfix(allocated);
Require(Fails(nullptr, 22));
Require(Fails("", 2));
Require(Fails((guest + "/missing").c_str(), 2, guest + "/missing"));
Require(Fails((guest + "/missing/../file.txt").c_str(), 2, guest + "/missing"));
Require(Fails((guest + "/file.txt/").c_str(), 20, guest + "/file.txt"));
Require(Fails((guest + "/file.txt/x").c_str(), 20, guest + "/file.txt"));
Require(Fails((guest + "/file.txt/.").c_str(), 20));
Require(Fails((guest + "/file.txt/..").c_str(), 20));
const std::string overflowing = "/" + std::string(1023, 'a');
Require(Fails(overflowing.c_str(), 63, overflowing.substr(0, 1023)));
const std::string longInput = guest + "/sub/" + std::string(1024, '.') + "/inner.txt";
Require(Fails(longInput.c_str(), 63));
std::string boundary = guest;
while (boundary.size() + 8 <= 1023) boundary += "/missing";
boundary += "/" + std::string(1023 - boundary.size() - 1, 'm');
Require(boundary.size() == 1023);
Require(Fails(boundary.c_str(), 2, guest + "/missing"));
*__error_nid_postfix() = 0;
Require(realpath_nid_postfix((guest + "/missing").c_str(), nullptr) == nullptr && *__error_nid_postfix() == 2);
std::filesystem::remove_all(directory);
return 0;
}