Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,13 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
if(WIN32)
add_test_executable(guest_windows_directory_link_types_tests ${LIBS_INCLUDE_DIR}/tests/GuestWindowsDirectoryLinkTypes.cpp)
target_include_directories(guest_windows_directory_link_types_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_windows_directory_link_types_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_windows_directory_link_types_tests)
add_test(NAME guest_windows_directory_link_types COMMAND guest_windows_directory_link_types_tests)
set_tests_properties(guest_windows_directory_link_types PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
add_test_executable(guest_directory_buffer_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryBuffer.cpp)
target_include_directories(guest_directory_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_directory_buffer_tests PRIVATE libkernel libc)
Expand Down
61 changes: 56 additions & 5 deletions core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include "prx/libc/include/FileStream.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/File/include/NativeStat.hpp"

#include <cerrno>
#include <cstdint>
Expand All @@ -30,8 +31,10 @@

namespace {

constexpr std::uint8_t GuestUnknownType = 0;
constexpr std::uint8_t GuestDirectoryType = 4;
constexpr std::uint8_t GuestRegularType = 8;
constexpr std::uint8_t GuestSymbolicLinkType = 10;

struct DirectoryState {
std::mutex mutex;
Expand All @@ -50,18 +53,66 @@ std::shared_ptr<DirectoryState> Find(int fd) {
return found == g_directories.end() ? nullptr : found->second;
}

bool InsideApp0(const std::filesystem::path& directory) {
const auto root = ResolveDiskPath_nid_no_patch("/app0");
const auto handle = CreateFileW(root.c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (handle == INVALID_HANDLE_VALUE) return false;
const int descriptor = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), _O_RDONLY | _O_BINARY);
if (descriptor < 0) {
CloseHandle(handle);
return false;
}
const auto app0 = File::WindowsDescriptorPath_nid_no_patch(descriptor);
::_close(descriptor);
if (!app0) return false;
auto entry = directory.begin();
for (const auto& component : *app0) {
if (entry == directory.end()) return false;
const auto& left = entry->native();
const auto& right = component.native();
if (CompareStringOrdinal(left.c_str(), static_cast<int>(left.size()),
right.c_str(), static_cast<int>(right.size()), TRUE) != CSTR_EQUAL) return false;
++entry;
}
return true;
}

std::uint8_t EntryType(const std::filesystem::path& path, bool followLinks) {
FileStat status{};
if (!File::FillLinkStat(path, &status))
return GuestUnknownType;
if (followLinks && (status.st_mode & 0170000) == 0120000) {
FileStat target{};
const int savedErrno = errno;
const bool found = File::TryFillFileStat(path, &target);
errno = savedErrno;
if (found && ((target.st_mode & 0170000) == 0040000 || (target.st_mode & 0170000) == 0100000))
status = target;
}
switch (status.st_mode & 0170000) {
case 0040000: return GuestDirectoryType;
case 0100000: return GuestRegularType;
case 0120000: return GuestSymbolicLinkType;
default: NotImplemented_nid_no_patch("getdents of a Windows file type other than a directory, regular file or symbolic link");
}
}

void Load(DirectoryState& state, int fd) {
if (state.loaded) return;
const auto path = File::WindowsDescriptorPath_nid_no_patch(fd);
if (!path) throw std::system_error(errno, std::generic_category(), "directory descriptor path");
state.loaded = true;
state.entries.emplace_back(".", GuestDirectoryType);
state.entries.emplace_back("..", GuestDirectoryType);
const int savedErrno = errno;
const bool followLinks = InsideApp0(*path);
errno = savedErrno;
std::vector<std::pair<std::string, std::uint8_t>> entries{{".", GuestDirectoryType}, {"..", GuestDirectoryType}};
std::error_code error;
for (const auto& entry : std::filesystem::directory_iterator(*path, error)) {
std::error_code typeError;
state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType);
entries.emplace_back(entry.path().filename().string(), EntryType(entry.path(), followLinks));
}
state.entries = std::move(entries);
state.loaded = true;
}

int OpenStreamDirectory(const wchar_t* path) {
Expand Down
197 changes: 197 additions & 0 deletions core/libs/tests/GuestWindowsDirectoryLinkTypes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/File/include/FileFlags.hpp"
#include "prx/libkernel/File/include/File.hpp"
#include "prx/libkernel/include/Api.hpp"
#include <windows.h>
#include <winioctl.h>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <string>
#include <vector>

static void Check(bool value, int line) {
if (!value) {
std::fprintf(stderr, "Directory link type check failed at line %d\n", line);
std::exit(1);
}
}

#define Require(value) Check((value), __LINE__)

static void Junction(const std::filesystem::path& link, const std::filesystem::path& target) {
Require(std::filesystem::create_directory(link));
const auto printName = std::filesystem::absolute(target).make_preferred().wstring();
const auto substituteName = L"\\??\\" + printName;
struct MountPoint {
ULONG tag;
USHORT dataLength;
USHORT reserved;
USHORT substituteOffset;
USHORT substituteLength;
USHORT printOffset;
USHORT printLength;
wchar_t names[2048];
} data{};
Require(substituteName.size() + printName.size() + 2 <= std::size(data.names));
data.tag = IO_REPARSE_TAG_MOUNT_POINT;
data.substituteLength = static_cast<USHORT>(substituteName.size() * sizeof(wchar_t));
data.printOffset = static_cast<USHORT>((substituteName.size() + 1) * sizeof(wchar_t));
data.printLength = static_cast<USHORT>(printName.size() * sizeof(wchar_t));
data.dataLength = static_cast<USHORT>(8 + data.printOffset + data.printLength + sizeof(wchar_t));
std::memcpy(data.names, substituteName.c_str(), data.substituteLength + sizeof(wchar_t));
std::memcpy(reinterpret_cast<char*>(data.names) + data.printOffset, printName.c_str(), data.printLength + sizeof(wchar_t));
const auto handle = CreateFileW(link.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr);
Require(handle != INVALID_HANDLE_VALUE);
DWORD returned = 0;
const auto result = DeviceIoControl(handle, FSCTL_SET_REPARSE_POINT, &data, 8 + data.dataLength, nullptr, 0, &returned, nullptr);
Require(CloseHandle(handle) != 0);
Require(result != 0);
}

static void OutsideApp0() {
const auto root = std::filesystem::absolute("anyps5-directory-links-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directory(root));
Require(std::filesystem::create_directory(root / "directory"));
{ std::ofstream file(root / "regular"); file << "data"; }
std::vector<std::string> links{"junction", "dangling-junction", "loop-junction"};
Junction(root / links[0], root / "directory");
Junction(root / links[1], root / "missing");
Junction(root / links[2], root / links[2]);
if (CreateSymbolicLinkW((root / "file-link").c_str(), L"regular", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) {
links.push_back("file-link");
Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"directory",
SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0);
links.push_back("directory-link");
Require(CreateSymbolicLinkW((root / "dangling-link").c_str(), L"missing", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0);
links.push_back("dangling-link");
} else {
Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD);
std::fprintf(stderr, "Symbolic-link cases require Windows developer mode or symbolic-link privilege; junction cases still run\n");
}
const int descriptor = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0);
Require(descriptor >= 0);
std::map<std::string, std::uint8_t> entries;
std::array<char, 64> buffer{};
for (;;) {
const int bytes = sceKernelGetdents(descriptor, buffer.data(), static_cast<int>(buffer.size()));
Require(bytes >= 0 && bytes <= static_cast<int>(buffer.size()));
if (bytes == 0) break;
for (int offset = 0; offset < bytes;) {
std::uint16_t length = 0;
std::memcpy(&length, buffer.data() + offset + 4, sizeof(length));
const auto nameLength = static_cast<std::uint8_t>(buffer[offset + 7]);
Require(length >= 8 + nameLength + 1 && offset + length <= bytes);
const std::string name(buffer.data() + offset + 8, nameLength);
Require(entries.emplace(name, static_cast<std::uint8_t>(buffer[offset + 6])).second);
offset += length;
}
}
Require(sceKernelClose(descriptor) == 0);
Require(entries.size() == links.size() + 4);
Require(entries.at(".") == 4 && entries.at("..") == 4);
Require(entries.at("directory") == 4 && entries.at("regular") == 8);
for (const auto& name : links) {
if (entries.at(name) != 10)
std::fprintf(stderr, "%s: getdents type %u, expected DT_LNK (10)\n", name.c_str(), entries.at(name));
Require(entries.at(name) == 10);
const auto path = root / name;
FileStat status{};
Require(lstat_nid_postfix(path.string().c_str(), &status) == 0);
Require((status.st_mode & 0170000) == 0120000);
}
for (const auto& name : links) Require(RemoveDirectoryW((root / name).c_str()) != 0 || DeleteFileW((root / name).c_str()) != 0);
Require(std::filesystem::remove_all(root) == 3);
}


static std::map<std::string, std::uint8_t> Types(const char* path) {
const int descriptor = sceKernelOpen(path, SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0);
Require(descriptor >= 0);
std::map<std::string, std::uint8_t> entries;
std::array<char, 64> buffer{};
for (;;) {
const int bytes = sceKernelGetdents(descriptor, buffer.data(), static_cast<int>(buffer.size()));
Require(bytes >= 0 && bytes <= static_cast<int>(buffer.size()));
if (bytes == 0) break;
for (int offset = 0; offset < bytes;) {
std::uint16_t length = 0;
std::memcpy(&length, buffer.data() + offset + 4, sizeof(length));
const auto nameLength = static_cast<std::uint8_t>(buffer[offset + 7]);
Require(length >= 8 + nameLength + 1 && offset + length <= bytes);
Require(entries.emplace(std::string(buffer.data() + offset + 8, nameLength),
static_cast<std::uint8_t>(buffer[offset + 6])).second);
offset += length;
}
}
Require(sceKernelClose(descriptor) == 0);
return entries;
}

int main() {
const auto previous = std::filesystem::current_path();
const auto root = previous / ("anyps5-app0-links-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
Require(std::filesystem::create_directories(root / "app0" / "real"));
Require(std::filesystem::create_directories(root / "external"));
Require(std::filesystem::create_directories(root / "data"));
Require(std::filesystem::create_directories(root / "app0-sibling"));
std::filesystem::current_path(root);
OutsideApp0();
std::vector<std::filesystem::path> links;
const auto junction = [&](const std::filesystem::path& link, const std::filesystem::path& target) {
Junction(root / link, root / target);
links.push_back(link);
};
junction("app0/packed", "external");
junction("app0/chain", "app0/packed");
junction("app0/dangling", "missing");
junction("app0/loop", "app0/loop");
junction("app0/real/nested", "external");
junction("external/nested", "app0/real");
junction("data/saved", "external");
junction("app0-sibling/saved", "external");
{ std::ofstream file(root / "external/regular"); file << "data"; }
const bool symbolic = CreateSymbolicLinkW((root / "app0/file-link").c_str(), L"..\\external\\regular",
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0;
if (symbolic) {
links.push_back("app0/file-link");
Require(CreateSymbolicLinkW((root / "app0/relative").c_str(), L"..\\external",
SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0);
links.push_back("app0/relative");
} else {
Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD);
std::fprintf(stderr, "app0 symbolic-link subcases skipped: privilege unavailable\n");
}
auto app0 = Types("/app0");
std::fprintf(stderr, "app0 packed/chain/real types: %u/%u/%u (expected 4/4/4)\n", app0.at("packed"), app0.at("chain"), app0.at("real"));
Require(app0.at("real") == 4 && app0.at("packed") == 4 && app0.at("chain") == 4);
Require(app0.at("dangling") == 10 && app0.at("loop") == 10);
if (symbolic) Require(app0.at("file-link") == 8 && app0.at("relative") == 4);
Require(Types("/app0/real").at("nested") == 4);
Require(Types("/app0/packed").at("nested") == 10);
Require(Types("/data").at("saved") == 10);
Require(Types((root / "app0-sibling").string().c_str()).at("saved") == 10);
FileStat status{};
Require(lstat_nid_postfix("/app0/packed", &status) == 0 && (status.st_mode & 0170000) == 0120000);
for (auto it = links.rbegin(); it != links.rend(); ++it) Require(RemoveDirectoryW((root / *it).c_str()) != 0 || DeleteFileW((root / *it).c_str()) != 0);
std::filesystem::rename(root / "app0", root / "application");
Junction(root / "app0", root / "application");
Junction(root / "application/packed", root / "external");
Require(Types("/app0").at("packed") == 4);
Require(std::filesystem::remove(root / "application/packed"));
Require(std::filesystem::remove(root / "app0"));
std::filesystem::current_path(previous);
Require(root.parent_path() == previous && root.filename().string().starts_with("anyps5-app0-links-"));
std::filesystem::remove_all(root);
std::puts("Windows directory link type tests passed");
}
Loading
Loading