diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index be7cad24d9..20ec69a1e9 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include #include @@ -49,7 +50,7 @@ bool IsTemporaryMountPoint(const AppContentMountPoint* mountPoint) { } static void ClearDirectory(const std::filesystem::path& directory) { - for (const auto& entry : std::filesystem::directory_iterator(directory)) std::filesystem::remove_all(entry.path()); + for (const auto& entry : std::filesystem::directory_iterator(directory)) RemoveTree(entry.path()); RecordWrittenPath_nid_no_patch(directory); } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 925cabeb13..b5d52e5013 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -340,7 +341,7 @@ static int deleteSave(const SaveDataDelete* del) { std::lock_guard lock(g_slots_mutex); if (isMountedDirectory(path)) return SAVE_DATA_ERROR_BUSY; if (std::filesystem::is_directory(path)) { - std::filesystem::remove_all(path); + RemoveTree(path); } std::error_code ec; std::filesystem::remove(param_path(path), ec); diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp index 4705d8d62e..58afdfa721 100644 --- a/core/libs/prx/libSceSaveData/SaveDataCore.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -22,6 +22,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" namespace savedata { @@ -498,14 +499,14 @@ int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, created = true; if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } SaveDataParam initial {}; std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } } @@ -574,7 +575,7 @@ int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, if (!std::filesystem::exists(directory, error) || error) { return SD_ERROR_NOT_FOUND; } - std::filesystem::remove_all(directory, error); + RemoveTree(directory, error); RecordWrittenPath_nid_no_patch(directory); if (error) { return SD_ERROR_INTERNAL; diff --git a/core/libs/prx/libc/include/RemoveTree.hpp b/core/libs/prx/libc/include/RemoveTree.hpp new file mode 100644 index 0000000000..ec5429a0aa --- /dev/null +++ b/core/libs/prx/libc/include/RemoveTree.hpp @@ -0,0 +1,62 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP + +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +inline void RemoveTree(const std::filesystem::path& target, std::error_code& error) { + error.clear(); +#ifdef _WIN32 + const std::wstring wide = target.wstring(); + const DWORD attributes = GetFileAttributesW(wide.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + const DWORD missing = GetLastError(); + if (missing != ERROR_FILE_NOT_FOUND && missing != ERROR_PATH_NOT_FOUND) + error = std::error_code(static_cast(missing), std::system_category()); + return; + } + const bool directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + if (directory && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { + std::error_code listing; + { + std::filesystem::directory_iterator entries(target, listing); + while (!listing && entries != std::filesystem::directory_iterator()) { + RemoveTree(entries->path(), error); + if (error) return; + entries.increment(listing); + } + } + if (listing) { + error = listing; + return; + } + } + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + const DWORD failed = GetLastError(); + SetFileAttributesW(wide.c_str(), FILE_ATTRIBUTE_NORMAL); + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + error = std::error_code(static_cast(failed), std::system_category()); +#else + std::error_code removal; + std::filesystem::remove_all(target, removal); + if (removal) error = removal; +#endif +} + +inline void RemoveTree(const std::filesystem::path& target) { + std::error_code error; + RemoveTree(target, error); + if (error) throw std::filesystem::filesystem_error("RemoveTree", target, error); +} + +#endif diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a00be4c9e3..803b105272 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "SceTypes.hpp" #include @@ -658,16 +659,6 @@ int main() { Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } #endif -#ifdef _WIN32 - // std::filesystem::remove_all loops forever when the tree contains a - // dangling symbolic link: fs::remove reports such a link as missing and - // clears the error, so recursive_directory_iterator::__erase keeps - // retrying it. The fixture always contains dangling links here, so - // remove it through the shell instead. - const auto cleanup = L"cmd /c rd /s /q \"" + root.wstring() + L"\""; - Require(_wsystem(cleanup.c_str()) == 0); -#else - std::filesystem::remove_all(root); -#endif + RemoveTree(root); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff94841f67..886ded4f9f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -84,6 +84,7 @@ - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way +- [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.