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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .github/workflows/cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:

permissions:
contents: read
contents: write

jobs:
build:
Expand All @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-22.04, windows-latest]
gui: ['OFF', 'ON']
env:
PYTHONUTF8: '1'
Expand All @@ -30,3 +30,26 @@ jobs:
run: cmake --build cpp/build --config Release --parallel 2
- name: Test
run: ctest --test-dir cpp/build -C Release --output-on-failure
- name: Report test failures
if: failure()
run: python cpp/scripts/report_test_failures.py
- name: Package Linux x86-64
if: matrix.gui == 'ON' && runner.os == 'Linux'
run: python cpp/scripts/package.py --build cpp/build --platform linux --output packages/fear-arch00-tool-0.1.0-alpha-linux-x86_64.zip
- name: Package Windows x86-64
if: matrix.gui == 'ON' && runner.os == 'Windows'
run: python cpp/scripts/package.py --build cpp/build --platform windows --output packages/fear-arch00-tool-0.1.0-alpha-windows-x86_64.zip
- name: Upload tested package
if: matrix.gui == 'ON'
uses: actions/upload-artifact@v4
with:
name: package-${{ runner.os }}-x86_64
path: packages/*.zip
if-no-files-found: error
retention-days: 7
# Only the explicitly requested alpha release commit uploads assets.
- name: Upload alpha asset to draft release
if: matrix.gui == 'ON' && github.event_name == 'push' && github.ref == 'refs/heads/arena/2682f006-arch00tool-fear-1-arch00-ltar' && startsWith(github.event.head_commit.message, '[alpha-release]')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python cpp/scripts/upload_alpha.py
6 changes: 3 additions & 3 deletions cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a first-stage LTAR v3 metadata parser with `list`, `test`, `extract`, `test-all`, `extract-all`, and `repack` commands. Raw and zlib payloads are processed in bounded chunks; extraction creates files exclusively and rejects lexical traversal paths. The parser and extractor have been tested on generated fixtures on Linux, not on real game archives or Windows.

**Not yet a replacement for the Python tool:** Complete CLI compatibility has not been established. An experimental browser GUI is available separately, but it does not implement live progress or cancellation. Pattern matching supports `*`, `?` and character classes, but has not been exhaustively compared with Python for unusual patterns. Extraction does not protect against existing directory symlinks or races: use a new private output directory. Partial files remain on error. Do not use untrusted archives. See the synthetic benchmark below; its results do not establish real-game performance.
**Not yet a replacement for the Python tool:** Complete CLI compatibility has not been established. An experimental browser GUI is available separately, with cooperative progress and cancellation. Pattern matching supports `*`, `?` and character classes, but has not been exhaustively compared with Python for unusual patterns. Extraction does not protect against existing directory symlinks or races: use a new private output directory. Partial files remain on error. Do not use untrusted archives. See the synthetic benchmark below; its results do not establish real-game performance.

## Build and test

Expand Down Expand Up @@ -37,9 +37,9 @@ ctest --test-dir cpp/build-gui --output-on-failure -C Release

On Windows, run the generated `arch00tool-gui.exe` from the configuration directory (for example `cpp\build-gui\Release\arch00tool-gui.exe` with Visual Studio). CMake fetches pinned versions of cpp-httplib and nlohmann/json from GitHub for this optional target; the CLI-only build does not fetch them.

Copy the complete URL printed by the program into a browser **on the same computer**. Enter local paths manually or use the in-app directory browser; the interface can list (first 2,000 entries), test, extract, and repack one archive, or test/extract all archives in an input directory. Single-archive extraction supports an optional case-sensitive pattern; batch extraction supports skipping existing destinations. It does not automatically open the browser, show live progress or cancel an operation. For repacking, select the output folder and append a new archive filename manually. The picker lists at most 2,000 items per directory and runs with the same local filesystem privileges as the tool. The browser waits for each operation to finish. Batch errors are reported after processing; an incomplete extraction directory remains after failures and must be inspected and removed or moved manually before retrying. Stop the server with Ctrl+C after use. Closing the tab does not stop it.
Copy the complete URL printed by the program into a browser **on the same computer**. Enter local paths manually or use the in-app directory browser; the interface can list (first 2,000 entries), test, extract, and repack one archive, or test/extract all archives in an input directory. Single-archive extraction supports an optional case-sensitive pattern; batch extraction supports skipping existing destinations. It does not automatically open the browser. For repacking, select the output folder and append a new archive filename manually. The picker lists at most 2,000 items per directory and runs with the same local filesystem privileges as the tool. The browser shows the current phase and completed-entry count, and offers cooperative cancellation while waiting. Progress resets for each phase and each archive in batch operations; it is not a byte-based percentage or time estimate. Metadata parsing, filesystem calls, syncing and publication may delay cancellation. During repack publication cancellation is rejected as too late. Extraction cancellation leaves completed and partially written files; batch extraction may retain completed archive directories and an `.incomplete` directory. Repack attempts to remove its `.partial` file when cancelled before publication, but a crash or cleanup failure can leave it behind. Batch errors are reported after processing; an incomplete extraction directory remains after failures and must be inspected and removed or moved manually before retrying. Stop the server with Ctrl+C after use. Closing the tab does not stop it.

The server binds only to IPv4 loopback, checks the host and origin, and requires a per-run token for operations. The URL contains that token: do not share it, expose the server to a network, or run it in a shared or untrusted local environment. It is not a multi-user service or a sandbox. A local process or browser extension with access to the token can invoke operations with the user's filesystem privileges. Source and destination safety limitations described above also apply. The GUI has been built and its HTTP API tested on Linux using synthetic archives; Windows, interactive browsers, and real game archives have not been tested.
The server binds only to IPv4 loopback, checks the host and origin, and requires a per-run token for operations. The URL contains that token: do not share it, expose the server to a network, or run it in a shared or untrusted local environment. It is not a multi-user service or a sandbox. A local process or browser extension with access to the token can invoke operations with the user's filesystem privileges. Source and destination safety limitations described above also apply. The GUI has been built and its HTTP API, including cooperative cancellation, tested on Linux using synthetic archives; Windows, interactive browsers, and real game archives have not been tested.

## Synthetic performance benchmark

Expand Down
17 changes: 17 additions & 0 deletions cpp/RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# fear-arch00-tool — experimental alpha

These are pre-release builds of the C++ port. Back up your files and work on copies. The tool has not yet been validated against original F.E.A.R. archives or in-game; the GUI has not been tested interactively on all platforms. Extract files only into a new private directory without symlinks. Cancellation or errors may leave partially extracted files; inspect incomplete output before removing it. Repack verifies copied bytes but cannot guarantee game compatibility. See the repository documentation for full safety limitations.

## Windows x86-64

Extract the ZIP and run `arch00tool-cpp.exe` from PowerShell or Command Prompt, or run `arch00tool-gui.exe` and open the complete URL printed in its console on the same machine. The C++ runtime distributed with Windows or Microsoft Visual C++ Redistributable may be required.

## Linux x86-64

Extract the ZIP. Make the executables runnable if your extraction program does not preserve permissions (`chmod +x arch00tool-cpp arch00tool-gui`). In a terminal run `./arch00tool-cpp` or `./arch00tool-gui` and open the printed URL locally. Built on Ubuntu 22.04; requires compatible glibc (2.35 or newer), libstdc++, and zlib runtime. Other Linux distributions and CPU architectures are not guaranteed to work.

## GUI and CLI

The GUI binds to loopback only. Its URL contains a session token: do not share it. Closing the browser does not stop the server; press Ctrl+C in its terminal. The interface lets you browse local paths, list, test, extract, and repack. Use a new output path for repacking. Test the rebuilt archive separately. The CLI prints usage when run without arguments; the number of workers is set with `--jobs N` (1–64).

Progress counts entries, not bytes; cancellation is cooperative and can be delayed during metadata parsing, blocking filesystem operations, syncing, or publication. Batch operations can leave completed outputs and `.incomplete` directories. Refer to the full C++ README and bilingual docs in the repository for details.
26 changes: 23 additions & 3 deletions cpp/include/arch00.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
#pragma once
#include <cstdint>
#include <atomic>
#include <mutex>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <vector>

namespace arch00 {
struct Error : std::runtime_error { using std::runtime_error::runtime_error; };
struct Cancelled : Error { Cancelled() : Error("operation cancelled") {} };
// Optional cooperative control, owned by the caller for the operation lifetime.
struct Control {
std::atomic<bool> cancelled{false};
std::atomic<std::size_t> completed{0}, total{0};
mutable std::mutex mutex;
std::string phase = "Preparing";
bool publishing = false;
void check() const { if (cancelled.load()) throw Cancelled(); }
void start_phase(const std::string& name, std::size_t count) {
std::lock_guard<std::mutex> lock(mutex); phase=name; completed=0; total=count;
}
bool request_cancel() {
std::lock_guard<std::mutex> lock(mutex);
if (publishing) return false;
cancelled.store(true); return true;
}
};
struct Header {
std::uint32_t version, names_size, folder_count, file_count, retail, download, crc_enabled;
std::vector<unsigned char> guid;
Expand All @@ -22,14 +42,14 @@ Archive parse(const std::filesystem::path& path);

namespace arch00 {
// Validates stored payloads; raw_mode copies stored bytes without decompression.
void test(const std::filesystem::path& path, const Archive& archive, bool raw_mode = false, bool verbose = false, unsigned jobs = 1);
void test(const std::filesystem::path& path, const Archive& archive, bool raw_mode = false, bool verbose = false, unsigned jobs = 1, Control* control = nullptr);
// Extracts all entries; files are created exclusively and are not rolled back on error.
void extract(const std::filesystem::path& path, const Archive& archive,
const std::filesystem::path& output, bool raw_mode = false, bool verbose = false, unsigned jobs = 1);
const std::filesystem::path& output, bool raw_mode = false, bool verbose = false, unsigned jobs = 1, Control* control = nullptr);
}

namespace arch00 {
struct RepackResult { std::size_t files, replaced; std::uint64_t payload_bytes; };
RepackResult repack(const std::filesystem::path& source, const std::filesystem::path& output,
const std::filesystem::path& replacements = {}, unsigned jobs = 1);
const std::filesystem::path& replacements = {}, unsigned jobs = 1, Control* control = nullptr);
}
29 changes: 29 additions & 0 deletions cpp/scripts/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Create one release ZIP from an already tested CMake GUI build."""
import argparse
from pathlib import Path
import zipfile

p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--build', required=True, type=Path)
p.add_argument('--platform', required=True, choices=('windows', 'linux'))
p.add_argument('--output', required=True, type=Path)
a = p.parse_args()
root = Path(__file__).resolve().parents[1]
exe = '.exe' if a.platform == 'windows' else ''
bindir = a.build / 'Release' if a.platform == 'windows' else a.build
files = [(bindir / ('arch00tool-cpp' + exe), 'arch00tool-cpp' + exe, 0o755),
(bindir / ('arch00tool-gui' + exe), 'arch00tool-gui' + exe, 0o755),
(root / 'RELEASE.md', 'README.md', 0o644)]
for src, _, _ in files:
if not src.is_file():
p.error(f'missing build output: {src}')
a.output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(a.output, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
for src, name, mode in files:
info = zipfile.ZipInfo(name)
info.create_system = 3
info.external_attr = (0o100000 | mode) << 16
info.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(info, src.read_bytes())
print(a.output)
7 changes: 7 additions & 0 deletions cpp/scripts/report_test_failures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Expose CTest failures as a CI annotation, including when raw logs are unavailable."""
from pathlib import Path
p = Path('cpp/build/Testing/Temporary/LastTest.log')
if p.exists():
text = p.read_text(encoding='utf-8', errors='replace')
escaped = text[-50000:].replace('%', '%25').replace('\r', '%0D').replace('\n', '%0A')
print('::error title=CTest log::' + escaped)
16 changes: 16 additions & 0 deletions cpp/scripts/upload_alpha.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Upload a tested package from CI to the explicitly created alpha draft."""
import subprocess
import sys
import time
from pathlib import Path

release = 'v0.1.0-alpha'
package = next(Path('packages').glob('*.zip'))
for _ in range(60):
result = subprocess.run(['gh', 'release', 'view', release], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if result.returncode == 0:
break
time.sleep(5)
else:
sys.exit('Alpha draft release was not found within five minutes')
subprocess.run(['gh', 'release', 'upload', release, str(package)], check=True)
Loading
Loading