diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b84547e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +**IMPORTANT: This project ONLY builds on Windows with MSVC toolchain. Do not attempt to build on Linux.** + +This project uses CMake with vcpkg for dependency management and requires Visual Studio 2017+ on Windows: + +```bash +# Configure build (requires VCPKG_ROOT environment variable) +cmake --preset x64-release # or x86-release + +# Build all modules +cmake --build build/x64-release + +# Run tests +cd build/x64-release && ctest + +# Package for distribution +cd build/x64-release && cpack --config CPackConfig.cmake -C RelWithDebInfo +``` + +Build presets available: `x64-release`, `x64-debug`, `x86-release`, `x86-debug` + +## Development Guidelines + +This project uses **C++23** and follows KISS (Keep It Simple, Stupid) and DRY (Don't Repeat Yourself) principles. + +**Avoid magic numbers** - use named constants instead. + +## Architecture Overview + +This project implements Observer plugin modules for FAR Manager that handle exotic archive formats. The codebase follows a plugin architecture where each module implements the Observer API to support different archive formats. + +### Core Components + +- **API Layer** (`src/api.h`, `src/dll.cpp`): Implements the Observer plugin API with standard functions like `OpenStorage`, `CloseStorage`, `GetItem`, `ExtractItem` +- **Archive Wrapper** (`src/archive.h`, `src/archive.cpp`): Provides a unified interface that wraps format-specific extractors +- **Extractor Interface** (`src/modules/extractor.h`): Defines the abstract interface that all format extractors must implement + +### Module Structure + +Each supported format has its own module under `src/modules/`: +- `renpy/`: RenPy visual novel archives (.rpa files) with pickle support +- `zanzarah/`: Zanzarah game archives (.pak files) +- `rpgmaker/`: RPG Maker archives (in development) + +Each module contains: +- Format-specific implementation (e.g., `renpy.cpp`) +- Module definition file (`.def`) for DLL exports +- Configuration file (`observer_user.ini`) + +### Data Flow + +1. FAR Manager loads the module DLL via `LoadSubModule()` +2. `OpenStorage()` creates an archive wrapper with format-specific extractor +3. `PrepareFiles()` scans and indexes archive contents +4. `GetItem()` provides file metadata for FAR's file browser +5. `ExtractItem()` handles actual file extraction with progress callbacks + +### Testing Framework + +Located in `src/tests/` with a custom framework (`framework/observer.h`) that simulates the Observer API for testing archive operations without requiring FAR Manager. \ No newline at end of file diff --git a/src/archive.cpp b/src/archive.cpp index 62f6b11..aae595c 100644 --- a/src/archive.cpp +++ b/src/archive.cpp @@ -34,7 +34,6 @@ namespace archive throw std::runtime_error("Failed to open file"); } stream_->exceptions(std::ifstream::failbit | std::ifstream::badbit); - stream_->seekg(std::ssize(signature)); return extractor_->get_archive_info(data); } diff --git a/src/modules/renpy/renpy.cpp b/src/modules/renpy/renpy.cpp index 5a179f8..d3c9399 100644 --- a/src/modules/renpy/renpy.cpp +++ b/src/modules/renpy/renpy.cpp @@ -2,6 +2,7 @@ #include "pickle.h" #include +#include #include @@ -17,7 +18,7 @@ namespace extractor std::vector extractor::get_signature() noexcept { - const std::string str = "RPA-3.0 "; + const std::string str = "RPA-"; std::vector signature(str.size()); std::memcpy(signature.data(), str.data(), str.size()); return signature; @@ -43,11 +44,43 @@ namespace extractor return result; } + std::pair(int64_t, int64_t)> > parse_header( + std::ifstream &stream) + { + std::string version_check(3, '\0'); + stream.seekg(static_cast(extractor::get_signature().size())); + stream.read(version_check.data(), 3); + + if (version_check == "2.0") { + stream.seekg(static_cast(std::string("RPA-2.0 ").length())); + const auto index_offset = read_int64(stream); + return { + index_offset, [](int64_t offset, int64_t length) + { + return std::make_pair(offset, length); + } + }; + } + + if (version_check == "3.0") { + stream.seekg(static_cast(std::string("RPA-3.0 ").length())); + const auto index_offset = read_int64(stream); + const auto encryption_key = read_int64(stream); + return { + index_offset, [encryption_key](int64_t offset, int64_t length) + { + return std::make_pair(offset ^ encryption_key, length ^ encryption_key); + } + }; + } + + throw std::runtime_error("Unsupported RPA version"); + } + // ReSharper disable once CppMemberFunctionMayBeStatic std::vector > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static) { - const auto index_offset = read_int64(stream); - const auto encryption_key = read_int64(stream); + const auto [index_offset, decoder] = parse_header(stream); stream.seekg(index_offset); @@ -75,8 +108,8 @@ namespace extractor throw std::logic_error("Expected at least 2 elements in tuple"); } - const auto offset = props[0]->as_int64() ^ encryption_key; - const auto body_size = props[1]->as_int64() ^ encryption_key; + const auto [offset, body_size] = decoder(props[0]->as_int64(), props[1]->as_int64()); + auto header = std::string(); if (props.size() >= 3) { if (props[2]->get_type() != pickle::value::type::none) { diff --git a/src/modules/rpgmaker/rpgmaker.cpp b/src/modules/rpgmaker/rpgmaker.cpp index 71229f1..65c55c0 100644 --- a/src/modules/rpgmaker/rpgmaker.cpp +++ b/src/modules/rpgmaker/rpgmaker.cpp @@ -43,6 +43,8 @@ namespace extractor // ReSharper disable once CppMemberFunctionMayBeStatic std::vector > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static) { + stream.seekg(static_cast(get_signature().size())); + std::vector > files; const uint32_t magic = read_u32(stream) * 9 + 3; while (true) { diff --git a/src/modules/zanzarah/zanzarah.cpp b/src/modules/zanzarah/zanzarah.cpp index f91315e..ae619be 100644 --- a/src/modules/zanzarah/zanzarah.cpp +++ b/src/modules/zanzarah/zanzarah.cpp @@ -53,6 +53,8 @@ namespace extractor // ReSharper disable once CppMemberFunctionMayBeStatic std::vector > extractor::list_files(std::ifstream &stream) // NOLINT(*-convert-member-functions-to-static) { + stream.seekg(static_cast(get_signature().size())); + auto files = std::vector >(); files.reserve(read_positive_int32(stream)); diff --git a/src/tests/framework/tools/.gitignore b/src/tests/framework/tools/.gitignore new file mode 100644 index 0000000..87a15c2 --- /dev/null +++ b/src/tests/framework/tools/.gitignore @@ -0,0 +1 @@ +/.venv diff --git a/src/tests/framework/tools/.python-version b/src/tests/framework/tools/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/src/tests/framework/tools/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/src/tests/framework/make_listing.py b/src/tests/framework/tools/make_listing.py similarity index 100% rename from src/tests/framework/make_listing.py rename to src/tests/framework/tools/make_listing.py diff --git a/src/tests/framework/tools/pyproject.toml b/src/tests/framework/tools/pyproject.toml new file mode 100644 index 0000000..e1b382a --- /dev/null +++ b/src/tests/framework/tools/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "observer" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "xxhash>=3.5.0", +] diff --git a/src/tests/framework/tools/uv.lock b/src/tests/framework/tools/uv.lock new file mode 100644 index 0000000..15b0c01 --- /dev/null +++ b/src/tests/framework/tools/uv.lock @@ -0,0 +1,37 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "observer" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "xxhash" }, +] + +[package.metadata] +requires-dist = [{ name = "xxhash", specifier = ">=3.5.0" }] + +[[package]] +name = "xxhash" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241, upload-time = "2024-08-17T09:20:38.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795, upload-time = "2024-08-17T09:18:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792, upload-time = "2024-08-17T09:18:47.862Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950, upload-time = "2024-08-17T09:18:49.06Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980, upload-time = "2024-08-17T09:18:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324, upload-time = "2024-08-17T09:18:51.988Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370, upload-time = "2024-08-17T09:18:54.164Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911, upload-time = "2024-08-17T09:18:55.509Z" }, + { url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352, upload-time = "2024-08-17T09:18:57.073Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410, upload-time = "2024-08-17T09:18:58.54Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322, upload-time = "2024-08-17T09:18:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725, upload-time = "2024-08-17T09:19:01.332Z" }, + { url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070, upload-time = "2024-08-17T09:19:03.007Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172, upload-time = "2024-08-17T09:19:04.355Z" }, + { url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041, upload-time = "2024-08-17T09:19:05.435Z" }, + { url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801, upload-time = "2024-08-17T09:19:06.547Z" }, +] diff --git a/src/tests/renpy.cpp b/src/tests/renpy.cpp index beb0d9c..161ca37 100644 --- a/src/tests/renpy.cpp +++ b/src/tests/renpy.cpp @@ -4,6 +4,11 @@ using namespace test; +TEST_CASE("renpy: rpa20_binary_hearts") +{ + test_on("renpy\\rpa20_binary_hearts.rpa"); +} + TEST_CASE("renpy: rpa30_army_gals") { test_on("renpy\\rpa30_army_gals.rpa");