mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
Zarchive Loading (#724)
* Simplify archive loading and harden filesystem integration * Restore directory entry name length assertion * Keep graphics-dependent filesystem tests out of CI --------- Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
@@ -115,12 +115,12 @@ jobs:
|
||||
cmake --build _Build/windows --target launcher ^
|
||||
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests --parallel
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
|
||||
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
@@ -203,14 +203,14 @@ jobs:
|
||||
cmake --build _Build/macos \
|
||||
--target launcher audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
@@ -386,14 +386,14 @@ jobs:
|
||||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
|
||||
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
|
||||
Vendored
+43
@@ -28,6 +28,49 @@ FetchContent_Declare(
|
||||
|
||||
FetchContent_MakeAvailable(xbyak zydis)
|
||||
|
||||
# ZArchive provides random-access reads for compressed game dumps. Build only
|
||||
# the reader/writer library and its zstd dependency; the command-line tool is
|
||||
# not part of KytyPS5's runtime distribution.
|
||||
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE)
|
||||
set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
zstd
|
||||
GIT_REPOSITORY https://github.com/facebook/zstd.git
|
||||
GIT_TAG f8745da6ff1ad1e7bab384bd1f9d742439278e99
|
||||
GIT_SHALLOW FALSE
|
||||
SOURCE_SUBDIR build/cmake
|
||||
)
|
||||
FetchContent_Declare(
|
||||
zarchive_source
|
||||
GIT_REPOSITORY https://github.com/Exzap/ZArchive.git
|
||||
GIT_TAG d2c717730092c7bf8cbb033b12fd4001b7c4d932
|
||||
GIT_SHALLOW FALSE
|
||||
PATCH_COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/patches/zarchive-reader.patch"
|
||||
SOURCE_SUBDIR kyty-library-only
|
||||
)
|
||||
FetchContent_MakeAvailable(zstd zarchive_source)
|
||||
|
||||
add_library(zarchive STATIC
|
||||
"${zarchive_source_SOURCE_DIR}/src/zarchivereader.cpp"
|
||||
"${zarchive_source_SOURCE_DIR}/src/zarchivewriter.cpp"
|
||||
"${zarchive_source_SOURCE_DIR}/src/sha_256.c"
|
||||
)
|
||||
add_library(ZArchive::zarchive ALIAS zarchive)
|
||||
target_include_directories(zarchive SYSTEM PUBLIC "${zarchive_source_SOURCE_DIR}/include")
|
||||
target_include_directories(zarchive PRIVATE "${zstd_SOURCE_DIR}/lib")
|
||||
target_link_libraries(zarchive PRIVATE libzstd_static)
|
||||
target_compile_features(zarchive PRIVATE cxx_std_20)
|
||||
if(MSVC)
|
||||
target_compile_options(zarchive PRIVATE /W0)
|
||||
else()
|
||||
target_compile_options(zarchive PRIVATE -w)
|
||||
endif()
|
||||
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
if(APPLE)
|
||||
# SDL's built-in iconv fallback does not support CP932.
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
--- a/include/zarchive/zarchivecommon.h
|
||||
+++ b/include/zarchive/zarchivecommon.h
|
||||
@@ -256,7 +256,7 @@
|
||||
|
||||
bool IsWithinValidRange(uint64_t fileSize) const
|
||||
{
|
||||
- return (offset + size) <= fileSize;
|
||||
+ return offset <= fileSize && size <= fileSize - offset;
|
||||
}
|
||||
};
|
||||
|
||||
--- a/src/zarchivereader.cpp
|
||||
+++ b/src/zarchivereader.cpp
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
static bool _ifstream_readBytes(std::ifstream& file, uint64_t offset, void* buffer, uint32_t size)
|
||||
{
|
||||
+ file.clear();
|
||||
file.seekg(offset, std::ios_base::beg);
|
||||
file.read((char*)buffer, size);
|
||||
return file.gcount() == size;
|
||||
@@ -80,9 +81,27 @@
|
||||
// verify file tree
|
||||
if (fileTree[0].IsFile())
|
||||
return nullptr; // first entry must be root directory
|
||||
- auto rootName = GetName(nameTable, fileTree[0].GetNameOffset());
|
||||
- if (!rootName.empty())
|
||||
+ if (fileTree[0].GetNameOffset() != 0x7FFFFFFF)
|
||||
return nullptr; // root node must not have a name
|
||||
+ const uint64_t dataCapacity = (uint64_t)offsetRecords.size() * _ZARCHIVE::ENTRIES_PER_OFFSETRECORD * _ZARCHIVE::COMPRESSED_BLOCK_SIZE;
|
||||
+ for (size_t i = 0; i < fileTree.size(); ++i)
|
||||
+ {
|
||||
+ auto& entry = fileTree[i];
|
||||
+ if (i != 0)
|
||||
+ {
|
||||
+ auto name = GetName(nameTable, entry.GetNameOffset());
|
||||
+ if (name.empty() || name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos || name.find('\0') != std::string_view::npos)
|
||||
+ return nullptr;
|
||||
+ }
|
||||
+ if (entry.IsFile())
|
||||
+ {
|
||||
+ if (entry.GetFileOffset() > dataCapacity || entry.GetFileSize() > dataCapacity - entry.GetFileOffset())
|
||||
+ return nullptr;
|
||||
+ }
|
||||
+ else if (entry.directoryRecord.nodeStartIndex > fileTree.size() ||
|
||||
+ entry.directoryRecord.count > fileTree.size() - entry.directoryRecord.nodeStartIndex)
|
||||
+ return nullptr;
|
||||
+ }
|
||||
// read meta data
|
||||
// todo
|
||||
|
||||
@@ -238,7 +257,7 @@
|
||||
uint32_t stepSize = std::min<uint32_t>(remainingBytes, _ZARCHIVE::COMPRESSED_BLOCK_SIZE - blockOffset);
|
||||
CacheBlock* block = GetCachedBlock(blockIdx);
|
||||
if (!block)
|
||||
- return 0;
|
||||
+ return bytesToRead - remainingBytes;
|
||||
std::memcpy(bufferU8, block->data + blockOffset, stepSize);
|
||||
rawReadOffset += stepSize;
|
||||
remainingBytes -= stepSize;
|
||||
@@ -325,14 +344,18 @@
|
||||
// determine offset and size of compressed block
|
||||
auto& record = m_offsetRecords[recordIndex];
|
||||
uint64_t offset = record.baseOffset;
|
||||
+ if (offset > m_compressedDataSize)
|
||||
+ return false;
|
||||
for (uint32_t i = 0; i < recordSubIndex; i++)
|
||||
{
|
||||
- offset += (uint64_t)record.size[i];
|
||||
- offset++;
|
||||
+ const uint64_t size = (uint64_t)record.size[i] + 1;
|
||||
+ if (size > m_compressedDataSize - offset)
|
||||
+ return false;
|
||||
+ offset += size;
|
||||
}
|
||||
uint32_t compressedSize = (uint32_t)record.size[recordSubIndex] + 1;
|
||||
// load file data
|
||||
- if ((offset + compressedSize) > m_compressedDataSize)
|
||||
+ if (compressedSize > m_compressedDataSize - offset)
|
||||
return false;
|
||||
offset += m_compressedDataOffset;
|
||||
if (compressedSize == _ZARCHIVE::COMPRESSED_BLOCK_SIZE)
|
||||
@@ -350,7 +373,7 @@
|
||||
// returns empty view on failure
|
||||
std::string_view ZArchiveReader::GetName(const std::vector<uint8_t>& nameTable, uint32_t nameOffset)
|
||||
{
|
||||
- if (nameOffset == 0x7FFFFFFF || nameOffset > nameTable.size())
|
||||
+ if (nameOffset == 0x7FFFFFFF || nameOffset >= nameTable.size())
|
||||
return "";
|
||||
// parse header
|
||||
uint16_t nameLength = nameTable[nameOffset] & 0x7F;
|
||||
@@ -359,7 +382,7 @@
|
||||
// extended 2-byte length
|
||||
if (nameOffset + 1 >= nameTable.size())
|
||||
return "";
|
||||
- nameLength |= ((uint16_t)nameTable[nameOffset] << 7);
|
||||
+ nameLength |= ((uint16_t)nameTable[nameOffset + 1] << 7);
|
||||
nameOffset += 2;
|
||||
}
|
||||
else
|
||||
+16
-1
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16...4.3)
|
||||
cmake_minimum_required(VERSION 3.18...4.3)
|
||||
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "Minimum policy version for bundled dependencies")
|
||||
@@ -407,6 +407,16 @@ add_executable(page_manager_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(page_manager_tests common)
|
||||
target_include_directories(page_manager_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(archive_file_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/ArchiveFileTests.cpp"
|
||||
)
|
||||
target_link_libraries(archive_file_tests common ZArchive::zarchive)
|
||||
target_include_directories(archive_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp")
|
||||
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
|
||||
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(bit_array_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
|
||||
)
|
||||
@@ -514,6 +524,7 @@ target_include_directories(event_queue_lifetime_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_kyty_full_emulator_test(kernel_file_system_tests
|
||||
"${KYTY_TESTS_DIR}/KernelFileSystemTests.cpp")
|
||||
target_link_libraries(kernel_file_system_tests ZArchive::zarchive)
|
||||
|
||||
add_executable(sync_on_address_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/SyncOnAddressTests.cpp"
|
||||
@@ -580,6 +591,8 @@ if(BUILD_TESTING)
|
||||
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
|
||||
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
|
||||
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
|
||||
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
|
||||
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
|
||||
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
|
||||
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
|
||||
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
|
||||
@@ -649,6 +662,8 @@ if(BUILD_TESTING)
|
||||
image_page_table_tests
|
||||
memory_tracker_tests
|
||||
page_manager_tests
|
||||
archive_file_tests
|
||||
avplayer_file_tests
|
||||
bit_array_tests
|
||||
lru_cache_tests
|
||||
shader_vertex_metadata_tests
|
||||
|
||||
@@ -236,9 +236,10 @@ cmake --build _Build/linux --target launcher --parallel
|
||||
cmake --install _Build/linux --prefix _Build/linux/install
|
||||
```
|
||||
|
||||
The configure step downloads the FFmpeg prebuilts and the `xbyak`/`zydis` sources, so it needs
|
||||
network access; a fully sandboxed `nix build` would require vendoring those inputs. A Vulkan 1.3
|
||||
driver must be available at runtime (on NixOS, `hardware.graphics.enable = true`).
|
||||
The configure step downloads the FFmpeg prebuilts and the `xbyak`, `zydis`, `zstd`, and ZArchive
|
||||
sources, so it needs network access; a fully sandboxed `nix build` would require vendoring those
|
||||
inputs. A Vulkan 1.3 driver must be available at runtime (on NixOS,
|
||||
`hardware.graphics.enable = true`).
|
||||
|
||||
### Building on macOS
|
||||
|
||||
@@ -332,17 +333,21 @@ open _Build/macos/install/KytyPS5.app # or double-click in Finder
|
||||
```
|
||||
|
||||
On first launch, add one or more game folders in the global settings. The launcher searches those
|
||||
folders recursively for game directories containing `eboot.bin`. Select a detected game and run it
|
||||
from the game list.
|
||||
folders recursively for game directories containing `eboot.bin` and ZArchive (`.zar`) game dumps
|
||||
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
|
||||
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
|
||||
|
||||
The emulator can also be started directly with a legally obtained game directory or ELF file:
|
||||
The emulator can also be started directly with a legally obtained game directory, ELF file, or
|
||||
ZArchive dump:
|
||||
|
||||
```powershell
|
||||
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame"
|
||||
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame.zar"
|
||||
```
|
||||
|
||||
```bash
|
||||
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame"
|
||||
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame.zar"
|
||||
```
|
||||
|
||||
On macOS, the adjacent flat or app-bundled `libMoltenVK.dylib` is found automatically; no
|
||||
|
||||
@@ -26,6 +26,7 @@ target_link_libraries(common
|
||||
cpuinfo
|
||||
kyty_git_version
|
||||
spdlog::spdlog
|
||||
ZArchive::zarchive
|
||||
)
|
||||
|
||||
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
#include "common/archive.h"
|
||||
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <zarchive/zarchivereader.h>
|
||||
|
||||
// POSIX hosts and MinGW's pthread-backed libstdc++ have cancellable condition waits.
|
||||
// MSVC/ClangCL and MinGW's native Win32 threading backend do not use pthread waits.
|
||||
#if !defined(_WIN32) || defined(_GLIBCXX_GCC_GTHR_POSIX_H)
|
||||
#include <pthread.h>
|
||||
#define KYTY_ARCHIVE_PTHREAD_CANCELLATION
|
||||
#endif
|
||||
|
||||
namespace Common {
|
||||
|
||||
// Backends expose immutable metadata and offset-based reads. Cursor management, path routing,
|
||||
// reader lifetime and the host-stack worker are shared by every archive format.
|
||||
class ArchiveReader {
|
||||
public:
|
||||
struct Entry {
|
||||
uint64_t id;
|
||||
uint64_t size;
|
||||
bool is_file;
|
||||
};
|
||||
|
||||
virtual ~ArchiveReader() = default;
|
||||
virtual std::optional<Entry> Find(std::string_view member) = 0;
|
||||
virtual std::vector<File::DirEntry> List(std::string_view member) = 0;
|
||||
virtual uint64_t Read(uint64_t id, uint64_t offset, uint32_t size, void* data) = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
class ZArchiveReaderBackend final: public ArchiveReader {
|
||||
public:
|
||||
explicit ZArchiveReaderBackend(std::unique_ptr<ZArchiveReader> reader)
|
||||
: m_reader(std::move(reader)) {}
|
||||
|
||||
std::optional<Entry> Find(std::string_view member) override {
|
||||
const auto node = m_reader->LookUp(member);
|
||||
if (node == ZARCHIVE_INVALID_NODE) {
|
||||
return {};
|
||||
}
|
||||
return Entry {node, m_reader->GetFileSize(node), m_reader->IsFile(node)};
|
||||
}
|
||||
|
||||
std::vector<File::DirEntry> List(std::string_view member) override {
|
||||
const auto node = m_reader->LookUp(member);
|
||||
if (!m_reader->IsDirectory(node)) {
|
||||
return {};
|
||||
}
|
||||
const auto count = m_reader->GetDirEntryCount(node);
|
||||
if (count > (1u << 20u)) {
|
||||
return {};
|
||||
}
|
||||
std::vector<File::DirEntry> result;
|
||||
result.reserve(count);
|
||||
for (uint32_t index = 0; index < count; ++index) {
|
||||
ZArchiveReader::DirEntry entry {};
|
||||
if (m_reader->GetDirEntry(node, index, entry)) {
|
||||
result.push_back({std::string(entry.name), entry.isFile});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t Read(uint64_t id, uint64_t offset, uint32_t size, void* data) override {
|
||||
// ZArchiveReader already serializes access to its stream and decompression cache.
|
||||
return m_reader->ReadFromFile(static_cast<ZArchiveNodeHandle>(id), offset, size, data);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<ZArchiveReader> m_reader;
|
||||
};
|
||||
|
||||
std::shared_ptr<ArchiveReader> OpenZArchive(const std::filesystem::path& path) {
|
||||
auto reader = std::unique_ptr<ZArchiveReader>(ZArchiveReader::OpenFromFile(path));
|
||||
return reader ? std::make_shared<ZArchiveReaderBackend>(std::move(reader)) : nullptr;
|
||||
}
|
||||
|
||||
struct ArchiveFormat {
|
||||
std::string_view extension;
|
||||
std::shared_ptr<ArchiveReader> (*open)(const std::filesystem::path&);
|
||||
};
|
||||
|
||||
constexpr ArchiveFormat Formats[] = {{".zar", OpenZArchive}};
|
||||
using NativeView = std::basic_string_view<std::filesystem::path::value_type>;
|
||||
|
||||
const ArchiveFormat* FindFormat(NativeView path) {
|
||||
for (const auto& format: Formats) {
|
||||
if (path.size() < format.extension.size()) {
|
||||
continue;
|
||||
}
|
||||
const auto extension = path.substr(path.size() - format.extension.size());
|
||||
if (std::equal(
|
||||
extension.begin(), extension.end(), format.extension.begin(),
|
||||
[](auto a, char b) { return (a >= 'A' && a <= 'Z' ? a + ('a' - 'A') : a) == b; })) {
|
||||
return &format;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t FindMarker(NativeView path) {
|
||||
// Native files use this check on every operation. The usual path has no marker and allocates
|
||||
// nothing.
|
||||
for (auto marker = path.find('!'); marker != NativeView::npos;
|
||||
marker = path.find('!', marker + 1)) {
|
||||
if ((marker + 1 == path.size() || path[marker + 1] == '/' || path[marker + 1] == '\\') &&
|
||||
FindFormat(path.substr(0, marker)) != nullptr) {
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
return NativeView::npos;
|
||||
}
|
||||
|
||||
struct ParsedPath {
|
||||
std::filesystem::path archive;
|
||||
std::string member;
|
||||
};
|
||||
|
||||
std::optional<ParsedPath> ParsePath(const std::filesystem::path& path) {
|
||||
const auto& native = path.native();
|
||||
const auto marker = FindMarker(native);
|
||||
if (marker == NativeView::npos) {
|
||||
return {};
|
||||
}
|
||||
ParsedPath parsed {.archive = std::filesystem::path(native.substr(0, marker))};
|
||||
auto member = PathToGenericString(std::filesystem::path(native.substr(marker + 1)));
|
||||
std::replace(member.begin(), member.end(), '\\', '/');
|
||||
std::string_view remaining = member;
|
||||
while (!remaining.empty()) {
|
||||
const auto separator = remaining.find('/');
|
||||
const auto component = remaining.substr(0, separator);
|
||||
remaining.remove_prefix(separator == std::string_view::npos ? remaining.size() : separator + 1);
|
||||
if (component.empty() || component == ".") {
|
||||
continue;
|
||||
}
|
||||
if (component == "..") {
|
||||
if (parsed.member.empty()) {
|
||||
return {};
|
||||
}
|
||||
const auto separator = parsed.member.rfind('/');
|
||||
parsed.member.resize(separator == std::string::npos ? 0 : separator);
|
||||
} else {
|
||||
if (!parsed.member.empty()) {
|
||||
parsed.member += '/';
|
||||
}
|
||||
parsed.member += component;
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
std::mutex g_readers_mutex;
|
||||
std::unordered_map<std::filesystem::path, std::weak_ptr<ArchiveReader>> g_readers;
|
||||
|
||||
// Guest stacks can be only 64 KiB. Decompress on an ordinary host stack, then copy into guest
|
||||
// memory on the caller so GPU-tracked writes use its fault handler. A single worker bounds host
|
||||
// threads and avoids duplicating the archive's own block cache. Requests live on waiting callers.
|
||||
class ArchiveIoThread {
|
||||
public:
|
||||
static ArchiveIoThread& Instance() {
|
||||
// Guest exit can run static destructors while other guest threads are still reading.
|
||||
// Keep the worker alive until process termination, as with the guest threads themselves.
|
||||
static auto* instance = new ArchiveIoThread;
|
||||
return *instance;
|
||||
}
|
||||
|
||||
uint64_t Read(ArchiveReader& reader, uint64_t id, uint64_t offset, uint32_t size, void* data) {
|
||||
Request request {reader, id, offset, size, data};
|
||||
std::unique_lock lock(m_mutex);
|
||||
if (m_last != nullptr) {
|
||||
m_last->next = &request;
|
||||
} else {
|
||||
m_first = &request;
|
||||
}
|
||||
m_last = &request;
|
||||
m_wake.notify_one();
|
||||
request.ready.wait(lock, [&] { return request.done; });
|
||||
return request.result;
|
||||
}
|
||||
|
||||
KYTY_CLASS_NO_COPY(ArchiveIoThread);
|
||||
|
||||
private:
|
||||
struct Request {
|
||||
ArchiveReader& reader;
|
||||
uint64_t id;
|
||||
uint64_t offset;
|
||||
uint32_t size;
|
||||
void* data;
|
||||
Request* next = nullptr;
|
||||
uint64_t result = 0;
|
||||
bool done = false;
|
||||
std::condition_variable ready {};
|
||||
};
|
||||
|
||||
ArchiveIoThread() {
|
||||
std::thread([this] { Loop(); }).detach();
|
||||
}
|
||||
|
||||
void Loop() {
|
||||
for (;;) {
|
||||
Request* request;
|
||||
{
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_wake.wait(lock, [this] { return m_first != nullptr; });
|
||||
request = m_first;
|
||||
m_first = request->next;
|
||||
if (m_first == nullptr) {
|
||||
m_last = nullptr;
|
||||
}
|
||||
}
|
||||
const auto result =
|
||||
request->reader.Read(request->id, request->offset, request->size, request->data);
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
request->result = result;
|
||||
request->done = true;
|
||||
// Notify before releasing the mutex: the waiting caller owns the condition
|
||||
// variable.
|
||||
request->ready.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_wake;
|
||||
Request* m_first = nullptr;
|
||||
Request* m_last = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ArchiveFile::Private {
|
||||
std::shared_ptr<ArchiveReader> reader;
|
||||
ArchiveReader::Entry entry;
|
||||
uint64_t position = 0;
|
||||
};
|
||||
|
||||
ArchiveFile::ArchiveFile(std::unique_ptr<Private> p): m_p(std::move(p)) {}
|
||||
ArchiveFile::~ArchiveFile() = default;
|
||||
|
||||
uint64_t ArchiveFile::Size() const {
|
||||
return m_p->entry.size;
|
||||
}
|
||||
uint64_t ArchiveFile::Tell() const {
|
||||
return m_p->position;
|
||||
}
|
||||
bool ArchiveFile::Seek(uint64_t offset) {
|
||||
m_p->position = offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ArchiveFile::Read(void* data, uint32_t size, uint32_t* bytes_read) {
|
||||
#if defined(KYTY_ARCHIVE_PTHREAD_CANCELLATION)
|
||||
// A pending cancellation must not destroy the waiting request or its TLS buffer while the
|
||||
// worker still uses them. Restore the caller's state only after the copy is complete.
|
||||
int cancellation_state = PTHREAD_CANCEL_ENABLE;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancellation_state);
|
||||
#endif
|
||||
constexpr uint32_t chunk_size = 1u << 20u;
|
||||
thread_local std::vector<uint8_t> chunk;
|
||||
uint32_t read = 0;
|
||||
if (data != nullptr && size != 0 && m_p->position < Size()) {
|
||||
const auto requested =
|
||||
static_cast<uint32_t>(std::min<uint64_t>(size, Size() - m_p->position));
|
||||
if (chunk.size() < std::min(chunk_size, requested)) {
|
||||
chunk.resize(std::min(chunk_size, requested));
|
||||
}
|
||||
while (read < requested) {
|
||||
const auto step = std::min(chunk_size, requested - read);
|
||||
const auto got = ArchiveIoThread::Instance().Read(*m_p->reader, m_p->entry.id,
|
||||
m_p->position, step, chunk.data());
|
||||
if (got == 0 || got > step) {
|
||||
break;
|
||||
}
|
||||
std::memcpy(static_cast<uint8_t*>(data) + read, chunk.data(), got);
|
||||
read += static_cast<uint32_t>(got);
|
||||
m_p->position += got;
|
||||
if (got < step) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bytes_read != nullptr) {
|
||||
*bytes_read = read;
|
||||
}
|
||||
#if defined(KYTY_ARCHIVE_PTHREAD_CANCELLATION)
|
||||
pthread_setcancelstate(cancellation_state, nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path MakeArchivePath(const std::filesystem::path& archive,
|
||||
const std::filesystem::path& member) {
|
||||
auto root = archive;
|
||||
root += "!";
|
||||
return member.empty() ? root : root / member.relative_path();
|
||||
}
|
||||
|
||||
bool IsSupportedArchive(const std::filesystem::path& path) {
|
||||
return FindFormat(path.native()) != nullptr;
|
||||
}
|
||||
|
||||
bool IsArchivePath(const std::filesystem::path& path) {
|
||||
return FindMarker(path.native()) != NativeView::npos;
|
||||
}
|
||||
|
||||
std::filesystem::path GetArchiveHostPath(const std::filesystem::path& path) {
|
||||
const auto marker = FindMarker(path.native());
|
||||
return marker == NativeView::npos ? std::filesystem::path {}
|
||||
: std::filesystem::path(path.native().substr(0, marker));
|
||||
}
|
||||
|
||||
std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path) {
|
||||
const auto archive = IsArchivePath(path) ? GetArchiveHostPath(path) : path;
|
||||
const auto* format = FindFormat(archive.native());
|
||||
if (format == nullptr) {
|
||||
return {};
|
||||
}
|
||||
std::error_code error;
|
||||
auto key = std::filesystem::absolute(archive, error);
|
||||
if (error) {
|
||||
return {};
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(g_readers_mutex);
|
||||
if (auto it = g_readers.find(key); it != g_readers.end()) {
|
||||
if (auto reader = it->second.lock()) {
|
||||
return reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not hold the cache mutex while opening an unrelated archive from disk.
|
||||
auto reader = format->open(archive);
|
||||
if (reader == nullptr) {
|
||||
return {};
|
||||
}
|
||||
std::lock_guard lock(g_readers_mutex);
|
||||
std::erase_if(g_readers, [](const auto& entry) { return entry.second.expired(); });
|
||||
auto& cached = g_readers[key];
|
||||
if (auto existing = cached.lock()) {
|
||||
return existing;
|
||||
}
|
||||
cached = reader;
|
||||
return reader;
|
||||
}
|
||||
|
||||
std::optional<File::Info> GetArchiveInfo(const std::filesystem::path& path) {
|
||||
const auto parsed = ParsePath(path);
|
||||
if (parsed) {
|
||||
if (auto reader = OpenArchive(parsed->archive)) {
|
||||
if (auto entry = reader->Find(parsed->member)) {
|
||||
return File::Info {entry->is_file, entry->size};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<File::DirEntry> GetArchiveDirEntries(const std::filesystem::path& path) {
|
||||
const auto parsed = ParsePath(path);
|
||||
if (parsed) {
|
||||
if (auto reader = OpenArchive(parsed->archive)) {
|
||||
return reader->List(parsed->member);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path) {
|
||||
auto parsed = ParsePath(path);
|
||||
if (!parsed) {
|
||||
return {};
|
||||
}
|
||||
auto reader = OpenArchive(parsed->archive);
|
||||
const auto entry = reader ? reader->Find(parsed->member) : std::nullopt;
|
||||
if (!entry || !entry->is_file) {
|
||||
return {};
|
||||
}
|
||||
auto p = std::make_unique<ArchiveFile::Private>();
|
||||
p->reader = std::move(reader);
|
||||
p->entry = *entry;
|
||||
return std::unique_ptr<ArchiveFile>(new ArchiveFile(std::move(p)));
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef KYTY_COMMON_ARCHIVE_H_
|
||||
#define KYTY_COMMON_ARCHIVE_H_
|
||||
|
||||
#include "common/file.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Common {
|
||||
|
||||
class ArchiveReader;
|
||||
|
||||
class ArchiveFile {
|
||||
public:
|
||||
~ArchiveFile();
|
||||
|
||||
[[nodiscard]] uint64_t Size() const;
|
||||
[[nodiscard]] uint64_t Tell() const;
|
||||
bool Seek(uint64_t offset);
|
||||
void Read(void* data, uint32_t size, uint32_t* bytes_read);
|
||||
|
||||
KYTY_CLASS_NO_COPY(ArchiveFile);
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
explicit ArchiveFile(std::unique_ptr<Private> p);
|
||||
std::unique_ptr<Private> m_p;
|
||||
|
||||
friend std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path);
|
||||
};
|
||||
|
||||
// Virtual paths use "archive.ext!/member" and retain native host-path encoding.
|
||||
[[nodiscard]] std::filesystem::path MakeArchivePath(const std::filesystem::path& archive,
|
||||
const std::filesystem::path& member = {});
|
||||
[[nodiscard]] bool IsSupportedArchive(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool IsArchivePath(const std::filesystem::path& path);
|
||||
[[nodiscard]] std::optional<File::Info> GetArchiveInfo(const std::filesystem::path& path);
|
||||
[[nodiscard]] std::vector<File::DirEntry> GetArchiveDirEntries(const std::filesystem::path& path);
|
||||
[[nodiscard]] std::filesystem::path GetArchiveHostPath(const std::filesystem::path& path);
|
||||
[[nodiscard]] std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path);
|
||||
|
||||
// Retain this handle while a mount or metadata scan is active to reuse the index and block cache.
|
||||
// Accepts either a host archive filename or a virtual path inside the archive.
|
||||
[[nodiscard]] std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif /* KYTY_COMMON_ARCHIVE_H_ */
|
||||
+139
-16
@@ -1,5 +1,6 @@
|
||||
#include "common/file.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/platform/sysFileIO.h"
|
||||
@@ -62,6 +63,7 @@ SysFileTimeStruct DateTimeToFileTimeUtc(const DateTime& date_time) {
|
||||
|
||||
struct File::FilePrivate {
|
||||
sys_file_t* f;
|
||||
std::unique_ptr<ArchiveFile> archive;
|
||||
};
|
||||
|
||||
File::File(): m_p(std::make_unique<FilePrivate>()) {
|
||||
@@ -85,11 +87,15 @@ File::File(const std::filesystem::path& name, Mode mode): m_p(std::make_unique<F
|
||||
}
|
||||
|
||||
bool File::IsInvalid() const {
|
||||
return m_p->f == nullptr;
|
||||
return m_p->f == nullptr && m_p->archive == nullptr;
|
||||
}
|
||||
|
||||
bool File::Create(const std::filesystem::path& name) {
|
||||
EXIT_IF(m_p->f != nullptr);
|
||||
EXIT_IF(!IsInvalid());
|
||||
|
||||
if (IsArchivePath(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_file_name = name;
|
||||
|
||||
@@ -104,9 +110,16 @@ bool File::Create(const std::filesystem::path& name) {
|
||||
}
|
||||
|
||||
bool File::Open(const std::filesystem::path& name, Mode mode) {
|
||||
EXIT_IF(m_p->f != nullptr);
|
||||
EXIT_IF(!IsInvalid());
|
||||
|
||||
m_file_name = name;
|
||||
if (IsArchivePath(name)) {
|
||||
if (mode != Mode::Read) {
|
||||
return false;
|
||||
}
|
||||
m_p->archive = OpenArchiveFile(name);
|
||||
return m_p->archive != nullptr;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case Mode::Read: m_p->f = SysFileOpenR(name); break;
|
||||
@@ -124,7 +137,7 @@ bool File::Open(const std::filesystem::path& name, Mode mode) {
|
||||
}
|
||||
|
||||
bool File::OpenInMem(void* buf, uint32_t buf_size) {
|
||||
EXIT_IF(m_p->f != nullptr);
|
||||
EXIT_IF(!IsInvalid());
|
||||
|
||||
m_p->f = SysFileOpen(static_cast<uint8_t*>(buf), buf_size);
|
||||
|
||||
@@ -137,7 +150,7 @@ bool File::OpenInMem(void* buf, uint32_t buf_size) {
|
||||
}
|
||||
|
||||
bool File::CreateInMem() {
|
||||
EXIT_IF(m_p->f != nullptr);
|
||||
EXIT_IF(!IsInvalid());
|
||||
|
||||
m_p->f = SysFileCreate();
|
||||
|
||||
@@ -150,6 +163,7 @@ bool File::CreateInMem() {
|
||||
}
|
||||
|
||||
void File::Close() {
|
||||
m_p->archive.reset();
|
||||
if (m_p->f != nullptr) {
|
||||
SysFileClose(m_p->f);
|
||||
m_p->f = nullptr;
|
||||
@@ -157,7 +171,11 @@ void File::Close() {
|
||||
}
|
||||
|
||||
uint64_t File::Size() const {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
return m_p->archive->Size();
|
||||
}
|
||||
|
||||
if (m_p->f == nullptr) {
|
||||
return 0;
|
||||
@@ -167,41 +185,76 @@ uint64_t File::Size() const {
|
||||
}
|
||||
|
||||
uint64_t File::Remaining() const {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
return Size() - Tell();
|
||||
const auto size = Size();
|
||||
const auto position = Tell();
|
||||
return position < size ? size - position : 0;
|
||||
}
|
||||
|
||||
uint64_t File::Size(const std::filesystem::path& name) {
|
||||
if (IsArchivePath(name)) {
|
||||
const auto info = GetArchiveInfo(name);
|
||||
return info ? info->size : 0;
|
||||
}
|
||||
return SysFileSize(name);
|
||||
}
|
||||
|
||||
std::optional<File::Info> File::GetInfo(const std::filesystem::path& name) {
|
||||
if (IsArchivePath(name)) {
|
||||
return GetArchiveInfo(name);
|
||||
}
|
||||
Info info {};
|
||||
return SysFileGetInfo(name, &info.is_file, &info.size) ? std::optional(info) : std::nullopt;
|
||||
}
|
||||
|
||||
bool File::Seek(uint64_t offset) {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
return m_p->archive->Seek(offset);
|
||||
}
|
||||
|
||||
return SysFileSeek(*m_p->f, offset);
|
||||
}
|
||||
|
||||
bool File::Truncate(uint64_t size) {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SysFileTruncate(*m_p->f, size);
|
||||
}
|
||||
|
||||
bool File::Unlink() {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SysFileUnlink(*m_p->f, m_file_name);
|
||||
}
|
||||
|
||||
uint64_t File::Tell() const {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
return m_p->archive->Tell();
|
||||
}
|
||||
|
||||
return SysFileTell(*m_p->f);
|
||||
}
|
||||
|
||||
void File::Read(void* data, uint32_t size, uint32_t* bytes_read) {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
m_p->archive->Read(data, size, bytes_read);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_p->f != nullptr) {
|
||||
SysFileRead(data, size, *m_p->f, bytes_read);
|
||||
@@ -209,7 +262,14 @@ void File::Read(void* data, uint32_t size, uint32_t* bytes_read) {
|
||||
}
|
||||
|
||||
void File::Write(const void* data, uint32_t size, uint32_t* bytes_written) {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
if (bytes_written != nullptr) {
|
||||
*bytes_written = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SysFileWrite(data, size, *m_p->f, bytes_written);
|
||||
}
|
||||
@@ -257,24 +317,41 @@ static std::filesystem::path WithoutTrailingSeparator(
|
||||
|
||||
|
||||
bool File::IsDirectoryExisting(const std::filesystem::path& path) {
|
||||
if (IsArchivePath(path)) {
|
||||
const auto info = GetArchiveInfo(path);
|
||||
return info && !info->is_file;
|
||||
}
|
||||
return SysFileIsDirectoryExisting(WithoutTrailingSeparator(path));
|
||||
}
|
||||
|
||||
bool File::IsFileExisting(const std::filesystem::path& name) {
|
||||
if (IsArchivePath(name)) {
|
||||
const auto info = GetArchiveInfo(name);
|
||||
return info && info->is_file;
|
||||
}
|
||||
return SysFileIsFileExisting(name);
|
||||
}
|
||||
|
||||
bool File::CreateDirectory(const std::filesystem::path& path) {
|
||||
if (IsArchivePath(path)) {
|
||||
return false;
|
||||
}
|
||||
return SysFileCreateDirectory(
|
||||
WithoutTrailingSeparator(path));
|
||||
}
|
||||
|
||||
bool File::DeleteDirectory(const std::filesystem::path& path) {
|
||||
if (IsArchivePath(path)) {
|
||||
return false;
|
||||
}
|
||||
return SysFileDeleteDirectory(
|
||||
WithoutTrailingSeparator(path));
|
||||
}
|
||||
|
||||
bool File::CreateDirectories(const std::filesystem::path& path) {
|
||||
if (IsArchivePath(path)) {
|
||||
return false;
|
||||
}
|
||||
const auto normalized_path =
|
||||
WithoutTrailingSeparator(path);
|
||||
|
||||
@@ -297,6 +374,9 @@ bool File::CreateDirectories(const std::filesystem::path& path) {
|
||||
}
|
||||
|
||||
bool File::DeleteDirectories(const std::filesystem::path& path) {
|
||||
if (IsArchivePath(path)) {
|
||||
return false;
|
||||
}
|
||||
const auto normalized_path =
|
||||
WithoutTrailingSeparator(path);
|
||||
|
||||
@@ -322,11 +402,18 @@ bool File::DeleteDirectories(const std::filesystem::path& path) {
|
||||
bool File::DeleteFile(
|
||||
const std::filesystem::path& name) // @suppress("Member declaration not found")
|
||||
{
|
||||
if (IsArchivePath(name)) {
|
||||
return false;
|
||||
}
|
||||
return SysFileDeleteFile(name);
|
||||
}
|
||||
|
||||
bool File::Flush() {
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return SysFileFlush(*m_p->f);
|
||||
}
|
||||
@@ -348,6 +435,9 @@ std::vector<std::byte> File::ReadWholeBuffer() {
|
||||
}
|
||||
|
||||
DateTime File::GetLastAccessTimeUTC(const std::filesystem::path& name) {
|
||||
if (IsArchivePath(name)) {
|
||||
return GetLastAccessTimeUTC(GetArchiveHostPath(name));
|
||||
}
|
||||
SysTimeStruct t {};
|
||||
SysFileToSystemTimeUtc(SysFileGetLastAccessTimeUtc(name), t);
|
||||
|
||||
@@ -359,6 +449,9 @@ DateTime File::GetLastAccessTimeUTC(const std::filesystem::path& name) {
|
||||
}
|
||||
|
||||
DateTime File::GetLastWriteTimeUTC(const std::filesystem::path& name) {
|
||||
if (IsArchivePath(name)) {
|
||||
return GetLastWriteTimeUTC(GetArchiveHostPath(name));
|
||||
}
|
||||
SysTimeStruct t {};
|
||||
SysFileToSystemTimeUtc(SysFileGetLastWriteTimeUtc(name), t);
|
||||
|
||||
@@ -373,6 +466,10 @@ void File::GetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, DateT
|
||||
DateTime* write) {
|
||||
EXIT_IF(access == nullptr);
|
||||
EXIT_IF(write == nullptr);
|
||||
if (IsArchivePath(name)) {
|
||||
GetLastAccessAndWriteTimeUTC(GetArchiveHostPath(name), access, write);
|
||||
return;
|
||||
}
|
||||
|
||||
SysTimeStruct at {};
|
||||
SysTimeStruct wt {};
|
||||
@@ -399,7 +496,12 @@ void File::GetLastAccessAndWriteTimeUTC(DateTime* access, DateTime* write) {
|
||||
EXIT_IF(access == nullptr);
|
||||
EXIT_IF(write == nullptr);
|
||||
|
||||
EXIT_IF(m_p->f == nullptr);
|
||||
EXIT_IF(IsInvalid());
|
||||
|
||||
if (m_p->archive != nullptr) {
|
||||
GetLastAccessAndWriteTimeUTC(m_file_name, access, write);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_p->f == nullptr) {
|
||||
*access = DateTime();
|
||||
@@ -429,23 +531,35 @@ void File::GetLastAccessAndWriteTimeUTC(DateTime* access, DateTime* write) {
|
||||
}
|
||||
|
||||
bool File::SetLastAccessTimeUTC(const std::filesystem::path& name, const DateTime& dt) {
|
||||
if (IsArchivePath(name)) {
|
||||
return false;
|
||||
}
|
||||
auto f = DateTimeToFileTimeUtc(dt);
|
||||
return SysFileSetLastAccessTimeUtc(name, f);
|
||||
}
|
||||
|
||||
bool File::SetLastWriteTimeUTC(const std::filesystem::path& name, const DateTime& dt) {
|
||||
if (IsArchivePath(name)) {
|
||||
return false;
|
||||
}
|
||||
auto f = DateTimeToFileTimeUtc(dt);
|
||||
return SysFileSetLastWriteTimeUtc(name, f);
|
||||
}
|
||||
|
||||
bool File::SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, const DateTime& access,
|
||||
const DateTime& write) {
|
||||
if (IsArchivePath(name)) {
|
||||
return false;
|
||||
}
|
||||
auto af = DateTimeToFileTimeUtc(access);
|
||||
auto wf = DateTimeToFileTimeUtc(write);
|
||||
return SysFileSetLastAccessAndWriteTimeUtc(name, af, wf);
|
||||
}
|
||||
|
||||
std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& path) {
|
||||
if (IsArchivePath(path)) {
|
||||
return GetArchiveDirEntries(path);
|
||||
}
|
||||
std::vector<sys_dir_entry_t> files;
|
||||
|
||||
SysFileGetDents(path, files);
|
||||
@@ -463,12 +577,18 @@ std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& pat
|
||||
bool File::CopyFile(const std::filesystem::path& src,
|
||||
const std::filesystem::path& dst) // @suppress("Member declaration not found")
|
||||
{
|
||||
if (IsArchivePath(src) || IsArchivePath(dst)) {
|
||||
return false;
|
||||
}
|
||||
return SysFileCopyFile(src, dst);
|
||||
}
|
||||
|
||||
bool File::RenameFile(const std::filesystem::path& src,
|
||||
const std::filesystem::path& dst) // @suppress("Member declaration not found")
|
||||
{
|
||||
if (IsArchivePath(src) || IsArchivePath(dst)) {
|
||||
return false;
|
||||
}
|
||||
if (IsFileExisting(dst)) {
|
||||
DeleteFile(dst); // @suppress("Invalid arguments")
|
||||
}
|
||||
@@ -477,6 +597,9 @@ bool File::RenameFile(const std::filesystem::path& src,
|
||||
}
|
||||
|
||||
void File::RemoveReadonly(const std::filesystem::path& name) {
|
||||
if (IsArchivePath(name)) {
|
||||
return;
|
||||
}
|
||||
SysFileRemoveReadonly(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -34,6 +35,10 @@ public:
|
||||
std::string name;
|
||||
bool is_file;
|
||||
};
|
||||
struct Info {
|
||||
bool is_file;
|
||||
uint64_t size;
|
||||
};
|
||||
|
||||
File();
|
||||
explicit File(const std::filesystem::path& name);
|
||||
@@ -72,6 +77,7 @@ public:
|
||||
std::vector<std::byte> ReadWholeBuffer();
|
||||
|
||||
static uint64_t Size(const std::filesystem::path& name);
|
||||
static std::optional<Info> GetInfo(const std::filesystem::path& name);
|
||||
|
||||
static bool IsDirectoryExisting(const std::filesystem::path& path);
|
||||
static bool IsFileExisting(const std::filesystem::path& name);
|
||||
|
||||
@@ -45,6 +45,7 @@ bool SysFileTruncate(sys_file_t& f, uint64_t size); // NOLINT(google-runt
|
||||
bool SysFileUnlink(sys_file_t& f,
|
||||
const std::filesystem::path& name); // NOLINT(google-runtime-references)
|
||||
uint64_t SysFileSize(const std::filesystem::path& file_name);
|
||||
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size);
|
||||
bool SysFileIsError(sys_file_t& f); // NOLINT(google-runtime-references)
|
||||
bool SysFileIsDirectoryExisting(const std::filesystem::path& path);
|
||||
bool SysFileIsFileExisting(const std::filesystem::path& name);
|
||||
|
||||
@@ -297,10 +297,21 @@ uint64_t SysFileSize(sys_file_t& f) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size) {
|
||||
const auto path = get_internal_name(name);
|
||||
struct stat info {};
|
||||
if (stat(path.c_str(), &info) != 0) {
|
||||
return false;
|
||||
}
|
||||
*is_file = !S_ISDIR(info.st_mode);
|
||||
*size = *is_file ? static_cast<uint64_t>(info.st_size) : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t SysFileSize(const std::filesystem::path& file_name) {
|
||||
sys_file_t* f = SysFileOpenR(file_name);
|
||||
uint64_t size = SysFileSize(*f);
|
||||
SysFileClose(f);
|
||||
bool is_file;
|
||||
uint64_t size = 0;
|
||||
SysFileGetInfo(file_name, &is_file, &size);
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -353,29 +364,15 @@ bool SysFileIsError(sys_file_t& f) {
|
||||
}
|
||||
|
||||
bool SysFileIsDirectoryExisting(const std::filesystem::path& path) {
|
||||
auto real_name = get_internal_name(path);
|
||||
auto real_name_str = real_name.string();
|
||||
|
||||
struct stat s {};
|
||||
|
||||
if (0 != stat(real_name_str.c_str(), &s)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return S_ISDIR(s.st_mode); // NOLINT
|
||||
bool is_file;
|
||||
uint64_t size;
|
||||
return SysFileGetInfo(path, &is_file, &size) && !is_file;
|
||||
}
|
||||
|
||||
bool SysFileIsFileExisting(const std::filesystem::path& name) {
|
||||
auto real_name = get_internal_name(name);
|
||||
auto real_name_str = real_name.string();
|
||||
|
||||
struct stat s {};
|
||||
|
||||
if (0 != stat(real_name_str.c_str(), &s)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !S_ISDIR(s.st_mode); // NOLINT
|
||||
bool is_file;
|
||||
uint64_t size;
|
||||
return SysFileGetInfo(name, &is_file, &size) && is_file;
|
||||
}
|
||||
|
||||
bool SysFileCreateDirectory(const std::filesystem::path& path) {
|
||||
|
||||
@@ -266,19 +266,27 @@ uint64_t SysFileSize(sys_file_t& f) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t SysFileSize(const std::filesystem::path& file_name) {
|
||||
LARGE_INTEGER s;
|
||||
WIN32_FILE_ATTRIBUTE_DATA a;
|
||||
|
||||
auto wide = file_name.wstring();
|
||||
if (GetFileAttributesExW(wide.c_str(), GetFileExInfoStandard, &a) == 0) {
|
||||
return 0;
|
||||
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size) {
|
||||
const bool directory_path = !name.empty() && !name.has_filename() && name != name.root_path();
|
||||
WIN32_FILE_ATTRIBUTE_DATA info {};
|
||||
if (GetFileAttributesExW(directory_path ? name.parent_path().c_str() : name.c_str(),
|
||||
GetFileExInfoStandard, &info) == 0) {
|
||||
return false;
|
||||
}
|
||||
const bool file = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||
if (directory_path && file) {
|
||||
return false;
|
||||
}
|
||||
*is_file = file;
|
||||
*size = *is_file ? ((static_cast<uint64_t>(info.nFileSizeHigh) << 32u) | info.nFileSizeLow) : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
s.HighPart = static_cast<LONG>(a.nFileSizeHigh);
|
||||
s.LowPart = a.nFileSizeLow;
|
||||
|
||||
return s.QuadPart;
|
||||
uint64_t SysFileSize(const std::filesystem::path& file_name) {
|
||||
bool is_file;
|
||||
uint64_t size = 0;
|
||||
SysFileGetInfo(file_name, &is_file, &size);
|
||||
return size;
|
||||
}
|
||||
|
||||
bool SysFileTruncate(sys_file_t& f, uint64_t size) {
|
||||
@@ -346,17 +354,15 @@ bool SysFileIsError(sys_file_t& f) {
|
||||
}
|
||||
|
||||
bool SysFileIsDirectoryExisting(const std::filesystem::path& path) {
|
||||
auto wide = path.wstring();
|
||||
DWORD a = GetFileAttributesW(wide.c_str());
|
||||
return a != INVALID_FILE_ATTRIBUTES &&
|
||||
((a & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) != 0u);
|
||||
bool is_file;
|
||||
uint64_t size;
|
||||
return SysFileGetInfo(path, &is_file, &size) && !is_file;
|
||||
}
|
||||
|
||||
bool SysFileIsFileExisting(const std::filesystem::path& name) {
|
||||
auto wide = name.wstring();
|
||||
DWORD a = GetFileAttributesW(wide.c_str());
|
||||
return a != INVALID_FILE_ATTRIBUTES &&
|
||||
((a & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) == 0u);
|
||||
bool is_file;
|
||||
uint64_t size;
|
||||
return SysFileGetInfo(name, &is_file, &size) && is_file;
|
||||
}
|
||||
|
||||
bool SysFileCreateDirectory(const std::filesystem::path& path) {
|
||||
|
||||
+33
-12
@@ -1,5 +1,6 @@
|
||||
#include "kernel/fileSystem.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/common.h"
|
||||
#include "common/dateTime.h"
|
||||
@@ -41,6 +42,7 @@ public:
|
||||
struct MountPair {
|
||||
std::filesystem::path dir;
|
||||
std::string point;
|
||||
std::shared_ptr<Common::ArchiveReader> archive;
|
||||
};
|
||||
|
||||
MountPoints() { EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread()); }
|
||||
@@ -260,6 +262,7 @@ void MountPoints::Mount(const std::filesystem::path& folder, const std::string&
|
||||
MountPair p;
|
||||
p.dir = folder;
|
||||
p.point = point_str;
|
||||
p.archive = Common::OpenArchive(folder);
|
||||
|
||||
m_mount_pairs.push_back(std::move(p));
|
||||
}
|
||||
@@ -346,11 +349,13 @@ std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name)
|
||||
}
|
||||
|
||||
const auto native_rel_path = Common::PathFromUtf8(rel_path);
|
||||
if (Common::IsArchivePath(p.dir)) {
|
||||
return p.dir / native_rel_path;
|
||||
}
|
||||
|
||||
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
||||
if (HasWindowsForbiddenFilenameCharacter(rel_path)) {
|
||||
::printf("FileSystem: Windows-incompatible guest filename: %s\n",
|
||||
mounted_name.c_str());
|
||||
::printf("FileSystem: Windows-incompatible guest filename: %s\n", mounted_name.c_str());
|
||||
}
|
||||
return p.dir / native_rel_path;
|
||||
#else
|
||||
@@ -466,14 +471,20 @@ int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode) {
|
||||
g_files->DeleteDescriptor(descriptor);
|
||||
return KERNEL_ERROR_ENOENT;
|
||||
}
|
||||
if (Common::IsArchivePath(file->real_name) &&
|
||||
(rw_mode != Common::File::Mode::Read || trunc || creat)) {
|
||||
g_files->DeleteDescriptor(descriptor);
|
||||
return KERNEL_ERROR_EROFS;
|
||||
}
|
||||
|
||||
if (trunc && rw_mode == Common::File::Mode::Read) {
|
||||
g_files->DeleteDescriptor(descriptor);
|
||||
return KERNEL_ERROR_EACCES;
|
||||
}
|
||||
|
||||
bool dir_exist = Common::File::IsDirectoryExisting(file->real_name);
|
||||
bool file_exist = Common::File::IsFileExisting(file->real_name);
|
||||
const auto info = Common::File::GetInfo(file->real_name);
|
||||
const bool dir_exist = info && !info->is_file;
|
||||
const bool file_exist = info && info->is_file;
|
||||
|
||||
// A missing path opened without O_CREAT is ENOENT
|
||||
if (!creat && !dir_exist && !file_exist) {
|
||||
@@ -671,7 +682,7 @@ int64_t KYTY_SYSV_ABI KernelWrite(int d, const void* buf, size_t nbytes) {
|
||||
|
||||
auto* file = g_files->GetFile(d);
|
||||
|
||||
if (file == nullptr) {
|
||||
if (file == nullptr || !file->opened || !file->writable) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
@@ -886,7 +897,7 @@ int64_t KYTY_SYSV_ABI KernelPwrite(int d, const void* buf, size_t nbytes, int64_
|
||||
|
||||
auto* file = g_files->GetFile(d);
|
||||
|
||||
if (file == nullptr) {
|
||||
if (file == nullptr || !file->opened || !file->writable) {
|
||||
return KERNEL_ERROR_EBADF;
|
||||
}
|
||||
|
||||
@@ -1063,15 +1074,13 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb) {
|
||||
|
||||
auto real_file_name = g_mount_points->ResolvePath(path);
|
||||
|
||||
bool is_dir = Common::File::IsDirectoryExisting(real_file_name);
|
||||
bool is_file = Common::File::IsFileExisting(real_file_name);
|
||||
|
||||
if (!is_dir && !is_file) {
|
||||
const auto info = Common::File::GetInfo(real_file_name);
|
||||
if (!info) {
|
||||
LOGF("\t file not found\n");
|
||||
return KERNEL_ERROR_ENOENT;
|
||||
}
|
||||
|
||||
EXIT_NOT_IMPLEMENTED(is_dir && is_file);
|
||||
const bool is_dir = !info->is_file;
|
||||
|
||||
FileStat stat {};
|
||||
stat.st_mode = 0000777u | (is_dir ? 0040000u : 0100000u);
|
||||
@@ -1085,7 +1094,7 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb) {
|
||||
stat.st_blksize = 512;
|
||||
stat.st_blocks = stat.st_size / 512;
|
||||
} else {
|
||||
stat.st_size = static_cast<int64_t>(Common::File::Size(real_file_name));
|
||||
stat.st_size = static_cast<int64_t>(info->size);
|
||||
stat.st_blksize = 512;
|
||||
stat.st_blocks = (stat.st_size + 511) / 512;
|
||||
|
||||
@@ -1222,6 +1231,9 @@ int KYTY_SYSV_ABI KernelUnlink(const char* path) {
|
||||
}
|
||||
|
||||
auto real_file_name = g_mount_points->ResolvePath(path);
|
||||
if (Common::IsArchivePath(real_file_name)) {
|
||||
return KERNEL_ERROR_EROFS;
|
||||
}
|
||||
|
||||
bool is_dir = Common::File::IsDirectoryExisting(real_file_name);
|
||||
bool is_file = Common::File::IsFileExisting(real_file_name);
|
||||
@@ -1259,6 +1271,9 @@ int KYTY_SYSV_ABI KernelRename(const char* from, const char* to) {
|
||||
auto to_path = std::string(to);
|
||||
auto real_from = g_mount_points->ResolvePath(from_path);
|
||||
auto real_to = g_mount_points->ResolvePath(to_path);
|
||||
if (Common::IsArchivePath(real_from) || Common::IsArchivePath(real_to)) {
|
||||
return KERNEL_ERROR_EROFS;
|
||||
}
|
||||
|
||||
if (real_to.empty() || !Common::File::IsFileExisting(real_from)) {
|
||||
return KERNEL_ERROR_ENOENT;
|
||||
@@ -1343,6 +1358,9 @@ int KYTY_SYSV_ABI KernelMkdir(const char* path, uint16_t mode) {
|
||||
if (real_name.empty()) {
|
||||
return KERNEL_ERROR_ENOENT;
|
||||
}
|
||||
if (Common::IsArchivePath(real_name)) {
|
||||
return KERNEL_ERROR_EROFS;
|
||||
}
|
||||
|
||||
if (Common::File::IsDirectoryExisting(real_name)) {
|
||||
return KERNEL_ERROR_EEXIST;
|
||||
@@ -1369,6 +1387,9 @@ int KYTY_SYSV_ABI KernelRmdir(const char* path) {
|
||||
LOGF("\t path = %s\n", path);
|
||||
|
||||
auto real_name = g_mount_points->ResolvePath(std::string(path));
|
||||
if (Common::IsArchivePath(real_name)) {
|
||||
return KERNEL_ERROR_EROFS;
|
||||
}
|
||||
|
||||
if (!Common::File::IsDirectoryExisting(real_name)) {
|
||||
return KERNEL_ERROR_ENOENT;
|
||||
|
||||
@@ -20,7 +20,7 @@ add_executable(launcher WIN32
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version)
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common)
|
||||
if(WIN32)
|
||||
target_link_libraries(launcher setupapi)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CONFIGURATION_ITEM_H
|
||||
#define CONFIGURATION_ITEM_H
|
||||
|
||||
#include <QIcon>
|
||||
#include <QObject>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
@@ -18,7 +19,7 @@ public:
|
||||
explicit ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent);
|
||||
~ConfigurationItem() override;
|
||||
|
||||
void Update();
|
||||
void Update(bool reload_icon = false);
|
||||
bool operator<(const QTreeWidgetItem& other) const override;
|
||||
|
||||
Configuration& GetInfo() { return *m_info; }
|
||||
@@ -35,6 +36,7 @@ private:
|
||||
void UpdateStatusIndicator();
|
||||
|
||||
std::unique_ptr<Configuration> m_info;
|
||||
QIcon m_icon;
|
||||
bool m_running = false;
|
||||
QComboBox* m_status_combo = nullptr;
|
||||
QLabel* m_status_indicator = nullptr;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef GAME_CONTENT_H
|
||||
#define GAME_CONTENT_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
||||
namespace GameContent {
|
||||
|
||||
inline constexpr uint64_t MaxMetadataSize = uint64_t {1} << 20u;
|
||||
inline constexpr uint64_t MaxImageSize = uint64_t {32} << 20u;
|
||||
inline constexpr uint64_t MaxTrophyPackageSize = uint64_t {128} << 20u;
|
||||
|
||||
[[nodiscard]] std::filesystem::path ToPath(const QString& path);
|
||||
[[nodiscard]] QString FromPath(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool IsArchive(const QString& base);
|
||||
[[nodiscard]] std::filesystem::path Resolve(const QString& base, const QString& relative);
|
||||
[[nodiscard]] bool FileExists(const QString& base, const QString& relative);
|
||||
[[nodiscard]] QByteArray ReadPath(const std::filesystem::path& path, uint64_t max_size);
|
||||
[[nodiscard]] QByteArray ReadFile(const QString& base, const QString& relative, uint64_t max_size);
|
||||
[[nodiscard]] QStringList ListFiles(const QString& base, const QString& relative);
|
||||
|
||||
} // namespace GameContent
|
||||
|
||||
#endif // GAME_CONTENT_H
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef GAME_LIST_TREE_WIDGET_H
|
||||
#define GAME_LIST_TREE_WIDGET_H
|
||||
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
@@ -17,14 +19,15 @@ public:
|
||||
viewport()->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
void SetBackgroundImage(const QString& path) {
|
||||
if (m_path == path) {
|
||||
void SetBackgroundImage(const QString& key) {
|
||||
if (m_path == key) {
|
||||
return;
|
||||
}
|
||||
m_path = path;
|
||||
m_path = key;
|
||||
m_source = QPixmap();
|
||||
if (!path.isEmpty()) {
|
||||
m_source.load(path);
|
||||
if (!key.isEmpty()) {
|
||||
m_source.loadFromData(GameContent::ReadFile(key, QStringLiteral("sce_sys/pic0.png"),
|
||||
GameContent::MaxImageSize));
|
||||
}
|
||||
UpdateScaledBackground();
|
||||
viewport()->update();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "configurationItem.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QLocale>
|
||||
#include <QPixmap>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QStyle>
|
||||
@@ -154,7 +156,7 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
|
||||
m_comment_edit->setFrame(false);
|
||||
parent->setItemWidget(this, CommentsColumn, m_comment_edit);
|
||||
|
||||
Update();
|
||||
Update(true);
|
||||
SetRunning(false);
|
||||
|
||||
setData(SizeColumn, Qt::UserRole, qint64(-1));
|
||||
@@ -170,7 +172,14 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([path = m_info->basedir]() -> qint64 {
|
||||
if (path.isEmpty() || !QDir(path).exists()) {
|
||||
if (path.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
const QFileInfo info(path);
|
||||
if (GameContent::IsArchive(path)) {
|
||||
return info.size();
|
||||
}
|
||||
if (!QDir(path).exists()) {
|
||||
return -1;
|
||||
}
|
||||
qint64 bytes = 0;
|
||||
@@ -188,7 +197,7 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
|
||||
|
||||
ConfigurationItem::~ConfigurationItem() = default;
|
||||
|
||||
void ConfigurationItem::Update() {
|
||||
void ConfigurationItem::Update(bool reload_icon) {
|
||||
const auto display_text = GetDisplayText(*m_info);
|
||||
const auto path = GetPathText(*m_info);
|
||||
|
||||
@@ -209,6 +218,12 @@ void ConfigurationItem::Update() {
|
||||
m_comment_edit->setText(m_info->game_comment);
|
||||
}
|
||||
|
||||
if (reload_icon) {
|
||||
const auto icon_data = GameContent::ReadFile(
|
||||
m_info->basedir, QStringLiteral("sce_sys/icon0.png"), GameContent::MaxImageSize);
|
||||
QPixmap icon;
|
||||
m_icon = !icon_data.isEmpty() && icon.loadFromData(icon_data) ? QIcon(icon) : QIcon {};
|
||||
}
|
||||
UpdateIcon();
|
||||
UpdateStatusIndicator();
|
||||
}
|
||||
@@ -267,9 +282,8 @@ void ConfigurationItem::SetCompatibilityEditable(bool editable) {
|
||||
}
|
||||
|
||||
void ConfigurationItem::UpdateIcon() {
|
||||
const QString icon_file = QDir(m_info->basedir).filePath(QStringLiteral("sce_sys/icon0.png"));
|
||||
if (QFileInfo::exists(icon_file)) {
|
||||
setIcon(NameColumn, QIcon(icon_file));
|
||||
if (!m_icon.isNull()) {
|
||||
setIcon(NameColumn, m_icon);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "compatibilityDatabase.h"
|
||||
#include "configuration.h"
|
||||
#include "configurationEditDialog.h"
|
||||
#include "configurationItem.h"
|
||||
#include "gameContent.h"
|
||||
#include "gameListTreeWidget.h"
|
||||
#include "inputMappingDialog.h"
|
||||
#include "mainDialog.h"
|
||||
@@ -127,15 +129,6 @@ static QStringList SettingsStringList(const QVariant& value) {
|
||||
return text.isEmpty() ? QStringList() : QStringList({text});
|
||||
}
|
||||
|
||||
static QString GetPic0Path(const Configuration& info) {
|
||||
if (info.basedir.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto path = QDir(info.basedir).filePath(QStringLiteral("sce_sys/pic0.png"));
|
||||
return QFileInfo::exists(path) ? QFileInfo(path).absoluteFilePath() : QString();
|
||||
}
|
||||
|
||||
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
|
||||
ui->cfgs_list->setAlternatingRowColors(false);
|
||||
ui->cfgs_list->setAllColumnsShowFocus(true);
|
||||
@@ -490,17 +483,16 @@ static QString GetFirmwareVersion(const QJsonObject& root) {
|
||||
return version;
|
||||
}
|
||||
|
||||
static GameMetadata GetGameMetadata(const QString& param_file, const QString& fallback) {
|
||||
static GameMetadata GetGameMetadata(const QByteArray& param_data, const QString& fallback) {
|
||||
GameMetadata ret;
|
||||
ret.title_name = fallback;
|
||||
|
||||
QFile param(param_file);
|
||||
if (!param.open(QIODevice::ReadOnly)) {
|
||||
if (param_data.isEmpty()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJsonParseError parse_error;
|
||||
const auto doc = QJsonDocument::fromJson(param.readAll(), &parse_error);
|
||||
const auto doc = QJsonDocument::fromJson(param_data, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
return ret;
|
||||
}
|
||||
@@ -522,18 +514,18 @@ static GameMetadata GetGameMetadata(const QString& param_file, const QString& fa
|
||||
}
|
||||
|
||||
static void SetGameFiles(Configuration& info, const QString& game_dir, const QString& game_path,
|
||||
const GameMetadata& metadata) {
|
||||
QDir game(game_dir);
|
||||
const GameMetadata& metadata, bool archive) {
|
||||
const QFileInfo game(game_dir);
|
||||
|
||||
info.game_path = game_path;
|
||||
info.basedir = game.absolutePath();
|
||||
info.basedir = archive ? game.absoluteFilePath() : QDir(game_dir).absolutePath();
|
||||
info.name = metadata.title_name;
|
||||
info.title_id = metadata.title_id;
|
||||
info.gameVersion = metadata.gameVersion;
|
||||
info.firmwareVer = metadata.firmwareVer;
|
||||
|
||||
if (info.name.isEmpty()) {
|
||||
info.name = game.dirName();
|
||||
info.name = archive ? game.completeBaseName() : QDir(game_dir).dirName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,39 +601,31 @@ void ConfigurationListWidget::ScanGameDirectory() {
|
||||
|
||||
const QString eboot_name = QStringLiteral("eboot.bin");
|
||||
QSet<QString> found_games;
|
||||
|
||||
for (const auto& root_path: m_game_dirs) {
|
||||
QDir root(root_path);
|
||||
if (root_path.isEmpty() || !root.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QList<QDir> pending_dirs;
|
||||
const auto root_subdirs =
|
||||
root.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
for (const auto& subdir: root_subdirs) {
|
||||
pending_dirs.append(QDir(subdir.absoluteFilePath()));
|
||||
}
|
||||
|
||||
while (!pending_dirs.isEmpty()) {
|
||||
QDir game_dir = pending_dirs.takeFirst();
|
||||
|
||||
if (game_dir.exists(eboot_name)) {
|
||||
const QString game_path = NormalizeGameDirectory(game_dir.absolutePath());
|
||||
const QString legacy_game_path = root.relativeFilePath(game_dir.absolutePath());
|
||||
const auto add_game = [this, &found_games, &eboot_name,
|
||||
&running_items](const QString& base, const QString& game_path,
|
||||
const QString& legacy_game_path, const QString& fallback,
|
||||
bool archive) {
|
||||
const QString game_key = PathKey(game_path);
|
||||
if (game_key.isEmpty() || found_games.contains(game_key)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
// Keep the index and decompression cache alive through validation, metadata and icon
|
||||
// reads, after rejecting duplicate candidates from overlapping game folders.
|
||||
const auto reader = archive ? Common::OpenArchive(GameContent::ToPath(base)) : nullptr;
|
||||
if (archive && (reader == nullptr || !GameContent::FileExists(base, eboot_name))) {
|
||||
return;
|
||||
}
|
||||
found_games.insert(game_key);
|
||||
|
||||
auto metadata = GetGameMetadata(
|
||||
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
|
||||
|
||||
const auto metadata =
|
||||
GetGameMetadata(GameContent::ReadFile(base, QStringLiteral("sce_sys/param.json"),
|
||||
GameContent::MaxMetadataSize),
|
||||
fallback);
|
||||
auto info = std::make_unique<Configuration>();
|
||||
info->custom_settings =
|
||||
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
|
||||
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
|
||||
|
||||
SetGameFiles(*info, base, game_path, metadata, archive);
|
||||
const auto* compatibility = m_compatibility->Find(info->title_id);
|
||||
if (compatibility != nullptr) {
|
||||
info->game_status = compatibility->status;
|
||||
@@ -652,10 +636,10 @@ void ConfigurationListWidget::ScanGameDirectory() {
|
||||
// Refresh metadata without losing the running row's identity.
|
||||
const QSignalBlocker status_blocker(item->GetStatusCombo());
|
||||
item->GetInfo().CopyGameInfoFrom(*info);
|
||||
item->Update();
|
||||
item->Update(true);
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
|
||||
!item->GetInfo().title_id.trimmed().isEmpty());
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
|
||||
@@ -693,6 +677,42 @@ void ConfigurationListWidget::ScanGameDirectory() {
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
};
|
||||
const auto scan_archives = [&add_game](const QDir& directory, const QDir& root) {
|
||||
const auto entries = directory.entryInfoList(QDir::Files | QDir::NoSymLinks, QDir::Name);
|
||||
for (const auto& archive: entries) {
|
||||
const auto base = archive.absoluteFilePath();
|
||||
if (!Common::IsSupportedArchive(GameContent::ToPath(base))) {
|
||||
continue;
|
||||
}
|
||||
add_game(base, QDir::cleanPath(base), root.relativeFilePath(base),
|
||||
archive.completeBaseName(), true);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& root_path: m_game_dirs) {
|
||||
QDir root(root_path);
|
||||
if (root_path.isEmpty() || !root.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
scan_archives(root, root);
|
||||
QList<QDir> pending_dirs;
|
||||
const auto root_subdirs =
|
||||
root.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
for (const auto& subdir: root_subdirs) {
|
||||
pending_dirs.append(QDir(subdir.absoluteFilePath()));
|
||||
}
|
||||
|
||||
while (!pending_dirs.isEmpty()) {
|
||||
QDir game_dir = pending_dirs.takeFirst();
|
||||
scan_archives(game_dir, root);
|
||||
|
||||
if (game_dir.exists(eboot_name)) {
|
||||
const QString game_path = NormalizeGameDirectory(game_dir.absolutePath());
|
||||
const QString legacy_game_path = root.relativeFilePath(game_dir.absolutePath());
|
||||
add_game(game_dir.absolutePath(), game_path, legacy_game_path, game_dir.dirName(),
|
||||
false);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -805,7 +825,8 @@ void ConfigurationListWidget::open_game_folder() {
|
||||
return;
|
||||
}
|
||||
|
||||
const QDir game_dir(item->GetInfo().basedir);
|
||||
const auto base = item->GetInfo().basedir;
|
||||
const QDir game_dir(GameContent::IsArchive(base) ? QFileInfo(base).absolutePath() : base);
|
||||
if (!game_dir.exists()) {
|
||||
QMessageBox::warning(this, tr("Open game folder"), tr("Game folder does not exist."));
|
||||
return;
|
||||
@@ -896,7 +917,7 @@ void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
|
||||
m_ui->edit_button->setEnabled(!item->IsRunning());
|
||||
|
||||
m_selected_item = item;
|
||||
m_ui->cfgs_list->SetBackgroundImage(GetPic0Path(item->GetInfo()));
|
||||
m_ui->cfgs_list->SetBackgroundImage(item->GetInfo().basedir);
|
||||
|
||||
emit Select();
|
||||
}
|
||||
@@ -959,7 +980,8 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
|
||||
if (item != nullptr) {
|
||||
action_run->setDisabled(item->IsRunning());
|
||||
action_open_folder->setDisabled(!QDir(item->GetInfo().basedir).exists());
|
||||
const auto& base = item->GetInfo().basedir;
|
||||
action_open_folder->setDisabled(!QDir(base).exists() && !GameContent::IsArchive(base));
|
||||
action_view_trophies->setDisabled(!has_trophy_data);
|
||||
action_remove_save_data->setDisabled(item->IsRunning() || save_data_dirs.isEmpty());
|
||||
action_edit->setDisabled(item->IsRunning());
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "gameContent.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "common/file.h"
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace GameContent {
|
||||
|
||||
std::filesystem::path ToPath(const QString& path) {
|
||||
#if defined(_WIN32)
|
||||
return std::filesystem::path(path.toStdWString());
|
||||
#else
|
||||
return std::filesystem::path(path.toStdString());
|
||||
#endif
|
||||
}
|
||||
|
||||
QString FromPath(const std::filesystem::path& path) {
|
||||
#if defined(_WIN32)
|
||||
return QString::fromStdWString(path.wstring());
|
||||
#else
|
||||
return QString::fromStdString(Common::PathToString(path));
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsArchive(const QString& base) {
|
||||
const QFileInfo info(base);
|
||||
return info.isFile() && Common::IsSupportedArchive(ToPath(base));
|
||||
}
|
||||
|
||||
std::filesystem::path Resolve(const QString& base, const QString& relative) {
|
||||
const auto root = ToPath(base);
|
||||
return IsArchive(base) ? Common::MakeArchivePath(root, ToPath(relative))
|
||||
: root / ToPath(relative);
|
||||
}
|
||||
|
||||
bool FileExists(const QString& base, const QString& relative) {
|
||||
return Common::File::IsFileExisting(Resolve(base, relative));
|
||||
}
|
||||
|
||||
QByteArray ReadFile(const QString& base, const QString& relative, uint64_t max_size) {
|
||||
return ReadPath(Resolve(base, relative), max_size);
|
||||
}
|
||||
|
||||
QByteArray ReadPath(const std::filesystem::path& path, uint64_t max_size) {
|
||||
Common::File file(path, Common::File::Mode::Read);
|
||||
if (file.IsInvalid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto size = file.Size();
|
||||
if (size > max_size || size > static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray data(static_cast<int>(size), Qt::Uninitialized);
|
||||
uint32_t bytes_read = 0;
|
||||
file.Read(data.data(), static_cast<uint32_t>(size), &bytes_read);
|
||||
return bytes_read == size ? data : QByteArray {};
|
||||
}
|
||||
|
||||
QStringList ListFiles(const QString& base, const QString& relative) {
|
||||
const auto directory = Resolve(base, relative);
|
||||
const bool archive = Common::IsArchivePath(directory);
|
||||
QStringList result;
|
||||
for (const auto& entry: Common::File::GetDirEntries(directory)) {
|
||||
if (entry.is_file) {
|
||||
const auto path = FromPath(directory / Common::PathFromUtf8(entry.name));
|
||||
if (archive || !QFileInfo(path).isSymLink()) {
|
||||
result.append(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace GameContent
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
#include "gameContent.h"
|
||||
#include "patchesDialog.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
@@ -260,7 +261,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
|
||||
QString game = info.basedir;
|
||||
if (!info.elf.isEmpty()) {
|
||||
if (!info.elf.isEmpty() && !GameContent::IsArchive(info.basedir)) {
|
||||
game = QDir(info.basedir).filePath(info.elf);
|
||||
}
|
||||
args << "--game" << game;
|
||||
@@ -507,7 +508,7 @@ void MainDialogPrivate::Update() {
|
||||
if (run_enabled) {
|
||||
const auto& info = item->GetInfo();
|
||||
auto dir = info.basedir;
|
||||
run_enabled = !dir.isEmpty() && QDir(dir).exists();
|
||||
run_enabled = !dir.isEmpty() && (QDir(dir).exists() || GameContent::IsArchive(dir));
|
||||
}
|
||||
|
||||
m_ui->widget->SetRunEnabled(run_enabled);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "trophyViewerDialog.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "configuration.h"
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QByteArray>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
@@ -45,6 +46,8 @@ constexpr int UCP_HEADER_LEN = 0x40;
|
||||
constexpr int UCP_TOC_SKIP = 0x20;
|
||||
constexpr int UCP_ENTRY_LEN = 0x40;
|
||||
constexpr int UCP_NAME_LEN = 0x20;
|
||||
constexpr quint32 UCP_MAX_FILES = 4096;
|
||||
constexpr quint64 UCP_MAX_EXTRACTED_SIZE = quint64 {64} << 20u;
|
||||
|
||||
struct UcpEntry {
|
||||
QString name;
|
||||
@@ -98,14 +101,22 @@ static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizety
|
||||
return QString::fromLatin1(data.constData() + offset, len);
|
||||
}
|
||||
|
||||
static bool IsUsedUcpEntry(const QString& name) {
|
||||
const auto lower = name.toCaseFolded();
|
||||
return lower == QStringLiteral("tropconf.json") || lower == QStringLiteral("tropmeta.json") ||
|
||||
(lower.startsWith(QStringLiteral("tropmeta_")) &&
|
||||
lower.endsWith(QStringLiteral(".json"))) ||
|
||||
(lower.startsWith(QStringLiteral("trop")) && lower.endsWith(QStringLiteral(".png")));
|
||||
}
|
||||
|
||||
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
|
||||
QFile file(file_name);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
const QByteArray data =
|
||||
GameContent::ReadPath(GameContent::ToPath(file_name), GameContent::MaxTrophyPackageSize);
|
||||
if (data.isEmpty()) {
|
||||
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
if (data.size() < UCP_HEADER_LEN) {
|
||||
error = QObject::tr("%1 is too small to be a trophy package.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
@@ -127,14 +138,18 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
|
||||
}
|
||||
|
||||
const auto declared_size = qFromBigEndian<quint64>(data.constData() + 0x08);
|
||||
if (declared_size > static_cast<quint64>(data.size())) {
|
||||
if (declared_size < UCP_HEADER_LEN || declared_size > static_cast<quint64>(data.size())) {
|
||||
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto file_count = qFromBigEndian<quint32>(data.constData() + 0x10);
|
||||
if (file_count > UCP_MAX_FILES) {
|
||||
error = QObject::tr("%1 contains too many files.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
const auto toc_offset = static_cast<quint64>(qFromBigEndian<quint32>(data.constData() + 0x14));
|
||||
const auto data_size = static_cast<quint64>(data.size());
|
||||
const auto data_size = declared_size;
|
||||
|
||||
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
|
||||
if (toc_offset > data_size || table_size > data_size - toc_offset ||
|
||||
@@ -144,6 +159,7 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
|
||||
return false;
|
||||
}
|
||||
|
||||
quint64 extracted_size = 0;
|
||||
for (quint32 i = 0; i < file_count; i++) {
|
||||
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
|
||||
static_cast<quint64>(i) * UCP_ENTRY_LEN);
|
||||
@@ -161,9 +177,22 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
if (!IsUsedUcpEntry(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto entry_limit = entry.name.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive)
|
||||
? GameContent::MaxImageSize
|
||||
: GameContent::MaxMetadataSize;
|
||||
if (entry.size > entry_limit || entry.size > UCP_MAX_EXTRACTED_SIZE - extracted_size) {
|
||||
error = QObject::tr("%1 contains an oversized entry for %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
|
||||
static_cast<qsizetype>(entry.size)));
|
||||
extracted_size += entry.size;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -392,30 +421,31 @@ static QStringList FindTrophyFiles(const Configuration* info) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QDir trophy_dir(QDir(info->basedir).filePath(QStringLiteral("sce_sys/trophy2")));
|
||||
if (!trophy_dir.exists()) {
|
||||
return {};
|
||||
QStringList files;
|
||||
for (const auto& file:
|
||||
GameContent::ListFiles(info->basedir, QStringLiteral("sce_sys/trophy2"))) {
|
||||
const auto name = QFileInfo(file).fileName();
|
||||
if (name.startsWith(QStringLiteral("trophy"), Qt::CaseInsensitive) &&
|
||||
name.endsWith(QStringLiteral(".ucp"), Qt::CaseInsensitive)) {
|
||||
files.append(file);
|
||||
}
|
||||
|
||||
const auto files =
|
||||
trophy_dir.entryInfoList({QStringLiteral("Trophy*.ucp"), QStringLiteral("trophy*.ucp")},
|
||||
QDir::Files | QDir::NoSymLinks, QDir::Name | QDir::IgnoreCase);
|
||||
}
|
||||
files.sort(Qt::CaseInsensitive);
|
||||
|
||||
QStringList trophy_files;
|
||||
QSet<QString> seen;
|
||||
for (const auto& file: files) {
|
||||
auto key = file.canonicalFilePath();
|
||||
auto key = QFileInfo(file).canonicalFilePath();
|
||||
if (key.isEmpty()) {
|
||||
key = file.absoluteFilePath();
|
||||
key = file;
|
||||
}
|
||||
key = QDir::cleanPath(key).toCaseFolded();
|
||||
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
trophy_files.append(file.absoluteFilePath());
|
||||
trophy_files.append(file);
|
||||
}
|
||||
}
|
||||
|
||||
return trophy_files;
|
||||
}
|
||||
|
||||
@@ -506,6 +536,7 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
|
||||
}
|
||||
|
||||
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
|
||||
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
|
||||
const auto trophy_files = FindTrophyFiles(&info);
|
||||
if (trophy_files.isEmpty()) {
|
||||
error = tr("No trophy package found in sce_sys/trophy2.");
|
||||
|
||||
+60
-39
@@ -1,6 +1,6 @@
|
||||
#include "common/common.h"
|
||||
#include "common/file.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "kernel/pthread.h"
|
||||
#include "libs/audio.h"
|
||||
@@ -9,12 +9,14 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <magic_enum.hpp>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -577,30 +579,40 @@ public:
|
||||
explicit FileStreamer(AvPlayerFileReplacement f): file(f) {}
|
||||
~FileStreamer() {
|
||||
if (ctx != nullptr) {
|
||||
av_freep(&ctx->buffer);
|
||||
avio_context_free(&ctx);
|
||||
}
|
||||
if (opened && file.close != nullptr) {
|
||||
if (opened) {
|
||||
file.close(file.object_pointer);
|
||||
}
|
||||
}
|
||||
bool Init(const std::string& path) {
|
||||
if (file.open == nullptr || file.close == nullptr || file.read_offset == nullptr ||
|
||||
file.size == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (file.open(file.object_pointer, path.c_str()) < 0) {
|
||||
if (file.open != nullptr) {
|
||||
if (file.close == nullptr || file.read_offset == nullptr || file.size == nullptr ||
|
||||
file.open(file.object_pointer, path.c_str()) < 0) {
|
||||
return false;
|
||||
}
|
||||
opened = true;
|
||||
size = file.size(file.object_pointer);
|
||||
if (size == 0) {
|
||||
} else {
|
||||
if (!local_file.Open(LibKernel::FileSystem::GetRealFilename(path),
|
||||
Common::File::Mode::Read)) {
|
||||
return false;
|
||||
}
|
||||
auto* buf = static_cast<uint8_t*>(av_malloc(4096));
|
||||
size = local_file.Size();
|
||||
}
|
||||
if (size == 0 || size > static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
|
||||
return false;
|
||||
}
|
||||
constexpr int buffer_size = 64 * 1024;
|
||||
auto* buf = static_cast<uint8_t*>(av_malloc(buffer_size));
|
||||
if (buf == nullptr) {
|
||||
return false;
|
||||
}
|
||||
ctx = avio_alloc_context(buf, 4096, 0, this, Read, nullptr, Seek);
|
||||
ctx = avio_alloc_context(buf, buffer_size, 0, this, Read, nullptr, Seek);
|
||||
if (ctx == nullptr) {
|
||||
av_free(buf);
|
||||
}
|
||||
return ctx != nullptr;
|
||||
}
|
||||
AVIOContext* Context() const { return ctx; }
|
||||
@@ -612,32 +624,49 @@ private:
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
len = static_cast<int>(std::min<uint64_t>(len, s->size - s->pos));
|
||||
auto r =
|
||||
s->file.read_offset(s->file.object_pointer, buf, s->pos, static_cast<uint32_t>(len));
|
||||
if (r <= 0) {
|
||||
return r == 0 ? AVERROR_EOF : r;
|
||||
int read = 0;
|
||||
if (s->opened) {
|
||||
read = s->file.read_offset(s->file.object_pointer, buf, s->pos,
|
||||
static_cast<uint32_t>(len));
|
||||
} else {
|
||||
uint32_t bytes = 0;
|
||||
s->local_file.Read(buf, static_cast<uint32_t>(len), &bytes);
|
||||
read = static_cast<int>(bytes);
|
||||
}
|
||||
s->pos += static_cast<uint64_t>(r);
|
||||
return r;
|
||||
if (read <= 0 || read > len) {
|
||||
return read < 0 ? read : AVERROR(EIO);
|
||||
}
|
||||
static int64_t Seek(void* opaque, int64_t off, int whence) {
|
||||
s->pos += static_cast<uint64_t>(read);
|
||||
return read;
|
||||
}
|
||||
static int64_t Seek(void* opaque, int64_t offset, int whence) {
|
||||
auto* s = static_cast<FileStreamer*>(opaque);
|
||||
if ((whence & AVSEEK_SIZE) != 0) {
|
||||
return static_cast<int64_t>(s->size);
|
||||
}
|
||||
int64_t p = whence == SEEK_SET
|
||||
? off
|
||||
: (whence == SEEK_CUR
|
||||
? static_cast<int64_t>(s->pos) + off
|
||||
: (whence == SEEK_END ? static_cast<int64_t>(s->size) + off : -1));
|
||||
if (p < 0) {
|
||||
return -1;
|
||||
whence &= ~AVSEEK_FORCE;
|
||||
uint64_t base = 0;
|
||||
switch (whence) {
|
||||
case SEEK_SET: break;
|
||||
case SEEK_CUR: base = s->pos; break;
|
||||
case SEEK_END: base = s->size; break;
|
||||
default: return AVERROR(EINVAL);
|
||||
}
|
||||
p = std::min<int64_t>(p, static_cast<int64_t>(s->size));
|
||||
s->pos = static_cast<uint64_t>(p);
|
||||
return p;
|
||||
const auto distance = offset < 0 ? uint64_t {0} - static_cast<uint64_t>(offset)
|
||||
: static_cast<uint64_t>(offset);
|
||||
if ((offset < 0 && distance > base) ||
|
||||
(offset >= 0 && distance > std::numeric_limits<int64_t>::max() - base)) {
|
||||
return AVERROR(EINVAL);
|
||||
}
|
||||
const auto position = offset < 0 ? base - distance : base + distance;
|
||||
if (!s->opened && !s->local_file.Seek(position)) {
|
||||
return AVERROR(EIO);
|
||||
}
|
||||
s->pos = position;
|
||||
return static_cast<int64_t>(position);
|
||||
}
|
||||
AvPlayerFileReplacement file;
|
||||
Common::File local_file;
|
||||
bool opened = false;
|
||||
uint64_t pos = 0;
|
||||
uint64_t size = 0;
|
||||
@@ -675,26 +704,18 @@ public:
|
||||
return static_cast<Source*>(opaque)->interrupt_io.load() ? 1 : 0;
|
||||
};
|
||||
raw->interrupt_callback.opaque = this;
|
||||
if (file.open != nullptr) {
|
||||
streamer = std::make_unique<FileStreamer>(file);
|
||||
if (!streamer->Init(path)) {
|
||||
avformat_free_context(raw);
|
||||
return AVPLAYER_ERROR_OPERATION_FAILED;
|
||||
}
|
||||
raw->pb = streamer->Context();
|
||||
if (auto rc = avformat_open_input(&raw, nullptr, nullptr, nullptr); rc < 0) {
|
||||
LOGF("\t avformat_open_input callback failed: %s\n", fferr(rc).c_str());
|
||||
if (const auto rc = avformat_open_input(&raw, file.open == nullptr ? path.c_str() : nullptr,
|
||||
nullptr, nullptr);
|
||||
rc < 0) {
|
||||
LOGF("\t avformat_open_input failed: %s path=%s\n", fferr(rc).c_str(), path.c_str());
|
||||
return AVPLAYER_ERROR_OPERATION_FAILED;
|
||||
}
|
||||
} else {
|
||||
auto real = LibKernel::FileSystem::GetRealFilename(std::string(path.c_str()));
|
||||
auto real_str = Common::PathToString(real);
|
||||
if (auto rc = avformat_open_input(&raw, real_str.c_str(), nullptr, nullptr); rc < 0) {
|
||||
LOGF("\t avformat_open_input failed: %s path=%s\n", fferr(rc).c_str(),
|
||||
real_str.c_str());
|
||||
return AVPLAYER_ERROR_OPERATION_FAILED;
|
||||
}
|
||||
}
|
||||
fmt = raw;
|
||||
if (auto rc = avformat_find_stream_info(fmt, nullptr); rc < 0) {
|
||||
LOGF("\t avformat_find_stream_info failed: %s\n", fferr(rc).c_str());
|
||||
|
||||
+14
-3
@@ -1,3 +1,4 @@
|
||||
#include "common/archive.h"
|
||||
#include "common/common.h"
|
||||
#include "common/dateTime.h"
|
||||
#include "common/debug.h"
|
||||
@@ -10,6 +11,7 @@
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <fmt/format.h>
|
||||
@@ -40,9 +42,9 @@ static std::string GetBuildString() {
|
||||
|
||||
static void PrintUsage() {
|
||||
::printf("%s\n", GetBuildString().c_str());
|
||||
::printf("kyty_emulator --game <dir|elf> [options]\n\n");
|
||||
::printf("kyty_emulator --game <dir|elf|zar> [options]\n\n");
|
||||
::printf("Options:\n");
|
||||
::printf(" --game <dir|elf> Game directory or ELF to load.\n");
|
||||
::printf(" --game <dir|elf|zar> Game directory, ELF, or ZArchive to load.\n");
|
||||
::printf(" --game-patch <json> ETAHen cheat file.\n");
|
||||
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
||||
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
||||
@@ -240,6 +242,14 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
if (Common::File::IsDirectoryExisting(path)) {
|
||||
options.app0_dir = path;
|
||||
options.elf = "/app0/eboot.bin";
|
||||
} else if (Common::IsSupportedArchive(path) && Common::File::IsFileExisting(path)) {
|
||||
const auto root = Common::MakeArchivePath(path);
|
||||
if (!Common::File::IsFileExisting(root / "eboot.bin")) {
|
||||
::printf("Archive does not contain eboot.bin: %s\n", value.c_str());
|
||||
return false;
|
||||
}
|
||||
options.app0_dir = root;
|
||||
options.elf = "/app0/eboot.bin";
|
||||
} else if (Common::File::IsFileExisting(path)) {
|
||||
options.app0_dir = path.parent_path();
|
||||
|
||||
@@ -249,7 +259,8 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
|
||||
options.elf = std::filesystem::path("/app0") / path.filename();
|
||||
} else {
|
||||
::printf("--game must point to an existing directory or ELF: %s\n", value.c_str());
|
||||
::printf("--game must point to an existing directory, ELF, or archive: %s\n",
|
||||
value.c_str());
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "--game-patch") {
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
#include "common/file.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "ArchiveTestFixture.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <zarchive/zarchivecommon.h>
|
||||
|
||||
#if !defined(_WIN32) || defined(_GLIBCXX_GCC_GTHR_POSIX_H)
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool value, const char *text) {
|
||||
if (!value) {
|
||||
std::fprintf(stderr, "ArchiveFileTests: 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_zarchive_test_!_" + 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;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void SetValue(std::vector<std::byte> &bytes, size_t offset, T value) {
|
||||
value = _ZARCHIVE::_store(value);
|
||||
Check(offset <= bytes.size() && sizeof(value) <= bytes.size() - offset,
|
||||
"patch lies inside fixture");
|
||||
std::memcpy(bytes.data() + offset, &value, sizeof(value));
|
||||
}
|
||||
|
||||
void SetMemberNameOffset(std::vector<std::byte> &bytes,
|
||||
const _ZARCHIVE::Footer &footer, uint32_t name_offset) {
|
||||
const auto entry_offset = footer.sectionFileTree.offset + sizeof(_ZARCHIVE::FileDirectoryEntry);
|
||||
uint32_t original = 0;
|
||||
std::memcpy(&original, bytes.data() + entry_offset, sizeof(original));
|
||||
SetValue(bytes, entry_offset, (_ZARCHIVE::_load(original) & 0x80000000u) | name_offset);
|
||||
}
|
||||
|
||||
template <typename Edit>
|
||||
std::filesystem::path MutateArchive(const std::filesystem::path &source,
|
||||
const char *name, Edit edit) {
|
||||
Common::File input(source, Common::File::Mode::Read);
|
||||
auto bytes = input.ReadWholeBuffer();
|
||||
_ZARCHIVE::Footer footer{};
|
||||
Check(bytes.size() >= sizeof(footer), "fixture has a footer");
|
||||
std::memcpy(&footer, bytes.data() + bytes.size() - sizeof(footer), sizeof(footer));
|
||||
_ZARCHIVE::Footer::Deserialize(&footer, &footer);
|
||||
edit(bytes, footer);
|
||||
_ZARCHIVE::Footer::Serialize(&footer, &footer);
|
||||
std::memcpy(bytes.data() + bytes.size() - sizeof(footer), &footer, sizeof(footer));
|
||||
const auto destination = source.parent_path() / name;
|
||||
Common::File output;
|
||||
Check(output.Create(destination), "create malformed archive fixture");
|
||||
uint32_t written = 0;
|
||||
output.Write(bytes.data(), static_cast<uint32_t>(bytes.size()), &written);
|
||||
Check(written == bytes.size(), "write malformed archive fixture");
|
||||
return destination;
|
||||
}
|
||||
|
||||
void CheckMalformedArchives(const std::filesystem::path &source,
|
||||
const std::vector<uint8_t> &payload) {
|
||||
const auto bad_name = MutateArchive(source, "bad-name.zar", [](auto &bytes, auto &footer) {
|
||||
SetMemberNameOffset(bytes, footer, static_cast<uint32_t>(footer.sectionNames.size));
|
||||
});
|
||||
Check(!Common::OpenArchive(bad_name), "reject name offset at the end of name table");
|
||||
|
||||
const auto bad_length = MutateArchive(source, "bad-length.zar", [](auto &bytes, auto &footer) {
|
||||
SetMemberNameOffset(bytes, footer, static_cast<uint32_t>(footer.sectionNames.size - 1));
|
||||
bytes[footer.sectionNames.offset + footer.sectionNames.size - 1] = std::byte{0x80};
|
||||
});
|
||||
Check(!Common::OpenArchive(bad_length), "reject truncated extended name length");
|
||||
|
||||
const auto bad_children = MutateArchive(source, "bad-children.zar", [](auto &bytes, auto &footer) {
|
||||
SetValue(bytes, footer.sectionFileTree.offset + sizeof(uint32_t), UINT32_MAX);
|
||||
});
|
||||
Check(!Common::OpenArchive(bad_children), "reject invalid directory children without throwing");
|
||||
|
||||
const auto bad_range = MutateArchive(source, "bad-range.zar", [](auto &, auto &footer) {
|
||||
footer.sectionCompressedData.offset = std::numeric_limits<uint64_t>::max();
|
||||
});
|
||||
Check(!Common::OpenArchive(bad_range), "reject overflowing footer section range");
|
||||
|
||||
const auto truncated = source.parent_path() / "truncated.zar";
|
||||
Check(std::filesystem::copy_file(source, truncated), "copy truncated archive fixture");
|
||||
std::filesystem::resize_file(truncated, std::filesystem::file_size(truncated) - 1);
|
||||
Check(!Common::OpenArchive(truncated), "reject missing footer byte");
|
||||
|
||||
const auto corrupted = MutateArchive(source, "corrupt-data.zar", [](auto &bytes, auto &footer) {
|
||||
_ZARCHIVE::CompressionOffsetRecord record{};
|
||||
std::memcpy(&record, bytes.data() + footer.sectionOffsetRecords.offset, sizeof(record));
|
||||
_ZARCHIVE::CompressionOffsetRecord::Deserialize(&record, 1, &record);
|
||||
const auto second_block = footer.sectionCompressedData.offset + record.baseOffset + record.size[0] + 1;
|
||||
std::fill_n(bytes.data() + second_block, record.size[1] + 1, std::byte{});
|
||||
});
|
||||
Common::File damaged(Common::MakeArchivePath(corrupted, "assets/subdir/data.bin"),
|
||||
Common::File::Mode::Read);
|
||||
Check(!damaged.IsInvalid(), "open archive with damaged compressed data");
|
||||
std::vector<uint8_t> bytes(payload.size());
|
||||
uint32_t read = 0;
|
||||
damaged.Read(bytes.data(), static_cast<uint32_t>(bytes.size()), &read);
|
||||
Check(read > 0 && read < 64 * 1024 && damaged.Tell() == read &&
|
||||
std::equal(bytes.begin(), bytes.begin() + read, payload.begin()),
|
||||
"report the valid prefix before a corrupt compressed block");
|
||||
Check(damaged.Seek(2 * 64 * 1024), "seek past corrupt compressed block");
|
||||
damaged.Read(bytes.data(), 97, &read);
|
||||
Check(read == 97 && std::equal(bytes.begin(), bytes.begin() + read,
|
||||
payload.begin() + 2 * 64 * 1024),
|
||||
"recover by reading an unaffected compressed block");
|
||||
}
|
||||
|
||||
#if !defined(_WIN32) || defined(_GLIBCXX_GCC_GTHR_POSIX_H)
|
||||
void CheckCancellation(const std::filesystem::path &path,
|
||||
const std::vector<uint8_t> &payload) {
|
||||
// Keep the file and destination on the parent thread: cancellation must only release the
|
||||
// reader thread's request and staging buffer after the worker has finished using them.
|
||||
struct CancelledRead {
|
||||
Common::File file;
|
||||
std::vector<uint8_t> data;
|
||||
std::atomic_bool ready = false;
|
||||
std::atomic_bool start = false;
|
||||
uint32_t bytes = 0;
|
||||
} read;
|
||||
Check(read.file.Open(path, Common::File::Mode::Read), "open cancellation fixture");
|
||||
read.data.resize(payload.size());
|
||||
pthread_t thread;
|
||||
Check(pthread_create(&thread, nullptr, [](void *opaque) -> void * {
|
||||
auto &read = *static_cast<CancelledRead *>(opaque);
|
||||
read.ready = true;
|
||||
while (!read.start.load()) {}
|
||||
read.file.Read(read.data.data(), static_cast<uint32_t>(read.data.size()), &read.bytes);
|
||||
pthread_testcancel();
|
||||
return nullptr;
|
||||
}, &read) == 0, "start cancellable archive reader");
|
||||
while (!read.ready.load()) {}
|
||||
Check(pthread_cancel(thread) == 0, "queue cancellation before the archive worker wait");
|
||||
read.start = true;
|
||||
void *result = nullptr;
|
||||
Check(pthread_join(thread, &result) == 0 && result == PTHREAD_CANCELED,
|
||||
"restore cancellation after the archive read");
|
||||
Check(read.bytes == payload.size() && read.data == payload,
|
||||
"finish worker reads and copies before releasing a cancelled caller");
|
||||
|
||||
int previous = PTHREAD_CANCEL_ENABLE;
|
||||
int observed = PTHREAD_CANCEL_ENABLE;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &previous);
|
||||
read.file.Seek(0);
|
||||
read.file.Read(read.data.data(), 97, &read.bytes);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &observed);
|
||||
pthread_setcancelstate(previous, nullptr);
|
||||
Check(observed == PTHREAD_CANCEL_DISABLE && read.bytes == 97,
|
||||
"preserve a caller's disabled cancellation state");
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TempDirectory temporary;
|
||||
const auto archive_path =
|
||||
temporary.Path() / std::filesystem::path(u8"game-テスト.zar");
|
||||
|
||||
std::vector<uint8_t> payload(3 * 1024 * 1024 + 37);
|
||||
for (size_t index = 0; index < payload.size(); index++) {
|
||||
payload[index] = static_cast<uint8_t>((index * 37 + 11) & 0xff);
|
||||
}
|
||||
Check(ArchiveTests::CreateArchive(archive_path, payload), "create archive fixture");
|
||||
|
||||
const auto native_file = Common::File::GetInfo(archive_path);
|
||||
const auto native_directory = Common::File::GetInfo(temporary.Path() / "");
|
||||
Check(native_file && native_file->is_file &&
|
||||
native_file->size == std::filesystem::file_size(archive_path),
|
||||
"query native file type and size together");
|
||||
Check(native_directory && !native_directory->is_file && native_directory->size == 0,
|
||||
"query native directory with trailing separator");
|
||||
Check(!Common::File::GetInfo(archive_path / "") && Common::File::Size(archive_path / "") == 0 &&
|
||||
!Common::File::GetInfo(temporary.Path() / "missing.bin"),
|
||||
"reject missing native paths and files with a trailing separator");
|
||||
|
||||
const auto root = Common::MakeArchivePath(archive_path);
|
||||
auto reader_pin = Common::OpenArchive(archive_path);
|
||||
Check(reader_pin && Common::OpenArchive(root) == reader_pin,
|
||||
"mounted archive pin reuses the same reader and index");
|
||||
const auto archive_file = Common::File::GetInfo(root / "assets/subdir/data.bin");
|
||||
const auto archive_directory = Common::File::GetInfo(root / "assets/");
|
||||
Check(archive_file && archive_file->is_file && archive_file->size == payload.size(),
|
||||
"query archive member type and size together");
|
||||
Check(archive_directory && !archive_directory->is_file && archive_directory->size == 0 &&
|
||||
!Common::File::GetInfo(root / "missing.bin"),
|
||||
"distinguish archive directories from missing members");
|
||||
Check(Common::IsSupportedArchive(archive_path) &&
|
||||
Common::IsSupportedArchive("GAME.ZAR") &&
|
||||
!Common::IsSupportedArchive("game.zip"),
|
||||
"select an archive backend by supported extension");
|
||||
Check(!Common::IsArchivePath(temporary.Path() / "ordinary!" / "asset.bin"),
|
||||
"ordinary host paths with exclamation marks stay host paths");
|
||||
Check(Common::IsArchivePath(root), "recognize virtual archive root");
|
||||
Check(Common::GetArchiveHostPath(root) == archive_path,
|
||||
"recover host archive path");
|
||||
Check(Common::File::IsDirectoryExisting(root), "find archive root directory");
|
||||
Check(Common::File::IsFileExisting(root / "EBOOT.BIN"),
|
||||
"lookup is case insensitive");
|
||||
Check(!Common::File::IsFileExisting(root / "missing.bin"),
|
||||
"reject missing member");
|
||||
Check(Common::IsArchivePath(root / "../outside.bin"),
|
||||
"retain archive classification for invalid traversal");
|
||||
Check(!Common::File::IsFileExisting(root / "../outside.bin"),
|
||||
"reject leading parent traversal");
|
||||
Check(Common::File::IsFileExisting(root / "assets/../eboot.bin"),
|
||||
"normalize internal parent traversal");
|
||||
Check(Common::File::IsFileExisting(root / ArchiveTests::LongFilename),
|
||||
"decode extended-length archive names");
|
||||
Check(Common::File::IsFileExisting(root / Common::PathFromUtf8(ArchiveTests::UnicodeFilename)),
|
||||
"lookup Unicode archive member");
|
||||
|
||||
const auto entries = Common::File::GetDirEntries(root);
|
||||
Check(std::ranges::any_of(entries,
|
||||
[](const auto &entry) {
|
||||
return entry.is_file && entry.name == "eboot.bin";
|
||||
}),
|
||||
"enumerate root file");
|
||||
Check(std::ranges::any_of(entries,
|
||||
[](const auto &entry) {
|
||||
return !entry.is_file && entry.name == "assets";
|
||||
}),
|
||||
"enumerate root directory");
|
||||
Check(std::ranges::any_of(entries, [](const auto &entry) {
|
||||
return entry.is_file && entry.name == ArchiveTests::UnicodeFilename;
|
||||
}), "enumerate Unicode member names as UTF-8");
|
||||
|
||||
const auto data_path = root / "assets/subdir/data.bin";
|
||||
Check(Common::File::Size(data_path) == payload.size(), "report member size");
|
||||
Common::File file(data_path, Common::File::Mode::Read);
|
||||
Check(!file.IsInvalid(), "open archive member");
|
||||
Check(file.Remaining() == payload.size(), "report initial remaining bytes");
|
||||
Check(file.Seek(64 * 1024 - 19), "seek before compression block boundary");
|
||||
std::vector<uint8_t> slice(97);
|
||||
uint32_t bytes_read = 0;
|
||||
file.Read(slice.data(), static_cast<uint32_t>(slice.size()), &bytes_read);
|
||||
Check(bytes_read == slice.size(), "read across compression block boundary");
|
||||
Check(
|
||||
std::equal(slice.begin(), slice.end(), payload.begin() + 64 * 1024 - 19),
|
||||
"preserve data across compression block boundary");
|
||||
|
||||
Check(file.Seek(payload.size() - 9), "seek near member end");
|
||||
file.Read(slice.data(), static_cast<uint32_t>(slice.size()), &bytes_read);
|
||||
Check(bytes_read == 9, "truncate read at member end");
|
||||
Check(file.IsEOF(), "report member end of file");
|
||||
Check(file.Seek(payload.size() + 100), "allow seek past member end");
|
||||
Check(file.Remaining() == 0, "report no bytes remaining past member end");
|
||||
file.Read(slice.data(), static_cast<uint32_t>(slice.size()), &bytes_read);
|
||||
Check(bytes_read == 0, "read past member end returns no data");
|
||||
|
||||
Check(file.Seek(0), "seek to member start");
|
||||
std::vector<uint8_t> full(payload.size());
|
||||
file.Read(full.data(), static_cast<uint32_t>(full.size()), &bytes_read);
|
||||
Check(bytes_read == full.size(), "read a multi-chunk member");
|
||||
Check(full == payload, "preserve a multi-chunk member");
|
||||
|
||||
std::atomic_bool concurrent_reads_ok = true;
|
||||
std::vector<std::thread> readers;
|
||||
for (uint32_t worker = 0; worker < 8; worker++) {
|
||||
readers.emplace_back([&, worker] {
|
||||
Common::File concurrent_file(data_path, Common::File::Mode::Read);
|
||||
std::array<uint8_t, 4096> concurrent_data{};
|
||||
for (uint32_t iteration = 0; iteration < 16; iteration++) {
|
||||
const auto offset =
|
||||
static_cast<size_t>(worker * 7919 + iteration * 12347) %
|
||||
(payload.size() - concurrent_data.size());
|
||||
uint32_t concurrent_bytes_read = 0;
|
||||
if (concurrent_file.IsInvalid() || !concurrent_file.Seek(offset)) {
|
||||
concurrent_reads_ok = false;
|
||||
return;
|
||||
}
|
||||
concurrent_file.Read(concurrent_data.data(), concurrent_data.size(),
|
||||
&concurrent_bytes_read);
|
||||
if (concurrent_bytes_read != concurrent_data.size() ||
|
||||
!std::equal(concurrent_data.begin(), concurrent_data.end(),
|
||||
payload.begin() +
|
||||
static_cast<std::ptrdiff_t>(offset))) {
|
||||
concurrent_reads_ok = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto &reader : readers) {
|
||||
reader.join();
|
||||
}
|
||||
Check(concurrent_reads_ok, "read concurrently through independent file handles");
|
||||
|
||||
#if !defined(_WIN32) || defined(_GLIBCXX_GCC_GTHR_POSIX_H)
|
||||
CheckCancellation(data_path, payload);
|
||||
#endif
|
||||
|
||||
#if !defined(_WIN32)
|
||||
// Guest threads have much smaller stacks than std::thread's default stack.
|
||||
pthread_attr_t attributes;
|
||||
Check(pthread_attr_init(&attributes) == 0 &&
|
||||
pthread_attr_setstacksize(&attributes, 64 * 1024) == 0,
|
||||
"configure a guest-sized stack");
|
||||
struct SmallStackRead {
|
||||
const std::filesystem::path &path;
|
||||
const std::vector<uint8_t> &expected;
|
||||
bool passed = false;
|
||||
} small_stack{data_path, payload};
|
||||
pthread_t thread;
|
||||
Check(pthread_create(&thread, &attributes, [](void *opaque) -> void * {
|
||||
auto &test = *static_cast<SmallStackRead *>(opaque);
|
||||
Common::File input(test.path, Common::File::Mode::Read);
|
||||
auto data = input.ReadWholeBuffer();
|
||||
test.passed = data.size() == test.expected.size() &&
|
||||
std::memcmp(data.data(), test.expected.data(), data.size()) == 0;
|
||||
return nullptr;
|
||||
}, &small_stack) == 0, "start guest-sized-stack reader");
|
||||
pthread_attr_destroy(&attributes);
|
||||
Check(pthread_join(thread, nullptr) == 0 && small_stack.passed,
|
||||
"decompress a multi-block file on a guest-sized stack");
|
||||
#endif
|
||||
|
||||
Common::File metadata(root / "sce_sys/param.json", Common::File::Mode::Read);
|
||||
const auto metadata_data = metadata.ReadWholeBuffer();
|
||||
const std::string metadata_text(
|
||||
reinterpret_cast<const char *>(metadata_data.data()),
|
||||
metadata_data.size());
|
||||
Check(metadata_text.find("Archive Test") != std::string::npos,
|
||||
"read metadata member");
|
||||
|
||||
Common::File writable;
|
||||
Check(!writable.Open(data_path, Common::File::Mode::ReadWrite),
|
||||
"reject writable member open");
|
||||
Check(!Common::File::DeleteFile(data_path), "reject member deletion");
|
||||
Check(!Common::File::CreateDirectory(root / "new-dir"),
|
||||
"reject member directory creation");
|
||||
Check(!file.Truncate(0) && !file.Unlink(), "reject descriptor mutations");
|
||||
const auto copied = temporary.Path() / "copied.bin";
|
||||
Common::File host_fixture;
|
||||
Check(host_fixture.Create(copied), "create host mutation fixture");
|
||||
host_fixture.Close();
|
||||
Check(!Common::File::CopyFile(data_path, copied) &&
|
||||
!Common::File::CopyFile(copied, data_path) &&
|
||||
!Common::File::RenameFile(data_path, copied) &&
|
||||
!Common::File::RenameFile(copied, data_path),
|
||||
"reject unsupported copy and rename operations involving archives");
|
||||
|
||||
CheckMalformedArchives(archive_path, payload);
|
||||
|
||||
file.Close();
|
||||
metadata.Close();
|
||||
Check(Common::OpenArchive(archive_path) == reader_pin,
|
||||
"mount pin retains the archive reader after the last member closes");
|
||||
reader_pin.reset();
|
||||
std::error_code remove_error;
|
||||
Check(std::filesystem::remove(archive_path, remove_error) && !remove_error,
|
||||
"release archive handle after the final reader closes");
|
||||
const std::array<uint8_t, 3> replacement{42, 7, 19};
|
||||
Check(ArchiveTests::CreateArchive(archive_path, replacement), "replace a closed archive");
|
||||
Common::File replaced(data_path, Common::File::Mode::Read);
|
||||
const auto replaced_data = replaced.ReadWholeBuffer();
|
||||
Check(replaced_data.size() == replacement.size() &&
|
||||
std::memcmp(replaced_data.data(), replacement.data(), replacement.size()) == 0,
|
||||
"reopening after the final reader closes sees replacement contents");
|
||||
|
||||
std::printf("ArchiveFileTests: all cases passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <zarchive/zarchivewriter.h>
|
||||
|
||||
namespace ArchiveTests {
|
||||
|
||||
inline constexpr std::string_view Eboot = "ELF fixture";
|
||||
inline constexpr std::string_view Param =
|
||||
R"({"titleId":"TEST00001","titleName":"Archive Test"})";
|
||||
inline constexpr std::string_view UnicodeFilename =
|
||||
"asset-\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e.bin";
|
||||
inline const std::string LongFilename = std::string(160, 'a') + ".bin";
|
||||
|
||||
// Both Common::File and guest filesystem tests read the same archive layout.
|
||||
inline bool CreateArchive(const std::filesystem::path &path,
|
||||
std::span<const uint8_t> payload) {
|
||||
struct Output {
|
||||
std::filesystem::path path;
|
||||
std::ofstream stream;
|
||||
bool good = true;
|
||||
} output{.path = path};
|
||||
{
|
||||
ZArchiveWriter writer(
|
||||
[](int32_t, void *opaque) {
|
||||
auto &out = *static_cast<Output *>(opaque);
|
||||
out.stream.open(out.path, std::ios::binary | std::ios::trunc);
|
||||
out.good = out.stream.is_open();
|
||||
},
|
||||
[](const void *data, size_t size, void *opaque) {
|
||||
auto &out = *static_cast<Output *>(opaque);
|
||||
out.stream.write(static_cast<const char *>(data),
|
||||
static_cast<std::streamsize>(size));
|
||||
out.good = out.good && out.stream.good();
|
||||
},
|
||||
&output);
|
||||
if (!output.good || !writer.MakeDir("sce_sys", true) ||
|
||||
!writer.MakeDir("assets/subdir", true)) {
|
||||
return false;
|
||||
}
|
||||
const auto add = [&](std::string_view name, const void *data, size_t size) {
|
||||
if (!writer.StartNewFile(name.data())) {
|
||||
return false;
|
||||
}
|
||||
writer.AppendData(data, size);
|
||||
return true;
|
||||
};
|
||||
if (!add("eboot.bin", Eboot.data(), Eboot.size()) ||
|
||||
!add("sce_sys/param.json", Param.data(), Param.size()) ||
|
||||
!add("assets/subdir/data.bin", payload.data(), payload.size()) ||
|
||||
!add(UnicodeFilename, Eboot.data(), Eboot.size()) ||
|
||||
!add(LongFilename, Eboot.data(), Eboot.size())) {
|
||||
return false;
|
||||
}
|
||||
writer.Finalize();
|
||||
}
|
||||
output.stream.close();
|
||||
return output.good && !output.stream.fail();
|
||||
}
|
||||
|
||||
} // namespace ArchiveTests
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "libs/avPlayer.cpp"
|
||||
#include "common/archive.h"
|
||||
#include "ArchiveTestFixture.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
|
||||
namespace {
|
||||
std::filesystem::path content_root;
|
||||
}
|
||||
|
||||
namespace Libs::LibKernel {
|
||||
int KYTY_SYSV_ABI PthreadCreate(Pthread*, const PthreadAttr*, pthread_entry_func_t, void*, const char*) {
|
||||
std::abort();
|
||||
}
|
||||
int KYTY_SYSV_ABI PthreadJoin(Pthread, void**) { std::abort(); }
|
||||
namespace FileSystem {
|
||||
std::filesystem::path GetRealFilename(const std::string& path) {
|
||||
return path.starts_with("/app0/") ? content_root / path.substr(6) : std::filesystem::path {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
using namespace Libs::Audio::AvPlayer;
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "AvPlayerFileTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
void CheckStream(FileStreamer& stream, const std::vector<uint8_t>& data) {
|
||||
auto* io = stream.Context();
|
||||
Check(io->seek(io->opaque, 0, AVSEEK_SIZE) == static_cast<int64_t>(data.size()), "query size");
|
||||
Check(io->seek(io->opaque, 65531, SEEK_SET | AVSEEK_FORCE) == 65531, "seek across block boundary");
|
||||
std::array<uint8_t, 32> bytes {};
|
||||
Check(io->read_packet(io->opaque, bytes.data(), bytes.size()) == bytes.size() &&
|
||||
std::equal(bytes.begin(), bytes.end(), data.begin() + 65531), "read across block boundary");
|
||||
Check(io->seek(io->opaque, -16, SEEK_CUR) == 65547, "seek relative to cursor");
|
||||
Check(io->seek(io->opaque, INT64_MIN, SEEK_CUR) < 0 &&
|
||||
io->seek(io->opaque, INT64_MAX, SEEK_END) < 0 &&
|
||||
io->seek(io->opaque, 0, 12345) < 0, "reject invalid and overflowing seeks");
|
||||
Check(io->seek(io->opaque, 0, SEEK_CUR) == 65547, "rejected seeks preserve the cursor");
|
||||
Check(io->seek(io->opaque, -7, SEEK_END) == static_cast<int64_t>(data.size() - 7), "seek near end");
|
||||
Check(io->read_packet(io->opaque, bytes.data(), bytes.size()) == 7 &&
|
||||
std::equal(bytes.begin(), bytes.begin() + 7, data.end() - 7), "short read at end");
|
||||
Check(io->read_packet(io->opaque, bytes.data(), bytes.size()) == AVERROR_EOF, "report EOF");
|
||||
Check(io->seek(io->opaque, 11, SEEK_END) == static_cast<int64_t>(data.size() + 11) &&
|
||||
io->read_packet(io->opaque, bytes.data(), bytes.size()) == AVERROR_EOF, "read beyond end");
|
||||
Check(io->seek(io->opaque, 0, SEEK_SET) == 0, "rewind");
|
||||
std::vector<uint8_t> buffered(data.size());
|
||||
Check(avio_read(io, buffered.data(), static_cast<int>(buffered.size())) == buffered.size() &&
|
||||
buffered == data, "FFmpeg buffered reads preserve content");
|
||||
Check(avio_seek(io, 123, SEEK_SET) == 123 &&
|
||||
avio_read(io, bytes.data(), bytes.size()) == bytes.size() &&
|
||||
std::equal(bytes.begin(), bytes.end(), data.begin() + 123), "FFmpeg seeks reset buffered reads");
|
||||
}
|
||||
|
||||
struct Callbacks {
|
||||
const std::vector<uint8_t>& data;
|
||||
unsigned opens = 0;
|
||||
unsigned closes = 0;
|
||||
bool premature_eof = false;
|
||||
};
|
||||
int KYTY_SYSV_ABI Open(void* object, const char*) {
|
||||
++static_cast<Callbacks*>(object)->opens;
|
||||
return 0;
|
||||
}
|
||||
int KYTY_SYSV_ABI Close(void* object) {
|
||||
++static_cast<Callbacks*>(object)->closes;
|
||||
return 0;
|
||||
}
|
||||
int KYTY_SYSV_ABI Read(void* object, uint8_t* buffer, uint64_t offset, uint32_t size) {
|
||||
auto& state = *static_cast<Callbacks*>(object);
|
||||
if (state.premature_eof) {
|
||||
return 0;
|
||||
}
|
||||
const auto bytes = std::min<uint64_t>(size, state.data.size() - offset);
|
||||
std::memcpy(buffer, state.data.data() + offset, bytes);
|
||||
return static_cast<int>(bytes);
|
||||
}
|
||||
uint64_t KYTY_SYSV_ABI Size(void* object) { return static_cast<Callbacks*>(object)->data.size(); }
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
("kyty_avio_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
Check(std::filesystem::create_directories(root), "create fixture directory");
|
||||
std::vector<uint8_t> payload(128 * 1024 + 37);
|
||||
for (size_t i = 0; i < payload.size(); ++i) {
|
||||
payload[i] = static_cast<uint8_t>(i * 37);
|
||||
}
|
||||
{
|
||||
Common::File output;
|
||||
Check(output.Create(root / "movie.bin"), "create native fixture");
|
||||
output.Write(payload.data(), static_cast<uint32_t>(payload.size()));
|
||||
}
|
||||
Check(ArchiveTests::CreateArchive(root / "game.zar", payload), "create archive fixture");
|
||||
content_root = root;
|
||||
{
|
||||
FileStreamer native({});
|
||||
Check(native.Init("/app0/movie.bin"), "open native stream");
|
||||
CheckStream(native, payload);
|
||||
}
|
||||
content_root = Common::MakeArchivePath(root / "game.zar");
|
||||
{
|
||||
FileStreamer archive({});
|
||||
Check(archive.Init("/app0/assets/subdir/data.bin"), "open archived stream");
|
||||
CheckStream(archive, payload);
|
||||
}
|
||||
Callbacks callbacks {payload};
|
||||
const AvPlayerFileReplacement replacement {&callbacks, Open, Close, Read, Size};
|
||||
{
|
||||
FileStreamer callback(replacement);
|
||||
Check(callback.Init("callback"), "open guest callback stream");
|
||||
CheckStream(callback, payload);
|
||||
callbacks.premature_eof = true;
|
||||
auto* io = callback.Context();
|
||||
Check(io->seek(io->opaque, 0, SEEK_SET) == 0, "rewind callback");
|
||||
uint8_t byte = 0;
|
||||
Check(io->read_packet(io->opaque, &byte, 1) == AVERROR(EIO), "reject premature EOF");
|
||||
}
|
||||
Check(callbacks.opens == 1 && callbacks.closes == 1, "close callback exactly once");
|
||||
{
|
||||
FileStreamer missing({});
|
||||
Check(!missing.Init("/unmounted/movie.bin"), "reject unmapped media");
|
||||
}
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(root, error);
|
||||
Check(!error, "release all archive handles");
|
||||
std::puts("AvPlayerFileTests: all cases passed");
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
#include <SDL3/SDL_main.h>
|
||||
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/file.h"
|
||||
#include "ArchiveTestFixture.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/subsystems.h"
|
||||
#include "common/threads.h"
|
||||
@@ -214,6 +216,119 @@ void CheckUnmappedPaths(const std::filesystem::path &root) {
|
||||
FileSystem::Umount("/app0");
|
||||
}
|
||||
|
||||
void CheckArchiveMount(const std::filesystem::path &root) {
|
||||
const auto archive = root / u8"game-日本語.zar";
|
||||
std::vector<uint8_t> payload(2 * 64 * 1024 + 33);
|
||||
for (size_t i = 0; i < payload.size(); ++i) {
|
||||
payload[i] = static_cast<uint8_t>(i * 37 + 11);
|
||||
}
|
||||
Check(ArchiveTests::CreateArchive(archive, payload), "create mounted archive fixture");
|
||||
const auto archive_root = Common::MakeArchivePath(archive);
|
||||
const auto host_member = archive_root / "assets/subdir/data.bin";
|
||||
constexpr char GuestMember[] = "/app0/assets/subdir/data.bin";
|
||||
FileSystem::Mount(archive_root, "/app0");
|
||||
Check(FileSystem::GetRealFilename(GuestMember) == host_member &&
|
||||
FileSystem::GetRealFilename("/app01/eboot.bin").empty(),
|
||||
"resolve archive mounts at path-component boundaries");
|
||||
Check(FileSystem::KernelOpen("/app0/../outside.bin", 0, 0) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_ENOENT &&
|
||||
FileSystem::KernelOpen("/app0/missing.bin", 0, 0) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_ENOENT,
|
||||
"reject traversal outside an archive and missing members");
|
||||
|
||||
FileSystem::FileStat path_stat{}, descriptor_stat{};
|
||||
Check(FileSystem::KernelStat(GuestMember, &path_stat) == OK &&
|
||||
path_stat.st_size == payload.size() &&
|
||||
path_stat.st_size == Common::File::Size(host_member) &&
|
||||
FileSystem::KernelCheckReachability(GuestMember) == OK,
|
||||
"archive path stat and reachability agree with Common::File");
|
||||
const int fd = FileSystem::KernelOpen("/app0/ASSETS/subdir/DATA.BIN", 0, 0);
|
||||
Check(fd >= 3 && FileSystem::KernelFstat(fd, &descriptor_stat) == OK &&
|
||||
descriptor_stat.st_size == path_stat.st_size &&
|
||||
descriptor_stat.st_mode == path_stat.st_mode,
|
||||
"case-insensitive archive open and descriptor stat agree");
|
||||
std::array<uint8_t, 97> bytes{};
|
||||
constexpr int64_t Offset = 64 * 1024 - 19;
|
||||
Check(FileSystem::KernelPread(fd, bytes.data(), bytes.size(), Offset) == bytes.size() &&
|
||||
std::equal(bytes.begin(), bytes.end(), payload.begin() + Offset) &&
|
||||
FileSystem::KernelLseek(fd, 0, 1) == 0,
|
||||
"archive pread crosses a compression block without moving position");
|
||||
Check(FileSystem::KernelLseek(fd, Offset, 0) == Offset &&
|
||||
FileSystem::KernelRead(fd, bytes.data(), bytes.size()) == bytes.size() &&
|
||||
std::equal(bytes.begin(), bytes.end(), payload.begin() + Offset) &&
|
||||
FileSystem::KernelLseek(fd, 0, 1) == Offset + bytes.size(),
|
||||
"archive seek and sequential read share descriptor position");
|
||||
Check(FileSystem::KernelLseek(fd, -9, 2) == payload.size() - 9 &&
|
||||
FileSystem::KernelRead(fd, bytes.data(), bytes.size()) == 9 &&
|
||||
std::equal(bytes.begin(), bytes.begin() + 9, payload.end() - 9) &&
|
||||
FileSystem::KernelRead(fd, bytes.data(), bytes.size()) == 0,
|
||||
"archive reads stop at the member boundary");
|
||||
Check(FileSystem::KernelWrite(fd, bytes.data(), 1) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EBADF &&
|
||||
FileSystem::KernelPwrite(fd, bytes.data(), 1, 0) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EBADF &&
|
||||
FileSystem::KernelFtruncate(fd, 0) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EBADF,
|
||||
"archive read-only descriptors reject write and truncate");
|
||||
|
||||
const auto unicode_guest = std::string("/app0/") + std::string(ArchiveTests::UnicodeFilename);
|
||||
const int unicode = FileSystem::KernelOpen(unicode_guest.c_str(), 0, 0);
|
||||
Check(unicode >= 3 && FileSystem::KernelRead(unicode, bytes.data(), bytes.size()) ==
|
||||
ArchiveTests::Eboot.size() &&
|
||||
std::memcmp(bytes.data(), ArchiveTests::Eboot.data(), ArchiveTests::Eboot.size()) == 0,
|
||||
"open and read a Unicode archive member");
|
||||
Check(FileSystem::KernelClose(unicode) == OK, "close Unicode archive member");
|
||||
|
||||
const int directory = FileSystem::KernelOpen("/app0/", 0x00020000, 0);
|
||||
Check(directory >= 3, "open mounted archive directory");
|
||||
std::array<char, 512> block{};
|
||||
const auto expected_entries = Common::File::GetDirEntries(archive_root);
|
||||
size_t entries_seen = 0;
|
||||
for (;;) {
|
||||
const int length = FileSystem::KernelGetdents(directory, block.data(), block.size());
|
||||
Check(length >= 0 && length <= block.size(), "read archive directory records");
|
||||
if (length == 0) {
|
||||
break;
|
||||
}
|
||||
for (size_t offset = 0; offset < static_cast<size_t>(length);) {
|
||||
uint16_t record_length = 0;
|
||||
Check(length - offset >= 8, "archive directory record header fits");
|
||||
std::memcpy(&record_length, block.data() + offset + 4, sizeof(record_length));
|
||||
const auto name_length = static_cast<uint8_t>(block[offset + 7]);
|
||||
Check(record_length >= 9 + name_length && record_length <= length - offset,
|
||||
"archive directory record fits");
|
||||
const std::string_view name(block.data() + offset + 8, name_length);
|
||||
Check(std::any_of(expected_entries.begin(), expected_entries.end(), [&](const auto &entry) {
|
||||
return entry.name == name && block[offset + 6] == (entry.is_file ? 8 : 4);
|
||||
}), "guest directory entry matches Common::File name and type");
|
||||
++entries_seen;
|
||||
offset += record_length;
|
||||
}
|
||||
}
|
||||
Check(entries_seen == expected_entries.size(), "guest enumerates every archive directory entry");
|
||||
Check(FileSystem::KernelClose(directory) == OK, "close archive directory");
|
||||
|
||||
for (const int flags : {1, 2, 0x0200, 0x0400}) {
|
||||
Check(FileSystem::KernelOpen(GuestMember, flags, 0777) ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EROFS,
|
||||
"archive rejects write, create and truncate open flags");
|
||||
}
|
||||
Check(FileSystem::KernelUnlink(GuestMember) == Libs::LibKernel::KERNEL_ERROR_EROFS &&
|
||||
FileSystem::KernelMkdir("/app0/new-dir", 0777) == Libs::LibKernel::KERNEL_ERROR_EROFS &&
|
||||
FileSystem::KernelRmdir("/app0/assets") == Libs::LibKernel::KERNEL_ERROR_EROFS &&
|
||||
FileSystem::KernelRename(GuestMember, "/app0/renamed.bin") ==
|
||||
Libs::LibKernel::KERNEL_ERROR_EROFS,
|
||||
"archive mount rejects path mutations");
|
||||
FileSystem::Umount("/app0");
|
||||
Check(FileSystem::GetRealFilename(GuestMember).empty() &&
|
||||
FileSystem::KernelOpen(GuestMember, 0, 0) == Libs::LibKernel::KERNEL_ERROR_ENOENT &&
|
||||
FileSystem::KernelLseek(fd, 0, 0) == 0 &&
|
||||
FileSystem::KernelRead(fd, bytes.data(), bytes.size()) == bytes.size() &&
|
||||
std::equal(bytes.begin(), bytes.end(), payload.begin()),
|
||||
"unmount hides archive paths while open descriptors retain the reader");
|
||||
Check(FileSystem::KernelClose(fd) == OK, "close last archive descriptor");
|
||||
}
|
||||
|
||||
void CheckUnicodePaths(const std::filesystem::path &root) {
|
||||
constexpr std::string_view HostDirectory =
|
||||
"Test\xc3\xa9-\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e";
|
||||
@@ -567,7 +682,7 @@ void CheckSocketWakeup() {
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
int main(int, char**) {
|
||||
Common::InitializeThreads();
|
||||
Common::Subsystems subsystems;
|
||||
subsystems.Initialize<Config::Lifecycle>();
|
||||
@@ -589,6 +704,7 @@ int main() {
|
||||
FileSystem::Initialize();
|
||||
CheckMountRoot(temporary.Path());
|
||||
CheckUnmappedPaths(temporary.Path());
|
||||
CheckArchiveMount(temporary.Path());
|
||||
CheckUnicodePaths(temporary.Path());
|
||||
CheckUnicodeLogPath(temporary.Path());
|
||||
CheckDirectoryStream(temporary.Path());
|
||||
|
||||
Reference in New Issue
Block a user