pax_global_header00006660000000000000000000000064151701055720014515gustar00rootroot0000000000000052 comment=05d7fbd70ef7f18f1f6b263444121213388a9c10 zxc-0.10.0/000077500000000000000000000000001517010557200123775ustar00rootroot00000000000000zxc-0.10.0/.clang-format000066400000000000000000000003051517010557200147500ustar00rootroot00000000000000--- Language: Cpp BasedOnStyle: Google IndentWidth: 4 TabWidth: 4 UseTab: Never ColumnLimit: 100 AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false BreakBeforeBraces: Attachzxc-0.10.0/.clusterfuzzlite/000077500000000000000000000000001517010557200157335ustar00rootroot00000000000000zxc-0.10.0/.clusterfuzzlite/Dockerfile000066400000000000000000000005741517010557200177330ustar00rootroot00000000000000# Copyright (c) 2025-2026, Bertrand Lebonnois # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. FROM gcr.io/oss-fuzz-base/base-builder:v1 RUN apt-get update && apt-get install -y make cmake COPY . $SRC/zxc WORKDIR $SRC/zxc COPY .clusterfuzzlite/build.sh $SRC/build.shzxc-0.10.0/.clusterfuzzlite/build.sh000066400000000000000000000015101517010557200173630ustar00rootroot00000000000000#!/bin/bash -eu # Copyright (c) 2025-2026, Bertrand Lebonnois # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. AVAILABLE_FUZZERS="decompress roundtrip" LIB_SOURCES="src/lib/zxc_common.c src/lib/zxc_compress.c src/lib/zxc_decompress.c src/lib/zxc_driver.c src/lib/zxc_dispatch.c src/lib/zxc_seekable.c" for fuzzer in $AVAILABLE_FUZZERS; do if [ -z "${FUZZER_TARGET:-}" ] || [ "${FUZZER_TARGET}" == "$fuzzer" ]; then $CC $CFLAGS -I include \ -I src/lib/vendors \ -DZXC_FUNCTION_SUFFIX=_default -DZXC_ONLY_DEFAULT \ $LIB_SOURCES \ tests/fuzz_${fuzzer}.c \ -o $OUT/zxc_fuzzer_${fuzzer} \ $LIB_FUZZING_ENGINE \ -lm -pthread fi donezxc-0.10.0/.github/000077500000000000000000000000001517010557200137375ustar00rootroot00000000000000zxc-0.10.0/.github/CODEOWNERS000066400000000000000000000000751517010557200153340ustar00rootroot00000000000000# Default owners for everything in the repo * @hellobertrand zxc-0.10.0/.github/CODE_OF_CONDUCT.md000066400000000000000000000054601517010557200165430ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex, gender characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at **zxc.codec@gmail.com**. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.htmlzxc-0.10.0/.github/CONTRIBUTING.md000066400000000000000000000027741517010557200162020ustar00rootroot00000000000000# Contributing to ZXC Thank you for your interest in contributing to ZXC! This guide will help you get started. ## Developer Certificate of Origin (DCO) By contributing, you certify that: - You have the right to submit the contribution - You agree to license your contribution under the BSD-3-Clause license Add this to your commits: ```bash git commit -s -m "Your commit message" ``` ## License Headers To maintain legal clarity and recognize all contributors, every new source file (.c, .h, .rs, .py, etc.) must include the following header at the very top: ```C /* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ ``` ## Quick Start ### Build and Test ```bash git clone https://github.com/hellobertrand/zxc.git cd zxc mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Debug make -j ctest --output-on-failure ``` ### Format Code ```bash clang-format -i src/lib/*.c include/*.h ``` ## Requirements - **C17** compiler (GCC, Clang, or MSVC) - **CMake** 3.10+ - Follow `.clang-format` style (Google) - All code must be ASCII-only - Pass `ctest` and static analysis ## Submitting Changes 1. Fork and create a feature branch 2. Add tests for new functionality 3. Ensure CI passes (build, tests, benchmarks) 4. Sign your commits with `-s` 5. Open a PR to `main` ## Reporting Issues Include: - ZXC version (`zxc --version`) - OS and architecture - Minimal reproduction steps Thank you for making ZXC better! zxc-0.10.0/.github/FUNDING.yml000066400000000000000000000000261517010557200155520ustar00rootroot00000000000000github: hellobertrand zxc-0.10.0/.github/SECURITY.md000066400000000000000000000004111517010557200155240ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability **Please do not report security vulnerabilities through public GitHub issues.** If you have discovered a security issue in ZXC, please email us privately at **zxc.codec@gmail.com**. We will respond within 48 hours.zxc-0.10.0/.github/codeql/000077500000000000000000000000001517010557200152065ustar00rootroot00000000000000zxc-0.10.0/.github/codeql/codeql-config.yml000066400000000000000000000001601517010557200204400ustar00rootroot00000000000000name: "CodeQL Config" paths-ignore: - 'src/lib/vendors/rapidhash.h' queries: - uses: security-and-quality zxc-0.10.0/.github/dependabot.yml000066400000000000000000000007631517010557200165750ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: monthly - package-ecosystem: pip directory: /wrappers/python schedule: interval: monthly - package-ecosystem: cargo directory: /wrappers/rust schedule: interval: monthly - package-ecosystem: npm directory: /wrappers/nodejs schedule: interval: monthly - package-ecosystem: gomod directory: /wrappers/go schedule: interval: monthlyzxc-0.10.0/.github/workflows/000077500000000000000000000000001517010557200157745ustar00rootroot00000000000000zxc-0.10.0/.github/workflows/README.md000066400000000000000000000100441517010557200172520ustar00rootroot00000000000000# GitHub Actions Workflows This directory contains CI/CD workflows for the ZXC compression library. ## Core Workflows ### build.yml - Build & Release **Triggers:** Push to main, tags, pull requests, manual dispatch Builds and tests ZXC across multiple platforms (Linux x86_64/ARM64, macOS ARM64, Windows x64). Generates release artifacts and uploads binaries when tags are pushed. ### multiarch.yml - Multi-Architecture Build **Triggers:** Push to main, pull requests, manual dispatch Comprehensive build matrix testing across multiple architectures including 32-bit and 64-bit variants for Linux (x64, x86, ARM64, ARM) and Windows (x64, x86). Validates compilation compatibility across different platforms. ### multicomp.yml - Compiler Compatibility **Triggers:** Push to main, pull requests, manual dispatch Tests the codebase against a wide range of compilers (various versions of GCC and Clang) to ensure compatibility and identify any compiler-specific issues or warnings. ### benchmark.yml - Performance Benchmark **Triggers:** Push to main (src changes), pull requests, manual dispatch Runs performance benchmarks using LZbench on Ubuntu and macOS. Integrates ZXC into the LZbench framework and tests compression/decompression performance against the Silesia corpus. ## Quality & Security ### coverage.yml - Code Coverage **Triggers:** Push to main, pull requests, manual dispatch Builds the project with coverage instrumentation (`-DZXC_ENABLE_COVERAGE=ON`), runs unit and CLI tests, and generates a coverage report using `lcov`. The report is then uploaded to Codecov for analysis. ### fuzzing.yml - Fuzz Testing **Triggers:** Pull requests, scheduled (every 3 days), manual dispatch Executes fuzz testing using ClusterFuzzLite with multiple sanitizers (address, undefined) on decompression and roundtrip fuzzers. Helps identify memory safety issues and edge cases. ### quality.yml - Code Quality **Triggers:** Push to main, pull requests, manual dispatch Performs static analysis using Cppcheck and Clang Static Analyzer. Runs memory leak detection with Valgrind to ensure code quality and identify potential bugs. ### security.yml - Code Security **Triggers:** Push to main, pull requests Runs CodeQL security analysis to detect potential security vulnerabilities and coding errors in the C/C++ codebase. ### vendors.yml - Vendor Maintenance **Triggers:** Scheduled (weekly), manual dispatch Automatically checks for and updates third-party dependencies (like `rapidhash.h`) to ensure the project uses the latest stable versions of its vendors. ## Language Bindings ### wrapper-rust-publish.yml - Publish Rust Crates **Triggers:** Release published, manual dispatch Tests and publishes Rust crates to crates.io. Verifies the version matches the release tag, runs tests across platforms, and publishes `zxc-compress-sys` (FFI bindings) followed by `zxc-compress` (safe wrapper). ### wrapper-python-publish.yml - Publish Python Package **Triggers:** Release published, manual dispatch Builds platform-specific wheels using `cibuildwheel` for Linux (x86_64, ARM64), macOS (ARM64, Intel), and Windows (AMD64, ARM64). Tests wheels against Python 3.12-3.13, then publishes to PyPI via trusted publishing. ### wrapper-wasm.yml - WASM Build & Test **Triggers:** Release published, publish on main, manual dispatch Builds the WebAssembly target using Emscripten SDK. Compiles the library with SIMD disabled (scalar codepath) and no threading, then runs a Node.js roundtrip test suite covering all compression levels, reusable contexts, and error handling. Uploads `zxc.js` + `zxc.wasm` as build artifacts. ### wrapper-nodejs-publish.yml - Publish Node.js Package **Triggers:** Release published, manual dispatch Builds and publishes the Node.js package to npm. Handles the compilation of native bindings and ensures the package is correctly versioned and distributed. ### wrapper-go-test.yml - Test Go Package **Triggers:** Release published, manual dispatch Runs comprehensive tests for the Go bindings across various platforms and architectures to ensure the Go package is stable and functional. zxc-0.10.0/.github/workflows/benchmark.yml000066400000000000000000000103171517010557200204530ustar00rootroot00000000000000name: Benchmark on: workflow_dispatch: push: branches: [ main ] paths: - 'src/**' - '.github/workflows/benchmark.yml' pull_request: branches: [ main ] permissions: contents: read concurrency: cancel-in-progress: true group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} jobs: lzbench-benchmark: name: Run Benchmark if: github.event.pull_request.draft == false runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-26-xlarge] env: LZBENCH_DIR: lzbench SILESIA_TAR: silesia.tar steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Select Latest Xcode if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Clone LZbench run: | # git clone --depth 1 https://github.com/inikep/lzbench "${LZBENCH_DIR}" git clone -b zxc-0.10.x https://github.com/hellobertrand/lzbench "${LZBENCH_DIR}" - name: Copy Lib ZXC run: | mkdir -p ${LZBENCH_DIR}/lz/zxc/include/ mkdir -p ${LZBENCH_DIR}/lz/zxc/src/lib/ cp -r include ${LZBENCH_DIR}/lz/zxc/ cp -r src/lib ${LZBENCH_DIR}/lz/zxc/src/ - name: Build Lzbench working-directory: ${{ env.LZBENCH_DIR }} run: | SKIP_CODECS="DONT_BUILD_BRIEFLZ=1 DONT_BUILD_BROTLI=1 DONT_BUILD_BSC=1" SKIP_CODECS+=" DONT_BUILD_BZIP2=1 DONT_BUILD_BZIP3=1 DONT_BUILD_CSC=1" SKIP_CODECS+=" DONT_BUILD_CRUSH=1 DONT_BUILD_DENSITY=1 DONT_BUILD_FASTLZ=1" SKIP_CODECS+=" DONT_BUILD_FASTLZMA2=1 DONT_BUILD_GIPFELI=1 DONT_BUILD_GLZA=1" SKIP_CODECS+=" DONT_BUILD_KANZI=1 DONT_BUILD_LIBDEFLATE=1 DONT_BUILD_LIZARD=1" SKIP_CODECS+=" DONT_BUILD_LZF=1 DONT_BUILD_LZFSE=1 DONT_BUILD_LZG=1" SKIP_CODECS+=" DONT_BUILD_LZHAM=1 DONT_BUILD_LZJB=1 DONT_BUILD_LZLIB=1" SKIP_CODECS+=" DONT_BUILD_LZMA=1 DONT_BUILD_LZMAT=1 DONT_BUILD_LZO=1" SKIP_CODECS+=" DONT_BUILD_LZRW=1 DONT_BUILD_LZSSE=1 DONT_BUILD_PPMD=1" SKIP_CODECS+=" DONT_BUILD_QUICKLZ=1 DONT_BUILD_SLZ=1 DONT_BUILD_TAMP=1" SKIP_CODECS+=" DONT_BUILD_TORNADO=1 DONT_BUILD_UCL=1 DONT_BUILD_WFLZ=1" SKIP_CODECS+=" DONT_BUILD_XZ=1 DONT_BUILD_YALZ77=1 DONT_BUILD_YAPPY=1" SKIP_CODECS+=" DONT_BUILD_ZLIB_NG=1 DONT_BUILD_ZLING=1 DONT_BUILD_ZPAQ=1" SKIP_CODECS+=" DONT_BUILD_MEMLZ=1" # Use latest available compiler per platform if [ "$RUNNER_OS" = "Linux" ]; then export CC=gcc-14 CXX=g++-14 fi make -j $SKIP_CODECS CC="${CC:-cc}" MOREFLAGS="-march=native" - name: Cache Silesia Corpus id: cache-silesia uses: actions/cache@v5 with: path: ${{ env.SILESIA_TAR }} key: silesia-tar-v1 - name: Download Silesia Corpus if: steps.cache-silesia.outputs.cache-hit != 'true' run: | wget -q https://github.com/DataCompression/corpus-collection/raw/main/Silesia-Corpus/silesia.tar.gz -O silesia.tar.gz gunzip silesia.tar.gz - name: SHA256 Checksum run: | shasum -a 256 "${SILESIA_TAR}" - name: Run Lzbench on Silesia Corpus working-directory: ${{ env.LZBENCH_DIR }} run: | if [ "${{ github.event_name }}" = "pull_request" ]; then CODECS="memcpy/zxc/lz4" else CODECS="memcpy/zxc/lz4/lz4fast,17/lz4hc,9/lzav,1/snappy/zstd_fast,-1/zstd,1/zlib,1" fi ./lzbench -e"${CODECS}" -j -t8,8 -o1 "../${SILESIA_TAR}" > ../benchmark.md echo "Benchmark finished. First lines:" head -n 40 ../benchmark.md - name: Add Benchmark to Job Summary run: | echo "### Benchmark Result (${{ matrix.os }})" >> $GITHUB_STEP_SUMMARY cat benchmark.md >> $GITHUB_STEP_SUMMARY - name: Upload Benchmark Result as Artifact uses: actions/upload-artifact@v7 with: name: benchmark-${{ matrix.os }}-${{ github.sha }} path: benchmark.md if-no-files-found: error retention-days: 10zxc-0.10.0/.github/workflows/build.yml000066400000000000000000000107331517010557200176220ustar00rootroot00000000000000name: Build & Release on: workflow_dispatch: push: branches: [ main ] tags: - 'v*' pull_request: branches: [ main ] permissions: contents: read concurrency: cancel-in-progress: true group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} jobs: build-and-test: name: Build ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - name: Linux x86_64 os: ubuntu-latest artifact_name: zxc asset_name: zxc-linux-x86_64 cmake_flags: "-DZXC_NATIVE_ARCH=OFF" - name: Linux ARM64 os: ubuntu-24.04-arm artifact_name: zxc asset_name: zxc-linux-aarch64 cmake_flags: "-DZXC_NATIVE_ARCH=OFF" - name: macOS ARM64 os: macos-26 artifact_name: zxc asset_name: zxc-macos-arm64 cmake_flags: "-DZXC_NATIVE_ARCH=OFF" - name: Windows x64 os: windows-latest artifact_name: zxc.exe asset_name: zxc-windows-x64 cmake_flags: "-A x64 -DZXC_NATIVE_ARCH=OFF" - name: Windows ARM64 os: windows-11-arm artifact_name: zxc.exe asset_name: zxc-windows-arm64 cmake_flags: "-A ARM64 -DZXC_NATIVE_ARCH=OFF" steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Select Latest Compiler (Linux) if: runner.os == 'Linux' run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV gcc-14 --version - name: Select Latest Xcode (macOS) if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Configure CMake run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flags }} - name: Build run: cmake --build build --config Release --parallel - name: Test (native) working-directory: build run: ctest -C Release --output-on-failure - name: Test CLI shell: bash run: | BUILD_DIR="build" # Find binary (handle both Ninja and Visual Studio generators) if [ -f "$BUILD_DIR/Release/${{ matrix.artifact_name }}" ]; then ZXC_BIN="$BUILD_DIR/Release/${{ matrix.artifact_name }}" elif [ -f "$BUILD_DIR/${{ matrix.artifact_name }}" ]; then ZXC_BIN="$BUILD_DIR/${{ matrix.artifact_name }}" else echo "Binary not found for CLI test!" find "$BUILD_DIR" -name "${{ matrix.artifact_name }}" exit 1 fi echo "Testing with binary: $ZXC_BIN" chmod +x tests/test_cli.sh ./tests/test_cli.sh "$ZXC_BIN" - name: Package Release (Archive) if: startsWith(github.ref, 'refs/tags/') shell: bash run: | # Install to staging directory using CMake (Standard way for all OS) cmake --install build --prefix release_staging --config Release # Strip binary on Unix-like systems only (installed to bin/) if [[ "${{ runner.os }}" != "Windows" ]]; then strip release_staging/bin/${{ matrix.artifact_name }} fi # Copy documentation to root cp LICENSE release_staging/LICENSE.txt cp README.md release_staging/README.md # Create archive cd release_staging if [[ "${{ runner.os }}" == "Windows" ]]; then 7z a ../${{ matrix.asset_name }}.zip * else tar -czvf ../${{ matrix.asset_name }}.tar.gz * fi - name: Upload Artifacts if: startsWith(github.ref, 'refs/tags/') uses: actions/upload-artifact@v7 with: name: ${{ matrix.asset_name }} path: | ${{ matrix.asset_name }}.tar.gz ${{ matrix.asset_name }}.zip release: name: Create GitHub Release needs: build-and-test runs-on: ubuntu-slim if: startsWith(github.ref, 'refs/tags/') permissions: contents: write steps: - name: Download all artifacts uses: actions/download-artifact@v8 - name: Create Release uses: softprops/action-gh-release@v2 with: files: | **/*.tar.gz **/*.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}zxc-0.10.0/.github/workflows/coverage.yml000066400000000000000000000036631517010557200203220ustar00rootroot00000000000000name: Code Coverage on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y lcov - name: Configure CMake run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DZXC_ENABLE_COVERAGE=ON -DZXC_NATIVE_ARCH=OFF - name: Build run: cmake --build build --parallel - name: Run Unit Tests working-directory: build run: ctest --output-on-failure - name: Run CLI Tests run: | chmod +x tests/test_cli.sh ./tests/test_cli.sh build/zxc - name: Generate Coverage Report run: | # Capture coverage data lcov --capture --directory . --output-file coverage.info --ignore-errors gcov,negative # Filter out system headers, external libraries, CLI frontend, and test files lcov --remove coverage.info '/usr/*' '*/tests/*' '*/cli/*' '*/src/lib/vendors/rapidhash.h' --output-file coverage.info --ignore-errors unused # Output file list lcov --list coverage.info - name: Generate HTML Report run: genhtml coverage.info --output-directory coverage-report - name: Publish Coverage Summary run: | echo "## Code Coverage Summary" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY lcov --summary coverage.info 2>&1 | tail -n +2 >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - name: Upload Coverage Artifact uses: actions/upload-artifact@v7 with: name: coverage-report path: coverage-report retention-days: 10 - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.info fail_ci_if_error: true zxc-0.10.0/.github/workflows/fuzzing.yml000066400000000000000000000143401517010557200202150ustar00rootroot00000000000000name: Fuzzing on: workflow_dispatch: inputs: mode: description: 'Fuzzing mode' required: true default: 'batch' type: choice options: - code-change - batch fuzz_seconds: description: 'Duration (seconds)' required: true default: '3600' pull_request: branches: [ main ] schedule: - cron: '0 1 * * 1' permissions: contents: read security-events: write jobs: fuzzing: name: Run Fuzzing (${{ matrix.fuzzer }} - ${{ matrix.sanitizer }}) runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ matrix.fuzzer }}-${{ matrix.sanitizer }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true strategy: fail-fast: false matrix: sanitizer: [address, undefined] fuzzer: [decompress, roundtrip] steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Configure Fuzzer Target run: | sed -i '2i export FUZZER_TARGET="${{ matrix.fuzzer }}"' .clusterfuzzlite/build.sh # TODO: Remove this step once ClusterFuzzLite updates to support Docker 29+ - name: Downgrade Docker (Temporary Workaround) run: | # ClusterFuzzLite v1 uses Docker API 1.41 which is incompatible with Docker 29.0+ # Downgrade to Docker 28 until the action is updated curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update # Install Docker 28.0.4 specifically sudo apt-get install -y --allow-downgrades docker-ce=5:28.0.4-1~ubuntu.$(lsb_release -rs)~$(lsb_release -cs) docker-ce-cli=5:28.0.4-1~ubuntu.$(lsb_release -rs)~$(lsb_release -cs) containerd.io sudo systemctl restart docker docker version - name: Build Fuzzers (${{ matrix.fuzzer }} - ${{ matrix.sanitizer }}) id: build uses: google/clusterfuzzlite/actions/build_fuzzers@v1 with: language: c github-token: ${{ secrets.GITHUB_TOKEN }} sanitizer: ${{ matrix.sanitizer }} - name: Run Fuzzers (${{ matrix.fuzzer }} - ${{ matrix.sanitizer }}) id: run uses: google/clusterfuzzlite/actions/run_fuzzers@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} mode: ${{ github.event_name == 'pull_request' && 'code-change' || inputs.mode || 'batch' }} fuzz-seconds: ${{ github.event_name == 'pull_request' && 120 || inputs.fuzz_seconds || 3600 }} sanitizer: ${{ matrix.sanitizer }} output-sarif: true storage-repo: https://${{ secrets.CFLITE_CORPUS_TOKEN }}@github.com/hellobertrand/zxc-fuzz-corpus.git storage-repo-branch: main - name: Upload SARIF to GitHub Security if: success() || failure() uses: github/codeql-action/upload-sarif@v4 with: sarif_file: . category: clusterfuzzlite-${{ matrix.fuzzer }}-${{ matrix.sanitizer }} tsan: name: Thread Sanitizer runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Build with TSan run: | cmake -B build -DCMAKE_C_FLAGS="-fsanitize=thread -g -fno-omit-frame-pointer" -DCMAKE_BUILD_TYPE=Debug cmake --build build - name: Run Tests run: ctest --test-dir build --output-on-failure coverage: name: Fuzz Coverage Report runs-on: ubuntu-latest if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v6 - name: Install LLVM Tools run: | sudo apt-get update -qq sudo apt-get install -y -qq llvm - name: Clone Fuzz Corpus run: | git clone --depth=1 \ https://${{ secrets.CFLITE_CORPUS_TOKEN }}@github.com/hellobertrand/zxc-fuzz-corpus.git \ corpus echo "roundtrip inputs: $(find corpus/corpus/zxc_fuzzer_roundtrip -type f | wc -l)" echo "decompress inputs: $(find corpus/corpus/zxc_fuzzer_decompress -type f | wc -l)" - name: Build Coverage Harnesses run: | mkdir -p build-cov CFLAGS="-g -O1 -fprofile-instr-generate -fcoverage-mapping" DEFS="-DZXC_FUNCTION_SUFFIX=_default -DZXC_ONLY_DEFAULT" INCLUDES="-I include -I src/lib/vendors" SOURCES="src/lib/zxc_common.c src/lib/zxc_compress.c src/lib/zxc_decompress.c src/lib/zxc_driver.c src/lib/zxc_dispatch.c src/lib/zxc_seekable.c" clang $CFLAGS $INCLUDES $DEFS $SOURCES tests/fuzz_roundtrip.c \ -fsanitize=fuzzer -lm -lpthread -o build-cov/fuzz_roundtrip clang $CFLAGS $INCLUDES $DEFS $SOURCES tests/fuzz_decompress.c \ -fsanitize=fuzzer -lm -lpthread -o build-cov/fuzz_decompress - name: Replay Corpora run: | LLVM_PROFILE_FILE="build-cov/roundtrip.profraw" \ build-cov/fuzz_roundtrip corpus/corpus/zxc_fuzzer_roundtrip/ -runs=0 2>&1 | tail -1 LLVM_PROFILE_FILE="build-cov/decompress.profraw" \ build-cov/fuzz_decompress corpus/corpus/zxc_fuzzer_decompress/ -runs=0 2>&1 | tail -1 - name: Generate Coverage Report run: | llvm-profdata merge \ build-cov/roundtrip.profraw build-cov/decompress.profraw \ -o build-cov/fuzz.profdata echo "=== COVERAGE SUMMARY ===" llvm-cov report \ build-cov/fuzz_roundtrip -object build-cov/fuzz_decompress \ -instr-profile=build-cov/fuzz.profdata \ --ignore-filename-regex='(vendors/|tests/|fuzz_|zxc_driver)' llvm-cov show \ build-cov/fuzz_roundtrip -object build-cov/fuzz_decompress \ -instr-profile=build-cov/fuzz.profdata \ --ignore-filename-regex='(vendors/|tests/|fuzz_|zxc_driver)' \ --format=html -output-dir=build-cov/coverage_html - name: Upload Coverage Report uses: actions/upload-artifact@v7 with: name: fuzz-coverage-report path: build-cov/coverage_html/ retention-days: 30zxc-0.10.0/.github/workflows/multiarch.yml000066400000000000000000000300771517010557200205160ustar00rootroot00000000000000name: Multi-Architecture Build on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read concurrency: cancel-in-progress: true group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} jobs: # =========================================================================== # Tier 1: Main architectures # =========================================================================== build-tier1: name: "Tier 1: ${{ matrix.name }}" runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - name: Linux x64 runner: ubuntu-latest os: linux arch_flags: "" - name: Linux x86 (32-bit) runner: ubuntu-latest os: linux install_32bit_x86: true arch_flags: "-m32" - name: Linux ARM64 runner: ubuntu-24.04-arm os: linux arch_flags: "" - name: Linux ARM (32-bit) runner: ubuntu-latest os: linux install_32bit_arm: true cross_compiler_c: arm-linux-gnueabihf-gcc cross_compiler_cxx: arm-linux-gnueabihf-g++ cross_strip: arm-linux-gnueabihf-strip cmake_system_name: "Linux" cmake_system_processor: "arm" - name: Windows x64 runner: windows-latest os: windows cmake_arch: "x64" - name: Windows x86 (32-bit) runner: windows-latest os: windows cmake_arch: "Win32" - name: Windows ARM64 runner: windows-11-arm os: windows cmake_arch: "ARM64" # Note: Windows ARM 32-bit removed - deprecated and unsupported by Windows SDK 10.0.26100+ - name: macOS Intel x64 runner: macos-26-intel os: macos - name: macOS ARM64 runner: macos-26 os: macos steps: - name: Checkout Repository uses: actions/checkout@v6 # Case: Linux x86 (32-bit) - name: Install x86 32-bit libs if: matrix.install_32bit_x86 run: | sudo apt-get update sudo apt-get install -y gcc-multilib g++-multilib # Case: Linux ARM (32-bit) - name: Install ARM 32-bit toolchain if: matrix.install_32bit_arm run: | sudo apt-get update sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf - name: Configure, Build & Test (Static & Shared) shell: bash run: | # Initialize variables EXTRA_ARGS="-DZXC_NATIVE_ARCH=OFF" # Disable native arch detection (Runtime Dispatch enabled) # Setup Environment for Linux/Mac (Flags & Cross-compilation) if [ "${{ matrix.os }}" != "windows" ]; then # Handle C flags (e.g., -m32) if [ ! -z "${{ matrix.arch_flags }}" ]; then export CFLAGS="${{ matrix.arch_flags }}" export CXXFLAGS="${{ matrix.arch_flags }}" fi # Handle cross-compiler (e.g., ARM 32-bit on Linux) if [ ! -z "${{ matrix.cross_compiler_c }}" ]; then EXTRA_ARGS="$EXTRA_ARGS -DCMAKE_C_COMPILER=${{ matrix.cross_compiler_c }} -DCMAKE_CXX_COMPILER=${{ matrix.cross_compiler_cxx }}" fi # Handle explicit System Name/Processor (crucial for cross-compilation) if [ ! -z "${{ matrix.cmake_system_name }}" ]; then EXTRA_ARGS="$EXTRA_ARGS -DCMAKE_SYSTEM_NAME=${{ matrix.cmake_system_name }}" fi if [ ! -z "${{ matrix.cmake_system_processor }}" ]; then EXTRA_ARGS="$EXTRA_ARGS -DCMAKE_SYSTEM_PROCESSOR=${{ matrix.cmake_system_processor }}" fi # Handle cross-strip tool (e.g., ARM 32-bit on Linux) if [ ! -z "${{ matrix.cross_strip }}" ]; then EXTRA_ARGS="$EXTRA_ARGS -DCMAKE_STRIP=/usr/bin/${{ matrix.cross_strip }}" fi fi # Iterate over Library Types for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi CURRENT_ARGS="$EXTRA_ARGS -DBUILD_SHARED_LIBS=$SHARED_FLAG" # 1. Configure if [ "${{ matrix.os }}" = "windows" ]; then # Visual Studio Generator cmake -S . -B $BUILD_DIR -A ${{ matrix.cmake_arch }} $CURRENT_ARGS else # Ninja/Makefiles cmake -S . -B $BUILD_DIR -DCMAKE_BUILD_TYPE=Release $CURRENT_ARGS fi # 2. Build cmake --build $BUILD_DIR --config Release --parallel # 3. Test (Skip if cross-compiling) if [ -z "${{ matrix.cross_compiler_c }}" ]; then echo "Running Tests for $LIB_TYPE..." ctest --test-dir $BUILD_DIR -C Release --output-on-failure else echo "Skipping tests for $LIB_TYPE (Cross-compilation)" fi echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # Tier 2: Extended Linux architectures (cross-compiled, tested via QEMU) # =========================================================================== build-tier2: name: "Tier 2: ${{ matrix.name }}" runs-on: ubuntu-latest container: debian:trixie strategy: fail-fast: false matrix: include: - name: Linux amd64 cross_pkg: gcc cross_prefix: x86_64-linux-gnu cmake_processor: x86_64 qemu_binary: qemu-x86_64 - name: Linux arm64 cross_pkg: gcc-aarch64-linux-gnu cross_prefix: aarch64-linux-gnu cmake_processor: aarch64 qemu_binary: qemu-aarch64 - name: Linux i386 cross_pkg: gcc-i686-linux-gnu cross_prefix: i686-linux-gnu cmake_processor: i686 qemu_binary: qemu-i386 - name: Linux RISC-V 64 cross_pkg: gcc-riscv64-linux-gnu cross_prefix: riscv64-linux-gnu cmake_processor: riscv64 qemu_binary: qemu-riscv64 - name: Linux s390x cross_pkg: gcc-s390x-linux-gnu cross_prefix: s390x-linux-gnu cmake_processor: s390x qemu_binary: qemu-s390x - name: Linux armhf cross_pkg: gcc-arm-linux-gnueabihf cross_prefix: arm-linux-gnueabihf cmake_processor: arm qemu_binary: qemu-arm - name: Linux ppc64el cross_pkg: gcc-powerpc64le-linux-gnu cross_prefix: powerpc64le-linux-gnu cmake_processor: ppc64le qemu_binary: qemu-ppc64le - name: Linux mipsel cross_pkg: gcc-mipsel-linux-gnu cross_prefix: mipsel-linux-gnu cmake_processor: mipsel qemu_binary: qemu-mipsel - name: Linux powerpc cross_pkg: gcc-powerpc-linux-gnu cross_prefix: powerpc-linux-gnu cmake_processor: powerpc qemu_binary: qemu-ppc - name: Linux ppc64 cross_pkg: gcc-powerpc64-linux-gnu cross_prefix: powerpc64-linux-gnu cmake_processor: ppc64 qemu_binary: qemu-ppc64 - name: Linux sparc64 cross_pkg: gcc-sparc64-linux-gnu cross_prefix: sparc64-linux-gnu cmake_processor: sparc64 qemu_binary: qemu-sparc64 - name: Linux armel cross_pkg: gcc-arm-linux-gnueabi cross_prefix: arm-linux-gnueabi cmake_processor: arm qemu_binary: qemu-arm steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install cross-compiler run: | apt-get update apt-get install -y cmake build-essential ninja-build ${{ matrix.cross_pkg }} qemu-user - name: Configure, Build & Test (Static & Shared) shell: bash run: | CROSS_C=${{ matrix.cross_prefix }}-gcc CROSS_CXX=${{ matrix.cross_prefix }}-g++ CROSS_STRIP=/usr/bin/${{ matrix.cross_prefix }}-strip SYSROOT=/usr/${{ matrix.cross_prefix }} for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=$CROSS_C \ -DCMAKE_CXX_COMPILER=$CROSS_CXX \ -DCMAKE_STRIP=$CROSS_STRIP \ -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=${{ matrix.cmake_processor }} \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE via QEMU..." ${{ matrix.qemu_binary }} -L $SYSROOT ./$BUILD_DIR/zxc_test echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # Tier 3: Experimental Linux architectures (cross-compiled, tested via QEMU) # =========================================================================== build-tier3: name: "Tier 3: ${{ matrix.name }}" runs-on: ubuntu-latest container: debian:trixie strategy: fail-fast: false matrix: include: - name: Linux mips64el cross_pkg: gcc-mips64el-linux-gnuabi64 cross_prefix: mips64el-linux-gnuabi64 cmake_processor: mips64 qemu_binary: qemu-mips64el - name: Linux loong64 cross_pkg: gcc-loongarch64-linux-gnu cross_prefix: loongarch64-linux-gnu cmake_processor: loongarch64 qemu_binary: qemu-loongarch64 - name: Linux alpha cross_pkg: gcc-alpha-linux-gnu cross_prefix: alpha-linux-gnu cmake_processor: alpha qemu_binary: qemu-alpha steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install cross-compiler run: | apt-get update apt-get install -y cmake build-essential ninja-build ${{ matrix.cross_pkg }} qemu-user - name: Configure, Build & Test (Static & Shared) shell: bash run: | CROSS_C=${{ matrix.cross_prefix }}-gcc CROSS_CXX=${{ matrix.cross_prefix }}-g++ CROSS_STRIP=/usr/bin/${{ matrix.cross_prefix }}-strip SYSROOT=/usr/${{ matrix.cross_prefix }} for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=$CROSS_C \ -DCMAKE_CXX_COMPILER=$CROSS_CXX \ -DCMAKE_STRIP=$CROSS_STRIP \ -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=${{ matrix.cmake_processor }} \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE via QEMU..." ${{ matrix.qemu_binary }} -L $SYSROOT ./$BUILD_DIR/zxc_test echo ">>> Completed Build: $LIB_TYPE" echo "" donezxc-0.10.0/.github/workflows/multicomp.yml000066400000000000000000000247031517010557200205360ustar00rootroot00000000000000name: Compiler Compatibility on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read concurrency: cancel-in-progress: true group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} jobs: # =========================================================================== # Clang 12–20 on Ubuntu # =========================================================================== clang: name: "Clang ${{ matrix.version }} (Ubuntu 22.04)" runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: version: [12, 13, 14, 15, 16, 17, 18, 19, 20] steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install Clang ${{ matrix.version }} run: | if [ ${{ matrix.version }} -ge 15 ]; then wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc echo "deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-${{ matrix.version }} main" | sudo tee /etc/apt/sources.list.d/llvm.list fi sudo apt-get update sudo apt-get install -y clang-${{ matrix.version }} - name: Configure, Build & Test (Static & Shared) shell: bash run: | for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: Clang ${{ matrix.version }} – $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=clang-${{ matrix.version }} \ -DCMAKE_C_STANDARD=17 \ -DZXC_NATIVE_ARCH=OFF \ -DZXC_ENABLE_LTO=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE..." ctest --test-dir $BUILD_DIR -C Release --output-on-failure echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # Apple Clang on macOS 14 # =========================================================================== clang-macos: name: "Apple Clang (macOS 14)" runs-on: macos-14 strategy: fail-fast: false steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Configure, Build & Test (Static & Shared) shell: bash run: | for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: Apple Clang – $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_STANDARD=17 \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE..." ctest --test-dir $BUILD_DIR -C Release --output-on-failure echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # GCC 9–14 on Ubuntu # =========================================================================== gcc-jammy: name: "GCC ${{ matrix.version }} (Ubuntu 22.04)" runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: version: [9, 10, 11, 12, 13] steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install GCC ${{ matrix.version }} run: | sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test sudo apt-get update sudo apt-get install -y gcc-${{ matrix.version }} g++-${{ matrix.version }} - name: Configure, Build & Test (Static & Shared) shell: bash run: | for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: GCC ${{ matrix.version }} – $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=gcc-${{ matrix.version }} \ -DCMAKE_CXX_COMPILER=g++-${{ matrix.version }} \ -DCMAKE_C_STANDARD=17 \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE..." ctest --test-dir $BUILD_DIR -C Release --output-on-failure echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # GCC 14 on Ubuntu 24.04 # =========================================================================== gcc-latest: name: "GCC 14 (Ubuntu 24.04)" runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Configure, Build & Test (Static & Shared) shell: bash run: | for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: GCC 14 – $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=gcc-14 \ -DCMAKE_CXX_COMPILER=g++-14 \ -DCMAKE_C_STANDARD=17 \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE..." ctest --test-dir $BUILD_DIR -C Release --output-on-failure echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # MinGW on Windows (32-bit & 64-bit) # =========================================================================== mingw: name: "MinGW ${{ matrix.name }}" runs-on: windows-latest strategy: fail-fast: false matrix: include: - name: "x86 32-bit" msystem: MINGW32 pkg_prefix: mingw-w64-i686 - name: "x86_64 64-bit" msystem: MINGW64 pkg_prefix: mingw-w64-x86_64 defaults: run: shell: msys2 {0} steps: - name: Setup MSYS2 uses: msys2/setup-msys2@v2 with: msystem: ${{ matrix.msystem }} update: true install: >- ${{ matrix.pkg_prefix }}-gcc ${{ matrix.pkg_prefix }}-cmake make - name: Checkout Repository uses: actions/checkout@v6 - name: Configure, Build & Test (Static & Shared) run: | for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: MinGW ${{ matrix.name }} – $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -G "MSYS Makefiles" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_STANDARD=17 \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE..." ctest --test-dir $BUILD_DIR -C Release --output-on-failure echo ">>> Completed Build: $LIB_TYPE" echo "" done # =========================================================================== # GCC ARM cross-compilation on Ubuntu (32-bit & 64-bit) # =========================================================================== gcc-arm: name: "GCC ARM ${{ matrix.name }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - name: "32-bit (armhf)" cross_pkg: gcc-arm-linux-gnueabihf cross_prefix: arm-linux-gnueabihf cmake_processor: arm qemu_binary: qemu-arm - name: "64-bit (aarch64)" cross_pkg: gcc-aarch64-linux-gnu cross_prefix: aarch64-linux-gnu cmake_processor: aarch64 qemu_binary: qemu-aarch64 steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install cross-compiler & QEMU run: | sudo apt-get update sudo apt-get install -y cmake build-essential ${{ matrix.cross_pkg }} qemu-user - name: Configure, Build & Test (Static & Shared) shell: bash run: | CROSS_C=${{ matrix.cross_prefix }}-gcc CROSS_CXX=${{ matrix.cross_prefix }}-g++ CROSS_STRIP=/usr/bin/${{ matrix.cross_prefix }}-strip SYSROOT=/usr/${{ matrix.cross_prefix }} for LIB_TYPE in "STATIC" "SHARED"; do echo ">>> Starting Build: GCC ARM ${{ matrix.name }} – $LIB_TYPE" BUILD_DIR="build_${LIB_TYPE}" SHARED_FLAG="OFF" if [ "$LIB_TYPE" == "SHARED" ]; then SHARED_FLAG="ON"; fi cmake -S . -B $BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=$CROSS_C \ -DCMAKE_CXX_COMPILER=$CROSS_CXX \ -DCMAKE_STRIP=$CROSS_STRIP \ -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=${{ matrix.cmake_processor }} \ -DCMAKE_C_STANDARD=17 \ -DZXC_NATIVE_ARCH=OFF \ -DBUILD_SHARED_LIBS=$SHARED_FLAG cmake --build $BUILD_DIR --config Release --parallel echo "Running Tests for $LIB_TYPE via QEMU..." ${{ matrix.qemu_binary }} -L $SYSROOT ./$BUILD_DIR/zxc_test echo ">>> Completed Build: $LIB_TYPE" echo "" done zxc-0.10.0/.github/workflows/quality.yml000066400000000000000000000074151517010557200202160ustar00rootroot00000000000000name: Code Quality on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read concurrency: cancel-in-progress: true group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} jobs: format: name: Code Quality - Clang-Format runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install clang-format 22 run: | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-22 main" | sudo tee /etc/apt/sources.list.d/llvm-22.list sudo apt-get update sudo apt-get install -y clang-format-22 - name: Check Formatting run: make format-check env: CLANG_FORMAT: clang-format-22 cppcheck: name: Code Quality - Cppcheck runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install Cppcheck and Cross-Compiler run: | sudo apt-get update sudo apt-get install -y cppcheck gcc-aarch64-linux-gnu libc6-dev-arm64-cross - name: Configure CMake (x86 Compile Database) run: mkdir build_x86 && cd build_x86 && cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. - name: Configure CMake (ARM64 Compile Database) run: | mkdir build_arm && cd build_arm cmake -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. - name: Run Cppcheck (x86 Variants) run: | cppcheck --project=build_x86/compile_commands.json \ --enable=all \ --suppress=missingIncludeSystem --suppress=*:src/lib/vendors/rapidhash.h \ --check-level=exhaustive \ --inline-suppr \ --error-exitcode=1 \ -j4 - name: Run Cppcheck (ARM64 Variants) run: | cppcheck --project=build_arm/compile_commands.json \ --enable=all \ --suppress=missingIncludeSystem --suppress=*:src/lib/vendors/rapidhash.h \ --check-level=exhaustive \ --inline-suppr \ --error-exitcode=1 \ -j4 quality-checks: name: Code Quality - Advanced Checks runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install Analysis Tools run: | sudo apt-get update sudo apt-get install -y clang-tools valgrind - name: Clang Static Analyzer (Scan-Build) run: | mkdir -p build_clang scan-build cmake -S . -B build_clang -DCMAKE_BUILD_TYPE=Debug scan-build --status-bugs cmake --build build_clang - name: Build for Valgrind (Debug) run: | cmake -S . -B build_valgrind -DCMAKE_BUILD_TYPE=Debug -DZXC_NATIVE_ARCH=OFF cmake --build build_valgrind - name: Valgrind Memory Check working-directory: build_valgrind run: | valgrind --leak-check=full \ --show-leak-kinds=all \ --track-origins=yes \ --error-exitcode=1 \ ./zxc_test - name: Unicode Linting shell: bash run: | echo "Scanning for non-ASCII characters in source files..." if grep -rP --include="*.c" --include="*.h" "[^\x00-\x7F]" .; then echo "::error::Non-ASCII characters found in source files." exit 1 else echo "No non-ASCII characters found." fizxc-0.10.0/.github/workflows/security.yml000066400000000000000000000031621517010557200203700ustar00rootroot00000000000000name: Code Security on: workflow_dispatch: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read security-events: write jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: language: [ 'c-cpp', 'rust', 'python', 'go', 'javascript-typescript' ] steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml - name: Build Project run: | if [ "${{ matrix.language }}" = "c-cpp" ]; then make elif [ "${{ matrix.language }}" = "rust" ]; then cd wrappers/rust cargo build --workspace --all-targets elif [ "${{ matrix.language }}" = "python" ]; then cd wrappers/python python3 -m pip install -e . elif [ "${{ matrix.language }}" = "go" ]; then cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DZXC_BUILD_CLI=OFF -DZXC_BUILD_TESTS=OFF cmake --build build --parallel cd wrappers/go CGO_ENABLED=1 CGO_LDFLAGS="-L../../build -lzxc -lpthread" go build ./... elif [ "${{ matrix.language }}" = "javascript-typescript" ]; then cd wrappers/nodejs npm install fi - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:${{ matrix.language }}" zxc-0.10.0/.github/workflows/vendors.yml000066400000000000000000000017571517010557200202110ustar00rootroot00000000000000name: Update Vendors on: workflow_dispatch: schedule: - cron: '0 4 * * 1' jobs: update-rapidhash-header: runs-on: ubuntu-slim permissions: contents: write pull-requests: write steps: - name: Checkout repo uses: actions/checkout@v6 - name: Download latest rapidhash.h run: | curl -o src/lib/vendors/rapidhash.h https://raw.githubusercontent.com/Nicoshev/rapidhash/refs/heads/master/rapidhash.h - name: Create Pull Request uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "chore(deps): update rapidhash.h from upstream" title: "Automatic update of rapidhash.h" body: | This PR updates `rapidhash.h` with the latest version from the official repository. Source : [Nicoshev/rapidhash](https://github.com/Nicoshev/rapidhash) branch: auto-update/rapidhash delete-branch: truezxc-0.10.0/.github/workflows/wrapper-go-test.yml000066400000000000000000000046041517010557200215630ustar00rootroot00000000000000name: Test Go Package on: workflow_dispatch: release: types: [ published ] permissions: contents: read jobs: test: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest - os: ubuntu-24.04-arm - os: macos-26 - os: windows-latest steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Select Latest Compiler (Linux) if: runner.os == 'Linux' run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV gcc-14 --version - name: Select Latest Xcode (macOS) if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Set up Go uses: actions/setup-go@v6 with: go-version: '1.21' cache: false - name: Build ZXC Core Library (Unix) if: runner.os != 'Windows' run: | cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DZXC_BUILD_CLI=OFF -DZXC_BUILD_TESTS=OFF cmake --build build --parallel - name: Build ZXC Core Library (Windows) if: runner.os == 'Windows' run: | cmake -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DZXC_BUILD_CLI=OFF -DZXC_BUILD_TESTS=OFF cmake --build build --parallel - name: Run Go Tests (Unix) if: runner.os != 'Windows' working-directory: ./wrappers/go run: CGO_ENABLED=1 CGO_LDFLAGS="-L../../build -lzxc -lpthread" go test -v -count=1 ./... - name: Run Go Tests (Windows) if: runner.os == 'Windows' working-directory: ./wrappers/go env: CGO_ENABLED: "1" CGO_LDFLAGS: "-L../../build -lzxc" run: go test -v -count=1 ./... - name: Run Go Benchmarks (Unix) if: runner.os != 'Windows' working-directory: ./wrappers/go run: CGO_ENABLED=1 CGO_LDFLAGS="-L../../build -lzxc -lpthread" go test -bench=. -benchmem -count=1 ./... - name: Run Go Benchmarks (Windows) if: runner.os == 'Windows' working-directory: ./wrappers/go env: CGO_ENABLED: "1" CGO_LDFLAGS: "-L../../build -lzxc" run: go test -bench=. -benchmem -count=1 ./... zxc-0.10.0/.github/workflows/wrapper-nodejs-publish.yml000066400000000000000000000105271517010557200231300ustar00rootroot00000000000000name: Publish Node.js Package on: workflow_dispatch: release: types: [ published ] permissions: contents: read jobs: build: name: Build on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - os: macos-26 arch: arm64 - os: macos-26-intel arch: x86_64 - os: windows-latest arch: AMD64 - os: windows-11-arm arch: ARM64 defaults: run: working-directory: ./wrappers/nodejs steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Select Latest Compiler (Linux) if: runner.os == 'Linux' run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV gcc-14 --version - name: Select Latest Xcode (macOS) if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: "22" - name: Install dependencies and build run: npm install - name: Run tests run: npx vitest run --globals - name: Package prebuilt addon run: | mkdir -p prebuilds/${{ matrix.os }}-${{ matrix.arch }} cp build/Release/zxc_nodejs.node prebuilds/${{ matrix.os }}-${{ matrix.arch }}/ - name: Upload prebuilt addon uses: actions/upload-artifact@v7 with: name: prebuilt-${{ matrix.os }}-${{ matrix.arch }} path: wrappers/nodejs/prebuilds/ test: name: Test on ${{ matrix.os }} Node ${{ matrix.node }} needs: [build] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-26, macos-26-intel, windows-latest, windows-11-arm] node: ["18", "20", "22"] exclude: # Node.js 18 and 20 don't have official Windows ARM64 builds - os: windows-11-arm node: "18" - os: windows-11-arm node: "20" defaults: run: working-directory: ./wrappers/nodejs steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Select Latest Compiler (Linux) if: runner.os == 'Linux' run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV gcc-14 --version - name: Select Latest Xcode (macOS) if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Install dependencies and build run: npm install - name: Run tests run: npx vitest run --globals publish: name: Publish to npm needs: [build, test] runs-on: ubuntu-slim if: github.event_name == 'release' permissions: id-token: write defaults: run: working-directory: ./wrappers/nodejs steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: "22" registry-url: "https://registry.npmjs.org" - name: Verify Version Matches Release Tag run: | RELEASE_VERSION="${{ github.event.release.tag_name }}" # Remove 'v' prefix if present RELEASE_VERSION="${RELEASE_VERSION#v}" PACKAGE_VERSION=$(node -p "require('./package.json').version") if [ "$RELEASE_VERSION" != "$PACKAGE_VERSION" ]; then echo "Version mismatch: Release tag is $RELEASE_VERSION but package.json has $PACKAGE_VERSION" exit 1 fi echo "Version matches: $RELEASE_VERSION" - name: Publish to npm run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Summary if: success() run: | echo "Successfully published zxc-compress to npm!" echo "Package: https://www.npmjs.com/package/zxc-compress" zxc-0.10.0/.github/workflows/wrapper-python-publish.yml000066400000000000000000000104121517010557200231600ustar00rootroot00000000000000name: Publish Python Package on: workflow_dispatch: release: types: [ published ] permissions: contents: read jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - os: macos-26 arch: arm64 - os: macos-26-intel arch: x86_64 - os: windows-latest arch: AMD64 - os: windows-11-arm arch: ARM64 steps: - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Select Latest Compiler (Linux) if: runner.os == 'Linux' run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV gcc-14 --version - name: Select Latest Xcode (macOS) if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install build tools run: python -m pip install cibuildwheel==3.3.1 setuptools-scm - name: Get version from git id: version run: echo "version=$(python -m setuptools_scm)" >> $GITHUB_OUTPUT working-directory: ./wrappers/python - name: Build wheels run: python -m cibuildwheel wrappers/python --output-dir wheelhouse env: # Build for Python 3.10-3.13 CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*" # Skip 32-bit, musllinux, and Python 3.10-3.11 on Windows (missing Development.Module) CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux_* cp310-win* cp311-win*" CIBW_ARCHS: "${{ matrix.arch }}" CIBW_ENVIRONMENT: "CMAKE_ARGS='-DZXC_NATIVE_ARCH=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON' SETUPTOOLS_SCM_PRETEND_VERSION=${{ steps.version.outputs.version }}" - name: Upload wheels uses: actions/upload-artifact@v7 with: name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: ./wheelhouse/*.whl build_sdist: name: Build source distribution runs-on: ubuntu-slim steps: - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install build run: python -m pip install build - name: Build sdist run: python -m build --sdist working-directory: ./wrappers/python - name: Upload sdist uses: actions/upload-artifact@v7 with: name: sdist path: ./wrappers/python/dist/*.tar.gz test_wheels: name: Test wheels on ${{ matrix.os }} ${{ matrix.python }} needs: [build_wheels] runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-26, macos-26-intel, windows-latest, windows-11-arm] python: ["3.12", "3.13"] steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} - name: Download wheels uses: actions/download-artifact@v8 with: path: wheelhouse pattern: wheels-* merge-multiple: true - name: Install wheel and test dependencies run: | pip install --no-index --find-links wheelhouse zxc-compress pip install pytest - name: Run test suite run: pytest wrappers/python/tests/ -v publish: name: Publish to PyPI needs: [build_wheels, build_sdist, test_wheels] runs-on: ubuntu-latest if: github.event_name == 'release' permissions: id-token: write steps: - name: Download all artifacts uses: actions/download-artifact@v8 with: path: dist merge-multiple: true - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ zxc-0.10.0/.github/workflows/wrapper-rust-publish.yml000066400000000000000000000060251517010557200226410ustar00rootroot00000000000000name: Publish Rust Crates on: workflow_dispatch: release: types: [ published ] permissions: contents: read jobs: test: name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-latest arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - os: macos-26 arch: x86_64 arm64 - os: windows-latest arch: AMD64 - os: windows-11-arm arch: ARM64 defaults: run: working-directory: ./wrappers/rust steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Select Latest Compiler (Linux) if: runner.os == 'Linux' run: | echo "CC=gcc-14" >> $GITHUB_ENV echo "CXX=g++-14" >> $GITHUB_ENV gcc-14 --version - name: Select Latest Xcode (macOS) if: runner.os == 'macOS' run: | sudo xcode-select -s /Applications/Xcode_26.4.app clang --version - name: Install Rust Toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - name: Run Tests run: cargo test --workspace publish: name: Publish to crates.io needs: [test] runs-on: ubuntu-slim if: github.event_name == 'release' defaults: run: working-directory: ./wrappers/rust steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Install Rust Toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable - name: Verify Version Matches Release Tag run: | RELEASE_VERSION="${{ github.event.release.tag_name }}" # Remove 'v' prefix if present RELEASE_VERSION="${RELEASE_VERSION#v}" CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') if [ "$RELEASE_VERSION" != "$CARGO_VERSION" ]; then echo "Version mismatch: Release tag is $RELEASE_VERSION but Cargo.toml has $CARGO_VERSION" exit 1 fi echo "Version matches: $RELEASE_VERSION" - name: Publish zxc-compress-sys (FFI bindings) run: | cd zxc-sys cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} continue-on-error: false # Wait for zxc-compress-sys to be available on crates.io before publishing zxc-compress - name: Wait for zxc-compress-sys to propagate run: sleep 30 - name: Publish zxc-compress (safe wrapper) run: | cd zxc cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} continue-on-error: false - name: Summary if: success() run: | echo "Successfully published:" echo "- zxc-compress-sys (FFI bindings)" echo "- zxc-compress (safe wrapper)" echo "" echo "Crates are now available on crates.io!" zxc-0.10.0/.github/workflows/wrapper-wasm.yml000066400000000000000000000032411517010557200211440ustar00rootroot00000000000000# ZXC - High-performance lossless compression # # Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. # SPDX-License-Identifier: BSD-3-Clause name: WASM Build on: push: workflow_dispatch: release: types: [ published ] branches: [ main ] paths: - 'src/**' - 'include/**' - 'wrappers/wasm/**' - 'CMakeLists.txt' - '.github/workflows/wrapper-wasm.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: wasm-build: name: Build & Test WASM runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Emscripten uses: mymindstorm/setup-emsdk@v14 with: version: 3.1.56 - name: Verify Emscripten run: emcc --version - name: Configure run: emcmake cmake -B build-wasm -DCMAKE_BUILD_TYPE=Release - name: Build run: cmake --build build-wasm --parallel - name: Verify outputs run: | ls -lh build-wasm/zxc.js build-wasm/zxc.wasm echo "--- WASM file size ---" wc -c build-wasm/zxc.wasm - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '20' - name: Run Tests run: BUILD_DIR=build-wasm node wrappers/wasm/test.mjs - name: Upload WASM Artifacts uses: actions/upload-artifact@v7 with: name: zxc-wasm path: | build-wasm/zxc.js build-wasm/zxc.wasm wrappers/wasm/zxc_wasm.js wrappers/wasm/package.json wrappers/wasm/README.md retention-days: 30 zxc-0.10.0/.gitignore000066400000000000000000000020711517010557200143670ustar00rootroot00000000000000# Prerequisites *.d # Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ *.su *.idb *.pdb # debug information files *.dwo CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts Testing Makefile !/Makefile cmake_install.cmake install_manifest.txt compile_commands.json CTestTestfile.cmake _deps CMakeUserPresets.json # CLion # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #cmake-build-* build/* # Doxygen docs/html/ docs/latex/ docs/rtf/ docs/man/*.1 docs/xml/ *.doxygen_sqlite3 doxyfile.inc searchdata.xml # Coverage build-cov/ zxc-0.10.0/.snyk000066400000000000000000000001051517010557200133600ustar00rootroot00000000000000# Snyk (https://snyk.io) policy file exclude: code: - tests/** zxc-0.10.0/CMakeLists.txt000066400000000000000000000577441517010557200151600ustar00rootroot00000000000000# ZXC - High-performance lossless compression # # Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. # SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.14) # ============================================================================= # Version extraction from header # ============================================================================= file(READ "include/zxc_constants.h" version_header) string(REGEX MATCH "#define ZXC_VERSION_MAJOR ([0-9]+)" _ "${version_header}") set(MAJOR_VER ${CMAKE_MATCH_1}) string(REGEX MATCH "#define ZXC_VERSION_MINOR ([0-9]+)" _ "${version_header}") set(MINOR_VER ${CMAKE_MATCH_1}) string(REGEX MATCH "#define ZXC_VERSION_PATCH ([0-9]+)" _ "${version_header}") set(PATCH_VER ${CMAKE_MATCH_1}) project(zxc VERSION ${MAJOR_VER}.${MINOR_VER}.${PATCH_VER} LANGUAGES C DESCRIPTION "High-performance asymmetric lossless compression library" ) # ============================================================================= # Build Options # ============================================================================= option(BUILD_SHARED_LIBS "Build shared libraries instead of static" OFF) option(ZXC_NATIVE_ARCH "Enable -march=native for maximum performance" ON) option(ZXC_ENABLE_LTO "Enable Interprocedural Optimization (LTO)" ON) set(ZXC_PGO_MODE "OFF" CACHE STRING "Profile-Guided Optimization mode (OFF/GENERATE/USE)") set_property(CACHE ZXC_PGO_MODE PROPERTY STRINGS OFF GENERATE USE) option(ZXC_BUILD_CLI "Build the command-line interface" ON) option(ZXC_BUILD_TESTS "Build unit tests" ON) option(ZXC_ENABLE_COVERAGE "Enable code coverage generation" OFF) option(ZXC_DISABLE_SIMD "Disable explicit SIMD intrinsics (no AVX/NEON code paths)" OFF) # ============================================================================= # Emscripten / WebAssembly overrides # ============================================================================= if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") message(STATUS "Emscripten detected — configuring for WebAssembly.") set(ZXC_DISABLE_SIMD ON CACHE BOOL "" FORCE) set(ZXC_BUILD_CLI OFF CACHE BOOL "" FORCE) set(ZXC_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(ZXC_NATIVE_ARCH OFF CACHE BOOL "" FORCE) set(ZXC_ENABLE_LTO OFF CACHE BOOL "" FORCE) set(ZXC_PGO_MODE "OFF" CACHE STRING "" FORCE) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) endif() if(ZXC_DISABLE_SIMD) add_compile_definitions(ZXC_DISABLE_SIMD) endif() # ============================================================================= # C Standard # ============================================================================= set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) # Enable _GNU_SOURCE for ftello/fseeko on Linux # Enable 64-bit off_t on 32-bit Linux to prevent fseeko/ftello/pread truncation if(UNIX AND NOT APPLE) add_compile_definitions(_GNU_SOURCE _FILE_OFFSET_BITS=64 _LARGEFILE_SOURCE) elseif(APPLE) add_compile_definitions(_GNU_SOURCE) endif() # Check for LTO support if(ZXC_ENABLE_LTO AND NOT ZXC_ENABLE_COVERAGE) include(CheckIPOSupported) check_ipo_supported(RESULT result OUTPUT output) if(result) message(STATUS "LTO/IPO is supported and enabled.") else() message(WARNING "LTO/IPO is not supported: ${output}") set(ZXC_ENABLE_LTO OFF) endif() elseif(ZXC_ENABLE_COVERAGE) message(STATUS "Code coverage enabled: Disabling LTO and PGO.") set(ZXC_ENABLE_LTO OFF) set(ZXC_PGO_MODE "OFF") endif() # ============================================================================= # Rapidhash: system-installed (e.g. vcpkg) or vendored fallback # ============================================================================= find_path(RAPIDHASH_INCLUDE_DIR rapidhash.h) if(NOT RAPIDHASH_INCLUDE_DIR) set(RAPIDHASH_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/lib/vendors") message(STATUS "Using vendored rapidhash from ${RAPIDHASH_INCLUDE_DIR}") else() message(STATUS "Found system rapidhash in ${RAPIDHASH_INCLUDE_DIR}") endif() # ============================================================================= # Core Library & Runtime Dispatch # ============================================================================= # --- PGO flag selection (Clang vs GCC) --- set(ZXC_PGO_DIR "${CMAKE_BINARY_DIR}/pgo") set(ZXC_PGO_GEN_CFLAGS "") set(ZXC_PGO_GEN_LDFLAGS "") set(ZXC_PGO_USE_CFLAGS "") set(ZXC_PGO_USE_LDFLAGS "") if(NOT MSVC AND NOT ZXC_PGO_MODE STREQUAL "OFF") if(CMAKE_C_COMPILER_ID MATCHES "Clang") # Clang: instrumentation-based PGO set(ZXC_PGO_PROFDATA "${ZXC_PGO_DIR}/default.profdata") set(ZXC_PGO_GEN_CFLAGS -fprofile-instr-generate=${ZXC_PGO_DIR}/default_%m.profraw) set(ZXC_PGO_GEN_LDFLAGS -fprofile-instr-generate) set(ZXC_PGO_USE_CFLAGS -fprofile-instr-use=${ZXC_PGO_PROFDATA}) set(ZXC_PGO_USE_LDFLAGS -fprofile-instr-use=${ZXC_PGO_PROFDATA}) else() # GCC: directory-based PGO set(ZXC_PGO_GEN_CFLAGS -fprofile-generate=${ZXC_PGO_DIR}) set(ZXC_PGO_GEN_LDFLAGS -fprofile-generate=${ZXC_PGO_DIR}) set(ZXC_PGO_USE_CFLAGS -fprofile-use=${ZXC_PGO_DIR} -fprofile-correction) set(ZXC_PGO_USE_LDFLAGS -fprofile-use=${ZXC_PGO_DIR}) endif() endif() # Helper: apply PGO flags to a target macro(zxc_apply_pgo target) if(ZXC_PGO_MODE STREQUAL "GENERATE") target_compile_options(${target} PRIVATE ${ZXC_PGO_GEN_CFLAGS}) target_link_options(${target} PRIVATE ${ZXC_PGO_GEN_LDFLAGS}) elseif(ZXC_PGO_MODE STREQUAL "USE") if(EXISTS "${ZXC_PGO_DIR}") target_compile_options(${target} PRIVATE ${ZXC_PGO_USE_CFLAGS}) target_link_options(${target} PRIVATE ${ZXC_PGO_USE_LDFLAGS}) endif() endif() endmacro() # Function Multi-Versioning Helper # Compiles src/lib/zxc_compress.c and src/lib/zxc_decompress.c with specific flags and suffix. macro(zxc_add_variant suffix flags) add_library(zxc_compress${suffix} OBJECT src/lib/zxc_compress.c) target_compile_options(zxc_compress${suffix} PRIVATE ${flags}) target_compile_definitions(zxc_compress${suffix} PRIVATE ZXC_FUNCTION_SUFFIX=${suffix}) # For static builds, define ZXC_STATIC_DEFINE if(NOT BUILD_SHARED_LIBS) target_compile_definitions(zxc_compress${suffix} PRIVATE ZXC_STATIC_DEFINE) else() # Mark as part of the DLL being built (avoids dllimport on internal symbols) target_compile_definitions(zxc_compress${suffix} PRIVATE zxc_lib_EXPORTS) set_target_properties(zxc_compress${suffix} PROPERTIES POSITION_INDEPENDENT_CODE ON) # Hide variant symbols from shared library public ABI if(NOT MSVC) target_compile_options(zxc_compress${suffix} PRIVATE -fvisibility=hidden) endif() endif() # Inherit include directories target_include_directories(zxc_compress${suffix} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lib ${RAPIDHASH_INCLUDE_DIR} PUBLIC $) add_library(zxc_decompress${suffix} OBJECT src/lib/zxc_decompress.c) target_compile_options(zxc_decompress${suffix} PRIVATE ${flags}) target_compile_definitions(zxc_decompress${suffix} PRIVATE ZXC_FUNCTION_SUFFIX=${suffix}) # For static builds, define ZXC_STATIC_DEFINE if(NOT BUILD_SHARED_LIBS) target_compile_definitions(zxc_decompress${suffix} PRIVATE ZXC_STATIC_DEFINE) else() # Mark as part of the DLL being built (avoids dllimport on internal symbols) target_compile_definitions(zxc_decompress${suffix} PRIVATE zxc_lib_EXPORTS) set_target_properties(zxc_decompress${suffix} PROPERTIES POSITION_INDEPENDENT_CODE ON) # Hide variant symbols from shared library public ABI if(NOT MSVC) target_compile_options(zxc_decompress${suffix} PRIVATE -fvisibility=hidden) endif() endif() target_include_directories(zxc_decompress${suffix} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lib ${RAPIDHASH_INCLUDE_DIR} PUBLIC $) # Propagate PGO flags to variant objects zxc_apply_pgo(zxc_compress${suffix}) zxc_apply_pgo(zxc_decompress${suffix}) list(APPEND ZXC_VARIANT_OBJECTS $ $) endmacro() set(ZXC_VARIANT_OBJECTS "") # --- 1. Default Variant (Scalar/Baseline) --- zxc_add_variant(_default "") # --- 2. Architecture Specific Variants (skipped in no-intrinsics mode) --- if(ZXC_DISABLE_SIMD) message(STATUS "ZXC_DISABLE_SIMD: Skipping SIMD variants (no explicit AVX/NEON code paths).") else() if(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64|x86_64|AMD64") message(STATUS "Building x86_64 AVX2 and AVX512 variants...") if(MSVC) # AVX2 for MSVC (Enables AVX2/BMI1/BMI2 sets) zxc_add_variant(_avx2 "/arch:AVX2;/D__BMI__;/D__BMI2__;/D__LZCNT__") # AVX512 for MSVC (VS2019 16.10+ supports /arch:AVX512) zxc_add_variant(_avx512 "/arch:AVX512;/D__BMI__;/D__BMI2__;/D__LZCNT__") else() # AVX2 for GCC/Clang zxc_add_variant(_avx2 "-mavx2;-mfma;-mbmi;-mbmi2;-mlzcnt") # AVX512 for GCC/Clang zxc_add_variant(_avx512 "-mavx512f;-mavx512bw;-mbmi;-mbmi2;-mlzcnt") endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") message(STATUS "Building AArch64 NEON variant...") if(MSVC) # MSVC for ARM64 implies NEON support by default. zxc_add_variant(_neon "") else() # NEON is usually default on AArch64, but we add a specific variant for structure zxc_add_variant(_neon "-march=armv8-a+simd") endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^arm") message(STATUS "Building ARM NEON variant...") zxc_add_variant(_neon "-march=armv7-a;-mfpu=neon") endif() endif() # Sources: exclude zxc_driver.c for Emscripten (requires pthreads + FILE* I/O) if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") set(ZXC_CORE_SOURCES src/lib/zxc_common.c src/lib/zxc_dispatch.c src/lib/zxc_seekable.c ) else() set(ZXC_CORE_SOURCES src/lib/zxc_common.c src/lib/zxc_driver.c src/lib/zxc_dispatch.c src/lib/zxc_seekable.c ) endif() add_library(zxc_lib ${ZXC_CORE_SOURCES} ${ZXC_VARIANT_OBJECTS} ) # ============================================================================= # ABI Versioning (Debian/Linux shared libraries) # ============================================================================= # Increment this number ONLY when breaking the ABI. set(ZXC_SOVERSION 3) # Set library output name and version set_target_properties(zxc_lib PROPERTIES OUTPUT_NAME zxc VERSION ${PROJECT_VERSION} SOVERSION ${ZXC_SOVERSION} ) # Target-based include directories for the main lib target_include_directories(zxc_lib PUBLIC $ $ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lib ${RAPIDHASH_INCLUDE_DIR} ) # Symbol visibility for shared libraries if(BUILD_SHARED_LIBS) # Set visibility for GCC/Clang if(NOT MSVC) target_compile_options(zxc_lib PRIVATE -fvisibility=hidden) set_target_properties(zxc_lib PROPERTIES C_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES ) endif() else() # For static libraries, define ZXC_STATIC_DEFINE to avoid dllimport/dllexport target_compile_definitions(zxc_lib PUBLIC ZXC_STATIC_DEFINE) endif() # ============================================================================= # Compiler-specific options (using generator expressions) # ============================================================================= if(NOT MSVC) target_compile_options(zxc_lib PRIVATE $<$>:-O3> -Wall -Wextra -fomit-frame-pointer -fstrict-aliasing # Native Arch $<$:-march=native> # Dead code elimination -ffunction-sections -fdata-sections ) # Profile-Guided Optimization zxc_apply_pgo(zxc_lib) if(ZXC_PGO_MODE STREQUAL "GENERATE") message(STATUS "PGO: Generating profile data to ${ZXC_PGO_DIR}") elseif(ZXC_PGO_MODE STREQUAL "USE") if(NOT EXISTS "${ZXC_PGO_DIR}") message(FATAL_ERROR "PGO: Profile data not found at ${ZXC_PGO_DIR}. Run with ZXC_PGO_MODE=GENERATE first.") endif() message(STATUS "PGO: Using profile data from ${ZXC_PGO_DIR}") endif() # Linker options for Dead Code Stripping if(APPLE) target_link_options(zxc_lib PRIVATE -Wl,-dead_strip) else() target_link_options(zxc_lib PRIVATE -Wl,--gc-sections) endif() else() target_compile_options(zxc_lib PRIVATE $<$:/O2> /W3) target_compile_definitions(zxc_lib PRIVATE _CRT_SECURE_NO_WARNINGS) endif() target_compile_definitions(zxc_lib PRIVATE $<$>:_GNU_SOURCE> ) # Coverage flags if(ZXC_ENABLE_COVERAGE) if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(zxc_lib PRIVATE --coverage -fprofile-update=atomic) target_link_options(zxc_lib PRIVATE --coverage) endif() endif() # Enable LTO cleanly if(ZXC_ENABLE_LTO) set_property(TARGET zxc_lib PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) if(NOT MSVC) target_compile_options(zxc_lib PRIVATE -flto) target_link_options(zxc_lib PRIVATE -flto) endif() endif() # Threading support (not available in WASM single-threaded builds) if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") find_package(Threads REQUIRED) target_link_libraries(zxc_lib PRIVATE Threads::Threads) endif() # ============================================================================= # CLI Executable # ============================================================================= if(ZXC_BUILD_CLI) add_executable(zxc src/cli/main.c) target_link_libraries(zxc PRIVATE zxc_lib) target_include_directories(zxc PRIVATE ${RAPIDHASH_INCLUDE_DIR}) # Math library on Unix if(UNIX) target_link_libraries(zxc PRIVATE m) endif() # Propagate compile options and definitions target_compile_options(zxc PRIVATE $<$>,$>:-march=native> ) target_compile_definitions(zxc PRIVATE $<$:_CRT_SECURE_NO_WARNINGS> $<$>:_GNU_SOURCE> ) # Coverage flags for CLI if(ZXC_ENABLE_COVERAGE) if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(zxc PRIVATE --coverage) target_link_options(zxc PRIVATE --coverage) endif() endif() # Enable LTO cleanly if(ZXC_ENABLE_LTO) set_property(TARGET zxc PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) if(NOT MSVC) target_compile_options(zxc PRIVATE -flto) target_link_options(zxc PRIVATE -flto) endif() endif() # Profile-Guided Optimization for CLI zxc_apply_pgo(zxc) # Linker options for Dead Code Stripping if(NOT MSVC) if(APPLE) target_link_options(zxc PRIVATE -Wl,-dead_strip) else() target_link_options(zxc PRIVATE -Wl,--gc-sections) endif() endif() # Strip symbols in Release mode for smaller binary if(NOT MSVC AND CMAKE_BUILD_TYPE STREQUAL "Release") # Set default strip command if not already set (e.g., for cross-compilation) if(NOT CMAKE_STRIP) set(CMAKE_STRIP strip) endif() add_custom_command(TARGET zxc POST_BUILD COMMAND ${CMAKE_STRIP} $ COMMENT "Stripping symbols from zxc" ) endif() endif() # ============================================================================= # Tests # ============================================================================= if(ZXC_BUILD_TESTS) enable_testing() add_executable(zxc_test tests/test.c) # When building shared libraries, create a static version for tests # This allows tests to access internal functions for unit testing if(BUILD_SHARED_LIBS) # Create a static library specifically for tests add_library(zxc_lib_static STATIC src/lib/zxc_common.c src/lib/zxc_driver.c src/lib/zxc_dispatch.c src/lib/zxc_seekable.c ${ZXC_VARIANT_OBJECTS} ) # Copy all properties from the shared library target_include_directories(zxc_lib_static PUBLIC $ $ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lib ${RAPIDHASH_INCLUDE_DIR} ) # Apply same compiler settings as main library target_compile_options(zxc_lib_static PRIVATE $ ) target_compile_definitions(zxc_lib_static PUBLIC ZXC_STATIC_DEFINE) target_link_libraries(zxc_lib_static PRIVATE Threads::Threads) # Link tests against static library target_link_libraries(zxc_test PRIVATE zxc_lib_static) else() # For static builds, use the main library target_link_libraries(zxc_test PRIVATE zxc_lib) endif() # Propagate compile options target_compile_options(zxc_test PRIVATE $<$>,$>:-march=native> ) # Coverage flags for Tests if(ZXC_ENABLE_COVERAGE) if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") target_link_options(zxc_test PRIVATE --coverage) endif() endif() # Profile-Guided Optimization for tests zxc_apply_pgo(zxc_test) target_include_directories(zxc_test PRIVATE src/lib ${RAPIDHASH_INCLUDE_DIR}) add_test(NAME UnitTests COMMAND zxc_test) endif() # ============================================================================= # Installation # ============================================================================= include(GNUInstallDirs) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/libzxc.pc.in ${CMAKE_CURRENT_BINARY_DIR}/libzxc.pc @ONLY ) install(TARGETS zxc_lib EXPORT zxc-targets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.h" ) if(ZXC_BUILD_CLI) install(TARGETS zxc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) endif() install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libzxc.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig ) # CMake package config files for find_package(zxc) include(CMakePackageConfigHelpers) install(EXPORT zxc-targets FILE zxc-targets.cmake NAMESPACE zxc:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/zxc ) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/zxcConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/zxcConfig.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/zxc ) write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/zxcConfigVersion.cmake VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/zxcConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/zxcConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/zxc ) # ============================================================================= # Code Formatting (clang-format) # ============================================================================= # Allow override via environment variable (e.g., CLANG_FORMAT=clang-format-22) if(DEFINED ENV{CLANG_FORMAT}) set(CLANG_FORMAT "$ENV{CLANG_FORMAT}") else() find_program(CLANG_FORMAT clang-format) endif() if(CLANG_FORMAT) file(GLOB_RECURSE ZXC_FORMAT_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h" "${CMAKE_CURRENT_SOURCE_DIR}/src/lib/*.c" "${CMAKE_CURRENT_SOURCE_DIR}/src/lib/*.h" ) # Exclude vendored third-party code list(FILTER ZXC_FORMAT_SOURCES EXCLUDE REGEX ".*/vendors/.*") add_custom_target(format COMMAND ${CLANG_FORMAT} --style=file -i ${ZXC_FORMAT_SOURCES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Formatting ${CMAKE_CURRENT_SOURCE_DIR}/include and src/lib with clang-format" VERBATIM ) add_custom_target(format-check COMMAND ${CLANG_FORMAT} --style=file --dry-run --Werror ${ZXC_FORMAT_SOURCES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Checking formatting of include/ and src/lib/" VERBATIM ) else() message(STATUS "clang-format not found - format/format-check targets disabled") endif() # ============================================================================= # Summary # ============================================================================= message(STATUS "") message(STATUS "ZXC Configuration Summary:") message(STATUS " Version: ${PROJECT_VERSION}") if(BUILD_SHARED_LIBS) message(STATUS " Library Type: Shared") else() message(STATUS " Library Type: Static") endif() message(STATUS " Native Arch: ${ZXC_NATIVE_ARCH}") message(STATUS " Disable SIMD: ${ZXC_DISABLE_SIMD}") message(STATUS " LTO Enabled: ${ZXC_ENABLE_LTO}") message(STATUS " PGO Mode: ${ZXC_PGO_MODE}") message(STATUS " Build CLI: ${ZXC_BUILD_CLI}") message(STATUS " Build Tests: ${ZXC_BUILD_TESTS}") message(STATUS "") # ============================================================================= # WebAssembly (Emscripten) Target # ============================================================================= if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # Exported C functions (with leading underscore for Emscripten convention) set(ZXC_WASM_EXPORTS "_zxc_compress" "_zxc_decompress" "_zxc_compress_bound" "_zxc_compress_block_bound" "_zxc_get_decompressed_size" "_zxc_create_cctx" "_zxc_free_cctx" "_zxc_compress_cctx" "_zxc_create_dctx" "_zxc_free_dctx" "_zxc_decompress_dctx" "_zxc_compress_block" "_zxc_decompress_block" "_zxc_min_level" "_zxc_max_level" "_zxc_default_level" "_zxc_version_string" "_zxc_error_name" "_malloc" "_free" ) # Join list with commas for Emscripten linker flag string(JOIN "," ZXC_WASM_EXPORTS_STR ${ZXC_WASM_EXPORTS}) add_executable(zxc_wasm wrappers/wasm/wasm_entry.c) target_link_libraries(zxc_wasm PRIVATE zxc_lib) set_target_properties(zxc_wasm PROPERTIES OUTPUT_NAME "zxc" SUFFIX ".js" ) target_link_options(zxc_wasm PRIVATE "-sEXPORTED_FUNCTIONS=[${ZXC_WASM_EXPORTS_STR}]" "-sEXPORTED_RUNTIME_METHODS=[ccall,cwrap,getValue,setValue,UTF8ToString,HEAPU8,HEAP32,HEAPU32]" "-sMODULARIZE=1" "-sEXPORT_NAME=ZXCModule" "-sALLOW_MEMORY_GROWTH=1" "-sINITIAL_MEMORY=2097152" "-sSTACK_SIZE=131072" "-sENVIRONMENT=web,node" "-sNO_FILESYSTEM=1" "-sSTRICT=1" ) target_compile_options(zxc_wasm PRIVATE -O3) message(STATUS " WASM Target: zxc.js + zxc.wasm") endif() # ============================================================================= # Documentation (Doxygen) # ============================================================================= find_package(Doxygen) if(DOXYGEN_FOUND) # Generate the Doxyfile with the current project version configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) # Add a 'doc' target to generate documentation (e.g. 'make doc' or 'cmake --build . --target doc') add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Generating API documentation with Doxygen" VERBATIM ) endif() zxc-0.10.0/Doxyfile.in000066400000000000000000000251371517010557200145220ustar00rootroot00000000000000# Doxyfile 1.9.1 #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- PROJECT_NAME = "@PROJECT_NAME@" PROJECT_NUMBER = "@PROJECT_VERSION@" PROJECT_BRIEF = "@PROJECT_DESCRIPTION@" OUTPUT_DIRECTORY = docs CREATE_SUBDIRS = NO ALLOW_UNICODE_NAMES = NO OUTPUT_LANGUAGE = English BRIEF_MEMBER_DESC = YES REPEAT_BRIEF = YES ABBREVIATE_BRIEF = ALWAYS_DETAILED_SEC = NO INLINE_INHERITED_MEMB = NO FULL_PATH_NAMES = YES STRIP_FROM_PATH = STRIP_FROM_INC_PATH = SHORT_NAMES = NO JAVADOC_AUTOBRIEF = YES JAVADOC_BANNER = NO QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO PYTHON_DOCSTRING = YES INHERIT_DOCS = YES SEPARATE_MEMBER_PAGES = NO TAB_SIZE = 4 ALIASES = OPTIMIZE_OUTPUT_FOR_C = YES OPTIMIZE_OUTPUT_JAVA = NO OPTIMIZE_FOR_FORTRAN = NO OPTIMIZE_OUTPUT_VHDL = NO OPTIMIZE_OUTPUT_SLICE = NO EXTENSION_MAPPING = MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 5 AUTOLINK_SUPPORT = YES BUILTIN_STL_SUPPORT = NO CPP_CLI_SUPPORT = NO SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES DISTRIBUTE_GROUP_DOC = NO GROUP_NESTED_COMPOUNDS = NO SUBGROUPING = YES INLINE_GROUPED_CLASSES = NO INLINE_SIMPLE_STRUCTS = NO TYPEDEF_HIDES_STRUCT = NO LOOKUP_CACHE_SIZE = 0 NUM_PROC_THREADS = 1 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- EXTRACT_ALL = NO EXTRACT_PRIVATE = NO EXTRACT_PRIV_VIRTUAL = NO EXTRACT_PACKAGE = NO EXTRACT_STATIC = YES EXTRACT_LOCAL_CLASSES = NO EXTRACT_LOCAL_METHODS = NO EXTRACT_ANON_NSPACES = NO RESOLVE_UNNAMED_PARAMS = YES HIDE_UNDOC_MEMBERS = NO HIDE_UNDOC_CLASSES = NO HIDE_FRIEND_COMPOUNDS = NO HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO CASE_SENSE_NAMES = YES HIDE_SCOPE_NAMES = NO HIDE_COMPOUND_REFERENCE= NO SHOW_HEADERFILE = YES SHOW_INCLUDE_FILES = YES SHOW_GROUPED_MEMB_INC = NO FORCE_LOCAL_INCLUDES = NO INLINE_INFO = YES SORT_MEMBER_DOCS = YES SORT_BRIEF_DOCS = NO SORT_MEMBERS_CTORS_1ST = NO SORT_GROUP_NAMES = NO SORT_BY_SCOPE_NAME = NO STRICT_PROTO_MATCHING = NO GENERATE_TODOLIST = YES GENERATE_TESTLIST = YES GENERATE_BUGLIST = YES GENERATE_DEPRECATEDLIST= YES ENABLED_SECTIONS = MAX_INITIALIZER_LINES = 30 SHOW_USED_FILES = YES SHOW_FILES = YES SHOW_NAMESPACES = YES FILE_VERSION_FILTER = LAYOUT_FILE = CITE_BIB_FILES = #--------------------------------------------------------------------------- # Configuration options related to warning and progress messages #--------------------------------------------------------------------------- QUIET = NO WARNINGS = YES WARN_IF_UNDOCUMENTED = NO WARN_IF_DOC_ERROR = YES WARN_IF_INCOMPLETE_DOC = YES WARN_NO_PARAMDOC = YES WARN_AS_ERROR = NO WARN_FORMAT = "$file:$line: $text" WARN_LINE_FORMAT = "at line $line of file $file" WARN_LOGFILE = #--------------------------------------------------------------------------- # Configuration options related to the input files #--------------------------------------------------------------------------- INPUT = include \ src/lib INPUT_ENCODING = UTF-8 INPUT_FILE_ENCODING = FILE_PATTERNS = *.h \ *.c RECURSIVE = YES EXCLUDE = EXCLUDE_SYMLINKS = NO EXCLUDE_PATTERNS = */test/* \ */tests/* \ */build/* \ */target/* \ */src/lib/*.c EXCLUDE_SYMBOLS = EXAMPLE_PATH = EXAMPLE_PATTERNS = * EXAMPLE_RECURSIVE = NO IMAGE_PATH = INPUT_FILTER = FILTER_PATTERNS = FILTER_SOURCE_FILES = NO FILTER_SOURCE_PATTERNS = USE_MDFILE_AS_MAINPAGE = README.md #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- SOURCE_BROWSER = YES INLINE_SOURCES = NO STRIP_CODE_COMMENTS = YES REFERENCED_BY_RELATION = YES REFERENCES_RELATION = YES REFERENCES_LINK_SOURCE = YES SOURCE_TOOLTIPS = YES USE_HTAGS = NO VERBATIM_HEADERS = YES CLANG_ASSISTED_PARSING = NO CLANG_ADD_INC_PATHS = YES CLANG_OPTIONS = CLANG_DATABASE_PATH = #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- ALPHABETICAL_INDEX = YES IGNORE_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the HTML output #--------------------------------------------------------------------------- GENERATE_HTML = YES HTML_OUTPUT = html HTML_FILE_EXTENSION = .html HTML_HEADER = HTML_FOOTER = HTML_STYLESHEET = HTML_EXTRA_STYLESHEET = HTML_EXTRA_FILES = HTML_COLORSTYLE = AUTO_LIGHT HTML_COLORSTYLE_HUE = 220 HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 HTML_TIMESTAMP = NO HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = NO HTML_INDEX_NUM_ENTRIES = 100 GENERATE_DOCSET = NO GENERATE_HTMLHELP = NO GENERATE_QHP = NO GENERATE_ECLIPSEHELP = NO DISABLE_INDEX = NO GENERATE_TREEVIEW = YES FULL_SIDEBAR = NO ENUM_VALUES_PER_LINE = 4 TREEVIEW_WIDTH = 250 EXT_LINKS_IN_WINDOW = NO OBFUSCATE_EMAILS = YES HTML_FORMULA_FORMAT = png FORMULA_FONTSIZE = 10 FORMULA_MACROFILE = USE_MATHJAX = NO MATHJAX_VERSION = MathJax_2 MATHJAX_FORMAT = HTML-CSS MATHJAX_RELPATH = MATHJAX_EXTENSIONS = MATHJAX_CODEFILE = SEARCHENGINE = YES SERVER_BASED_SEARCH = NO EXTERNAL_SEARCH = NO SEARCHENGINE_URL = SEARCHDATA_FILE = searchdata.xml EXTERNAL_SEARCH_ID = EXTRA_SEARCH_MAPPINGS = #--------------------------------------------------------------------------- # Configuration options related to the LaTeX output #--------------------------------------------------------------------------- GENERATE_LATEX = NO #--------------------------------------------------------------------------- # Configuration options related to the RTF output #--------------------------------------------------------------------------- GENERATE_RTF = NO #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- GENERATE_MAN = NO #--------------------------------------------------------------------------- # Configuration options related to the XML output #--------------------------------------------------------------------------- GENERATE_XML = NO #--------------------------------------------------------------------------- # Configuration options related to the DOCBOOK output #--------------------------------------------------------------------------- GENERATE_DOCBOOK = NO #--------------------------------------------------------------------------- # Configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # Configuration options related to Sqlite3 output #--------------------------------------------------------------------------- GENERATE_SQLITE3 = NO #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- GENERATE_PERLMOD = NO #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- ENABLE_PREPROCESSING = YES MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = NO SEARCH_INCLUDES = YES INCLUDE_PATH = include INCLUDE_FILE_PATTERNS = PREDEFINED = ZXC_EXPORT= \ ZXC_NO_EXPORT= \ ZXC_DEPRECATED= \ __attribute__(x)= EXPAND_AS_DEFINED = SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration options related to external references #--------------------------------------------------------------------------- TAGFILES = GENERATE_TAGFILE = ALLEXTERNALS = NO EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- # Configuration options related to diagram generator tools #--------------------------------------------------------------------------- HIDE_UNDOC_RELATIONS = YES HAVE_DOT = NO DOT_NUM_THREADS = 0 DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = CLASS_GRAPH = YES COLLABORATION_GRAPH = YES GROUP_GRAPHS = YES UML_LOOK = NO UML_LIMIT_NUM_FIELDS = 10 DOT_UML_DETAILS = NO DOT_WRAP_THRESHOLD = 17 TEMPLATE_RELATIONS = NO INCLUDE_GRAPH = YES INCLUDED_BY_GRAPH = YES CALL_GRAPH = NO CALLER_GRAPH = NO GRAPHICAL_HIERARCHY = YES DIRECTORY_GRAPH = YES DIR_GRAPH_MAX_DEPTH = 1 DOT_IMAGE_FORMAT = png INTERACTIVE_SVG = NO DOT_PATH = DOTFILE_DIRS = DIA_PATH = DIAFILE_DIRS = PLANTUML_JAR_PATH = PLANTUML_CFG_FILE = PLANTUML_INCLUDE_PATH = DOT_GRAPH_MAX_NODES = 50 MAX_DOT_GRAPH_DEPTH = 0 DOT_MULTI_TARGETS = NO GENERATE_LEGEND = YES DOT_CLEANUP = YES MSCGEN_TOOL = MSCFILE_DIRS = zxc-0.10.0/LICENSE000066400000000000000000000063351517010557200134130ustar00rootroot00000000000000============================================================================== ZXC Copyright (c) 2025-2026, Bertrand Lebonnois and contributors License: BSD 3-Clause ============================================================================== BSD 3-Clause License ==================== Copyright (c) 2025-2026, Bertrand Lebonnois and contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of ZXC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BERTRAND LEBONNOIS OR CONTRIBUTORS BE LIABLE FOR DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ============================================================================== This project includes code from rapidhash Copyright (C) 2025 Nicolas De Carli License: MIT (Expat) ============================================================================== rapidhash - Very fast, high quality, platform-independent hashing algorithm. Copyright (C) 2025 Nicolas De Carli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. You can contact the author at: - rapidhash source repository: https://github.com/Nicoshev/rapidhash zxc-0.10.0/Makefile000066400000000000000000000042621517010557200140430ustar00rootroot00000000000000# ZXC - High-performance lossless compression # # Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. # SPDX-License-Identifier: BSD-3-Clause # Top-level convenience Makefile (wraps CMake) # # Usage: # make Build the library + CLI (Release) # make test Build and run tests # make format Format source code with clang-format # make format-check Check formatting (CI mode) # make doc Generate Doxygen documentation # make clean Remove build directory # # Override build directory: make BUILD=mybuild # Pass extra CMake flags: make CMAKE_EXTRA="-DZXC_NATIVE_ARCH=OFF" BUILD ?= build CMAKE ?= cmake CMAKE_EXTRA ?= JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) .PHONY: all test format format-check doc clean # ── Build ──────────────────────────────────────────────────── all: @$(CMAKE) -S . -B $(BUILD) -DCMAKE_BUILD_TYPE=Release $(CMAKE_EXTRA) @$(CMAKE) --build $(BUILD) -j$(JOBS) # ── Test ───────────────────────────────────────────────────── test: @$(CMAKE) -S . -B $(BUILD) -DCMAKE_BUILD_TYPE=Debug -DZXC_BUILD_TESTS=ON $(CMAKE_EXTRA) @$(CMAKE) --build $(BUILD) -j$(JOBS) @cd $(BUILD) && ctest --output-on-failure # ── Formatting ─────────────────────────────────────────────── format: @$(CMAKE) -S . -B $(BUILD) @$(CMAKE) --build $(BUILD) --target format format-check: @$(CMAKE) -S . -B $(BUILD) @$(CMAKE) --build $(BUILD) --target format-check # ── Documentation ──────────────────────────────────────────── doc: @$(CMAKE) -S . -B $(BUILD) @$(CMAKE) --build $(BUILD) --target doc # ── Clean ──────────────────────────────────────────────────── clean: @rm -rf $(BUILD) zxc-0.10.0/README.md000066400000000000000000000655701517010557200136730ustar00rootroot00000000000000# ZXC: High-Performance Asymmetric Lossless Compression [![Build & Release](https://github.com/hellobertrand/zxc/actions/workflows/build.yml/badge.svg)](https://github.com/hellobertrand/zxc/actions/workflows/build.yml) [![Code Quality](https://github.com/hellobertrand/zxc/actions/workflows/quality.yml/badge.svg)](https://github.com/hellobertrand/zxc/actions/workflows/quality.yml) [![Fuzzing](https://github.com/hellobertrand/zxc/actions/workflows/fuzzing.yml/badge.svg)](https://github.com/hellobertrand/zxc/actions/workflows/fuzzing.yml) [![Benchmark](https://github.com/hellobertrand/zxc/actions/workflows/benchmark.yml/badge.svg)](https://github.com/hellobertrand/zxc/actions/workflows/benchmark.yml) [![Code Security](https://github.com/hellobertrand/zxc/actions/workflows/security.yml/badge.svg)](https://github.com/hellobertrand/zxc/actions/workflows/security.yml) [![Code Coverage](https://codecov.io/github/hellobertrand/zxc/branch/main/graph/badge.svg?token=LHA03HOA1X)](https://codecov.io/github/hellobertrand/zxc) [![ConanCenter](https://repology.org/badge/version-for-repo/conancenter/zxc.svg)](https://repology.org/project/zxc/versions) [![Vcpkg](https://repology.org/badge/version-for-repo/vcpkg/zxc.svg)](https://repology.org/project/zxc/versions) [![Homebrew](https://repology.org/badge/version-for-repo/homebrew/zxc.svg)](https://repology.org/project/zxc/versions) [![Debian 14](https://repology.org/badge/version-for-repo/debian_14/zxc.svg)](https://repology.org/project/zxc/versions) [![Ubuntu 26.04](https://repology.org/badge/version-for-repo/ubuntu_26_04/zxc.svg)](https://repology.org/project/zxc/versions) [![Crates.io](https://img.shields.io/crates/v/zxc-compress)](https://crates.io/crates/zxc-compress) [![PyPi](https://img.shields.io/pypi/v/zxc-compress)](https://pypi.org/project/zxc-compress) [![npm](https://img.shields.io/npm/v/zxc-compress)](https://www.npmjs.com/package/zxc-compress) [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](LICENSE) **ZXC** is a high-performance, lossless, asymmetric compression library optimized for Content Delivery and Embedded Systems (Game Assets, Firmware, App Bundles). It is designed to be **"Write Once, Read Many"** *(WORM)*. Unlike codecs like LZ4, ZXC trades compression speed (build-time) for **maximum decompression throughput** (run-time). ## TL;DR - **What:** A C library for lossless compression, optimized for **maximum decompression speed**. - **Key Result:** Up to **>40% faster** decompression than LZ4 on Apple Silicon, **>20% faster** on Google Axion (ARM64), **>10% faster** on x86_64, **all with better compression ratios**. - **Use Cases:** Game assets, firmware, app bundles, anything *compressed once, decompressed millions of times*. - **Seekable:** Built-in seek table for **O(1) random-access** decompression, load any block without scanning the entire file. - **Install:** `conan install --requires="zxc/[*]"` · `vcpkg install zxc` · `brew install zxc` · `pip install zxc-compress` · `cargo add zxc-compress` · `npm i zxc-compress` - **Quality:** Fuzzed, sanitized, formally tested, thread-safe API. BSD-3-Clause. > **Verified:** ZXC has been officially merged into the **[lzbench master branch](https://github.com/inikep/lzbench)**. You can now verify these results independently using the industry-standard benchmark suite. ## ZXC Design Philosophy Traditional codecs often force a trade-off between **symmetric speed** (LZ4) and **archival density** (Zstd). **ZXC focuses on Asymmetric Efficiency.** Designed for the "Write-Once, Read-Many" reality of software distribution, ZXC utilizes a computationally intensive encoder to generate a bitstream specifically structured to **maximize decompression throughput**. By performing heavy analysis upfront, the encoder produces a layout optimized for the instruction pipelining and branch prediction capabilities of modern CPUs, particularly ARMv8, effectively offloading complexity from the decoder to the encoder. * **Build Time:** You generally compress only once (on CI/CD). * **Run Time:** You decompress millions of times (on every user's device). **ZXC respects this asymmetry.** [👉 **Read the Technical Whitepaper**](docs/WHITEPAPER.md) ## Benchmarks To ensure consistent performance, benchmarks are automatically executed on every commit via GitHub Actions. We monitor metrics on both **x86_64** (Linux) and **ARM64** (Apple Silicon M2) runners to track compression speed, decompression speed, and ratios. *(See the [latest benchmark logs](https://github.com/hellobertrand/zxc/actions/workflows/benchmark.yml))* ### 1. Mobile & Client: Apple Silicon (M2) *Scenario: Game Assets loading, App startup.* | Target | ZXC vs Competitor | Decompression Speed | Ratio | Verdict | | :--- | :--- | :--- | :--- | :--- | | **1. Max Speed** | **ZXC -1** vs *LZ4 --fast* | **12,195 MB/s** vs 5,633 MB/s **2.16x Faster** | **61.6** vs 62.2 **Smaller** (-0.6%) | **ZXC** leads in raw throughput. | | **2. Standard** | **ZXC -3** vs *LZ4 Default* | **7,008 MB/s** vs 4,787 MB/s **1.46x Faster** | **46.4** vs 47.6 **Smaller** (-2.6%) | **ZXC** outperforms LZ4 in read speed and ratio. | | **3. High Density** | **ZXC -5** vs *Zstd --fast 1* | **6,181 MB/s** vs 2,527 MB/s **2.45x Faster** | **40.7** vs 41.0 **Equivalent** (-0.8%) | **ZXC** outperforms Zstd in decoding speed. | ### 2. Cloud Server: Google Axion (ARM Neoverse V2) *Scenario: High-throughput Microservices, ARM Cloud Instances.* | Target | ZXC vs Competitor | Decompression Speed | Ratio | Verdict | | :--- | :--- | :--- | :--- | :--- | | **1. Max Speed** | **ZXC -1** vs *LZ4 --fast* | **8,924 MB/s** vs 4,950 MB/s **1.80x Faster** | **61.6** vs 62.2 **Smaller** (-0.6%) | **ZXC** leads in raw throughput. | | **2. Standard** | **ZXC -3** vs *LZ4 Default* | **5,297 MB/s** vs 4,262 MB/s **1.24x Faster** | **46.4** vs 47.6 **Smaller** (-2.6%) | **ZXC** outperforms LZ4 in read speed and ratio. | | **3. High Density** | **ZXC -5** vs *Zstd --fast 1* | **4,676 MB/s** vs 2,293 MB/s **2.04x Faster** | **40.7** vs 41.0 **Equivalent** (-0.8%) | **ZXC** outperforms Zstd in decoding speed. | ### 3. Build Server: x86_64 (AMD EPYC 9B45) *Scenario: CI/CD Pipelines compatibility.* | Target | ZXC vs Competitor | Decompression Speed | Ratio | Verdict | | :--- | :--- | :--- | :--- | :--- | | **1. Max Speed** | **ZXC -1** vs *LZ4 --fast* | **10,526 MB/s** vs 5,080 MB/s **2.07x Faster** | **61.8** vs 62.2 **Smaller** (-0.6%) | **ZXC** achieves higher throughput. | | **2. Standard** | **ZXC -3** vs *LZ4 Default* | **5,815 MB/s** vs 4,825 MB/s **1.21x Faster** | **45.8** vs 47.6 **Smaller** (-3.8%) | ZXC offers improved speed and ratio. | | **3. High Density** | **ZXC -5** vs *Zstd --fast 1* | **5,217 MB/s** vs 2,335 MB/s **2.23x Faster** | **40.3** vs 41.0 **Smaller** (-1.8%) | **ZXC** provides faster decoding. | *(Benchmark Graph ARM64 : Decompression Throughput & Storage Ratio (Normalized to LZ4))* ![Benchmark Graph ARM64](docs/images/benchmark_arm64_0.10.0.webp) ### Benchmark ARM64 (Apple Silicon M2) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with Clang 21.0.0 using *MOREFLAGS="-march=native"* on macOS Tahoe 26.4 (Build 25E246). The reference hardware is an Apple M2 processor (ARM64). All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus. | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 52855 MB/s | 52786 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 904 MB/s | **12195 MB/s** | 130468706 | **61.56** | 1 files| | **zxc 0.10.0 -2** | 600 MB/s | **10044 MB/s** | 114455432 | **54.00** | 1 files| | **zxc 0.10.0 -3** | 257 MB/s | **7008 MB/s** | 98233034 | **46.35** | 1 files| | **zxc 0.10.0 -4** | 176 MB/s | **6636 MB/s** | 91429653 | **43.14** | 1 files| | **zxc 0.10.0 -5** | 104 MB/s | **6181 MB/s** | 86196446 | **40.67** | 1 files| | lz4 1.10.0 | 792 MB/s | 4787 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1316 MB/s | 5633 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 46.3 MB/s | 4531 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 625 MB/s | 3859 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 857 MB/s | 3258 MB/s | 101415443 | 47.85 | 1 files| | zstd 1.5.7 --fast --1 | 704 MB/s | 2527 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 625 MB/s | 1776 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 145 MB/s | 397 MB/s | 77259029 | 36.45 | 1 files| ### Benchmark ARM64 (Google Axion Neoverse-V2) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with GCC 14.3.0 using *MOREFLAGS="-march=native"* on Linux 64-bits Debian GNU/Linux 12 (bookworm). The reference hardware is a Google Neoverse-V2 processor (ARM64). All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus. | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 23971 MB/s | 23953 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 810 MB/s | **8924 MB/s** | 130468706 | **61.56** | 1 files| | **zxc 0.10.0 -2** | 523 MB/s | **7461 MB/s** | 114455432 | **54.00** | 1 files| | **zxc 0.10.0 -3** | 246 MB/s | **5297 MB/s** | 98233034 | **46.35** | 1 files| | **zxc 0.10.0 -4** | 170 MB/s | **5038 MB/s** | 91429653 | **43.14** | 1 files| | **zxc 0.10.0 -5** | 100 MB/s | **4676 MB/s** | 86196446 | **40.67** | 1 files| | lz4 1.10.0 | 731 MB/s | 4262 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1278 MB/s | 4950 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 43.3 MB/s | 3850 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 554 MB/s | 2776 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 757 MB/s | 2298 MB/s | 101415443 | 47.85 | 1 files| | zstd 1.5.7 --fast --1 | 606 MB/s | 2293 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 524 MB/s | 1645 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 57.2 MB/s | 390 MB/s | 77259029 | 36.45 | 1 files| ### Benchmark x86_64 (AMD EPYC 9B45) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with GCC 14.3.0 using *MOREFLAGS="-march=native"* on Linux 64-bits Ubuntu 24.04. The reference hardware is an AMD EPYC 9B45 processor (x86_64). All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus. | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 23564 MB/s | 23993 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 810 MB/s | **10526 MB/s** | 130969994 | **61.79** | 1 files| | **zxc 0.10.0 -2** | 511 MB/s | **9120 MB/s** | 113859997 | **53.72** | 1 files| | **zxc 0.10.0 -3** | 205 MB/s | **5815 MB/s** | 97083951 | **45.81** | 1 files| | **zxc 0.10.0 -4** | 147 MB/s | **5533 MB/s** | 90503394 | **42.70** | 1 files| | **zxc 0.10.0 -5** | 77.7 MB/s | **5217 MB/s** | 85344194 | **40.27** | 1 files| | lz4 1.10.0 | 745 MB/s | 4825 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1249 MB/s | 5080 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 42.8 MB/s | 4623 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 581 MB/s | 3422 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 721 MB/s | 2004 MB/s | 101512076 | 47.89 | 1 files| | zstd 1.5.7 --fast --1 | 627 MB/s | 2335 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 575 MB/s | 1814 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 129 MB/s | 376 MB/s | 77259029 | 36.45 | 1 files| ### Benchmark x86_64 (AMD EPYC 7763) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with GCC 14.2.0 using *MOREFLAGS="-march=native"* on Linux 64-bits Ubuntu 24.04. The reference hardware is an AMD EPYC 7763 64-Core processor (x86_64). All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus. | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 22410 MB/s | 22392 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 601 MB/s | **6921 MB/s** | 130468706 | **61.56** | 1 files| | **zxc 0.10.0 -2** | 388 MB/s | **5787 MB/s** | 114455432 | **54.00** | 1 files| | **zxc 0.10.0 -3** | 186 MB/s | **3903 MB/s** | 98233034 | **46.35** | 1 files| | **zxc 0.10.0 -4** | 130 MB/s | **3738 MB/s** | 91429653 | **43.14** | 1 files| | **zxc 0.10.0 -5** | 80.4 MB/s | **3565 MB/s** | 86196446 | **40.67** | 1 files| | lz4 1.10.0 | 582 MB/s | 3551 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1015 MB/s | 4102 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 33.3 MB/s | 3407 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 416 MB/s | 2647 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 613 MB/s | 1593 MB/s | 101512076 | 47.89 | 1 files| | zstd 1.5.7 --fast --1 | 448 MB/s | 1626 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 409 MB/s | 1221 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 98.5 MB/s | 328 MB/s | 77259029 | 36.45 | 1 files| --- ## Installation ### Option 1: Download Release (GitHub) 1. Go to the [Releases page](https://github.com/hellobertrand/zxc/releases). 2. Download the archive matching your architecture: **macOS:** * `zxc-macos-arm64.tar.gz` (NEON optimizations included). **Linux:** * `zxc-linux-aarch64.tar.gz` (NEON optimizations included). * `zxc-linux-x86_64.tar.gz` (Runtime dispatch for AVX2/AVX512). **Windows:** * `zxc-windows-x64.zip` (Runtime dispatch for AVX2/AVX512). * `zxc-windows-arm64.zip` (NEON optimizations included). 3. Extract and install: ```bash tar -xzf zxc-linux-x86_64.tar.gz -C /usr/local ``` Each archive contains: ``` bin/zxc # CLI binary include/ # C headers (zxc.h, zxc_buffer.h, ...) lib/libzxc.a # Static library lib/pkgconfig/libzxc.pc # pkg-config support lib/cmake/zxc/zxcConfig.cmake # CMake find_package(zxc) support ``` 4. Use in your project: **CMake:** ```cmake find_package(zxc REQUIRED) target_link_libraries(myapp PRIVATE zxc::zxc_lib) ``` **pkg-config:** ```bash cc myapp.c $(pkg-config --cflags --libs libzxc) -o myapp ``` ### Option 2: vcpkg **Classic mode:** ```bash vcpkg install zxc ``` **Manifest mode** (add to `vcpkg.json`): ```json { "dependencies": ["zxc"] } ``` Then in your CMake project: ```cmake find_package(zxc CONFIG REQUIRED) target_link_libraries(myapp PRIVATE zxc::zxc_lib) ``` ### Option 3: Conan You also can download and install zxc using the [Conan](https://conan.io/) package manager: ```bash conan install -r conancenter --requires="zxc/[*]" --build=missing ``` Or add to your `conanfile.txt`: ```ini [requires] zxc/[*] ``` The zxc package in Conan Center is kept up to date by [ConanCenterIndex](https://github.com/conan-io/conan-center-index) contributors. If the version is out of date, please create an issue or pull request on the Conan Center Index repository. ### Option 4: Homebrew ```bash brew install zxc ``` The formula is maintained in [homebrew-core](https://formulae.brew.sh/formula/zxc). ### Option 5: Building from Source **Requirements:** CMake (3.14+), C17 Compiler (Clang/GCC/MSVC). ```bash git clone https://github.com/hellobertrand/zxc.git cd zxc cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --parallel # Run tests ctest --test-dir build -C Release --output-on-failure # CLI usage ./build/zxc --help # Install library, headers, and CMake/pkg-config files sudo cmake --install build ``` #### CMake Options | Option | Default | Description | |--------|---------|-------------| | `BUILD_SHARED_LIBS` | OFF | Build shared libraries instead of static (`libzxc.so`, `libzxc.dylib`, `zxc.dll`) | | `ZXC_NATIVE_ARCH` | ON | Enable `-march=native` for maximum performance | | `ZXC_ENABLE_LTO` | ON | Enable Link-Time Optimization (LTO) | | `ZXC_PGO_MODE` | OFF | Profile-Guided Optimization mode (`OFF`, `GENERATE`, `USE`) | | `ZXC_BUILD_CLI` | ON | Build command-line interface | | `ZXC_BUILD_TESTS` | ON | Build unit tests | | `ZXC_ENABLE_COVERAGE` | OFF | Enable code coverage generation (disables LTO/PGO) | | `ZXC_DISABLE_SIMD` | OFF | Disable hand-written SIMD paths (AVX2/AVX512/NEON) | ```bash # Build shared library cmake -B build -DBUILD_SHARED_LIBS=ON # Portable build (without -march=native) cmake -B build -DZXC_NATIVE_ARCH=OFF # Library only (no CLI, no tests) cmake -B build -DZXC_BUILD_CLI=OFF -DZXC_BUILD_TESTS=OFF # Code coverage build cmake -B build -DZXC_ENABLE_COVERAGE=ON # Disable explicit SIMD code paths (compiler auto-vectorisation is unaffected) cmake -B build -DZXC_DISABLE_SIMD=ON ``` #### Profile-Guided Optimization (PGO) PGO uses runtime profiling data to optimize branch layout, inlining decisions, and code placement. **Step 1 - Build with instrumentation:** ```bash cmake -B build -DCMAKE_BUILD_TYPE=Release -DZXC_PGO_MODE=GENERATE cmake --build build --parallel ``` **Step 2 - Run a representative workload to collect profile data:** ```bash # Run the test suite (exercises all block types and compression levels) ./build/zxc_test # Or compress/decompress representative data ./build/zxc -b your_data_file ``` **Step 3 - (Clang only) Merge raw profiles:** ```bash # Clang generates .profraw files that must be merged before use llvm-profdata merge -output=build/pgo/default.profdata build/pgo/*.profraw ``` > GCC uses a directory-based format and does not require this step. **Step 4 - Rebuild with profile data:** ```bash cmake -B build -DCMAKE_BUILD_TYPE=Release -DZXC_PGO_MODE=USE cmake --build build --parallel ``` ### Packaging Status [![Packaging status](https://repology.org/badge/vertical-allrepos/zxc.svg)](https://repology.org/project/zxc/versions) --- ## Compression Levels * **Level 1, 2 (Fast):** Optimized for real-time assets (Gaming, UI). * **Level 3, 4 (Balanced):** A strong middle-ground offering efficient compression speed and a ratio superior to LZ4. * **Level 5 (Compact):** The best choice for Embedded, Firmware, or Archival. Better compression than LZ4 and significantly faster decoding than Zstd. ## Block Size Tuning The default block size is **256 KB**, a conservative choice that balances compression quality, memory usage, and random-access granularity. For **bulk/archival workloads** where maximum throughput matters, **512 KB blocks** are recommended. **Why larger blocks help:** Each block starts with a cold hash table, so the LZ match-finder has no history and produces more literals until the table warms up. Doubling the block size halves the number of cold-start penalties, improving both ratio and decompression speed. | Block Size | Memory (per context) | Ratio (level -3) | Decompression gain vs 256 KB | |:----------:|:--------------------:|:-----------------:|:----------------------------:| | 256 KB *(default)* | ~1.7 MB | 46.36% | — | | 512 KB | ~3.3 MB | 45.81% *(−0.55 pp)* | +1% to +8% depending on CPU | ```bash # CLI zxc -B 512K -5 input_file output_file # API zxc_compress_opts_t opts = { .level = ZXC_LEVEL_COMPACT, .block_size = 512 * 1024, }; ``` **Guideline:** Use 256 KB (default) for streaming, embedded, or memory-constrained environments. Use 512 KB for bulk compression pipelines, CI/CD asset packaging, and high-throughput servers. --- ## Usage ### 1. CLI The CLI is perfect for benchmarking or manually compressing assets. ```bash # Basic Compression (Level 3 is default) zxc -z input_file output_file # High Compression (Level 5) zxc -z -5 input_file output_file # Seekable Archive (enables O(1) random-access decompression) zxc -z -S input_file output_file # -z for compression can be omitted zxc input_file output_file # as well as output file; it will be automatically assigned to input_file.zxc zxc input_file # Decompression zxc -d compressed_file output_file # Benchmark Mode (Testing speed on your machine) zxc -b input_file ``` #### Using with `tar` ZXC works as a drop-in external compressor for `tar` (reads stdin, writes stdout, returns 0 on success): ```bash # GNU tar (Linux) tar -I 'zxc -5' -cf archive.tar.zxc data/ tar -I 'zxc -d' -xf archive.tar.zxc # bsdtar (macOS) tar --use-compress-program='zxc -5' -cf archive.tar.zxc data/ tar --use-compress-program='zxc -d' -xf archive.tar.zxc # Pipes (universal) tar cf - data/ | zxc > archive.tar.zxc zxc -d < archive.tar.zxc | tar xf - ``` ### 2. API ZXC provides a **thread-safe API** with two usage patterns. Parameters are passed through dedicated options structs, making call sites self-documenting and forward-compatible. #### Buffer API (In-Memory) ```c #include "zxc.h" // Compression uint64_t bound = zxc_compress_bound(src_size); zxc_compress_opts_t c_opts = { .level = ZXC_LEVEL_DEFAULT, .checksum_enabled = 1, /* .block_size = 0 -> 256 KB default */ }; int64_t compressed_size = zxc_compress(src, src_size, dst, bound, &c_opts); // Decompression zxc_decompress_opts_t d_opts = { .checksum_enabled = 1 }; int64_t decompressed_size = zxc_decompress(src, src_size, dst, dst_capacity, &d_opts); ``` #### Stream API (Files, Multi-Threaded) ```c #include "zxc.h" // Compression (auto-detect threads, level 3, checksum on) zxc_compress_opts_t c_opts = { .n_threads = 0, // 0 = auto .level = ZXC_LEVEL_DEFAULT, .checksum_enabled = 1, /* .block_size = 0 -> 256 KB default */ }; int64_t bytes_written = zxc_stream_compress(f_in, f_out, &c_opts); // Decompression zxc_decompress_opts_t d_opts = { .n_threads = 0, .checksum_enabled = 1 }; int64_t bytes_out = zxc_stream_decompress(f_in, f_out, &d_opts); ``` #### Reusable Context API (Low-Latency / Embedded) For tight loops (e.g. filesystem plug-ins) where per-call `malloc`/`free` overhead matters, use opaque reusable contexts. Options are **sticky** - settings from `zxc_create_cctx()` are reused when passing `NULL`: ```c #include "zxc.h" zxc_compress_opts_t opts = { .level = 3, .checksum_enabled = 0 }; zxc_cctx* cctx = zxc_create_cctx(&opts); // allocate once, settings remembered zxc_dctx* dctx = zxc_create_dctx(); // allocate once // reuse across many blocks - NULL reuses sticky settings: int64_t csz = zxc_compress_cctx(cctx, src, src_sz, dst, dst_cap, NULL); int64_t dsz = zxc_decompress_dctx(dctx, dst, csz, out, src_sz, NULL); zxc_free_cctx(cctx); zxc_free_dctx(dctx); ``` **Features:** - Caller-allocated buffers with explicit bounds - Thread-safe (stateless) - Configurable block sizes (4 KB – 2 MB, powers of 2) - Multi-threaded streaming (auto-detects CPU cores) - Optional checksum validation - Reusable contexts for high-frequency call sites - Seekable archives: optional seek table for O(1) random-access decompression (`.seekable = 1`) **[See complete examples and advanced usage ->](docs/EXAMPLES.md)** ## Language Bindings [![Crates.io](https://img.shields.io/crates/v/zxc-compress)](https://crates.io/crates/zxc-compress) [![PyPi](https://img.shields.io/pypi/v/zxc-compress)](https://pypi.org/project/zxc-compress) [![npm](https://img.shields.io/npm/v/zxc-compress)](https://www.npmjs.com/package/zxc-compress) Official wrappers maintained in this repository: | Language | Package Manager | Install Command | Documentation | Author | |----------|-----------------|-----------------|---------------|--------| | **Rust** | [`crates.io`](https://crates.io/crates/zxc-compress) | `cargo add zxc-compress` | [README](wrappers/rust/zxc/README.md) | [@hellobertrand](https://github.com/hellobertrand) | | **Python**| [`PyPI`](https://pypi.org/project/zxc-compress) | `pip install zxc-compress` | [README](wrappers/python/README.md) | [@nuberchardzer1](https://github.com/nuberchardzer1) | | **Node.js**| [`npm`](https://www.npmjs.com/package/zxc-compress) | `npm install zxc-compress` | [README](wrappers/nodejs/README.md) | [@hellobertrand](https://github.com/hellobertrand) | | **Go** | `go get` | `go get github.com/hellobertrand/zxc/wrappers/go` | [README](wrappers/go/README.md) | [@hellobertrand](https://github.com/hellobertrand) | | **WASM** | Build from source | `emcmake cmake -B build-wasm && cmake --build build-wasm` | [README](wrappers/wasm/README.md) | [@hellobertrand](https://github.com/hellobertrand) | Community-maintained bindings: | Language | Package Manager | Install Command | Repository | Author | | -------- | --------------- | --------------- | ---------- | ------ | | **Go** | pkg.go.dev | `go get github.com/meysam81/go-zxc` | | [@meysam81](https://github.com/meysam81) | ## Safety & Quality * **Unit Tests**: Comprehensive test suite with CTest integration. * **Continuous Fuzzing**: Integrated with ClusterFuzzLite suites. * **Static Analysis**: Checked with Cppcheck & Clang Static Analyzer. * **CodeQL Analysis**: GitHub Advanced Security scanning for vulnerabilities. * **Code Coverage**: Automated tracking with Codecov integration. * **Dynamic Analysis**: Validated with Valgrind and ASan/UBSan in CI pipelines. * **Safe API**: Explicit buffer capacity is required for all operations. ## License & Credits **ZXC** Copyright © 2025-2026, Bertrand Lebonnois and contributors. Licensed under the **BSD 3-Clause License**. See LICENSE for details. **Third-Party Components:** - **rapidhash** by Nicolas De Carli (MIT) - Used for high-speed, platform-independent checksums. zxc-0.10.0/codecov.yml000066400000000000000000000001361517010557200145440ustar00rootroot00000000000000coverage: status: project: default: target: 80% ignore: - "src/cli/**" zxc-0.10.0/docs/000077500000000000000000000000001517010557200133275ustar00rootroot00000000000000zxc-0.10.0/docs/API.md000066400000000000000000000624021517010557200142660ustar00rootroot00000000000000# ZXC API & ABI Reference **Library version**: 0.10.0 **SOVERSION**: 3 **License**: BSD-3-Clause This document is the authoritative reference for the public API surface and ABI guarantees of **libzxc**. It is intended for integrators, packagers, and language-binding authors. For usage examples see [`EXAMPLES.md`](EXAMPLES.md). For the on-disk binary format see [`FORMAT.md`](FORMAT.md). --- ## Table of Contents - [1. Headers and Include Graph](#1-headers-and-include-graph) - [2. Symbol Visibility](#2-symbol-visibility) - [3. ABI Versioning](#3-abi-versioning) - [4. Runtime Dependencies](#4-runtime-dependencies) - [5. Constants and Enumerations](#5-constants-and-enumerations) - [6. Type Definitions](#6-type-definitions) - [7. Buffer API](#7-buffer-api) - [8. Block API](#8-block-api) - [9. Reusable Context API](#9-reusable-context-api) - [10. Streaming API](#10-streaming-api) - [11. Seekable API](#11-seekable-api) - [12. Sans-IO API](#12-sans-io-api) - [13. Error Handling](#13-error-handling) - [14. Thread Safety](#14-thread-safety) - [15. Exported Symbols Summary](#15-exported-symbols-summary) --- ## 1. Headers and Include Graph ```text zxc.h <- umbrella header (includes everything below) ├── zxc_buffer.h <- Buffer API + Reusable Context API │ ├── zxc_export.h <- visibility macros │ └── zxc_stream.h <- Streaming API + opts structs │ └── zxc_export.h ├── zxc_constants.h <- version macros, compression levels, block sizes ├── zxc_error.h <- error codes + zxc_error_name() │ └── zxc_export.h └── (not included by zxc.h) zxc_seekable.h <- Seekable random-access API (opt-in) └── zxc_export.h zxc_sans_io.h <- Low-level primitives (opt-in) └── zxc_export.h ``` Include `` to access everything except the sans-IO and seekable layers. Include `` explicitly for random-access decompression. Include `` explicitly when building custom drivers. --- ## 2. Symbol Visibility libzxc uses an **opt-in** export strategy: | Build type | How symbols are exposed | |-----------|------------------------| | **Shared library** | Default visibility is `hidden`. Only functions annotated with `ZXC_EXPORT` are exported. Internal FMV variants (`_default`, `_neon`, `_avx2`, `_avx512`) are hidden. | | **Static library** | Define `ZXC_STATIC_DEFINE` (set automatically by CMake) to disable import/export annotations. | ### Macros | Macro | Purpose | |-------|---------| | `ZXC_EXPORT` | Marks a symbol as part of the public API (`__declspec(dllexport/dllimport)` on Windows, `visibility("default")` on GCC/Clang). | | `ZXC_NO_EXPORT` | Forces a symbol to be hidden (`visibility("hidden")`). | | `ZXC_DEPRECATED` | Emits a compiler warning when a deprecated symbol is used. | | `ZXC_STATIC_DEFINE` | Define when building or consuming as a static library. | | `zxc_lib_EXPORTS` | Set automatically by CMake when building the shared library. Do not define manually. | --- ## 3. ABI Versioning libzxc follows the shared-library versioning convention: ``` libzxc.so.{SOVERSION}.{MAJOR}.{MINOR}.{PATCH} ``` | Field | Description | Current | |-------|-------------|---------| | `SOVERSION` | Bumped on **ABI-breaking** changes (struct layout, removed symbols, changed signatures). | **3** | | `VERSION` | Tracks the library release. | **0.10.0** | **Compatibility rule**: any binary compiled against SOVERSION N will load against any libzxc with the same SOVERSION, regardless of the `VERSION` triple. ### Platform naming | Platform | Files | |----------|-------| | Linux | `libzxc.so` -> `libzxc.so.3` -> `libzxc.so.0.10.0` | | macOS | `libzxc.dylib` -> `libzxc.3.dylib` -> `libzxc.0.10.0.dylib` | | Windows | `zxc.dll` + `zxc.lib` (import) | --- ## 4. Runtime Dependencies libzxc has **zero external dependencies**. | Dependency | Notes | |-----------|-------| | C standard library | ``, ``, ``, `` | | POSIX threads | `pthread` on Unix/macOS; Windows threads on Win32 | No dependency on OpenSSL, zlib, or any other compression library. --- ## 5. Constants and Enumerations ### 5.1 Version (compile-time) Defined in `zxc_constants.h`: ```c #define ZXC_VERSION_MAJOR 0 #define ZXC_VERSION_MINOR 10 #define ZXC_VERSION_PATCH 0 #define ZXC_LIB_VERSION_STR "0.10.0" ``` ### 5.2 Block Size Constraints ```c #define ZXC_BLOCK_SIZE_MIN_LOG2 12 // exponent for minimum #define ZXC_BLOCK_SIZE_MAX_LOG2 21 // exponent for maximum #define ZXC_BLOCK_SIZE_MIN (1U << 12) // 4 KB #define ZXC_BLOCK_SIZE_MAX (1U << 21) // 2 MB #define ZXC_BLOCK_SIZE_DEFAULT (256 * 1024) // 256 KB ``` Block size must be a power of two within `[ZXC_BLOCK_SIZE_MIN, ZXC_BLOCK_SIZE_MAX]`. Pass `0` to any API to use `ZXC_BLOCK_SIZE_DEFAULT`. ### 5.3 Compression Levels ```c typedef enum { ZXC_LEVEL_FASTEST = 1, // Best throughput, lowest ratio ZXC_LEVEL_FAST = 2, // Fast, good for real-time ZXC_LEVEL_DEFAULT = 3, // Recommended balance ZXC_LEVEL_BALANCED = 4, // Higher ratio, good speed ZXC_LEVEL_COMPACT = 5 // Highest density } zxc_compression_level_t; ``` All levels produce data decompressible at the **same speed**. Pass `0` for level to use `ZXC_LEVEL_DEFAULT`. ### 5.4 Error Codes ```c typedef enum { ZXC_OK = 0, ZXC_ERROR_MEMORY = -1, // malloc failure ZXC_ERROR_DST_TOO_SMALL = -2, // output buffer too small ZXC_ERROR_SRC_TOO_SMALL = -3, // input truncated ZXC_ERROR_BAD_MAGIC = -4, // invalid magic word ZXC_ERROR_BAD_VERSION = -5, // unsupported format version ZXC_ERROR_BAD_HEADER = -6, // corrupted header (CRC mismatch) ZXC_ERROR_BAD_CHECKSUM = -7, // checksum verification failed ZXC_ERROR_CORRUPT_DATA = -8, // corrupted compressed data ZXC_ERROR_BAD_OFFSET = -9, // invalid match offset ZXC_ERROR_OVERFLOW = -10, // buffer overflow detected ZXC_ERROR_IO = -11, // file read/write/seek failure ZXC_ERROR_NULL_INPUT = -12, // required pointer is NULL ZXC_ERROR_BAD_BLOCK_TYPE = -13, // unknown block type ZXC_ERROR_BAD_BLOCK_SIZE = -14 // invalid block size } zxc_error_t; ``` All public functions that can fail return negative `zxc_error_t` values on error. --- ## 6. Type Definitions ### 6.1 Options Structs **Compression options** (defined in `zxc_stream.h`, used by all compression functions): ```c typedef struct { int n_threads; // Worker thread count (0 = auto-detect). int level; // Compression level 1–5 (0 = default). size_t block_size; // Block size in bytes (0 = 256 KB default). int checksum_enabled; // 1 = enable checksums, 0 = disable. int seekable; // 1 = append seek table for random access. zxc_progress_callback_t progress_cb; // Optional callback (NULL to disable). void* user_data; // Passed through to progress_cb. } zxc_compress_opts_t; ``` **Decompression options**: ```c typedef struct { int n_threads; // Worker thread count (0 = auto-detect). int checksum_enabled; // 1 = verify checksums, 0 = skip. zxc_progress_callback_t progress_cb; // Optional callback. void* user_data; // Passed through to progress_cb. } zxc_decompress_opts_t; ``` Both structs are safe to zero-initialize for default behavior. Pass `NULL` instead of an options pointer to use all defaults. ### 6.2 Progress Callback ```c typedef void (*zxc_progress_callback_t)( uint64_t bytes_processed, // Total input bytes processed so far uint64_t bytes_total, // Total input bytes (0 if unknown, e.g. stdin) const void* user_data // User-provided context ); ``` Called from the writer thread after each block is processed. Must be fast and non-blocking. ### 6.3 Opaque Context Types ```c typedef struct zxc_cctx_s zxc_cctx; // Opaque compression context typedef struct zxc_dctx_s zxc_dctx; // Opaque decompression context ``` Internal layout is hidden. Interact only through `zxc_create_*` / `zxc_free_*` / `zxc_*_cctx` / `zxc_*_dctx` functions. ### 6.4 Sans-IO Types **Compression context** (public struct, for advanced use only): ```c typedef struct { uint32_t* hash_table; // LZ77 hash table uint16_t* chain_table; // Collision chain table void* memory_block; // Single allocation owner uint32_t epoch; // Lazy hash invalidation counter uint32_t* buf_sequences; // Packed sequence records uint8_t* buf_tokens; // Token buffer uint16_t* buf_offsets; // Offset buffer uint8_t* buf_extras; // Extra-length buffer uint8_t* literals; // Literal bytes uint8_t* lit_buffer; // Scratch buffer for RLE size_t lit_buffer_cap; uint8_t* work_buf; // Padded scratch for buffer-API decompression size_t work_buf_cap; int checksum_enabled; int compression_level; size_t chunk_size; // Effective block size uint32_t offset_bits; // log2(chunk_size) uint32_t offset_mask; // (1 << offset_bits) - 1 uint32_t max_epoch; // 1 << (32 - offset_bits) } zxc_cctx_t; ``` **Block header** (8 bytes on disk): ```c typedef struct { uint8_t block_type; // See FORMAT.md §4 uint8_t block_flags; uint8_t reserved; uint8_t header_crc; // 1-byte header CRC uint32_t comp_size; // Compressed payload size (excl. header) } zxc_block_header_t; ``` --- ## 7. Buffer API Declared in `zxc_buffer.h`. Single-threaded, blocking, in-memory operations. ### `zxc_compress_bound` ```c ZXC_EXPORT uint64_t zxc_compress_bound(size_t input_size); ``` Returns the worst-case compressed size for `input_size` bytes. Use to allocate the destination buffer before compression. ### `zxc_compress` ```c ZXC_EXPORT int64_t zxc_compress( const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_compress_opts_t* opts // NULL = defaults ); ``` Compresses `src` into `dst`. Only `level`, `block_size`, `checksum_enabled`, and `seekable` fields of `opts` are used. `n_threads` is ignored (always single-threaded). **Returns**: compressed size (> 0) on success, or negative `zxc_error_t`. ### `zxc_decompress` ```c ZXC_EXPORT int64_t zxc_decompress( const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_decompress_opts_t* opts // NULL = defaults ); ``` Decompresses `src` into `dst`. Only `checksum_enabled` is used. **Returns**: decompressed size (> 0) on success, or negative `zxc_error_t`. ### `zxc_get_decompressed_size` ```c ZXC_EXPORT uint64_t zxc_get_decompressed_size( const void* src, size_t src_size ); ``` Reads the original size from the file footer without decompressing. **Returns**: original size, or `0` if the buffer is invalid. --- ## 8. Block API Declared in `zxc_buffer.h`. Single-block compression and decompression **without file framing** (no file header, EOF block, or footer). Designed for filesystem integrations (DwarFS, EROFS, SquashFS) where the caller manages its own block indexing. Output format: `block_header (8 B)` + compressed payload + optional `checksum (4 B)`. ### `zxc_compress_block_bound` ```c ZXC_EXPORT uint64_t zxc_compress_block_bound(size_t input_size); ``` Returns the maximum compressed size for a single block. Unlike `zxc_compress_bound()`, this does **not** include file header, EOF block, or footer overhead. **Returns**: upper bound in bytes, or `0` on overflow. ### `zxc_compress_block` ```c ZXC_EXPORT int64_t zxc_compress_block( zxc_cctx* cctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_compress_opts_t* opts // NULL = defaults ); ``` Compresses a single block using a reusable context. Only `level`, `block_size`, and `checksum_enabled` fields of `opts` are used. **Returns**: compressed block size (> 0) on success, or negative `zxc_error_t`. ### `zxc_decompress_block` ```c ZXC_EXPORT int64_t zxc_decompress_block( zxc_dctx* dctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_decompress_opts_t* opts // NULL = defaults ); ``` Decompresses a single block produced by `zxc_compress_block()`. Only `checksum_enabled` is used. **Returns**: decompressed size (> 0) on success, or negative `zxc_error_t`. --- ## 9. Reusable Context API Declared in `zxc_buffer.h`. Eliminates per-call allocation overhead for hot-path integrations (filesystem plug-ins, batch processing). ### `zxc_create_cctx` ```c ZXC_EXPORT zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts); ``` Creates a heap-allocated compression context. When `opts` is non-NULL, internal buffers are pre-allocated (eager init). When `opts` is NULL, allocation is deferred to first use (lazy init). **Returns**: context pointer, or `NULL` on allocation failure. ### `zxc_free_cctx` ```c ZXC_EXPORT void zxc_free_cctx(zxc_cctx* cctx); ``` Frees all resources. Safe to pass `NULL`. ### `zxc_compress_cctx` ```c ZXC_EXPORT int64_t zxc_compress_cctx( zxc_cctx* cctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_compress_opts_t* opts ); ``` Same as `zxc_compress()` but reuses internal buffers from `cctx`. Automatically re-initializes when `block_size` or `level` changes. ### `zxc_create_dctx` ```c ZXC_EXPORT zxc_dctx* zxc_create_dctx(void); ``` Creates a heap-allocated decompression context. ### `zxc_free_dctx` ```c ZXC_EXPORT void zxc_free_dctx(zxc_dctx* dctx); ``` Frees all resources. Safe to pass `NULL`. ### `zxc_decompress_dctx` ```c ZXC_EXPORT int64_t zxc_decompress_dctx( zxc_dctx* dctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_decompress_opts_t* opts ); ``` Same as `zxc_decompress()` but reuses buffers from `dctx`. --- ## 10. Streaming API Declared in `zxc_stream.h`. Multi-threaded, `FILE*`-based pipeline (reader -> workers -> writer). ### `zxc_stream_compress` ```c ZXC_EXPORT int64_t zxc_stream_compress( FILE* f_in, FILE* f_out, const zxc_compress_opts_t* opts ); ``` Compresses `f_in` -> `f_out` using a parallel pipeline. All fields of `opts` are used, including `n_threads` and `progress_cb`. **Returns**: total compressed bytes written, or negative `zxc_error_t`. ### `zxc_stream_decompress` ```c ZXC_EXPORT int64_t zxc_stream_decompress( FILE* f_in, FILE* f_out, const zxc_decompress_opts_t* opts ); ``` Decompresses `f_in` -> `f_out` using a parallel pipeline. **Returns**: total decompressed bytes written, or negative `zxc_error_t`. ### `zxc_stream_get_decompressed_size` ```c ZXC_EXPORT int64_t zxc_stream_get_decompressed_size(FILE* f_in); ``` Reads the original size from the file footer. File position is restored. **Returns**: original size, or negative `zxc_error_t`. --- ## 11. Seekable API Declared in `zxc_seekable.h` (not included by `zxc.h` — opt-in). Random-access decompression of seekable archives produced with `seekable = 1`. ### Creating a Seekable Archive Set `seekable = 1` in `zxc_compress_opts_t`. Works with both the Buffer API and the Streaming API: ```c zxc_compress_opts_t opts = { .level = 3, .seekable = 1 }; int64_t csize = zxc_compress(src, src_size, dst, dst_cap, &opts); ``` The resulting archive contains a Seek Table block (SEK) between the EOF block and the file footer. Standard decompressors handle seekable archives transparently — the seek table is skipped during sequential decompression. ### `zxc_seekable_open` ```c ZXC_EXPORT zxc_seekable* zxc_seekable_open(const void* src, size_t src_size); ``` Opens a seekable archive from a memory buffer. The buffer must remain valid for the lifetime of the handle. **Returns**: handle on success, or `NULL` if the buffer is not a valid seekable archive. ### `zxc_seekable_open_file` ```c ZXC_EXPORT zxc_seekable* zxc_seekable_open_file(FILE* f); ``` Opens a seekable archive from a `FILE*`. The file must be seekable (not stdin/pipe). The file position is saved and restored after parsing. **Returns**: handle on success, or `NULL` on error. ### `zxc_seekable_get_num_blocks` ```c ZXC_EXPORT uint32_t zxc_seekable_get_num_blocks(const zxc_seekable* s); ``` Returns the total number of data blocks in the archive. ### `zxc_seekable_get_decompressed_size` ```c ZXC_EXPORT uint64_t zxc_seekable_get_decompressed_size(const zxc_seekable* s); ``` Returns the total decompressed size of the archive. ### `zxc_seekable_get_block_comp_size` ```c ZXC_EXPORT uint32_t zxc_seekable_get_block_comp_size( const zxc_seekable* s, uint32_t block_idx ); ``` Returns the compressed size (on-disk, including header) of a specific block. ### `zxc_seekable_get_block_decomp_size` ```c ZXC_EXPORT uint32_t zxc_seekable_get_block_decomp_size( const zxc_seekable* s, uint32_t block_idx ); ``` Returns the decompressed size of a specific block. ### `zxc_seekable_decompress_range` ```c ZXC_EXPORT int64_t zxc_seekable_decompress_range( zxc_seekable* s, void* dst, size_t dst_capacity, uint64_t offset, size_t len ); ``` Decompresses `len` bytes starting at byte `offset` in the original uncompressed data. Only the blocks overlapping the requested range are read and decompressed. **Returns**: `len` on success, or negative `zxc_error_t`. ### `zxc_seekable_decompress_range_mt` ```c ZXC_EXPORT int64_t zxc_seekable_decompress_range_mt( zxc_seekable* s, void* dst, size_t dst_capacity, uint64_t offset, size_t len, int n_threads // 0 = auto-detect ); ``` Multi-threaded variant. Each worker thread uses `pread()` (POSIX) or `ReadFile()` (Windows) for lock-free concurrent I/O. Falls back to single-threaded mode when `n_threads <= 1` or the range spans a single block. **Returns**: `len` on success, or negative `zxc_error_t`. ### `zxc_seekable_free` ```c ZXC_EXPORT void zxc_seekable_free(zxc_seekable* s); ``` Frees a seekable handle and all associated resources. Safe to call with `NULL`. ### `zxc_write_seek_table` ```c ZXC_EXPORT int64_t zxc_write_seek_table( uint8_t* dst, size_t dst_capacity, const uint32_t* comp_sizes, uint32_t num_blocks ); ``` Low-level: writes a seek table (block header + entries) to `dst`. **Returns**: bytes written, or negative `zxc_error_t`. ### `zxc_seek_table_size` ```c ZXC_EXPORT size_t zxc_seek_table_size(uint32_t num_blocks); ``` Returns the encoded byte size of a seek table for `num_blocks` blocks. --- ## 12. Sans-IO API Declared in `zxc_sans_io.h` (not included by `zxc.h` - opt-in). Low-level primitives for building custom compression drivers. ### `zxc_cctx_init` ```c ZXC_EXPORT int zxc_cctx_init( zxc_cctx_t* ctx, size_t chunk_size, int mode, // 1 = compression, 0 = decompression int level, int checksum_enabled ); ``` Allocates internal buffers sized for `chunk_size`. **Returns**: `ZXC_OK` or `ZXC_ERROR_MEMORY`. ### `zxc_cctx_free` ```c ZXC_EXPORT void zxc_cctx_free(zxc_cctx_t* ctx); ``` Frees internal buffers. Does **not** free `ctx` itself. ### `zxc_write_file_header` ```c ZXC_EXPORT int zxc_write_file_header( uint8_t* dst, size_t dst_capacity, size_t chunk_size, int has_checksum ); ``` Writes the 16-byte file header. **Returns**: bytes written, or `ZXC_ERROR_DST_TOO_SMALL`. ### `zxc_read_file_header` ```c ZXC_EXPORT int zxc_read_file_header( const uint8_t* src, size_t src_size, size_t* out_block_size, // optional int* out_has_checksum // optional ); ``` Validates the file header (magic, version, CRC16). **Returns**: `ZXC_OK`, or `ZXC_ERROR_BAD_MAGIC` / `ZXC_ERROR_BAD_VERSION` / `ZXC_ERROR_SRC_TOO_SMALL`. ### `zxc_write_block_header` ```c ZXC_EXPORT int zxc_write_block_header( uint8_t* dst, size_t dst_capacity, const zxc_block_header_t* bh ); ``` Serializes a block header (8 bytes, little-endian). **Returns**: bytes written, or `ZXC_ERROR_DST_TOO_SMALL`. ### `zxc_read_block_header` ```c ZXC_EXPORT int zxc_read_block_header( const uint8_t* src, size_t src_size, zxc_block_header_t* bh ); ``` Parses a block header (endianness conversion included). **Returns**: `ZXC_OK` or `ZXC_ERROR_SRC_TOO_SMALL`. ### `zxc_write_file_footer` ```c ZXC_EXPORT int zxc_write_file_footer( uint8_t* dst, size_t dst_capacity, uint64_t src_size, uint32_t global_hash, int checksum_enabled ); ``` Writes the 12-byte footer (original size + optional global hash). **Returns**: bytes written, or `ZXC_ERROR_DST_TOO_SMALL`. --- ## 13. Error Handling ### `zxc_error_name` ```c ZXC_EXPORT const char* zxc_error_name(int code); ``` Returns a constant, null-terminated string for any error code (e.g. `"ZXC_OK"`, `"ZXC_ERROR_MEMORY"`). Returns `"ZXC_UNKNOWN_ERROR"` for unrecognized codes. ### Pattern All functions that can fail use the same convention: ```c int64_t result = zxc_compress(src, src_size, dst, dst_cap, &opts); if (result < 0) { fprintf(stderr, "Error: %s\n", zxc_error_name((int)result)); } ``` --- ## 14. Thread Safety | API Layer | Safe to call concurrently? | Notes | |-----------|---------------------------|-------| | **Buffer API** | Yes (stateless) | Each call is self-contained. Multiple threads can compress/decompress simultaneously with independent buffers. | | **Block API** | Per-context | Uses `zxc_cctx` / `zxc_dctx` — same rule as Context API. Create one context per thread. | | **Context API** | Per-context | A single `zxc_cctx` / `zxc_dctx` must not be shared between threads. Create one context per thread. | | **Streaming API** | Per-call | Each `zxc_stream_*` call manages its own thread pool internally. Do not call from multiple threads on the same `FILE*`. | | **Seekable API** | Per-handle | A single `zxc_seekable` handle must not be shared between threads for single-threaded decompression. Use `zxc_seekable_decompress_range_mt()` for parallel access. | | **Sans-IO API** | Per-context | Same rule as context API - one `zxc_cctx_t` per thread. | | `zxc_error_name` | Yes | Returns a pointer to a static string. | --- ## 15. Exported Symbols Summary The shared library exports exactly **35 symbols** (verified with `nm -gU`): | # | Symbol | API Layer | Header | |---|--------|-----------|--------| | 1 | `zxc_compress_bound` | Buffer | `zxc_buffer.h` | | 2 | `zxc_compress` | Buffer | `zxc_buffer.h` | | 3 | `zxc_decompress` | Buffer | `zxc_buffer.h` | | 4 | `zxc_get_decompressed_size` | Buffer | `zxc_buffer.h` | | 5 | `zxc_compress_block_bound` | Block | `zxc_buffer.h` | | 6 | `zxc_compress_block` | Block | `zxc_buffer.h` | | 7 | `zxc_decompress_block` | Block | `zxc_buffer.h` | | 8 | `zxc_create_cctx` | Context | `zxc_buffer.h` | | 9 | `zxc_free_cctx` | Context | `zxc_buffer.h` | | 10 | `zxc_compress_cctx` | Context | `zxc_buffer.h` | | 11 | `zxc_create_dctx` | Context | `zxc_buffer.h` | | 12 | `zxc_free_dctx` | Context | `zxc_buffer.h` | | 13 | `zxc_decompress_dctx` | Context | `zxc_buffer.h` | | 14 | `zxc_stream_compress` | Streaming | `zxc_stream.h` | | 15 | `zxc_stream_decompress` | Streaming | `zxc_stream.h` | | 16 | `zxc_stream_get_decompressed_size` | Streaming | `zxc_stream.h` | | 17 | `zxc_seekable_open` | Seekable | `zxc_seekable.h` | | 18 | `zxc_seekable_open_file` | Seekable | `zxc_seekable.h` | | 19 | `zxc_seekable_get_num_blocks` | Seekable | `zxc_seekable.h` | | 20 | `zxc_seekable_get_decompressed_size` | Seekable | `zxc_seekable.h` | | 21 | `zxc_seekable_get_block_comp_size` | Seekable | `zxc_seekable.h` | | 22 | `zxc_seekable_get_block_decomp_size` | Seekable | `zxc_seekable.h` | | 23 | `zxc_seekable_decompress_range` | Seekable | `zxc_seekable.h` | | 24 | `zxc_seekable_decompress_range_mt` | Seekable | `zxc_seekable.h` | | 25 | `zxc_seekable_free` | Seekable | `zxc_seekable.h` | | 26 | `zxc_write_seek_table` | Seekable | `zxc_seekable.h` | | 27 | `zxc_seek_table_size` | Seekable | `zxc_seekable.h` | | 28 | `zxc_cctx_init` | Sans-IO | `zxc_sans_io.h` | | 29 | `zxc_cctx_free` | Sans-IO | `zxc_sans_io.h` | | 30 | `zxc_write_file_header` | Sans-IO | `zxc_sans_io.h` | | 31 | `zxc_read_file_header` | Sans-IO | `zxc_sans_io.h` | | 32 | `zxc_write_block_header` | Sans-IO | `zxc_sans_io.h` | | 33 | `zxc_read_block_header` | Sans-IO | `zxc_sans_io.h` | | 34 | `zxc_write_file_footer` | Sans-IO | `zxc_sans_io.h` | | 35 | `zxc_error_name` | Error | `zxc_error.h` | No internal symbols leak into the public ABI. FMV dispatch variants (`_default`, `_neon`, `_avx2`, `_avx512`) are compiled with `-fvisibility=hidden` and are not exported. zxc-0.10.0/docs/EXAMPLES.md000066400000000000000000000254371517010557200151020ustar00rootroot00000000000000# ZXC API Examples This document provides complete, working examples for using the ZXC compression library in C. ## Table of Contents - [Buffer API (In-Memory)](#buffer-api-in-memory) - [Stream API (Multi-Threaded)](#stream-api-multi-threaded) - [Reusable Context API](#reusable-context-api) - [Language Bindings](#language-bindings) --- ## Buffer API (In-Memory) Ideal for small assets or simple integrations. Thread-safe and ready for highly concurrent environments (Go routines, Node.js workers, Python threads). ```c #include "zxc.h" #include #include #include int main(void) { // Original data to compress const char* original = "Hello, ZXC! This is a sample text for compression."; size_t original_size = strlen(original) + 1; // Include null terminator // Step 1: Calculate maximum compressed size uint64_t max_compressed_size = zxc_compress_bound(original_size); // Step 2: Allocate buffers void* compressed = malloc(max_compressed_size); void* decompressed = malloc(original_size); if (!compressed || !decompressed) { fprintf(stderr, "Memory allocation failed\n"); free(compressed); free(decompressed); return 1; } // Step 3: Compress data (level 3, checksum enabled, default block size) zxc_compress_opts_t c_opts = { .level = ZXC_LEVEL_DEFAULT, .checksum_enabled = 1, /* .block_size = 0 -> 256 KB default */ }; int64_t compressed_size = zxc_compress( original, // Source buffer original_size, // Source size compressed, // Destination buffer max_compressed_size, // Destination capacity &c_opts // Options (NULL = all defaults) ); if (compressed_size <= 0) { fprintf(stderr, "Compression failed (error %lld)\n", (long long)compressed_size); free(compressed); free(decompressed); return 1; } printf("Original size: %zu bytes\n", original_size); printf("Compressed size: %lld bytes (%.1f%% ratio)\n", (long long)compressed_size, 100.0 * (double)compressed_size / (double)original_size); // Step 4: Decompress data (checksum verification enabled) zxc_decompress_opts_t d_opts = { .checksum_enabled = 1 }; int64_t decompressed_size = zxc_decompress( compressed, // Source buffer (size_t)compressed_size, decompressed, // Destination buffer original_size, // Destination capacity &d_opts // Options (NULL = all defaults) ); if (decompressed_size <= 0) { fprintf(stderr, "Decompression failed (error %lld)\n", (long long)decompressed_size); free(compressed); free(decompressed); return 1; } // Step 5: Verify integrity if ((size_t)decompressed_size == original_size && memcmp(original, decompressed, original_size) == 0) { printf("Success! Data integrity verified.\n"); printf("Decompressed: %s\n", (char*)decompressed); } else { fprintf(stderr, "Data mismatch after decompression\n"); } // Cleanup free(compressed); free(decompressed); return 0; } ``` **Compilation:** ```bash gcc -o buffer_example buffer_example.c -I include -L build -lzxc_lib ``` --- ## Stream API (Multi-Threaded) For large files, use the streaming API to process data in parallel chunks. ```c #include "zxc.h" #include #include int main(int argc, char* argv[]) { if (argc != 4) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } const char* input_path = argv[1]; const char* compressed_path = argv[2]; const char* output_path = argv[3]; /* ------------------------------------------------------------------ */ /* Step 1: Compress */ /* ------------------------------------------------------------------ */ printf("Compressing '%s' to '%s'...\n", input_path, compressed_path); FILE* f_in = fopen(input_path, "rb"); if (!f_in) { fprintf(stderr, "Error: cannot open '%s'\n", input_path); return 1; } FILE* f_out = fopen(compressed_path, "wb"); if (!f_out) { fprintf(stderr, "Error: cannot create '%s'\n", compressed_path); fclose(f_in); return 1; } // 0 threads = auto-detect CPU cores; 0 block_size = 256 KB default zxc_compress_opts_t c_opts = { .n_threads = 0, .level = ZXC_LEVEL_DEFAULT, .checksum_enabled = 1, /* .block_size = 0 -> 256 KB */ }; int64_t compressed_bytes = zxc_stream_compress(f_in, f_out, &c_opts); fclose(f_in); fclose(f_out); if (compressed_bytes < 0) { fprintf(stderr, "Compression failed (error %lld)\n", (long long)compressed_bytes); return 1; } printf("Compression complete: %lld bytes written\n", (long long)compressed_bytes); /* ------------------------------------------------------------------ */ /* Step 2: Decompress */ /* ------------------------------------------------------------------ */ printf("\nDecompressing '%s' to '%s'...\n", compressed_path, output_path); FILE* f_compressed = fopen(compressed_path, "rb"); if (!f_compressed) { fprintf(stderr, "Error: cannot open '%s'\n", compressed_path); return 1; } FILE* f_decompressed = fopen(output_path, "wb"); if (!f_decompressed) { fprintf(stderr, "Error: cannot create '%s'\n", output_path); fclose(f_compressed); return 1; } zxc_decompress_opts_t d_opts = { .n_threads = 0, .checksum_enabled = 1 }; int64_t decompressed_bytes = zxc_stream_decompress(f_compressed, f_decompressed, &d_opts); fclose(f_compressed); fclose(f_decompressed); if (decompressed_bytes < 0) { fprintf(stderr, "Decompression failed (error %lld)\n", (long long)decompressed_bytes); return 1; } printf("Decompression complete: %lld bytes written\n", (long long)decompressed_bytes); printf("\nSuccess! Verify the output file matches the original.\n"); return 0; } ``` **Compilation:** ```bash gcc -o stream_example stream_example.c -I include -L build -lzxc_lib -lpthread -lm ``` **Usage:** ```bash ./stream_example large_file.bin compressed.zxc decompressed.bin ``` **Features demonstrated:** - Multi-threaded parallel processing (auto-detects CPU cores) - Checksum validation for data integrity - Error handling for file operations --- ## Reusable Context API For tight loops - such as filesystem plug-ins (squashfs, dwarfs, erofs) - where allocating and freeing internal buffers on every call would add latency, ZXC provides opaque reusable contexts. Internal buffers are only reallocated when the **block size changes**, so the common case (same block size, different data) is completely allocation-free. Options are **sticky**: settings passed via `opts` to `zxc_create_cctx()` or `zxc_compress_cctx()` are remembered and reused on subsequent calls where `opts` is `NULL`. ```c #include "zxc.h" #include #include #include #define BLOCK_SIZE (64 * 1024) // 64 KB - typical squashfs block size #define NUM_BLOCKS 32 int main(void) { // Create context with sticky options (level 3, no checksum, 64 KB blocks). // These settings are remembered for all subsequent calls. zxc_compress_opts_t create_opts = { .level = 3, .checksum_enabled = 0, .block_size = BLOCK_SIZE, }; zxc_cctx* cctx = zxc_create_cctx(&create_opts); zxc_dctx* dctx = zxc_create_dctx(); if (!cctx || !dctx) { fprintf(stderr, "Context creation failed\n"); zxc_free_cctx(cctx); zxc_free_dctx(dctx); return 1; } const size_t comp_cap = (size_t)zxc_compress_bound(BLOCK_SIZE); uint8_t* comp = malloc(comp_cap); uint8_t* src = malloc(BLOCK_SIZE); uint8_t* dec = malloc(BLOCK_SIZE); for (int i = 0; i < NUM_BLOCKS; i++) { // Fill block with pseudo-random data for (size_t j = 0; j < BLOCK_SIZE; j++) src[j] = (uint8_t)(i ^ j); // Compress - pass NULL to reuse sticky settings from create_opts. // No malloc/free inside, no need to pass opts again. int64_t csz = zxc_compress_cctx(cctx, src, BLOCK_SIZE, comp, comp_cap, NULL); if (csz <= 0) { fprintf(stderr, "Block %d: compress error %lld\n", i, (long long)csz); break; } // Decompress - no malloc/free inside int64_t dsz = zxc_decompress_dctx(dctx, comp, (size_t)csz, dec, BLOCK_SIZE, NULL); if (dsz != BLOCK_SIZE || memcmp(src, dec, BLOCK_SIZE) != 0) { fprintf(stderr, "Block %d: roundtrip mismatch\n", i); break; } } // Override sticky settings for a single call (e.g. switch to level 5). // This also updates the sticky settings for future NULL calls. zxc_compress_opts_t high_opts = { .level = 5 }; int64_t csz = zxc_compress_cctx(cctx, src, BLOCK_SIZE, comp, comp_cap, &high_opts); printf("Level-5 compressed: %lld bytes\n", (long long)csz); printf("Processed %d blocks of %d KB - no per-block allocation.\n", NUM_BLOCKS, BLOCK_SIZE / 1024); zxc_free_cctx(cctx); zxc_free_dctx(dctx); free(src); free(comp); free(dec); return 0; } ``` **Compilation:** ```bash gcc -o ctx_example ctx_example.c -I include -L build -lzxc_lib ``` --- ## Writing Your Own Streaming Driver The streaming multi-threaded API shown above is the default provided driver. However, ZXC is written in a **"sans-IO" style** that separates compute from I/O and multitasking. This allows you to write your own driver in any language of your choice, using the native I/O and multitasking capabilities of that language. To implement a custom driver: 1. Include the extra public header `zxc_sans_io.h` 2. Study the reference implementation in `src/lib/zxc_driver.c` 3. Implement your own I/O and threading logic --- ## Language Bindings For non-C languages, see the official bindings: | Language | Package | Install Command | Documentation | |----------|---------|-----------------|---------------| | **Rust** | [`crates.io`](https://crates.io/crates/zxc-compress) | `cargo add zxc-compress` | [README](../wrappers/rust/zxc/README.md) | | **Python**| [`PyPI`](https://pypi.org/project/zxc-compress) | `pip install zxc-compress` | [README](../wrappers/python/README.md) | | **Node.js**| [`npm`](https://www.npmjs.com/package/zxc-compress) | `npm install zxc-compress` | [README](../wrappers/nodejs/README.md) | | **Go** | `go get` | `go get github.com/hellobertrand/zxc/wrappers/go` | [README](../wrappers/go/README.md) | Community-maintained: - **Go**: https://github.com/meysam81/go-zxc zxc-0.10.0/docs/FORMAT.md000066400000000000000000000507331517010557200146510ustar00rootroot00000000000000# ZXC Compressed File Format (Technical Specification) **Date**: March 2026 **Format Version**: 5 This document describes the on-disk binary format of a ZXC compressed file. It formalizes the current reference implementation of format version **5**. ## 1. Conventions - **Byte order**: all multi-byte integers are **little-endian**. - **Unit**: offsets are in bytes, zero-based from the start of each structure. - **Checksum mode**: enabled globally by a flag in the file header. - **Block model**: a file is a sequence of blocks terminated by an EOF block, then a footer. --- ## 2. Full File Layout ```text +----------------------+ 16 bytes | File Header | +----------------------+ | Block #0 | | - 8B Block Header | | - Block Payload | | - Optional 4B CRC32 | +----------------------+ | Block #1 | | ... | +----------------------+ | EOF Block | 8 bytes (type=255, comp_size=0) +----------------------+ | SEK Block (Optional) | table of contents for random access +----------------------+ | File Footer | 12 bytes +----------------------+ ``` --- ## 3. File Header (16 bytes) ```text Offset Size Field 0x00 4 Magic Word 0x04 1 Format Version 0x05 1 Chunk Size Code 0x06 1 Flags 0x07 7 Reserved (must be 0) 0x0E 2 Header CRC16 ``` ### 3.1 Field definitions - **Magic Word** (`u32`): `0x9CB02EF5`. - **Format Version** (`u8`): currently `5`. - **Chunk Size Code** (`u8`): - If the value is in the range `[12, 21]`, it is an **exponent**: `block_size = 2^code`. - `12` = 4 KB, `13` = 8 KB, ..., `18` = 256 KB (default), ..., `21` = 2 MB. - The legacy value `64` (from older encoders) is accepted and maps to 256 KB (default). - All other values are rejected (`ZXC_ERROR_BAD_BLOCK_SIZE`). - Valid block sizes are powers of 2 in the range **4 KB – 2 MB**. - **Flags** (`u8`): - Bit 7 (`0x80`): `HAS_CHECKSUM`. - Bits 0..3: checksum algorithm id (`0` = RapidHash-based folding). - Bits 4..6: reserved. - **Reserved**: 7 bytes set to zero. - **Header CRC16** (`u16`): computed with `zxc_hash16` on the 16-byte header where bytes `0x0E..0x0F` are zeroed. --- ## 4. Generic Block Container Each block starts with a fixed 8-byte block header. ```text Offset Size Field 0x00 1 Block Type 0x01 1 Block Flags 0x02 1 Reserved 0x03 4 Compressed Payload Size (comp_size) 0x07 1 Header CRC8 ``` ### 4.1 Header semantics - **Block Type**: - `0` = RAW - `1` = GLO - `2` = NUM - `3` = GHI - `254` = SEK - `255` = EOF - **Block Flags**: currently not used by implementation (written as `0`). - **Reserved**: must be 0. - **comp_size**: payload size in bytes (does **not** include the optional trailing 4-byte block checksum). - **Header CRC8**: `zxc_hash8` over the 8-byte header with byte `0x07` forced to zero before hashing. ### 4.2 Block physical layout ```text [8B Block Header] + [comp_size bytes payload] + [optional 4B checksum] ``` When checksums are enabled at file level, each non-EOF block carries one trailing 4-byte checksum of its compressed payload. --- ## 5. Block Types and Payload Formats ## 5.1 RAW block (`type=0`) Payload is uncompressed data. ```text Payload = raw bytes raw_size = comp_size ``` No internal sub-header. --- ## 5.2 NUM block (`type=2`) Used for numeric data (32-bit integer stream), delta/zigzag + bitpacking. ### NUM payload layout ```text +--------------------------+ | NUM Header (16 bytes) | +--------------------------+ | Frame #0 header (16B) | | Frame #0 packed bits | +--------------------------+ | Frame #1 header (16B) | | Frame #1 packed bits | +--------------------------+ | ... | +--------------------------+ ``` ### NUM Header (16 bytes) ```text Offset Size Field 0x00 8 n_values (u64) 0x08 2 frame_size (u16, currently 128) 0x0A 6 reserved ``` ### NUM frame record (repeated) ```text Offset Size Field 0x00 2 nvals in frame (u16) 0x02 2 bits per value (u16) 0x04 8 base/running seed (u64) 0x0C 4 packed_size in bytes (u32) 0x10 ... packed delta bitstream ``` Notes: - Values are reconstructed by bit-unpacking, zigzag decode, then prefix accumulation. - `packed_size` bytes immediately follow each 16-byte frame header. --- ## 5.3 GLO block (`type=1`) General LZ-style format with separated streams. ### GLO payload layout ```text +-------------------------------+ | GLO Header (16 bytes) | +-------------------------------+ | 4 Section Descriptors (32B) | +-------------------------------+ | Literals stream | +-------------------------------+ | Tokens stream | +-------------------------------+ | Offsets stream | +-------------------------------+ | Extras stream | +-------------------------------+ ``` ### GLO Header (16 bytes) ```text Offset Size Field 0x00 4 n_sequences (u32) 0x04 4 n_literals (u32) 0x08 1 enc_lit (0=RAW, 1=RLE) 0x09 1 enc_litlen (reserved) 0x0A 1 enc_mlen (reserved) 0x0B 1 enc_off (0=16-bit offsets, 1=8-bit offsets) 0x0C 4 reserved ``` ### GLO section descriptors (4 × 8 bytes) Descriptor format (packed `u64`): - low 32 bits: compressed size - high 32 bits: raw size Section order: 1. Literals 2. Tokens 3. Offsets 4. Extras ### GLO stream content - **Literals stream**: - raw literal bytes, or - RLE tokenized if `enc_lit=1`. - **Tokens stream**: - one byte per sequence: `(LL << 4) | ML`. - `LL` and `ML` are 4-bit fields. - **Offsets stream**: - `n_sequences × 1` byte if `enc_off=1`, else `n_sequences × 2` bytes LE. - Values are **biased**: stored value = `actual_offset - 1`. Decoder adds `+ 1`. - This makes `offset == 0` impossible by construction (minimum decoded offset = 1). - **Extras stream**: - Prefix-varint overflow values for token saturations: - if `LL == 15`, read varint and add to LL - if `ML == 15`, read varint and add to ML - actual match length is `ML + 5` (minimum match = 5). --- ## 5.4 GHI block (`type=3`) High-throughput LZ format with packed 32-bit sequences. ### GHI payload layout ```text +-------------------------------+ | GHI Header (16 bytes) | +-------------------------------+ | 3 Section Descriptors (24B) | +-------------------------------+ | Literals stream | +-------------------------------+ | Sequences stream (N * 4B) | +-------------------------------+ | Extras stream | +-------------------------------+ ``` ### GHI Header (16 bytes) Same binary layout as GLO header: - `n_sequences`, `n_literals`, `enc_lit`, `enc_litlen`, `enc_mlen`, `enc_off`, reserved. In practice for GHI: - `enc_lit = 0` (raw literals) - `enc_off` is metadata (sequence words always store 16-bit offsets) ### GHI section descriptors (3 × 8 bytes) Section order: 1. Literals 2. Sequences 3. Extras Each descriptor uses the same packed size encoding as GLO (`u64`: comp32|raw32). ### GHI sequence word format (32 bits) ```text Bits 31..24 : LL (literal length, 8 bits) Bits 23..16 : ML (match length minus 5, 8 bits) Bits 15..0 : Offset - 1 (16 bits, biased; decode: stored + 1) ``` Memory order (little-endian word): ```text byte0 = offset low byte1 = offset high byte2 = ML byte3 = LL ``` Overflow rules: - if `LL == 255`, read varint from Extras and add it to LL. - if `ML == 255`, read varint, then add minimum match (`+5`). - otherwise decoded match length is `ML + 5`. --- ## 5.5 EOF block (`type=255`) EOF marks end of block stream. Constraints: - block header is present (8 bytes) - `comp_size` **must be 0** - no payload - no per-block trailing checksum Immediately after EOF block header comes the Optional SEK block, followed by the 12-byte file footer. --- ## 5.6 SEK block (`type=254`) The **Seek Table** block is an optional block appended between the EOF block and the File Footer. It provides `O(1)` random-access capabilities by recording the compressed size of every block in the archive. Decompressed sizes and block indices are derived from the file header's `block_size` (all blocks are `block_size` except the last, which may be smaller). **Layout of a SEK Block**: ```text Offset Size Field 0x00 8 Block Header (type=254, comp_size=N*4) 0x08 4 Block 0 Compressed Size (u32 LE) 0x0C 4 Block 1 Compressed Size (u32 LE) ... ... ... 8 + (N-1)*4 4 Block N-1 Compressed Size (u32 LE) ``` **Backward Detection Strategy**: 1. Read the **File Header** (first 16 bytes) -> extract `block_size`. 2. Read the **File Footer** (last 12 bytes) -> extract `total_decompressed_size`. 3. Derive `num_blocks = ceil(total_decompressed_size / block_size)`. 4. Calculate `seek_block_size = 8 + (N × 4)`. 5. Seek backward by `seek_block_size` bytes from the start of the footer to read the Block Header. 6. Validate `block_type == 254 (SEK)` and `comp_size == N × 4`. --- ## 6. Prefix Varint (Extras stream) ZXC extras use a prefix-length varint. Length is encoded in unary form in the high bits of first byte: - `0xxxxxxx` -> 1 byte total - `10xxxxxx` -> 2 bytes total - `110xxxxx` -> 3 bytes total - `1110xxxx` -> 4 bytes total - `11110xxx` -> 5 bytes total Payload bits from following bytes are concatenated little-endian style (low bits first). Used by GLO/GHI to carry LL/ML overflows beyond token/sequence inline limits. --- ## 7. Checksums and Integrity ## 7.1 Header checksums - File header: 16-bit (`zxc_hash16`). - Block header: 8-bit (`zxc_hash8`). These protect metadata/navigation fields. ## 7.2 Per-block checksum (optional) When file header has `HAS_CHECKSUM=1`: - each data block appends a 4-byte checksum after payload. - checksum input is **compressed payload bytes only** (not block header). - algorithm id currently `0` (RapidHash folded to 32-bit). ## 7.3 Global stream hash A rolling global hash is maintained from per-block checksums in stream order: ```text global = 0 for each data block checksum b: global = ((global << 1) | (global >> 31)) XOR b ``` This value is stored in the file footer (or zeroed when checksum mode is disabled). --- ## 8. File Footer (12 bytes) Footer is mandatory and placed immediately after EOF block header. ```text Offset Size Field 0x00 8 original_source_size (u64) 0x08 4 global_hash (u32) ``` - **original_source_size**: full uncompressed size of the file. - **global_hash**: - valid when checksum mode is active; - set to zero when checksum mode is disabled. --- ## 9. Decoder Validation Checklist (Practical) 1. Validate file header magic/version/CRC16. 2. Parse blocks sequentially: - validate block header CRC8, - check block bounds using `comp_size`, - if enabled, verify trailing block checksum. 3. Decode payload according to block type. 4. On EOF: - require `comp_size == 0`, - read footer, - compare footer `original_source_size` with produced output size, - if enabled, compare footer `global_hash` with recomputed rolling hash. --- ## 10. Versioning Policy ### 10.1 Format version field The format version is a single byte at offset `0x04` of the file header. A conforming decoder **MUST** reject any file whose version it does not support. ### 10.2 Version bump criteria | Change class | Version action | Example | |---|---|---| | New block type added | **No bump** (forward-compatible) | Adding a hypothetical `GLR` block type | | New flag bit defined | **No bump** (forward-compatible) | Using a reserved flag bit | | Existing block encoding changed | **Major bump** | Changing GLO token layout | | Header/footer layout changed | **Major bump** | Resizing the file header | | Checksum algorithm changed | **Major bump** | Replacing RapidHash with Komihash | ### 10.3 Compatibility rules - **Backward compatibility**: a decoder supporting version *N* **MUST** decode all files produced by encoders of version *N*. It **MAY** also accept earlier versions. - **Forward compatibility**: a decoder encountering an **unknown block type** (not RAW, GLO, NUM, GHI, or EOF) **SHOULD** skip it using `comp_size` to advance past its payload (and optional checksum), rather than rejecting the file outright. This allows older decoders to partially process files from newer encoders that introduce additive block types. - **Reserved fields**: all reserved bytes and flag bits **MUST** be written as zero by encoders. Decoders **MUST** ignore reserved fields (not reject non-zero values), unless a future version assigns them meaning. ### 10.4 Minimum conforming decoder A minimal conforming decoder for version 5 **MUST** support: - File header parsing and CRC16 validation. - **RAW** blocks (type 0) - passthrough copy. - **GLO** blocks (type 1) - full LZ decode with extras varint. - **GHI** blocks (type 3) - full LZ decode with extras varint. - **EOF** block (type 255) - stream termination. - File footer validation (source size check). Support for **NUM** (type 2) and checksum verification is **RECOMMENDED** but not strictly required for a minimal implementation. --- ## 11. Error Handling ### 11.1 Error classes Decoders **MUST** detect and handle the following error conditions. The recommended behavior for each class is specified below. | Error | Detection point | Required behavior | |---|---|---| | **Bad magic** | File header, offset 0x00 | Reject immediately. Not a ZXC file. | | **Unsupported version** | File header, offset 0x04 | Reject immediately. Version not supported. | | **Header CRC16 mismatch** | File header, offset 0x0E | Reject. Header is corrupt or truncated. | | **Invalid chunk size code** | File header, offset 0x05 | Reject. Code outside valid range `[12..21]` and not legacy `64`. | | **Block header CRC8 mismatch** | Block header, offset 0x07 | Reject block. Stream is corrupt. | | **Unknown block type** | Block header, offset 0x00 | Skip block using `comp_size` (see §10.3), or reject. | | **Block payload truncated** | During `fread` of `comp_size` bytes | Reject. Unexpected end of stream. | | **Block checksum mismatch** | Trailing 4-byte checksum | Reject block. Payload is corrupt. | | **EOF block with non-zero comp_size** | EOF block header | Reject. Malformed EOF marker. | | **Footer source size mismatch** | File footer, offset 0x00 | Reject. Output size does not match declared original size. | | **Footer global hash mismatch** | File footer, offset 0x08 | Reject (if checksum mode active). Integrity failure. | | **Decompressed output exceeds chunk size** | During LZ decode | Reject. Corrupt or malicious payload. | | **Match offset out of bounds** | During LZ copy | Reject. Offset references data before output start. | | **Varint exceeds maximum length** | Extras stream | Reject. Overflow or corrupt extras data. | ### 11.2 Severity levels - **Fatal**: the decoder **MUST** stop processing and report an error. All errors in the table above are fatal by default. - **Warning**: not currently defined. Future versions may introduce non-fatal conditions (e.g. unknown flag bits set in reserved positions). ### 11.3 Partial output When a fatal error occurs mid-stream, the decoder **SHOULD**: 1. Stop producing output immediately. 2. Report the specific error condition (see `zxc_error_name` in the reference implementation). 3. Not return partially decompressed data as a valid result. Buffer-mode decoders **MUST** return a negative error code. Stream-mode decoders **MUST** signal the error and cease writing to the output. ### 11.4 Decoder hardening recommendations For decoders processing untrusted input (e.g. network data, user uploads): - Validate **all** header checksums before processing payloads. - Enforce maximum allocation limits based on `comp_size` and chunk size code. - Reject files where `comp_size` exceeds `zxc_compress_bound(chunk_size)`. - Use bounded memory copies - never trust decoded lengths without cross-checking against output buffer capacity. --- ## 12. Summary of Useful Fixed Sizes - File header: **16** bytes - Block header: **8** bytes - Block checksum (optional): **4** bytes - NUM header: **16** bytes - GLO header: **16** bytes - GHI header: **16** bytes - Section descriptor: **8** bytes - GLO descriptors total: **32** bytes - GHI descriptors total: **24** bytes - File footer: **12** bytes --- ## 13. Worked Example (Real Hexdump) This example was produced with the CLI from a 10-byte input (`Hello ZXC\n`) using: ```bash zxc -z -C -1 sample.txt ``` Generated archive size: **58 bytes**. ### 13.1 Full hexdump ```text 00000000: F5 2E B0 9C 05 12 80 00 00 00 00 00 00 00 9E 53 00000010: 00 00 00 0A 00 00 00 69 48 65 6C 6C 6F 20 5A 58 00000020: 43 0A 90 BB A1 75 FF 00 00 00 00 00 00 02 0A 00 00000030: 00 00 00 00 00 00 90 BB A1 75 ``` ### 13.2 Byte-level decoding #### A) File Header (offset `0x00`, 16 bytes) ```text F5 2E B0 9C | 05 | 12 | 80 | 00 00 00 00 00 00 00 | 9E 53 ``` - `F5 2E B0 9C` -> magic word (LE) = `0x9CB02EF5`. - `05` -> format version 5. - `12` -> chunk-size code 18 (exponent encoding: `2^18 = 262144` bytes, i.e. 256 KiB). - `80` -> checksum enabled (`HAS_CHECKSUM=1`, algo id 0). - next 7 bytes are reserved zeros. - `9E 53` -> header CRC16. #### B) Data Block #0 (RAW) Block header at offset `0x10`: ```text 00 | 00 | 00 | 0A 00 00 00 | 69 ``` - type `00` = RAW. - flags `00`, reserved `00`. - `comp_size = 0x0000000A = 10` bytes. - header CRC8 = `0x69`. Payload at `0x18..0x21` (10 bytes): ```text 48 65 6C 6C 6F 20 5A 58 43 0A ``` ASCII: `Hello ZXC\n`. Trailing block checksum at `0x22..0x25`: ```text 90 BB A1 75 ``` LE value: `0x75A1BB90`. #### C) EOF Block (offset `0x26`, 8 bytes) ```text FF | 00 | 00 | 00 00 00 00 | 02 ``` - type `FF` = EOF. - `comp_size = 0` (mandatory). - header CRC8 = `0x02`. #### D) File Footer (offset `0x2E`, 12 bytes) ```text 0A 00 00 00 00 00 00 00 | 90 BB A1 75 ``` - original source size = `10` bytes. - global hash = `0x75A1BB90`. Since there is exactly one data block, the global hash equals that block checksum: ```text global0 = 0 global1 = rotl1(global0) XOR block_crc = block_crc ``` ### 13.3 Structural view with absolute offsets ```text 0x00..0x0F File Header (16) 0x10..0x17 RAW Block Header (8) 0x18..0x21 RAW Payload (10) 0x22..0x25 RAW Block Checksum (4) 0x26..0x2D EOF Block Header (8) 0x2E..0x39 File Footer (12) ``` ### 13.4 Seekable Variant (with Seek Table) Same 10-byte input (`Hello ZXC\n`), compressed with seekable mode enabled: ```bash zxc -z -C -1 -S sample.txt ``` Generated archive size: **70 bytes** (12 bytes larger than the non-seekable variant). #### Full hexdump ```text 00000000: F5 2E B0 9C 05 12 80 00 00 00 00 00 00 00 9E 53 00000010: 00 00 00 0A 00 00 00 69 48 65 6C 6C 6F 20 5A 58 00000020: 43 0A 90 BB A1 75 FF 00 00 00 00 00 00 02 FE 00 00000030: 00 04 00 00 00 D2 16 00 00 00 0A 00 00 00 00 00 00000040: 00 00 90 BB A1 75 ``` #### Byte-level decoding **A) File Header** (offset `0x00`, 16 bytes) - identical to non-seekable. **B) Data Block #0 (RAW)** (offset `0x10`, 22 bytes) - identical to non-seekable. **C) EOF Block** (offset `0x26`, 8 bytes) - identical to non-seekable. **D) SEK Block** (offset `0x2E`, 12 bytes) Block header at `0x2E`: ```text FE | 00 | 00 | 04 00 00 00 | D2 ``` - `FE` -> type 254 = SEK (Seek Table). - flags `00`, reserved `00`. - `comp_size = 0x00000004 = 4` bytes (one entry x 4 bytes/entry). - header CRC8 = `0xD2`. Seek table entry at `0x36`: ```text 16 00 00 00 ``` - Entry #0: compressed block size = `0x00000016 = 22` bytes. This is the total size of data block #0 including its header (8) + payload (10) + checksum (4) = 22. ✓ **E) File Footer** (offset `0x3A`, 12 bytes) ```text 0A 00 00 00 00 00 00 00 | 90 BB A1 75 ``` - original source size = `10` bytes. - global hash = `0x75A1BB90`. #### Structural view with absolute offsets ```text 0x00..0x0F File Header (16) 0x10..0x17 RAW Block Header (8) 0x18..0x21 RAW Payload (10) 0x22..0x25 RAW Block Checksum (4) 0x26..0x2D EOF Block Header (8) 0x2E..0x35 SEK Block Header (8) <- seek table 0x36..0x39 SEK Entry #0 (4) <- comp_size of block #0 0x3A..0x45 File Footer (12) ``` > **Compatibility note**: The SEK block is inserted between the EOF block and the file footer. The footer always remains the **last 12 bytes of the file**, so decoders that locate the footer from the end of the file (e.g. `src + src_size - 12` for buffer APIs, or `fseek(END - 12)` for file APIs) work unchanged with seekable archives. However, **streaming decoders** that read the footer sequentially immediately after the EOF block must be updated to detect and skip the SEK block. In practice, all ZXC decoders since v0.9.0 handle both seekable and non-seekable archives transparently. zxc-0.10.0/docs/WHITEPAPER.md000066400000000000000000001600111517010557200153200ustar00rootroot00000000000000# ZXC: High-Performance Asymmetric Lossless Compression **Version**: 0.10.0 **Date**: April 2026 **Author**: Bertrand Lebonnois --- ## 1. Executive Summary In modern software delivery pipelines-specifically **Mobile Gaming**, **Embedded Systems**, and **FOTA (Firmware Over-The-Air)**-data is typically generated on high-performance x86 workstations but consumed on energy-constrained ARM devices. Standard industry codecs like LZ4 offer excellent performance but fail to exploit the "Write-Once, Read-Many" (WORM) nature of these pipelines. **ZXC** is a lossless codec designed to bridge this gap. By utilizing an **asymmetric compression model**, ZXC achieves a **>40% increase in decompression speed on ARM** compared to LZ4, while simultaneously reducing storage footprints. On x86 development architecture, ZXC maintains competitive throughput, ensuring no disruption to build pipelines. ## 2. The Efficiency Gap The industry standard, LZ4, prioritizes symmetric speed (fast compression and fast decompression). While ideal for real-time logs or RAM swapping, this symmetry is useless for asset distribution. * **Wasted Cycles**: CPU cycles saved during the single compression event (on a build server) do not benefit the millions of end-users decoding the data. * **The Battery Tax**: On mobile devices, slower decompression keeps the CPU active longer, draining battery and generating heat. ## 3. The ZXC Solution ZXC utilizes a computationally intensive encoder to generate a bitstream specifically structured to **maximize decompression throughput**. By performing heavy analysis upfront, the encoder produces a layout optimized for the instruction pipelining and branch prediction capabilities of modern CPUs, particularly ARMv8, effectively offloading complexity from the decoder to the encoder. ### 3.1 Asymmetric Pipeline ZXC employs a Producer-Consumer architecture to decouple I/O operations from CPU-intensive tasks. This allows for parallel processing where input reading, compression/decompression, and output writing occur simultaneously, effectively hiding I/O latency. ### 3.2 Modular Architecture The ZXC file format is inherently modular. **Each block is independent and can be encoded and decoded using the algorithm best suited** for that specific data type. This flexibility allows the format to evolve and incorporate new compression strategies without breaking backward compatibility. ## 4. Core Algorithms ZXC utilizes a hybrid approach combining LZ77 (Lempel-Ziv) dictionary matching with advanced entropy coding and specialized data transforms. ### 4.1 LZ77 Engine The heart of ZXC is a heavily optimized LZ77 engine that adapts its behavior based on the requested compression level: * **Hash Chain & Collision Resolution**: Uses a fast hash table with chaining to find matches in the history window (configurable sliding window, power-of-2 from 4 KB to 2 MB, default 256 KB). * **Lazy Matching**: Implements a "lookahead" strategy to find better matches at the cost of slight encoding speed, significantly improving decompression density. ### 4.2 Specialized SIMD Acceleration & Hardware Hashing ZXC leverages modern instruction sets to maximize throughput on both ARM and x86 architectures. * **ARM NEON Optimization**: Extensive usage of vld1q_u8 (vector load) and vceqq_u8 (parallel comparison) allows scanning data at wire speed, while vminvq_u8 provides fast rejection of non-matches. * **x86 Vectorization**: Maintains high performance on Intel/AMD platforms via dedicated AVX2 and AVX512 paths (falling back to SSE4.1 on older hardware), ensuring parity with ARM throughput. * **High-Speed Integrity**: Block validation relies on **rapidhash**, a modern non-cryptographic hash algorithm that fully exploits hardware acceleration to verify data integrity without bottlenecking the decompression pipeline. ### 4.3 Entropy Coding & Bitpacking * **RLE (Run-Length Encoding)**: Automatically detects runs of identical bytes. * **Prefix Varint Encoding**: Variable-length integer encoding (similar to LEB128 but prefix-based) for overflow values. * **Bit-Packing**: Compressed sequences are packed into dedicated streams using minimal bit widths. #### Prefix Varint Format ZXC uses a **Prefix Varint** encoding for overflow values. Unlike standard VByte (which uses a continuation bit in every byte), Prefix Varint encodes the total length of the integer in the **unary prefix of the first byte**. This allows the decoder to determine the sequence length immediately, enabling branchless or highly predictable decoding without serial dependencies. **Encoding Scheme:** | Prefix (Binary) | Total Bytes | Data Bits (1st Byte) | Total Data Bits | Range (Value < X) | |-----------------|-------------|----------------------|-----------------|-------------------| | `0xxxxxxx` | 1 | 7 | 7 | 128 | | `10xxxxxx` | 2 | 6 | 14 (6+8) | 16,384 | | `110xxxxx` | 3 | 5 | 21 (5+8+8) | 2,097,152 | | `1110xxxx` | 4 | 4 | 28 (4+8+8+8) | 268,435,456 | | `11110xxx` | 5 | 3 | 35 (3+8+8+8+8) | 34,359,738,368 | **Example**: Encoding value `300` (binary: `100101100`): ```text Value 300 > 127 and < 16383 -> Uses 2-byte format (Prefix '10'). Step 1: Low 6 bits 300 & 0x3F = 44 (0x2C, binary 101100) Byte 1 = Prefix '10' | 101100 = 10101100 (0xAC) Step 2: Remaining high bits 300 >> 6 = 4 (0x04, binary 00000100) Byte 2 = 0x04 Result: 0xAC 0x04 Decoding Verification: Byte 1 (0xAC) & 0x3F = 44 Byte 2 (0x04) << 6 = 256 Total = 256 + 44 = 300 ``` ## 5. File Format Specification The ZXC file format is block-based, robust, and designed for parallel processing. ### 5.1 Global Structure (File Header) The file begins with a **16-byte** header that identifies the format and specifies decompression parameters. **FILE Header (16 bytes):** ``` Offset: 0 4 5 6 7 14 16 +---------------+-------+-------+-------+-----------------------+-------+ | Magic Word | Ver | Chunk | Flags | Reserved | CRC | | (4 bytes) | (1B) | (1B) | (1B) | (7 bytes, must be 0) | (2B) | +---------------+-------+-------+-------+-----------------------+-------+ ``` * **Magic Word (4 bytes)**: `0x9 0xCB 0x02E 0xF5`. * **Version (1 byte)**: Current version is `5`. * **Chunk Size Code (1 byte)**: Defines the processing block size using **exponent encoding**: - If the value is in `[12, 21]`: block size = `2^value` bytes (4 KB to 2 MB). - `12` = 4 KB, `13` = 8 KB, `14` = 16 KB, `15` = 32 KB, `16` = 64 KB, `17` = 128 KB, `18` = 256 KB (default), `19` = 512 KB, `20` = 1 MB, `21` = 2 MB. - Legacy value `64` is accepted for backward compatibility (maps to 256 KB). - Block sizes must be powers of 2. * **Flags (1 byte)**: Global configuration flags. - **Bit 7 (MSB)**: `HAS_CHECKSUM`. If `1`, checksums are enabled for the stream. Every block will carry a trailing 4-byte checksum, and the footer will contain a global checksum. If `0`, no checksums are present. - **Bits 4-6**: Reserved. - **Bits 0-3**: Checksum Algorithm ID (e.g., `0` = RapidHash). * **Reserved (7 bytes)**: Reserved for future use (must be 0). * **CRC (2 bytes)**: 16-bit Header Checksum. Calculated on the 16-byte header (with CRC bytes set to 0) using `zxc_hash16`. ### 5.2 Block Header Structure Each data block consists of an **8-byte** generic header that precedes the specific payload. This header allows the decoder to navigate the stream and identify the processing method required for the next chunk of data. **BLOCK Header (8 bytes):** ``` Offset: 0 1 2 3 7 8 +-------+-------+-------+-----------------------+-------+ | Type | Flags | Rsrvd | Comp Size | CRC | | (1B) | (1B) | (1B) | (4 bytes) | (1B) | +-------+-------+-------+-----------------------+-------+ Block Layout: [ Header (8B) ] + [ Compressed Payload (Comp Size bytes) ] + [ Optional Checksum (4B) ] ``` **Note**: The Checksum (if enabled in File Header) is **4 bytes** (32-bit), is always located **at the end** of the compressed data, and is calculated **on the compressed payload**. * **Type**: Block encoding type (0=RAW, 1=GLO, 2=NUM, 3=GHI, 255=EOF). * **Flags**: Not used for now. * **Rsrvd**: Reserved for future use (must be 0). * **Comp Size**: Compressed payload size (excluding header and optional checksum). * **CRC**: 1-byte Header Checksum (located at the end of the header). Calculated on the 8-byte header (with CRC byte set to 0) using `zxc_hash8`. > **Note**: The decompressed size is not stored in the block header. It is derived from internal Section Descriptors within the compressed payload (for GLO/GHI blocks), from the NUM header (for NUM blocks), or equals `Comp Size` (for RAW blocks). > **Note**: While the format is designed for threaded execution, a single-threaded API is also available for constrained environments or simple integration cases. ### 5.3 Specific Header: NUM (Numeric) (Present immediately after the Block Header) **NUM Header (16 bytes):** ``` Offset: 0 8 10 16 +-------------------------------+-------+-------------------------+ | N Values | Frame | Reserved | | (8 bytes) | (2B) | (6 bytes) | +-------------------------------+-------+-------------------------+ ``` * **N Values**: Total count of integers encoded in the block. * **Frame**: Processing window size (currently always 128). * **Reserved**: Padding for alignment. ### 5.4 Specific Header: GLO (Generic Low) (Present immediately after the Block Header) **GLO Header (16 bytes):** ``` Offset: 0 4 8 9 10 11 12 16 +---------------+---------------+---+---+---+---+---------------+ | N Sequences | N Literals |Lit|LL |ML |Off| Reserved | | (4 bytes) | (4 bytes) |Enc|Enc|Enc|Enc| (4 bytes) | +---------------+---------------+---+---+---+---+---------------+ ``` * **N Sequences**: Total count of LZ sequences in the block. * **N Literals**: Total count of literal bytes. * **Encoding Types** - `Lit Enc`: Literal stream encoding (0=RAW, 1=RLE). **Currently used.** - `LL Enc`: Literal lengths encoding. **Reserved for future use** (lengths are packed in tokens). - `ML Enc`: Match lengths encoding. **Reserved for future use** (lengths are packed in tokens). - `Off Enc`: Offset encoding mode. **Currently used** - `0` = 16-bit offsets (2 bytes each, max distance 65535) - `1` = 8-bit offsets (1 byte each, max distance 255) * **Reserved**: Padding for alignment. **Section Descriptors (4 × 8 bytes = 32 bytes total):** Each descriptor stores sizes as a packed 64-bit value: ``` Single Descriptor (8 bytes): +-----------------------------------+-----------------------------------+ | Compressed Size (4 bytes) | Raw Size (4 bytes) | | (low 32 bits) | (high 32 bits) | +-----------------------------------+-----------------------------------+ Full Layout (32 bytes): Offset: 0 8 16 24 32 +---------------+---------------+---------------+---------------+ | Literals Desc | Tokens Desc | Offsets Desc | Extras Desc | | (8 bytes) | (8 bytes) | (8 bytes) | (8 bytes) | +---------------+---------------+---------------+---------------+ ``` **Section Contents:** | # | Section | Description | |---|-------------|-------------------------------------------------------| | 0 | **Literals**| Raw bytes to copy, or RLE-compressed if `enc_lit=1` | | 1 | **Tokens** | Packed bytes: `(LiteralLen << 4) \| MatchLen` | | 2 | **Offsets** | Match distances: 8-bit if `enc_off=1`, else 16-bit LE | | 3 | **Extras** | Prefix Varint overflow values when LitLen or MatchLen ≥ 15 | **Data Flow Example:** ``` GLO Block Data Layout: +------------------------------------------------------------------------+ | Literals Stream | Tokens Stream | Offsets Stream | Extras Stream | | (desc[0] bytes) | (desc[1] bytes)| (desc[2] bytes)| (desc[3] bytes) | +------------------------------------------------------------------------+ ↓ ↓ ↓ ↓ Raw bytes Token parsing Match lookup Length overflow ``` **Why Comp Size and Raw Size?** Each descriptor stores both a compressed and raw size to support secondary encoding of streams: | Section | Comp Size | Raw Size | Different? | |-------------|----------------------|---------------------|----------------------| | **Literals**| RLE size (if used) | Original byte count | Yes, if RLE enabled | | **Tokens** | Stream size | Stream size | No | | **Offsets** | N×1 or N×2 bytes | N×1 or N×2 bytes | No (size depends on `enc_off`) | | **Extras** | Prefix Varint stream size | Prefix Varint stream size | No | Currently, the **Literals** section uses different sizes when RLE compression is applied (`enc_lit=1`). The **Offsets** section size depends on `enc_off`: N sequences × 1 byte (if `enc_off=1`) or N sequences × 2 bytes (if `enc_off=0`). > **Design Note**: This format is designed for future extensibility. The dual-size architecture allows adding entropy coding (FSE/ANS) or bitpacking to any stream without breaking backward compatibility. ### 5.5 Specific Header: GHI (Generic High) (Present immediately after the Block Header) The **GHI** (Generic High-Velocity) block format is optimized for maximum decompression speed. It uses a **packed 32-bit sequence** format that allows 4-byte aligned reads, reducing memory access latency and enabling efficient SIMD processing. **GHI Header (16 bytes):** ``` Offset: 0 4 8 9 10 11 12 16 +---------------+---------------+---+---+---+---+---------------+ | N Sequences | N Literals |Lit|LL |ML |Off| Reserved | | (4 bytes) | (4 bytes) |Enc|Enc|Enc|Enc| (4 bytes) | +---------------+---------------+---+---+---+---+---------------+ ``` * **N Sequences**: Total count of LZ sequences in the block. * **N Literals**: Total count of literal bytes. * **Encoding Types** - `Lit Enc`: Literal stream encoding (0=RAW). - `LL Enc`: Reserved for future use. - `ML Enc`: Reserved for future use. - `Off Enc`: Offset encoding mode: - `0` = 16-bit offsets (max distance 65535) - `1` = 8-bit offsets (max distance 255, enables smaller sequence packing) * **Reserved**: Padding for alignment. **Section Descriptors (3 × 8 bytes = 24 bytes total):** ``` Full Layout (24 bytes): Offset: 0 8 16 24 +---------------+---------------+---------------+ | Literals Desc | Sequences Desc| Extras Desc | | (8 bytes) | (8 bytes) | (8 bytes) | +---------------+---------------+---------------+ ``` **Section Contents:** | # | Section | Description | |---|---------------|-------------------------------------------------------| | 0 | **Literals** | Raw bytes to copy | | 1 | **Sequences** | Packed 32-bit sequences (see format below) | | 2 | **Extras** | Prefix Varint overflow values when LitLen or MatchLen ≥ 255 | **Packed Sequence Format (32 bits):** Unlike GLO which uses separate token and offset streams, GHI packs all sequence data into a single 32-bit word for cache-friendly sequential access: ``` 32-bit Sequence Word (Little Endian): +--------+--------+------------------+ | LL | ML | Offset | | 8 bits | 8 bits | 16 bits | +--------+--------+------------------+ [31:24] [23:16] [15:0] Byte Layout in Memory: Offset: 0 1 2 3 +--------+--------+--------+--------+ | Off Lo | Off Hi | ML | LL | +--------+--------+--------+--------+ ``` * **LL (Literal Length)**: 8 bits (0-254, value 255 triggers Prefix Varint overflow) * **ML (Match Length - 5)**: 8 bits (actual length = ML + 5, range 5-259, value 255 triggers Prefix Varint overflow) * **Offset**: 16 bits (match distance, 1-65535) **Data Flow Example:** ``` GHI Block Data Layout: +------------------------------------------------------------+ | Literals Stream | Sequences Stream | Extras Stream | | (desc[0] bytes) | (desc[1] bytes = N×4) | (desc[2] bytes) | +------------------------------------------------------------+ ↓ ↓ ↓ Raw bytes 32-bit seq read Length overflow ``` **Key Differences: GLO vs GHI** | Feature | GLO (Global) | GHI (High-Velocity) | |--------------------|---------------------------------|----------------------------------| | **Sections** | 4 (Lit, Tokens, Offsets, Extras)| 3 (Lit, Sequences, Extras) | | **Sequence Format**| 1-byte token + separate offset | Packed 32-bit word | | **LL/ML Bits** | 4 bits each (overflow at 15) | 8 bits each (overflow at 255) | | **Memory Access** | Multiple stream pointers | Single aligned 4-byte reads | | **Decoder Speed** | Fast | Fastest (optimized for ARM/x86) | | **RLE Support** | Yes (literals) | No | | **Best For** | General data, good compression | Maximum decode throughput | > **Design Rationale**: The 32-bit packed format eliminates pointer chasing between token and offset streams. By reading a single aligned word per sequence, the decoder achieves better cache utilization and enables aggressive loop unrolling (4x) for maximum throughput on modern CPUs. ### 5.6 Specific Header: EOF (End of File) (Block Type 255) The **EOF** block marks the end of the ZXC stream. It ensures that the decompressor knows exactly when to stop processing, allowing for robust stream termination even when file size metadata is unavailable or when concatenating streams. * **Structure**: Standard 8-byte Block Header. * **Flags**: * **Bit 7 (0x80)**: `has_checksum`. If set, implies the **Global Stream Checksum** in the footer is valid and should be verified. * **Comp Size**: Unlike other blocks, these **MUST be set to 0**. The decoder enforces strict validation (`Type == EOF` AND `Comp Size == 0`) to prevent processing of malformed termination blocks. * **CRC**: 1-byte Header Checksum (located at the end of the header). Calculated on the 8-byte header (with CRC byte set to 0) using `zxc_hash8`. ### 5.7 File Footer (Present immediately after the EOF Block) A mandatory **12-byte footer** closes the stream, providing total source size information and the global checksum. **Footer Structure (12 bytes):** ``` Offset: 0 8 12 +-------------------------------+---------------+ | Original Source Size | Global Hash | | (8 bytes) | (4 bytes) | +-------------------------------+---------------+ ``` * **Original Source Size** (8 bytes): Total size of the uncompressed data. * **Global Hash** (4 bytes): The **Global Stream Checksum**. Valid only if the EOF block has the `has_checksum` flag set (or the decoder context requires it). * **Algorithm**: `Rotation + XOR`. * For each block with a checksum: `global_hash = (global_hash << 1) | (global_hash >> 31); global_hash ^= block_hash;` ### 5.8 Block Encoding & Processing Algorithms The efficiency of ZXC relies on specialized algorithmic pipelines for each block type. #### Type 1: GLO (Global) This format is used for standard data. It employs a **multi-stage encoding pipeline**: **Encoding Process**: 1. **LZ77 Parsing**: The encoder iterates through the input using a rolling hash to detect matches. * *Hash Chain*: Collisions are resolved via a chain table to find optimal matches in dense data. * *Lazy Matching*: If a match is found, the encoder checks the next position. If a better match starts there, the current byte is emitted as a literal (deferred matching). 2. **Tokenization**: Matches are split into three components: * *Literal Length*: Number of raw bytes before the match. * *Match Length*: Duration of the repeated pattern. * *Offset*: Distance back to the pattern start. 3. **Stream Separation**: These components are routed to separate buffers: * *Literals Buffer*: Raw bytes. * *Tokens Buffer*: Packed `(LitLen << 4) | MatchLen`. * *Offsets Buffer*: Variable-width distances (8-bit or 16-bit, see below). * *Extras Buffer*: Overflow values for lengths >= 15 (Prefix Varint encoded). * *Offset Mode Selection*: The encoder tracks the maximum offset across all sequences. If all offsets are ≤ 255, the 8-bit mode (`enc_off=1`) is selected, saving 1 byte per sequence compared to 16-bit mode. 4. **RLE Pass**: The literals buffer is scanned for run-length encoding opportunities (runs of identical bytes). If beneficial (>10% gain), it is compressed in place. 5. **Final Serialization**: All buffers are concatenated into the payload, preceded by section descriptors. **Decoding Process**: 1. **Deserizalization**: The decoder reads the section descriptors to obtain pointers to the start of each stream (Literals, Tokens, Offsets). 2. **Vertical Execution**: The main loop reads from all three streams simultaneously. 3. **Wild Copy**: * *Literals*: Copied using unaligned 16-byte SIMD loads/stores (`vld1/vst1` on ARM). * *Matches*: Copied using 16-byte stores. Overlapping matches (e.g., repeating pattern "ABC" for 100 bytes) are handled naturally by the CPU's store forwarding or by specific overlapped-copy primitives. * **Safety**: A "Safe Zone" at the end of the buffer forces a switch to a cautious byte-by-byte loop, allowing the main loop to run without bounds checks. #### Type 3: GHI (High-Velocity) This format prioritizes decompression throughput over compression ratio. It uses a **unified sequence stream**: **Encoding Process**: 1. **LZ77 Parsing**: Same as GLO, with aggressive lazy matching and step skipping for optimal matches. 2. **Sequence Packing**: Each match is packed into a 32-bit word: * Bits [31:24]: Literal Length (8 bits) * Bits [23:16]: Match Length - 5 (8 bits) * Bits [15:0]: Offset (16 bits) 3. **Stream Assembly**: Only three streams are generated: * *Literals Buffer*: Raw bytes (no RLE). * *Sequences Buffer*: Packed 32-bit words (4 bytes each). * *Extras Buffer*: Prefix Varint overflow values for lengths >= 255. 4. **Final Serialization**: Streams are concatenated with 3 section descriptors. **Decoding Process**: 1. **Single-Read Loop**: The decoder reads one 32-bit word per sequence, extracting LL, ML, and offset in a single operation. 2. **4x Unrolled Fast Path**: When sufficient buffer margin exists, the decoder processes 4 sequences per iteration: * Pre-reads 4 sequences into registers * Copies literals and matches with 32-byte SIMD operations * Minimal branching for maximum instruction-level parallelism 3. **Offset Validation Threshold**: For the first 256 (8-bit mode) or 65536 (16-bit mode) bytes, offsets are validated against written bytes. After this threshold, all offsets are guaranteed valid. 4. **Wild Copy**: Same 32-byte SIMD copies as GLO, with special handling for overlapping matches (offset < 32). #### Type 2: NUM (Numeric) Triggered when data is detected as a dense array of 32-bit integers. **Encoding Process**: 1. **Vectorized Delta**: Computes `delta[i] = val[i] - val[i-1]` using SIMD integers (AVX2/NEON). 2. **ZigZag Transform**: Maps signed deltas to unsigned space: `(d << 1) ^ (d >> 31)`. 3. **Bit Analysis**: Determines the maximum number of bits `B` needed to represent the deltas in a 128-value frame. 4. **Bit-Packing**: Packs 128 integers into `128 * B` bits. **Decoding Process**: 1. **Bit-Unpacking**: Unpacks bitstreams back into integers. 2. **ZigZag Decode**: Reverses the mapping. 3. **Integration**: Computes the prefix sum (cumulative addition) to restore original values. *Note: ZXC utilizes a 4x unrolled loop here to pipeline the dependency chain.* ### 5.9 Data Integrity Every compressed block can optionally be protected by a **32-bit checksum** to ensure data reliability. #### Post-Compression Verification Unlike traditional codecs that verify the integrity of the original uncompressed data, ZXC calculates checksums on the **compressed** payload. * **Zero-Overhead Decompression**: Verifying uncompressed data requires computing a hash over the output *after* decompression, contending for cache and CPU resources with the decompression logic itself. By checksumming the compressed stream, verification happens *during* the read phase, before the data even enters the decoder. * **Early Failure Detection**: Corruption is detected before attempting to decompress, preventing potential crashes or buffer overruns in the decoder caused by malformed data. * **Reduced Memory Bandwidth**: The checksum is computed over a much smaller dataset (the compressed block), saving significant memory bandwidth. #### Multi-Algorithm Support ZXC supports multiple integrity verification algorithms (though currently standardized on rapidhash). * **Identified Algorithm (0x00: rapidhash)**: The default algorithm. The 64-bit rapidhash result is folded (XORed) into a 32-bit value to minimize storage overhead while maintaining strong collision resistance for block-level integrity. * **Performance First**: By using a modern non-cryptographic hash, ZXC ensures that integrity checks do not bottleneck decompression throughput. #### Credit The default `rapidhash` algorithm is based on wyhash and was developed by Nicolas De Carli. It is designed to fully exploit hardware performance while maintaining top-tier mathematical distribution qualities. ### 5.10 Seekable Archives (Random Access) ZXC supports **O(1)** random-access decompression without decoding the entire stream. This is achieved by appending an optional **Seek Table** (a `SEK` block) at the end of the archive, immediately before the file footer. * **Structure**: The seek table contains an array of 4-byte entries (compressed block size, LE uint32) for every block in the archive. * **Performance**: Reading backward from the file footer instantly locates the seek table. Since blocks have a fixed power-of-2 size, the target block is found by a single division (`block_index = offset / block_size`), with no binary search required. * **Use Cases**: This feature transforms ZXC from a sequential stream into a random-access volume format. ## 6. System Architecture (Threading) ZXC leverages a threaded **Producer-Consumer** model to saturate modern multi-core CPUs. ### 6.1 Asynchronous Compression Pipeline 1. **Block Splitting (Main Thread)**: The input file is read and sliced into fixed-size chunks (configurable, default 256 KB, power of 2 from 4 KB to 2 MB). 2. **Ring Buffer Submission**: Chunks are placed into a lock-free ring buffer. 3. **Parallel Compression (Worker Threads)**: * Workers pull chunks from the queue. * Each worker compresses its chunk independently in its own context (`zxc_cctx_t`). * Output is written to a thread-local buffer. 4. **Reordering & Write (Writer Thread)**: The writer thread ensures chunks are written to disk in the correct original order, regardless of which worker finished first. ### 6.2 Asynchronous Decompression Pipeline 1. **Header Parsing (Main Thread)**: The main thread scans block headers to identify boundaries and payload sizes. 2. **Dispatch**: Compressed payloads are fed into the worker job queue. 3. **Parallel Decoding (Worker Threads)**: * Workers decode chunks into pre-allocated output buffers. * **Fast Path**: If the output buffer has sufficient margin, the decoder uses "wild copies" (16-byte SIMD stores) to bypass bounds checking for maximal speed. 4. **Serialization**: Decompressed blocks are committed to the output stream sequentially. ## 7. Performance Analysis (Benchmarks) **Methodology:** Benchmarks were conducted using `lzbench` (by inikep) with the **default block size of 256 KB**, checksums disabled, single-threaded execution, on the standard Silesia Corpus ([silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), 202 MB). * **Target 1 (Client):** Apple M2 / macOS 26 (Clang 21) * **Target 2 (Cloud):** Google Axion / Linux (GCC 14) * **Target 3 (Build):** AMD EPYC 9B45 / Linux (GCC 14) **Figure A**: Decompression Throughput & Storage Ratio (Normalized to LZ4) ![Benchmark Graph ARM64](./images/benchmark_arm64_0.10.0.webp) ### 7.1 Client ARM64 Summary (Apple Silicon M2) | Compressor | Decompression Speed (Ratio vs LZ4) | Compressed Size (Index LZ4=100) (Lower is Better) | | :--- | :--- | :--- | | **zxc 0.10.0 -1** | **2.55x** | **129.33** | | **zxc 0.10.0 -2** | **2.10x** | **113.46** | | **zxc 0.10.0 -3** | **1.46x** | **97.38** | | **zxc 0.10.0 -4** | **1.39x** | **90.63** | | **zxc 0.10.0 -5** | **1.29x** | **85.44** | | lz4 1.10.0 --fast -17 | 1.18x | 130.58 | | lz4 1.10.0 (Ref) | 1.00x | 100.00 | | lz4hc 1.10.0 -9 | 0.95x | 77.20 | | lzav 5.7 -1 | 0.81x | 83.91 | | snappy 1.2.2 | 0.68x | 100.53 | | zstd 1.5.7 --fast --1 | 0.53x | 86.16 | | zstd 1.5.7 -1 | 0.37x | 72.55 | | zlib 1.3.1 -1 | 0.08x | 76.58 | **Decompression Efficiency (Cycles per Byte @ 3.5 GHz)** | Compressor. | Cycles/Byte | Performance vs memcpy (*) | | ----------------------- | ----------- | --------------------- | | memcpy | 0.066 | 1.00x (baseline) | | **zxc 0.10.0 -1** | **0.287** | **4.3x** | | **zxc 0.10.0 -2** | **0.348** | **5.3x** | | **zxc 0.10.0 -3** | **0.499** | **7.5x** | | **zxc 0.10.0 -4** | **0.527** | **8.0x** | | **zxc 0.10.0 -5** | **0.566** | **8.5x** | | lz4 1.10.0 | 0.731 | 11.0x | | lz4 1.10.0 --fast -17 | 0.621 | 9.4x | | lz4hc 1.10.0 -9 | 0.772 | 11.6x | | lzav 5.7 -1 | 0.907 | 13.7x | | zstd 1.5.7 -1 | 1.971 | 29.7x | | zstd 1.5.7 --fast --1 | 1.385 | 20.9x | | snappy 1.2.2 | 1.074 | 16.2x | | zlib 1.3.1 -1 | 8.816 | 133x | *Lower is better. Calculated using Apple M2 Performance Core frequency (3.5 GHz). Formula: `Cycles/Byte = 3500 / Decompression Speed (MB/s)`.* ### 7.2 Cloud Server Summary (ARM64 / Google Axion Neoverse-V2) | Compressor | Decompression Speed (Ratio vs LZ4) | Compressed Size (Index LZ4=100) (Lower is Better) | | :--- | :--- | :--- | | **zxc 0.10.0 -1** | **2.09x** | **129.33** | | **zxc 0.10.0 -2** | **1.75x** | **113.46** | | **zxc 0.10.0 -3** | **1.24x** | **97.38** | | **zxc 0.10.0 -4** | **1.18x** | **90.63** | | **zxc 0.10.0 -5** | **1.10x** | **85.44** | | lz4 1.10.0 --fast -17 | 1.16x | 130.58 | | lz4 1.10.0 (Ref) | 1.00x | 100.00 | | lz4hc 1.10.0 -9 | 0.90x | 77.20 | | lzav 5.7 -1 | 0.65x | 83.91 | | snappy 1.2.2 | 0.54x | 100.53 | | zstd 1.5.7 --fast --1 | 0.54x | 86.16 | | zstd 1.5.7 -1 | 0.39x | 72.55 | | zlib 1.3.1 -1 | 0.09x | 76.58 | **Decompression Efficiency (Cycles per Byte @ 2.6 GHz)** | Compressor. | Cycles/Byte | Performance vs memcpy (*) | | ----------------------- | ----------- | --------------------- | | memcpy | 0.109 | 1.00x (baseline) | | **zxc 0.10.0 -1** | **0.291** | **2.7x** | | **zxc 0.10.0 -2** | **0.348** | **3.2x** | | **zxc 0.10.0 -3** | **0.491** | **4.5x** | | **zxc 0.10.0 -4** | **0.516** | **4.8x** | | **zxc 0.10.0 -5** | **0.556** | **5.1x** | | lz4 1.10.0 | 0.610 | 5.6x | | lz4 1.10.0 --fast -17 | 0.525 | 4.8x | | lz4hc 1.10.0 -9 | 0.675 | 6.2x | | lzav 5.7 -1 | 0.937 | 8.6x | | zstd 1.5.7 -1 | 1.581 | 14.6x | | zstd 1.5.7 --fast --1 | 1.134 | 10.4x | | snappy 1.2.2 | 1.131 | 10.4x | | zlib 1.3.1 -1 | 6.667 | 61.4x | *Lower is better. Calculated using Neoverse-V2 base frequency (2.6 GHz). Formula: `Cycles/Byte = 2600 / Decompression Speed (MB/s)`.* ### 7.3 Build Server Summary (x86_64 / AMD EPYC 9B45) | Compressor | Decompression Speed (Ratio vs LZ4) | Compressed Size (Index LZ4=100) (Lower is Better) | | :--- | :--- | :--- | | **zxc 0.10.0 -1** | **2.18x** | **129.83** | | **zxc 0.10.0 -2** | **1.89x** | **112.87** | | **zxc 0.10.0 -3** | **1.21x** | **96.24** | | **zxc 0.10.0 -4** | **1.15x** | **89.71** | | **zxc 0.10.0 -5** | **1.08x** | **84.60** | | lz4 1.10.0 --fast -17 | 1.05x | 130.58 | | lz4 1.10.0 (Ref) | 1.00x | 100.00 | | lz4hc 1.10.0 -9 | 0.96x | 77.20 | | lzav 5.7 -1 | 0.71x | 83.91 | | snappy 1.2.2 | 0.42x | 100.63 | | zstd 1.5.7 --fast --1 | 0.48x | 86.16 | | zstd 1.5.7 -1 | 0.38x | 72.55 | | zlib 1.3.1 -1 | 0.08x | 76.58 | **Decompression Efficiency (Cycles per Byte @ 2.1 GHz)** | Compressor. | Cycles/Byte | Performance vs memcpy (*) | | ----------------------- | ----------- | --------------------- | | memcpy | 0.088 | 1.00x (baseline) | | **zxc 0.10.0 -1** | **0.200** | **2.3x** | | **zxc 0.10.0 -2** | **0.230** | **2.6x** | | **zxc 0.10.0 -3** | **0.361** | **4.1x** | | **zxc 0.10.0 -4** | **0.380** | **4.3x** | | **zxc 0.10.0 -5** | **0.403** | **4.6x** | | lz4 1.10.0 | 0.435 | 5.0x | | lz4 1.10.0 --fast -17 | 0.413 | 4.7x | | lz4hc 1.10.0 -9 | 0.454 | 5.2x | | lzav 5.7 -1 | 0.614 | 7.0x | | zstd 1.5.7 -1 | 1.158 | 13.2x | | zstd 1.5.7 --fast --1 | 0.899 | 10.3x | | snappy 1.2.2 | 1.048 | 12.0x | | zlib 1.3.1 -1 | 5.585 | 63.8x | *Lower is better. Calculated using AMD EPYC 9B45 base frequency (2.1 GHz). Formula: `Cycles/Byte = 2100 / Decompression Speed (MB/s)`.* ### 7.4 Benchmarks Results **Figure B**: Decompression Efficiency : Cycles Per Byte Comparaison ![Benchmark Cycles Per Byte](./images/benchmark_decompression_cycles_0.10.0.webp) #### 7.4.1 ARM64 Architecture (Apple Silicon M2) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with Clang 21.0.0 using *MOREFLAGS="-march=native"* on macOS Tahoe 26.4 (Build 25E246). The reference hardware is an Apple M2 processor (ARM64). **All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus.** | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 52855 MB/s | 52786 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 904 MB/s | **12195 MB/s** | 130468706 | **61.56** | 1 files| | **zxc 0.10.0 -2** | 600 MB/s | **10044 MB/s** | 114455432 | **54.00** | 1 files| | **zxc 0.10.0 -3** | 257 MB/s | **7008 MB/s** | 98233034 | **46.35** | 1 files| | **zxc 0.10.0 -4** | 176 MB/s | **6636 MB/s** | 91429653 | **43.14** | 1 files| | **zxc 0.10.0 -5** | 104 MB/s | **6181 MB/s** | 86196446 | **40.67** | 1 files| | lz4 1.10.0 | 792 MB/s | 4787 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1316 MB/s | 5633 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 46.3 MB/s | 4531 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 625 MB/s | 3859 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 857 MB/s | 3258 MB/s | 101415443 | 47.85 | 1 files| | zstd 1.5.7 --fast --1 | 704 MB/s | 2527 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 625 MB/s | 1776 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 145 MB/s | 397 MB/s | 77259029 | 36.45 | 1 files| #### 7.4.2 ARM64 Architecture (Google Axion Neoverse-V2) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with GCC 14.3.0 using *MOREFLAGS="-march=native"* on Linux 64-bits Debian GNU/Linux 12 (bookworm). The reference hardware is a Google Neoverse-V2 processor (ARM64). **All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus.** | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 23971 MB/s | 23953 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 810 MB/s | **8924 MB/s** | 130468706 | **61.56** | 1 files| | **zxc 0.10.0 -2** | 523 MB/s | **7461 MB/s** | 114455432 | **54.00** | 1 files| | **zxc 0.10.0 -3** | 246 MB/s | **5297 MB/s** | 98233034 | **46.35** | 1 files| | **zxc 0.10.0 -4** | 170 MB/s | **5038 MB/s** | 91429653 | **43.14** | 1 files| | **zxc 0.10.0 -5** | 100 MB/s | **4676 MB/s** | 86196446 | **40.67** | 1 files| | lz4 1.10.0 | 731 MB/s | 4262 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1278 MB/s | 4950 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 43.3 MB/s | 3850 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 554 MB/s | 2776 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 757 MB/s | 2298 MB/s | 101415443 | 47.85 | 1 files| | zstd 1.5.7 --fast --1 | 606 MB/s | 2293 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 524 MB/s | 1645 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 57.2 MB/s | 390 MB/s | 77259029 | 36.45 | 1 files| #### 7.4.3 x86_64 Architecture (AMD EPYC 9B45) Benchmarks were conducted using lzbench 2.2.1 (from @inikep), compiled with GCC 14.3.0 using *MOREFLAGS="-march=native"* on Linux 64-bits Ubuntu 24.04. The reference hardware is an AMD EPYC 9B45 processor (x86_64). **All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus.** | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 23564 MB/s | 23993 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 810 MB/s | **10526 MB/s** | 130969994 | **61.79** | 1 files| | **zxc 0.10.0 -2** | 511 MB/s | **9120 MB/s** | 113859997 | **53.72** | 1 files| | **zxc 0.10.0 -3** | 205 MB/s | **5815 MB/s** | 97083951 | **45.81** | 1 files| | **zxc 0.10.0 -4** | 147 MB/s | **5533 MB/s** | 90503394 | **42.70** | 1 files| | **zxc 0.10.0 -5** | 77.7 MB/s | **5217 MB/s** | 85344194 | **40.27** | 1 files| | lz4 1.10.0 | 745 MB/s | 4825 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1249 MB/s | 5080 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 42.8 MB/s | 4623 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 581 MB/s | 3422 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 721 MB/s | 2004 MB/s | 101512076 | 47.89 | 1 files| | zstd 1.5.7 --fast --1 | 627 MB/s | 2335 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 575 MB/s | 1814 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 129 MB/s | 376 MB/s | 77259029 | 36.45 | 1 files| ### 7.5 Block Size Impact: 256 KB vs 512 KB All benchmarks in sections 7.1–7.4 use the **default block size of 256 KB**. This section evaluates the performance impact of increasing the block size to **512 KB**, measured on three architectures using lzbench 2.2.1 under identical conditions. **Why block size matters:** Each block starts with a cold hash table, so the LZ77 match-finder has no history and produces more literals until the table warms up. Doubling the block size halves the number of cold-start penalties (~809 blocks to ~405 blocks on the Silesia corpus), improving both compression ratio and decompression throughput. **All performance metrics reflect single-threaded execution on the standard Silesia Corpus and the benchmark made use of [silesia.tar](https://github.com/DataCompression/corpus-collection/tree/main/Silesia-Corpus), which contains tarred files from the Silesia compression corpus.** #### 7.5.1 Apple Silicon M2 (ARM64) Benchmarked using lzbench 2.2.1, compiled with Clang 21.0.0 using *MOREFLAGS="-march=native"* on macOS Tahoe 26.4 (Build 25E246). Apple M2 processor (ARM64, P-core frequency 3.5 GHz). **Block Size: 256 KB (default)** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 904 MB/s | **12195 MB/s** | 130468706 | **61.56** | | **zxc 0.10.0 -2** | 600 MB/s | **10044 MB/s** | 114455432 | **54.00** | | **zxc 0.10.0 -3** | 257 MB/s | **7008 MB/s** | 98233034 | **46.35** | | **zxc 0.10.0 -4** | 176 MB/s | **6636 MB/s** | 91429653 | **43.14** | | **zxc 0.10.0 -5** | 104 MB/s | **6181 MB/s** | 86196446 | **40.67** | **Block Size: 512 KB** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 893 MB/s | **12595 MB/s** | 130356444 | **61.50** | | **zxc 0.10.0 -2** | 593 MB/s | **10414 MB/s** | 113634139 | **53.61** | | **zxc 0.10.0 -3** | 255 MB/s | **7065 MB/s** | 97051816 | **45.79** | | **zxc 0.10.0 -4** | 176 MB/s | **6712 MB/s** | 90393215 | **42.65** | | **zxc 0.10.0 -5** | 104 MB/s | **6282 MB/s** | 85341643 | **40.27** | **Decompression Efficiency (Cycles per Byte @ 3.5 GHz)** | Compressor | 256 KB (c/B) | 512 KB (c/B) | Δ | | ----------------------- | ------------ | ------------ | ------- | | **zxc 0.10.0 -1** | **0.287** | **0.278** | −3.2% | | **zxc 0.10.0 -2** | **0.348** | **0.336** | −3.6% | | **zxc 0.10.0 -3** | **0.499** | **0.495** | −0.8% | | **zxc 0.10.0 -4** | **0.527** | **0.521** | −1.1% | | **zxc 0.10.0 -5** | **0.566** | **0.557** | −1.6% | *Lower is better. Consistent improvement across all levels with 512 KB blocks. Formula: `Cycles/Byte = 3500 / Decompression Speed (MB/s)`.* #### 7.5.2 Google Axion (ARM64 Neoverse-V2) Benchmarked using lzbench 2.2.1, compiled with GCC 14.3.0 using *MOREFLAGS="-march=native"* on Linux Debian 12. Google Neoverse-V2 processor (ARM64, 2.6 GHz). **Block Size: 256 KB (default)** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 810 MB/s | **8924 MB/s** | 130468706 | **61.56** | | **zxc 0.10.0 -2** | 523 MB/s | **7461 MB/s** | 114455432 | **54.00** | | **zxc 0.10.0 -3** | 246 MB/s | **5297 MB/s** | 98233034 | **46.35** | | **zxc 0.10.0 -4** | 170 MB/s | **5038 MB/s** | 91429653 | **43.14** | | **zxc 0.10.0 -5** | 100 MB/s | **4676 MB/s** | 86196446 | **40.67** | **Block Size: 512 KB** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 790 MB/s | **8878 MB/s** | 130356444 | **61.50** | | **zxc 0.10.0 -2** | 527 MB/s | **7414 MB/s** | 113634139 | **53.61** | | **zxc 0.10.0 -3** | 242 MB/s | **5245 MB/s** | 97051816 | **45.79** | | **zxc 0.10.0 -4** | 169 MB/s | **4988 MB/s** | 90393215 | **42.65** | | **zxc 0.10.0 -5** | 100 MB/s | **4643 MB/s** | 85341643 | **40.27** | **Decompression Efficiency (Cycles per Byte @ 2.6 GHz)** | Compressor | 256 KB (c/B) | 512 KB (c/B) | Δ | | ----------------------- | ------------ | ------------ | ------- | | **zxc 0.10.0 -1** | **0.291** | **0.293** | +0.5% | | **zxc 0.10.0 -2** | **0.348** | **0.351** | +0.6% | | **zxc 0.10.0 -3** | **0.491** | **0.496** | +1.0% | | **zxc 0.10.0 -4** | **0.516** | **0.521** | +1.0% | | **zxc 0.10.0 -5** | **0.556** | **0.560** | +0.7% | *Lower is better. Minor reduction in performance with 512 KB blocks. Formula: `Cycles/Byte = 2600 / Decompression Speed (MB/s)`.* #### 7.5.3 AMD EPYC 9B45 (x86_64) Benchmarked using lzbench 2.2.1, compiled with GCC 14.3.0 using *MOREFLAGS="-march=native"* on Linux Ubuntu 24.04. AMD EPYC 9B45 processor (x86_64, 2.1 GHz). **Block Size: 256 KB (default)** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 810 MB/s | **10526 MB/s** | 130969994 | **61.79** | | **zxc 0.10.0 -2** | 511 MB/s | **9120 MB/s** | 113859997 | **53.72** | | **zxc 0.10.0 -3** | 205 MB/s | **5815 MB/s** | 97083951 | **45.81** | | **zxc 0.10.0 -4** | 147 MB/s | **5533 MB/s** | 90503394 | **42.70** | | **zxc 0.10.0 -5** | 77.7 MB/s | **5217 MB/s** | 85344194 | **40.27** | **Block Size: 512 KB** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 743 MB/s | **10955 MB/s** | 130356444 | **61.50** | | **zxc 0.10.0 -2** | 484 MB/s | **9683 MB/s** | 113634139 | **53.61** | | **zxc 0.10.0 -3** | 231 MB/s | **6018 MB/s** | 97051816 | **45.79** | | **zxc 0.10.0 -4** | 164 MB/s | **5623 MB/s** | 90393215 | **42.65** | | **zxc 0.10.0 -5** | 95.8 MB/s | **5306 MB/s** | 85341643 | **40.27** | **Decompression Efficiency (Cycles per Byte @ 2.1 GHz)** | Compressor | 256 KB (c/B) | 512 KB (c/B) | Δ | | ----------------------- | ------------ | ------------ | ------- | | **zxc 0.10.0 -1** | **0.200** | **0.192** | −3.9% | | **zxc 0.10.0 -2** | **0.230** | **0.217** | −5.8% | | **zxc 0.10.0 -3** | **0.361** | **0.349** | −3.4% | | **zxc 0.10.0 -4** | **0.380** | **0.373** | −1.6% | | **zxc 0.10.0 -5** | **0.403** | **0.396** | −1.7% | *Lower is better. Consistent improvement across all levels with 512 KB blocks. Formula: `Cycles/Byte = 2100 / Decompression Speed (MB/s)`.* #### 7.5.4 AMD EPYC 7763 (x86_64) Benchmarked using lzbench 2.2.1, compiled with GCC 14.2.0 using *MOREFLAGS="-march=native"* on Linux Ubuntu 24.04. AMD EPYC 7763 64-Core processor (x86_64, 2.45 GHz). **Block Size: 256 KB (default)** | Compressor name | Compression| Decompress.| Compr. size | Ratio | Filename | | --------------- | -----------| -----------| ----------- | ----- | -------- | | memcpy | 22410 MB/s | 22392 MB/s | 211947520 |100.00 | 1 files| | **zxc 0.10.0 -1** | 601 MB/s | **6921 MB/s** | 130468706 | **61.56** | 1 files| | **zxc 0.10.0 -2** | 388 MB/s | **5787 MB/s** | 114455432 | **54.00** | 1 files| | **zxc 0.10.0 -3** | 186 MB/s | **3903 MB/s** | 98233034 | **46.35** | 1 files| | **zxc 0.10.0 -4** | 130 MB/s | **3738 MB/s** | 91429653 | **43.14** | 1 files| | **zxc 0.10.0 -5** | 80.4 MB/s | **3565 MB/s** | 86196446 | **40.67** | 1 files| | lz4 1.10.0 | 582 MB/s | 3551 MB/s | 100880800 | 47.60 | 1 files| | lz4 1.10.0 --fast -17 | 1015 MB/s | 4102 MB/s | 131732802 | 62.15 | 1 files| | lz4hc 1.10.0 -9 | 33.3 MB/s | 3407 MB/s | 77884448 | 36.75 | 1 files| | lzav 5.7 -1 | 416 MB/s | 2647 MB/s | 84644732 | 39.94 | 1 files| | snappy 1.2.2 | 613 MB/s | 1593 MB/s | 101512076 | 47.89 | 1 files| | zstd 1.5.7 --fast --1 | 448 MB/s | 1626 MB/s | 86916294 | 41.01 | 1 files| | zstd 1.5.7 -1 | 409 MB/s | 1221 MB/s | 73193704 | 34.53 | 1 files| | zlib 1.3.1 -1 | 98.5 MB/s | 328 MB/s | 77259029 | 36.45 | 1 files| **Block Size: 512 KB** | Compressor name | Compression| Decompress.| Compr. size | Ratio | | --------------- | -----------| -----------| ----------- | ----- | | **zxc 0.10.0 -1** | 587 MB/s | **7057 MB/s** | 130356444 | **61.50** | | **zxc 0.10.0 -2** | 387 MB/s | **5917 MB/s** | 113634139 | **53.61** | | **zxc 0.10.0 -3** | 181 MB/s | **3919 MB/s** | 97051816 | **45.79** | | **zxc 0.10.0 -4** | 127 MB/s | **3764 MB/s** | 90393215 | **42.65** | | **zxc 0.10.0 -5** | 77.7 MB/s | **3605 MB/s** | 85341643 | **40.27** | **Decompression Efficiency (Cycles per Byte @ 2.45 GHz)** | Compressor | 256 KB (c/B) | 512 KB (c/B) | Δ | | ----------------------- | ------------ | ------------ | ------- | | **zxc 0.10.0 -1** | **0.354** | **0.347** | −1.9% | | **zxc 0.10.0 -2** | **0.423** | **0.414** | −2.2% | | **zxc 0.10.0 -3** | **0.628** | **0.625** | −0.4% | | **zxc 0.10.0 -4** | **0.655** | **0.651** | −0.7% | | **zxc 0.10.0 -5** | **0.687** | **0.680** | −1.1% | *Lower is better. Consistent improvement across all levels with 512 KB blocks. Formula: `Cycles/Byte = 2450 / Decompression Speed (MB/s)`.* #### 7.5.5 Summary: Block Size Trade-offs **Compression Ratio (Silesia Corpus, 202 MB)** | Level | 256 KB Ratio | 512 KB Ratio | Δ (pp) | Δ (bytes) | |:-----:|:------------:|:------------:|:------:|:---------:| | -1 | 61.56% | 61.50% | −0.06 | −112 KB | | -2 | 54.00% | 53.51% | −0.49 | −821 KB | | -3 | 46.35% | 45.79% | −0.56 | −1,181 KB | | -4 | 43.14% | 42.65% | −0.49 | −1,036 KB | | -5 | 40.67% | 40.27% | −0.40 | −854 KB | **Memory Usage per Compression Context** | Block Size | Context Memory | Δ vs 256 KB | |:----------:|:--------------:|:-----------:| | 256 KB *(default)* | ~1.7 MB | — | | 512 KB | ~3.3 MB | +92% | > **Guideline:** Use 256 KB (default) for streaming, embedded, or memory-constrained environments. Use 512 KB (`-B 512K`) for bulk compression pipelines and high-throughput servers where memory is not a constraint. ## 8. Compression Ratio Benchmarks To evaluate compression effectiveness across diverse data distributions, the compressed size is reported as a percentage of the original input for each corpus. All measurements were performed using lzbench 2.2.1 (inikep), compiled with GCC 13.3.0 and *MOREFLAGS="-march=native"* on Linux 64-bit Ubuntu 24.04. The reference platform is an AMD EPYC 7763 (x86_64). ZXC was configured with its default block size of 256KB. Lower values indicate superior compression density. | Corpus | zxc 0.9.0 -1 | zxc 0.9.0 -2 | zxc 0.9.0 -3 | zxc 0.9.0 -4 | zxc 0.9.0 -5 | lz4 1.10.0 | lz4 1.10.0 --fast -17 | lz4hc 1.10.0 -12 | zstd 1.5.7 -1 | zstd 1.5.7 --fast --1 | Source | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 4SICS-GeekLounge-151020 | 23.66 | 24.74 | 19.76 | 19.73 | 19.39 | 21.82 | 29.02 | 17.54 | 12.62 | 13.64 | [www.netresec.com](https://www.netresec.com/?page=PCAP4SICS) | | 4SICS-GeekLounge-151022 | 40.73 | 39.84 | 33.21 | 32.85 | 31.94 | 37.35 | 48.38 | 30.15 | 23.51 | 24.34 | [www.netresec.com](https://www.netresec.com/?page=PCAP4SICS) | | Calgary Large | 80.07 | 62.03 | 48.51 | 44.88 | 42.83 | 51.97 | 74.37 | 38.38 | 35.80 | 46.49 | [data-compression.info](https://www.data-compression.info/Corpora/CalgaryCorpus/) | | Canterbury | 59.45 | 52.64 | 38.71 | 35.21 | 33.69 | 43.73 | 58.12 | 33.27 | 24.43 | 31.42 | [corpus.canterbury.ac.nz](https://corpus.canterbury.ac.nz/) | | Canterbury Artificial | 34.64 | 34.65 | 34.64 | 34.64 | 34.64 | 33.74 | 33.75 | 33.70 | 25.04 | 33.36 | [corpus.canterbury.ac.nz](https://corpus.canterbury.ac.nz/) | | Canterbury Large | 71.75 | 59.85 | 44.16 | 43.52 | 39.54 | 51.97 | 66.89 | 33.78 | 31.13 | 37.05 | [corpus.canterbury.ac.nz](https://corpus.canterbury.ac.nz/) | | employees_100KB | 14.70 | 13.33 | 11.20 | 11.20 | 9.42 | 12.70 | 16.96 | 8.51 | 7.97 | 9.52 | [sample.json-format.com](https://sample.json-format.com/) | | employees_10KB | 29.68 | 28.23 | 21.51 | 21.51 | 18.85 | 22.02 | 30.07 | 16.92 | 14.44 | 17.22 | [sample.json-format.com](https://sample.json-format.com/) | | employees_500MB | 13.37 | 12.17 | 10.28 | 10.22 | 8.70 | 11.44 | 15.34 | 7.09 | 6.42 | 7.48 | [sample.json-format.com](https://sample.json-format.com/) | | employees_50MB | 13.36 | 12.17 | 10.27 | 10.21 | 8.69 | 11.43 | 15.36 | 7.09 | 6.42 | 7.48 | [sample.json-format.com](https://sample.json-format.com/) | | enwik8 | 90.27 | 69.87 | 53.84 | 49.22 | 47.43 | 57.26 | 86.21 | 41.91 | 40.66 | 51.62 | [www.mattmahoney.net](https://www.mattmahoney.net/dc/textdata.html) | | enwik9 | 78.60 | 61.57 | 46.82 | 43.84 | 42.25 | 50.92 | 76.73 | 37.17 | 35.68 | 45.30 | [www.mattmahoney.net](https://www.mattmahoney.net/dc/textdata.html) | | Manzini (tar) | 52.68 | 45.06 | 34.17 | 32.60 | 30.94 | 37.47 | 54.61 | 26.49 | 23.91 | 30.66 | [www.unsw.adfa.edu.au](https://people.unipmn.it/manzini/lightweight/corpus) | | Silesia | 61.53 | 54.10 | 46.35 | 43.27 | 40.60 | 47.60 | 62.15 | 36.46 | 34.55 | 41.02 | [sun.aei.polsl.pl](https://sun.aei.polsl.pl/~sdeor/index.php?page=silesia) | | Taxi (raw) | 23.02 | 22.27 | 17.95 | 17.81 | 15.40 | 21.20 | 21.56 | 11.37 | 12.17 | 16.38 | [www.kaggle.com](https://www.kaggle.com/datasets/shayanshahid997/yellow-taxi-trip-record-of-january-2024/data?select=yellow_tripdata_2024-01.parquet) | ## 9. Strategic Implementation ZXC is designed to adapt to various deployment scenarios by selecting the appropriate compression level: * **Interactive Media & Gaming (Levels 1-2-3)**: Optimized for hard real-time constraints. Ideal for texture streaming and asset loading, offering **~40% faster** load times to minimize latency and frame drops. * **Embedded Systems & Firmware (Levels 3-4-5)**: The sweet spot for maximizing storage density on limited flash memory (e.g., Kernel, Initramfs) while ensuring rapid "instant-on" (XIP-like) boot performance. * **Data Archival (Levels 4-5)**: A high-efficiency alternative for cold storage, providing better compression ratios than LZ4 and significantly faster retrieval speeds than Zstd. ## 10. Conclusion ZXC redefines asset distribution by prioritizing the end-user experience. Through its asymmetric design and modular architecture, it shifts computational cost to the build pipeline, unlocking unparalleled decompression speeds on ARM devices. This efficiency translates directly into faster load times, reduced battery consumption, and a smoother user experience, making ZXC a best choice for modern, high-performance deployment constraints. zxc-0.10.0/docs/images/000077500000000000000000000000001517010557200145745ustar00rootroot00000000000000zxc-0.10.0/docs/images/benchmark_arm64_0.10.0.webp000066400000000000000000005201761517010557200212250ustar00rootroot00000000000000RIFFvWEBPVP8 j0* >m6I"! 8Ƞ in녧@ g?Q=\a ..8!?tPoL(D?ګ]Wo/zo~+篠Oӿ?/7G?h@~)?S? @S@c?'}{?wo~}?{Ku7ܿt϶Ojyr=kKnu g?'_gW·S//o/??5??O?KO\?a?q|*>Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Lq*_6c,z ɣ%@z Ol:!N |HfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(Ye*rԽwyc(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,yƇ76 C ˝ I^R%ztSx6s=8)Ēv?;T!aØ=qr,U >E1$ |dlnp3ˎz  4( 8|gYkU#g0nzs/ [=Cp,jvPs"c]hnsYDar9iF IaDE'a"h .A?g xkIÎdXAL??ZPQKq#T+Љ~T߀$@ J-x6?Z(AJZ_TOTO&E1$ |dlnp3ˎz  4( 8|gYkU^xSUbzx( v*oJ&ߙ0,bXw) ]u+TUP^Gr;sX )J@ywOl=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6zu# eh[A1̸5u!x2Q {n'l:Y)ZtD1,vj[GxӎY-aK.tV>Xyb(|$~BIB1))@T(qMO@q$Cnb5kk!:zwĐo4 s˕d<*n tH[c"NbX<y]1b#\yU%tƶ98<_2LY=.x# 8ipq23%`K3ipOZBXgIi!sp!4d7pM?:0D#EQOǸL/{ѥ iMNKՃ̹#*XMTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa بrvT<󄵗xJEm _ۺpPVta؂H,T#dTW}N I[! )92A `+oyrFGZKbqcD` @!8 8n7-B!gog@5-<|̹8DewA{W$L<4+vL_x}is=}<9w'KLbxϯ{%4 @<8EOz$h5 n%")YDζ+oC8Srd  @W <1H>@ugZHG_o#y'0w|kY]BP=%B2!S ~n<lwp(أt+xz Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6pFSU>(Vm_Nӕ-v23)F)`j6Jj@e7ھ*Zez vu,U6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z O5l}O|A01z|P G[qP6pISK{U{lKK1Jd!NMy0V/J ۬0sq Dր7a=m3~xѻR}챪;*OϥМ\z}3Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTf&{c6}(f;㑻bXdrA&E#.ecaxO"fXaU nVQP?kh]Fiا,!#4v8 q4z$ FF/,3 ϙ$dabyrA1fQ_Pn U@,5V3ҷNӘF^i2(;3M,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکdPZhkAh،lt"Zv%7kmę{ `bM_~%y6▿U)3<fƘ)Rre_L2?8yA5N1*Jv6Yvd9f$(Zø16($T3JQG%P@z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mRR?ImP9_-K-|9$F'mrf4҈{Ս `CJ"iTLMlLjg-u5xl/!|BD\0λrgj­qAK'НQjrgQ>XDZ8N|>SG575f2LsLr(YM,z Ol=Sj 6S⅛ATlPTS 6SU>([2c$,_]qoyM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(Y02B͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکVJ+gp:e8SzGNS;YN+gp:e8SzGNS;YN+gp:e8SzGNS;YN+gp:e8[GNS;YN+gp:e8SzGNS;YN+gp:e8SzGNS;YN+gp:e8SzGNS;YN+gp:e8SzF%y;XMTfU6<+gp:e8S޶S;YN+gp:e8SzG$KD@ zGNS;YN+gp:e8_EJm<%Pg8 vVHt)q[=#Чk)l|q[=#Чk)lB8 vVHt)ԠB8 vVHt)q[=#Чk)lٰ&ƪ zGNS;YN+gp:e8nb$I] vVHt)q[=#Чk)lB2MSj 6HU2R8 vV t)q[4򎄒 mTfU-w TکB͇mTféOl=Sj 6NZ 6S⅛A?8FN*U6|Pa*V,z Ol=Siź$ k^m.uqvCa*U>(Y4eH| ! _ ;^0mXhT @-ûmRv$}W"w *jǐ@*p<*Y3! S⅛ATکA M,z & ga*U>(YM֬d"`e5S⅛ATکAĽmTfU6|M*5S⅛ATکB͇߼mTfU6|Pa S⅛ATکAN!gQ1+-@ =<᷉)8j؁N$x5AҗZBׇU6|Pa*fGJvSU>(YM,X6S⅛ATکc;l=Sj 6s4*͇mTfU6bS),z O QSx䗽pBu6=NKED8g Cm&wpÿwvnb:>-n`\AnCz SU>(YMTH!&E2)Z Cޡ|Q_$%`vwQk C~@%X6ۓ`J@Sz'zکB͇mTܕa*U>(YMMce5S⅛ATکB̲̈́U6|Pa*U>T mTfU-nU6|Pa*U0_G¦Ug ƹ6U ;"`b N )B0O3@WޔK8U2VV*<; q=qG¡4;YXrU0#?$>b-f|j 6Sj)A@S6#x\X^XUATکB͇a*U>(YMXOl=Sj 6XRVl=Sj 6S5Ej 6SL>(YM,u}0u˂Kf?6'NUV$ar 8\+QH{1b䃗.n`/*r ``#tCqC|F6SU>ATکB͇.=)39]/ou d(j |qV:qe:j?מXvjp-eUATکB͇O2B͇mTYATکB͇mRH*6S⅛ATک8),z O[uהOl=Sj $d\~CY8"v 6#{HNB,yMTfU-0i {|W]ewOMWn? (= &Ă'F4q7XPuPOC=OpbBTuATکB͇%efU6|Pa*5Ol=Sj 6~{WtT mTfU6kQv|Pa*U>(YP\کB͇mTeit7k= PMTfU6|PamTf'\P7}&h (c& x/6t j&6ngCSyX-D`v"H l`6pZWp"Y[?a DM < zr!.ɗ:N%U*7SZjrnj7Y'C#J =A 0CL3a : eڂvQdqʣH @7}y`rl }`UN:]h4e$rظM'FTQcsP#?)7_jyNZNj}b ve6<H1 )[{q&u)6eB,)/ʖ5xb=<|vV`$mʅ!" x/,`N[фZQo:Z-0YGAy!.9)?o&Rl=Sju(YMO&Ċj 6S⅛t=Sj 6S-3T mTfU-`B͇mTfU6zX6S⅛ERF_\P?4$)|#h63-`^*6X)+LUguy9),z ODߥ*U>(YMd6S⅛ATک贈TکB͇mTf\DoU6|Pa*U>gSj 6Siu1Pa*U>(YMT mTfV0u-0}WGc(YکB͇mT GuaIxGg,aS^l d/TJ>A;&$Fh'w15 ->E5S⅛ATکb?mTfU6|P&S⅛ATکB̈́bZ e5S⅛ATکBU6|Pa*U>ԟU>(YM,y,z Ol=),z L}媞u8LI+Mp:&8Sz8."MTfU6sکB͇mTfëN),z O[AU6|Pa*U>& 'V e5S⅛ATکBj l=Sj 6RIգ8@z Ol=R|Pa*U>(Y'J.\'/}LMTfU6C SZB͇mTfP=bz Ol=Sj)d1U6|Pa*U>&Z';qJM,z {z Ol=Sjz=2B͇mTe̜j 6Ol<).M,yJM,z ,z Ol=LT&e5S⅛ATکB̓1mTfU6|$@9mTfU6|PVuSj 6S⅛M,z OU6|##a*U>(YM'븡fU6|Pa*|Pa*U>(Y2?옿02B͇mTe!&mTfU6|P*U>(YMʀM,z OU6|Pa*7⅛ATATکB͇mToOl=Sj 6IYz Ol=Sj55S⅛ATکB̈́0)!*U6|Pa*z Ol=Sj,>(YM,z **U6|PaE5S⅛5S:/S⅛ATکBqgATکB͇['Ol=Sj -)P6S⅛AT …ATکB͇m7UM,z O\ )T mTfU6s 6S⅛M,uS_ATکB͇SU>(YM,y *U>(YME(u mTfU6b ca*U>(YKkZZP6S⅛ATn`e5S⅛ATکBP6S⅛@hݪ,zSj 6S⅛x@j 6S⅖֌ SU>(YM,TmTfU6|Pa˳0M,z O3K),z Ol:֚B͇mTfU6VyP6S⅛5),y}5M,z Oj?U6|Pa*T"B͇mTfê4j 6S⅖YB͇mTfTŏ*U>(YM+E2x[M,z OKfU6|Pa(0?MTfymS⅛ATکB͇8ATکB͇m5Gd6S⅛ATکaOl=Sj 6ROl`e5S⅛ATکA.mTfU6|PAQ<4j 6S⅛s),z L*U6aTکB͇mTfŢǯ,z Ol j7j 6S⅛5 n! GtfU6|Pa*eZ=I5S⅛ATکB͇' Ol=Sj "ATکB͇mTVl=Sj 6Oj)S⅛ATکB͇*@z Ol=Si'a*U>(YMNs}ޢ\Pa*U>(YM}d#R mTfU6zU6|Pa*U>(Y ɭ㵁Ol=Sj E@z Ol=v|Pa ֒T mTfU6>T mTfU5pz Ol=SjЛ7zmTfU6|%@z Ol=Sjo5|Pa*U>(Y- 6S⅛ASHj,z O\YMYdATکB͇mTH )P6S⅛AT),z O[X' |Pa*U>(YjSR=Sj 6Sj\fU6|Pa*Me(Ol=Sj 6>(YM,a*U= U>(YM,urN@M,z O̦|Pa*U>(Yn"bfj 6S⅛5>hSU>(YM,u7%@z Ol=SjPz۪*U6|Pa*N9⅛ATکB͇[֦Ol:MjU>(YM,u4,z Ol=S^Ol=Sj 6S%F`e5S⅛ATکBXU6|PaT:8h`P,b-G&]E;Sj 6S⅖XPa*U>(YMB͇mTfíSj 6`ebOl=Sj -2}U6|Pa*U= >E5S⅛ATکB͇WT#a*U>(YMS |٧ -h5?҂- *P-U[фm`ړV;#M _%ޏIC8mB;R&w>V=Sj 6S,[cATکB͇mTNl=Sj 6>(Y2XU>(YM,zYmS⅛ATکB͇2yP6S⅛ATms;XMTfU6|M*2T PlM*[}_!Pѐ2 0?˛,˃ʦZ\wwD uH-͓"Jx5Y#ӯӶI*ZtF#Rty:_dQ' 훛ӫ/cATکB͇mRٯr}TfU6|PamTfU.o 6n2Sj 6S⅛"N)P6S⅛AT"l=Sj 6I;,z Ol=LҝPv!:wwf3ܗ'Rl=Sj 6k]!*U6|Pa*ATکB˘6S]l=Sj 6Si$mTfU6|PaFB͇mTfOgc+2aXMTfU6|P9@uI(Ol=SiuS⅛ATکApLr͇mTfU6zX6~,и3x 6|P*͇mTKmTfU6|Pa /MTfU6|Pa[B͇mTfT˫"emTfU6zBk#k/e Ol=L~]S⅛ATکB͇.-E͇mTfU6zX6`xHD b&=GM*͇mTyʍSj 6S⅛a*U>(YMP"B͇mTfë/-]0z Ol=Sja$#Vl=T:a*U>(Yp0M,z OW# 6S⅛ATjFVM O'k~EP[Yw¬dMz OHOl=Sj 6jӄ mTfU6aSj 6S⅛=Gt*U>(YMMZj 6S⅛MTfU6|Pa 2YE@z Ol=SiP#3JлDlFB0pB  X4vȡ0eJHLy`^a@5`Jd %J 0fcDG` =a!H9 <(4 mI? >@oHznjLNS?Q1/R ڏS[\G G m(hC#0Q*d)֋ʰ|/| ~AN@TvcQ>P2]  KJ0m~TP1(Y%ABsJPJ ׅ,86K+ʊ amG#`_D (Z2HDkE ;얖Z% l@@Blj1B:(2B`WSPGYȦ|PبO X?9uT[T用n7zgn;v-&~'bv-[qضmaGŷn;v-[qضfVr [ŷn;v-[qX,łn;v-[qضmbל;.;v-[qضmbێmF$[ŷn;v-[q:o}j?T S9_U>(1S _pYei('a*U=+$ |Pa*U>(YLc*U>(YM-fU6|Pa*c,z Ol=L\Ol=Sj -dU>(YM,z CMTfU6|PW [eJQ=i؍ `*U>&5*U6|Pa*T!Rl=Sj 6m&ATکB͇mRiSU>(YM,@jOl=Sj -A?rT mTfU5ZJ),z Ol"My9:ԥ<\ 5}jU>(V,z Ol=Sie5S⅛ATکB\ATکB͇mTĭFV8@z Ol=RLz Ol=Sj S⅛ATکB̈́"fM MS⅛ATکB͇5Ol=Sj .dSU>(1U>(YM,z U>(YM,zQ),z Ol:l |Pa*U>(YOl=Sj 6)B͇mTfU-희 |Pa*U>(6ATڤhׂ*M+amT"k 6S⅛5 fb l=Sj 6S T5Ol=Sj 6GT mTfU- @z Ol=Sj_c Ol=Sj S@hz Ol=SjvCa*nOl"t6|PߕvOl=Sj 6_AnU>(YM,z k B͇mTfTLjO}>(YM,z `9@Ol=Sj 6±P6S⅛ATڥʾ? |Pa*U>(YU58ݪ,EvmTeSZOl=Sj 6LY@z Ol=Siˮ@j 6S⅖Ӎ=Sj 6SgBFSU>(YM,uE),z Oj5RkSU>(YM,Ega*M=?cOU6|L|Pa*U>(Y(Yn'MTfU6|P#Rl=Sj 6I mTfU6c#a2B͇mTfATکB͇nMSTکB͇mTfȕt mTfU6bh-fU6|Pa*e'l=Sj 6Sѥ`e5S⅛ATکB˂p@z Ol=Sjj\F*U6|Pa*T<@On]F@I@} l=T kL@l=Sj 6IFS⅛ATکBm*U>(YMCp(G@z Ol=Sj ̦|Pa*U>(YXM,z 4I:mTfU6|PW ZqXu@= ?*Pa@z & 6S⅛ATک%OATکB͇mTDnOl=Sj 6i ,z Ol: 5JM,z z Ol=Sj[Hj 6S⅗2sk)zXBvNl&SU>MSj ΁M,z L"GmTfU6|PzکB͇mTf$K`e5S⅛ATک3`S⅛ATکB͇E*U6|Pa*GM,z OKfT(/6FyP6*U61NOl=Sj 6mt 6S⅛AV5M,z Oa*U>(YMMka*U>(YM{S⅛ATکB͇. {M,z OU5$B "1`РC*U2 =Sj |Pa*U>(Yot*U>(YM,AA e5S⅛ATکBWaɊM,z )n@l=Sj 6I 6S⅛]k |Pa*U>(V*O0M-Ʋ/gKfU5UN)P6kp^k),z O#S⅛ATکB͇'>{!M,z OFM,z OS⅛ATکB3> 7Rl=Sj 6;gk),z OKrz LATک3ATکB͇mTĻ,> 6S⅛ATڤOl=Sj 6X}T mTfU6B/́Ol=Sj 5hTfU6|Pa(X),z Oj!KKiJ)M"!MSکS⅛ATکB͇Vc6|Pa*U>(YkMTfU6|Pa7Vl=Sj 6>fU6|Pa*M[jU>(YM,vG e5S⅛ATکB_d6@ kz>y)E 4JKh\Pa*U>(YMU 6S⅛ATY!|Pa*U>(Y9S⅛ATک4+iRl=Sj 6mTl=Sj 6$h),z Ol:޵6|LқoBc* !zxdOTe'Pa@z |Pa*U>(YKa*U>(YMAzکB͇mTfUo'Ol=Sj . +̦|Pa*U>(YhATکB͇mR`ATکB͇m8l=SimTfU6|PW k%F]mhrATqa*U= %6|Pa*U>(VFCmTfU6|L⅛ATکB͇D`j)P6S⅛AT΢&Rl=Sj 6FMTfU6|Paӥ7xn)P6S⅛ATU8@8~G iz+tqfMSj UIP6S⅛ATڤ⋊M,z 'U6|Pa*U>)FfU6|Pa*m\>(YM,z iOl=Sj 6e l=Sj 6Jʁi1fL^5zFi-a$8u~9LATک3? e5S⅛ATکBZ(6|Pa*U>(Y ݪ,z Ol< XqP6S⅛ATװU|] |Pa*U>(2GxYM,z ODR l=Sj 6O ( QNp gG2sk)ݪ,z Ol=P :u j 6Sf ֦Ol=Sj 6/*⅛ATکB͇k0)5I 6S⅛MTfU6|Pa PU6|Pa*U=,UAS#k'l(4 oZ ôrq2vee *)Q Xr!l=Si#hT mTfU68X<ATکB͇mTGW<ATکB͇m6|S⅛ATکBMz"T mTfU5-fU6|Pa*Tx,z Ol=Sjga }C$@GjP$v@Vo y:2CnzRl=L 6S⅛ATک6|Pa*U>(YRvS⅛ATک𒶝2Sk&@,pWOF$okQU񷑲&^UV;2B͇X5>j 6SD}TکB͇mTfës>|Ol=Sj 6kSU>(3C>M*$"\㨅*[,ށ.kd,+9Ld3x q@poYbc"- vP26g_Y Q0 ,xAp$={%[P}kJ>6OE@U#)=( wV(VDȍR;9?bH\ xh7G0 dGa0tۯ^ ;3G!]5&ߺZE[fdm=+gξ=j+ [@0NW wTd%ܕe/MДPCjx9b mb(:1gTG]ՊQ2#|bؒ:0j)(&Mő88L84Y"z:`(j/PaH}'&fq X9q.ÚS"fa4C_ט+~Afe x{+'+[GdhR#aZN/ %):N`+D-يXO"=ǭ2*N=mb7aTfu`ĝh5FhÜ ?s(B2|c)z(کGTHW ?cATƇ6lR WfjhSⅰ>hZN2 a54@j5Oj,!lI{Cz t]e:8@a*U=SU>(YM,BxjU>(YM+0^6|Pa*8ND&Rz Ol=Sjb*2fqJM,z z Ol=SjWCyMTfU6|PaShҿ Z\GӸ昚Is: e4(YM*U6|Pa*M^.SU>(YMd6APm䰹0\X Q* |Pa*U>(Yc`)Rl=Sj 6l8@z Ol=R-,ATکB͇mT=S_<嫠ퟅl2 IAHUB_d6S55M,z OjH)̦|Pa*U>(Yb"Z ވz^TնK*_0e/70yGȱ:bUFExE$&f.CrxDfE[!m?P֙(=Sj 6S#̈́W 6S⅛ATک<)P6S⅛AT׵' Sj 6S⅛=SjG:o/4`3m4B͇P,z Ol=X.*U>(YMȉa2L0%ZxPN^u1.Tjt++9 _8o$[6‚G"jq$9T(q\ɀ8*|Pa*U>(Y3f$TfU6|Pa( *3Ol=SjR<|Pa*U>(Yzک|6ύ&кq8 ;QV 4ni*|PT mTŃ 6S⅛ATکcj 6S⅛5=`C$zz*.sMmO wOҙ2A`̞-:5z, w<7w%D4$n+ADl g=l=Sj 6AU6|Pa*U>(YB͇mTf:B=5M,z O\Y x@ITc$9|%j$_"B͇o(x`e5S⅛ATکA:dIBa*U>(YM1/X!C$zʫRO*y@ۉ>(YM,z !&m`e5S⅛ATکA[h*U>(YM,\ATکB͇mT"l=L$N Us5"B͇YKM,z O5 mTfU6a=<4cU" X6S⅛AT']7 6S⅛AT}S⅛ATکB͇zj 6S⅖U6|Pa*U2A?mA`(g5),y}5M,z Oja78@z Ol:ƫ/0/Xr GfQ1D-PU6|Pa;@l=Sj 6JMBi=}TfU6|P`1M,z LXx/, 6S⅛ATکE@z C 9&-Rx$>!`#&SԠ)ĤDtAψaX7Qx.?=) mM8 6) .JP<}pr5M,z m}a*U>(YM~:$MSj 6S⅖ɬU6|Pa*U> 󊁰M,z ӎxf,&?kNK*,nIE>3 t(J|**U667lATکB͇mTeTfO wJPxCab/e7! ZF?=iEH]d!UZi°"*#ZGj)0iDpX(t mTfé둣J 6S⅛ATڥiT*U>(YMgk),z O ҷNL⅛ATکB͇5Oj6,}7(/6Y|ST%*͇mT{MzB͇mTfì JDqJKIIfM_kA:c(I/[ʇ mTfê݋0e5S⅛ATکB̈́)%(YM,z )!=}TfU6|PaG),z O\YKNԦ ti36G=yfíSj 6Ux@j 6S⅖uOl:Q x@ҵOC ga*U>(YM umTfU6|PWjY1M,z L>,z Ol=S_lTfU6|Pa(0?MTtm|fC{*U.o 6pr]1mTfU6|",z j f#8AS%v|Pa*U>(YO~lQM,z (>mTfU6|PaR)P6S⅛ATƉ'V<M,z Oj!Kp!(YٱmT0Ol=Sj 6PNl=Sj 6Sˁsa*U>(YMJM,z Ol )9uƵ "Ncjg[Hgk'B̓ɥI"bZX(E^6vҩr`e/OlWt6|Pa*U>(YAz Ol=SjX -TfU6|Pa'tMĠyl'i(Wg'OtN.x='ѧsLԋ s 6jͫQH4}fX|K)abMTNl=SjQEj}TfU6|PaK'q8qP6S⅛ATנ⅛ATکB͇)&Ol=Sj 6]⅛ATکB͇BS⅛ATکB̈́0\4a$`w{O{&,џhKC2Ejٺ Ct=܆=؞z "Wp(0?MTZ&)A羳hT@ikuԺ ;[ATکܠ=Sj 6ShL),z OZSU>(YM,uo1Ol=Sj 6(rxP6S⅛ATڤ>fU6|Pa*B|V]p\:Sa >Ƃ f{q쉓PGѤPOQ 1_"B͛Fݎ.MU6|$U>(YM,yJM,z S⅛ATکB͇ ;`&SU>(YM,a;XMTfU6|Pf~JM,z &ol:GzM&_̦VyP6YPDJ4{뚊d$PamT 6S⅛4Ē`ba*U>(YMzݪ,z Ol<4Uc M,z &V1S⅛ATکB͇b~ fU6|Pa*M:D'T mTfU5"A¥J!UϪAi^l@:- 5}z OdpS⅛ATکAU6|Pa*U0ʁM,z pn[dmTfU6|Pa3gATکB͇mTbba*U>(YM}TfU6|PaShmo$⏝Qz&Q(rUMT mTŀ_MSj 6S⅚a*U>(YKz2B͇mTe<L=Sj 6Sium̫6S⅛ATک@6S⅛ATک!9XM,z OKfTNXPj5LARKe5S⅚mTfU6|Pa橂;ATکB͇mTDnOl=Sj 6j+wSn 6S⅛ATj,z Ol=⅛ATکB͇k(y Ol=Sj .bz 5 CדV=wGz# j9fp+p$hUMT mTjp̦|Pa*U>(Y}Ol=Sj 6U6|Pa*U= #Ol=Sj 5nlP6S⅛ATڥ+V~ES⅛ATکB͇WATکB͇kE5S!rsc7nRL? Dc;SqZ*U>(2JOl=Sj 6^܌Ol=Sj ϛPz Ol=Sj_ںU6|Pa*U1._o 6S⅛=S^OZCkw,HʸڲtbP6oZU>(YS⅛ATکB͇VFSU>(YMĆB͇mTfPC#Z f'9aRΨI/OWoWjUʼ(KܰF٥"I=RCVH Tm UdoAs!p⏻_v0V 2B͇mTfêfOl=Sj 6To`M,z OTڤӆ#i^QAр'@ߎ9?J6yzϫ\Lga*U>&~9d a*U>(YMM,z Ol:2B͇mTZ`dZtPK6fٷ|BV M~+QC-{bHI˜-m<^* l~ΪQe6S⅛ATکXATکB͇mT fU6|Pa*U=,UAS)NAe7Ƙ"(߷և{BXEut~r" Q` V5H·А.Ⱥ-a?t!+~茱n4` 4B&fk8 yj}qWLSq,=?V=(ti`!,P9 ed,O ̮(L6֟b[L쨴 b̺O~V[6zTf6ir0~C8 "&wO˜ZRЁNT<ySS16pkq9i"0dKۚy #Q/HBr`2r/ `n?dg% b$,-0  l tYR!lQ?LXcyrбcW8RMa fʊ2-!-H)1^jwq [uzX8BD8X>mؾUbA5sB9{B 2L.2Il,.<)0(s_}kM7VJ1j4dPgOWj[մq~e㔑7gPלcDC苗WC,ߊʏodhN:~ J.SgOZ2SwG[fL6A~݈QT&Ggu s` IG4bF@3+sUIU8쥹w"NXջ)$whi 0k׃.2&!~e=/ZxDŪ/ f?E}sA&+m*=ʏ$f55k0f`ݟ':`0ʙI[~սAlpxk7V|KU lK"qtZL?]06{X؟DmU7h[Kz䥉7a|u|e'u/Kw$63G5WV]U|$W0h\UxW{cRF] ZX <nqTsjdE {mJC*: oJS$،USMZӢxfxQWu=+la9No~ m]ADZfjj^u:dX+v VbHo} $33ֲsakBP+ȿwgOkWK܌L? ATکj 6S\~URg6#5R)P6S⅛ATd6S⅛ATکؼEII2B͇mTf ,|SR=}TfU6|Papj 6S⅛c["U6|Pa*U=,UAS jH EBnyTf+;ATک4+VSU>(Y2T@_<`r!5M!z1ߠl=Sj 6S/ZU>(YM,A ZOl=Sj 69@Ol=Sj 63^|Pa*U>(YOKaҠl=Sj 6Jʁ/R?Y?7+BKl=v|Pa kTکB͇m4Y}>l@:dBő'Tt<ATکB͇TSU>(YM,z Ol=Sj'5vT mTfTș_U>(YM,?|Pa*U>(Yzک3;PU9f*U2ATکTکB͇mn&wt)ƱSSVzskѫySccNH0yնQE,z L jmTfU6|PaGu<$ l=Sj 6Sl=Sj 6IS⅛ATکB]zB͇mTfíSjM O$qYja*U=Pl=Sj 6# . F PO\yi N$6 \u3aR+58]>(YM,PE@z Ol=Sj {*|Pa*U>(Y2j 6S⅗*M,z O Ըo*U>(YM+aa(caP7Aڶ***U6D_ Ol=SjQΛk'!c d-ɫ9,I r?k|Pa*U>(YmX|=Sj 6ShmtfU6|Pa*e)FAOl=Sj 6i4z Ol=Sjd&r|Pa*U>(Y4B\ۉ6%i 3_Ol:޵6|Paܕa*U>(YMM"(w̦|"hUOCiI9ȇmTfU6|M_R,z Ol: UU>(YM,z\ 6S⅛AT'&|Pa*U>(Y_"Au갆&6T+$E- U6VyP6IMJ@j 6S⅚KOj]P6ۼ+JqXM0*!$Z(;],M,z Ö Ol=Sj 6iJN:p >0iB͇mTfTD|EI6S⅛ATک5JU6|Pa*U>&IiE 6S⅛ATڥ"Zwo_U>(YM,ujmTD; aԛl l=M!|Pa*b 6S⅛ATکJASOWpRզ'9t"͏%:bĝ嚺^DM,z Oåa*U>(YMUoATکB͇mTOl=Sj 6d8ATکB͇kE5S' !=[FMT=Sj؝&S⅛ATکBZE6|P.'=V:{ZdZD>ATکB͇mTz Ol=SjPRATکB͇mToրY),z Oj״a*U>(YMUT ,r4IKD!BTf+;ATک4*3U>(YM,zcvS⅛ATکB͇ZòATکB͇mTXiu|Ol=Sj 'P_U>(YM,@ƘMTfU6|PWi~ΜRl=Sj 6O _~ 礻dmT=Sj΢SU>(YM,y =TکB͇mTféS⅛ATکB͇ad2B͇mTfO),z Ol:ba*U>(YME>z Ol=Sj*MˉTlp1&$)B͇mTfU.o 6Ev'.« |Pa*U>(YKIP6S⅛ATjl=Sj 6S)(7Eؘm'7L(nլ2b{;I/0'5>{j_1ҵ8k]G5SiGSU>(YM+EH3Jl=Sj 6J6݃Rl=Sj 6(YO=FK5E^f7j 6e5S⅛ATکB͇Ww8ATکB͇k[B͇mTfTyy=\{m#Ůªzv'C~R9fg̯/T0iuV?[˞OWl"FLR#Шm'ɋcj 6S⅛58O6|Pa*U>(Yp.U6|Pa*U>(YTکAP=C2 _MSja*U= %6|Pa*U>(VFa*U>(YMmʁM,z bst IBl3C7DcU݊h ޡ_TQ;*F Rk S2P_q bt6{Q3k˂DMSj 6S⅖TکB͇mTZ?JM,z ӎxf,xVbo)XMTVl=Sj')P6S⅛AS$˾oOl=Sj 68͜B͇mTfU5ŌapS&(YMIEY |Pa*U>(YU-Ǖv%r8f =Si5M,z O[2ATکB͇mT9WO@z OT],z Ol(YM }l=Sj 6J6+'a*U>(YMzL@l=Sj 6SZba*U>(YMT r?[1Md+(YMnj>(YM,z N4Ol=Sj 6|j 6S⅛"{N*U6|Pa*ܴ,z Ol=Sixe5S⅛ATکBpc<|Pa*U>(Yzک4GD1tu1`e5SjMKkI`e5S⅛ATکAnX<ATکB͇mTGM,z &j 6S⅗JZMTfU6|Pa {Ol=Sj 6j(YM,z L*PJ8{o e5SjMHOl=Sj 6+ATکB͇mTDzATکB͇mTdoS`e5S⅛ATکByp3),z OATکB͇mTZ6}gU6|Pa*U>(YzکB͇mTNl=SjD8@z Ol=LR%5S⅛ATװЌ|Pc 6S⅛A@@39q.(YM,z U6|Pa*U>(3_*U6|Pa*8ATکB͇mT|=Sj 6Ol<|76|Pa*U>(Ym SU>(YM}潤rGd6S⅛ATکÀEj 6SL}"zjU>(YM,ոjh6|Pa*U>(YWa*U>(YM*͇mTY Tbz O Ol=Sj 6ԅB͇mTBMG(zv SQU>(YM,yɐ |Pa*U>(V:Ol=Sj 6irXPa*U>(YM4Nv?ATکB͇m8v0M+);0Xv$n\8[4*WZmSNJZL/Sjݏz|+f&QUu$ t%Nȏw 4m˴It0 YԬ -]߯eV, R\~.,cPbw`+Lwi]d|jDTy6[qb>OZkf޸(87mYdy~:0tTECoٱq.C̬tQt:.4qX2vHd< _{^ j؄A//0&mM 0NY Ji 1UDwsUU % u<kh0d 4 +rGmP-K#{jŗ %@a,\ ֠QcH,LhC r50<(0º +\AE@|2YY (BX0U~ E(g-\ N (7* @"@ oTXȸbX?"|GB30NHP>+hBtrd<k/s`7Q@UY* -LwР-syи5@EҀuHم1t"@EҀJ.U$od'J.](O+ +*`78Y)zUf$ҧfʀGqɐlʎWA:b yl~Z*giخ >>W/}N^9r&! ޝe2FBt"@EҀ%%M9K ҀJ.](Iq](Pt"@EҀfjD@EҀJ.](Od6S⅛zel=Sj,QuS⅛ATکB͇mT mTf ״9MVK/]^L2 u9{>.nATکB̈́N@L |Pa*U>(Yp[,z>(YM,z ɭj 6S⅚ y'S⅛ATکB˘6S⅛AAj 5jjOl=Sj 6`D]gFOl=SjUSAJS/SU>(YM4HS⅛ATکB]fj 6S⅖Թ0M,z OFD;66|Pa*U>(Y_"B͇mT=SjOl=Sj -bz Ol=SjE #TfU6|Pa( JGTکB͇mTe:l6S⅛ATک͇mTfU6|$J|Pa*U>(YKB͇mTfATک=BOegW8@z OA`e5S⅛ATکBXWcT\MTfU6|Pa槖Ij 6S⅖YB͇mTfTŏ*U>(YM+L<uATکB͇mT"l=Sj 6ѻU>(YpO-}5KKS02%a*U>(Y΃TکB͇mTfS⅛ATکB͇ T mTfU5-xz Ol=Sj`e5S⅛ATکA綽}TfU6|PaS⅛ATکz O\LB`Xaz Ol=:k),z O bCTfU6|Pa(+;hS⅛ATکB\VgE6S⅛ATک8?Sj 6S⅚51联Ol=Sj .bz Ol=),|?6Xe`3+J&feK&0豛M.@^jP #T mTfU-e5S⅛ATکBhATکB͇mT|)~Ϟ1mTfU6|%fU6|Pa*U=%}TfU6|PaS⅛ATکz OZ|Pa f~dNZMlmIβh3"Ãc/̞B.֓,e|p!a_6|Pa*U>(YFTfU6|PaWTG[MTfU6|P.U6|Pa*U>(2a*U>(YMZ=Sj 6SjM,z fU. .,z JAF]/:&330Du[{Vf0Q8#p#M([)eBLbVJ>\̖Ttns(1ctl[a Ol=Sj 65%a*U>(YM*U6|Pa* {0(YM,z }$3G,z Ol 9~>(YM,z CTfU6|P*͇mTr@B͇mTfU6JRբl=Sj 6I"B͇mTfëе{S⅛ATکB͇XWZ),z Ol:cLOl=Sj 5l$+Ol=Sj 6Yz Ol=R|Pa*dU6|Pa*U= HM,z Oz Ol=Sj8ATکB͇kv|Pa*U>(Yl=Sj 6ASU>(YM,ujmTfU6|'d6S(G%nOl=Sj -D=Sj 6S>(YM,z Ď l=Sj 6Ja;4fU6|Pa*f:yPmTfU6|Pa /EW<6Ol=Sj 6ѻU>(YMmT-zکB͇mTfP7kATکB͇ZP6S⅛ATڤn 6S⅚q'[8ATکB͇m6 mTfU6z;ROl=Sj 6Yz Ol=R|Pa*)6|Pa*U>(YJ mTfU60K;ATکB͇mTG}dmTfU6|Paŵ. e5S⅛ATکB\@QJM,z a2B͇mTeUATکB͇e5S⅚jU>(YM,հhMTfU6|PVکB͇mTf+Z1mS⅛ATکB͇Tsza*U>(YMOl=Sj 6Tи8ATکB͇m8l=Sj 6oZU>(YWB͇mTfU5Ol=Sj -a),z Ol:m% 6S⅛AToN^~9Ol=Sj 5lfU6|Pa*U1h*کB͇mTf7j 6SaMM`j 6S⅛@Q+mTfU6|P%Yz Ol=Sj5fY6|Pa*U>(YpLa*U>(YMKrjU>(YM+B_u 6S⅛M,z OU6|# e |Pa*U>(4^S⅛ATکa),z Ol:ۆSU>(YM,CV^4XMTfU6|PM,z Ol!mEv2B͇mT|=Sj 6Ol<՞QqcOl=Sj6eӰ{Az2,^d*H),z OCMTfU6|PaTS{ |Pa*U>(Y! |Pa*U>(V`}o8@z Ol=L"=}TfU6|PamTfU.o 6p-1mTfTKhW<'@ hK71sFzߪ}=g\HXڢl~0jZ7\bQwx l=Sj -id6S⅛ATک$l=Sj 6;})P6S⅛AS&Ol=Sj -Hp*U>(YM,ujmTfU6|'d6S% sMTfJv:51\1uQB(/oH<-Y z/U{JZ5i19 &AH2(::z/-xBsPS⅛tnOl=Sj 6jmUϕM,z Oj |Pa*U>(Yc ͇mTfU6|#JS⅛ATکB͇5Ol=Sj .bz OCK|;XMTf;ɔBO~Fo@?;~2`Ž# ћPYý}TfU68Ca*U>(YM L |Pa*U>(Ym{MTfU6|PVǝ3[l=Sj 6SǷl=Sj 6SjM,z fU-;,PT mTTў->}cI u!:Oa*U>(YSU>(YM,u[f |Pa*U>(Y"DRl=Sj 6Si}*cU6|Pa*U>(V2l=Sj 6SjM,z fU. U6|Pa*RjjOl=Sj 6]ATکB͇mR}n7*U>(YM,#uSj 6S⅛ATکB͇mRk 6S⅛5),z OU6|Mya*U>(YME>(YM,z Aa*U>(YMO#q02B͇mTe'ʑ 6S⅛+ATکB͇mTZ*gz Ol=Sja*U>(VOR*>\27qIYz OB6|Pa*U>(YS-ATکB͇mTe5S⅛ATکBxe%z Ol=Sjҏ*U>(YM,C s/@z Ol=SjG*8zjU>(YM,Ca*U>(Yps}-Q% y3vVHt)q[=7H#8wGŬvUbzG#;XW(F9H3^Kȡ/`Et{6zVHt)q[=#Чk)lB8Gm( vVHt)q[=#Чk)lBI_*tx#)q[=#Чk)h+gnእBSΑSϿK@SS:B8 vVO:GNSze8SzGNS;YN+gnY +gp:e8SzGNS w2B͇Qđ]IbD@0rz!`d(YMfU6|Pa*<S⅛ATک1(*U>(YM,fڣATکB͇mTC qR 6֩B̈́& v|Pa*U>(YN>e5S⅛ATکB̈́ &AFS⅛hk),ռS$<]U6brd,z Ol<bQ< 'S⅛ATکBt =xrq2ATکB͇̆H̘ y;RWUL⅛ATکB͇Qw@b~0nNѯd,%|Pa*U>(Yyn;ËPy4X"UV*=5*ATکB͇m4b\.Ͱɠn^!$@کB͇mTeCdϵ 5 gR[J*l=Sj 6Sg>ݿ[{P+q*U6|Pa*e*PŒNvX &`ATکB͇ Ba~TxɿR kЀPqa6|Pa*U>(3#B61 yF#b$̦|Pa*U=!b6>^165mTfU6;ǰ6(yR}A/hJqP6S⅛€J^@ zµN}5M,z ["%J(/QKY]_U>(YM,z [=& Zl mTfU6b\^= bi;z͊LOl=Sj 6%M8F:?3!*U>(YM *0bwɄ`a*U>(YKu]wjbxjhzT7*U6|Pa]ed02F3*nIg wQa*U>(YKq8PUAPky gHX72!5M,z OC87vODCPF^M檑M,z Os7:nM,z P@z Ol=.dr4>(YM,z[hijٵ6|Pa*U>(VʦnqYM,z ӓaYM z Ol=Rl8p CK |Pa*U=/JQԸfU6|Pa*U>(Y;MSj 6S⅝6|Pa*U>(Yf09^|Pa*b>X"u% #^bVeԍFy΃"w'Qۋ 4sATکB͇mTfU6|Pa𗯪,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,y ˾aUV)# c|_̥X:wrL1b0>"( ?Y:5FPD!rc PsJZf$UzQ9|X]f^.]wG3G6,dj-6 x0mxpef3 ӺviVDZ2bD7$t5T~6E FJoM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATڥ W9fd1՗!_Ӗe-7Y&S`ˎ%ח5X?@7[WP⵰"o/ V( F (4CU{v3r7a/p;#IXA{6w>/~WG_T,oHG:6Y*g#|Z#ڀ-Zuc 07[G%.l Q/p}in/` `>yX(n?Tm)؊\u=).hSU>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6r>k9vf|6(gW~ctH|1T١GfxR`[q#Ď֢ i(uHOģ~5d|*\*H~*Ƿ0i_: { mj<1 :67{`{skޜ7GctVWخjⓁb8 d&Gķ+. [{UPq>fb@ }fUn#3a0rƁ8HGMCᅩs^ 't 2L{dZ$Pйs(YM,z Ol=Sj 6S⅛ATکB͇- x㐋5FqOSCK=g $KA#"\"AN '^b,RѱjtEh%)cBZBث<6pMR>H S)gn`P j 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pr}_ eE>(YJq$\CꑸUmOÊM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfTnhMaP3h*s>MR-r0M;{x0}b3x׃/o  rG.C5p@''VmбKH|M^YjATuLL۱8ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=L)q!FYa&®!x %m"mpF=:+pоYj5rEbOrV58"b|"~68*kjUȀzd$.@EXq,w8@z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|PWEI102ϔCUQ Tw xl!-X]PStV T7*%G aSq5tlX$=yPFR =\lzLu-8aBxWQ(]WUKCIyb6ܫvبATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6֩B͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|Pa*U>(YM,z Ol=Sj 6S⅛ATکB͇mTfU6|P`~QAY{KNP*!q4JE$ Fm2LxY]7Y<9gPsJh[͔ lЌ4[q1L{g+f)n5Z6CBpQ$_.tuRލu+uet#TTMZm gq!e>XT@elۑใ)ykhrT9?:.d"|y%}9ޮz@ asY>+0ΦRFiYZrgTSL()#6hΎUL,$G-U٥c^sظ ^Z{աEoCX8':B ̡Je51d[DKe6r5.u6K9u,> E"Sk{AE%-UH~ulWTԃ}f:Ž r޿b4r%jV{uU;v>iLe0UenbQcrb}d3*?P`nV퀋.ir-IKeNV{<<i υ}>${3˚AQYD{ q2W(gXaګ oOsˤGͽ$kV5[x*#mKCrz-)H6`R3BܩݸTZS=.8YV>F僬g .0;@ty4ʈ7pB0rT3ɡ%&A&iyõB 20'waƂFníci<.H~O#}ܤHϷB*eNnHu;9629JeP(w܂+Gn,Ws6,4pU#w}<[Xk$N yB}OFҴa2ӸGuKoː6m+v/ǣ57`j},x` ýc)@Q9붧‚.))_3?#hB˫]x>]RЕEVn /6/=P\y[(V$WLիq@>y߼=3I&ן"ύ%R؋T[GnD]}^KtxHSRBM{ۣL$b1y'!:g@ #+Ȧ^N͓SC?7Y$@ƑI lsꎧjwvlHzd g /~9pvPnz+o>=:OIv˶ \r-Z|jbGt% y*ɫHd>., BX B^9_ a 'ɇyBtȭuFbMʈ7Al)Y8 ؆Ն$mCH25 V L^*_^}NLa9GY΂6# `5*v;,dK]@9 )zoޛjq2}~c{5srW.;B;D2|7ij(kjgK9({z+ħanKXONc4y %tW!emP"Xsf!>5(F*F'0ߌ?h,W{*s>tX D N{6* IE!Ѿ]&_%ۃJC2ʞmus!O%]6a7^^thL-^a qJ%=eF5jki&7zLވE=! &b9N{8974mÜj(N# }@f$ g^]wP~鸝 >q73ʬcnˢsq=dÒOIcus]1$O,jɂ(s\ *Wl.Q9Ptr.KqKmx#j$C̵(ϪZǷЦEֲ9 ڡcs LY"}ڍ#M*Bב'^WڒN'T܂z!Kle#mne" 1H<'Ei۴zɤxNHG_q/MğUaȇIZZ.'D kۏw"|'Il\+1f=liWXz&kleVXzѦ7lN_ ^Yp<=u\+g] E7}.a>-35m*Bz}a8|'0|D#~,,ma^oH([V82"m:)}y/5Ӆ:ڱ ".ӻ=U9mWެik;rch3`Ds ۺ:ͼO{-9ā)OhhgҚ9&vu:Ԁ/cq);\2&ge \Qj~˝^D#t}%uڧ.HE=#My]Hzfb+4(޿Z۵ff5zj⵪]UV-\,mj{*kj`UXCbvG?\Q dm UA;0JߤSXL=:wv ͏MV*i% =bz{. كoѓ(9ZDiD] 9]H<u1 "X;uɍZEYr]OOS%0 <;mbwqmo^-cňt(=JMF#f'Sa⋐u;S0+䐱]Spx5AKz i ˺H|κ>Qzwmć0fN~9p2 _k7]<pZ8j.M!d`pkqa<"qj.+MpBYpɋldC뜡92T*j@ȍlxAD*ov|z`jD)N]_o!A4ܨxD•(}jmXhhF)4/+/ZN_y`l%⮽ua Cx~#b1 P2c"H٥ BZ-1X C'7ZȸX]7?%|K㻪9K**HZۃC'pt>J~KYI43GWEBVۊx8(e79j"*RiRa7줙'WaGAtX GqzzZ'|1'+(Ӂ ˧l,=y--e8}]L1ATkaV 5DI]L-2[IWwoPظ-)?n֞S[L`N, t>#Q^/ ,UvizMM "JnyᆻVi Kl^~`nfS'(É{x_1v>IFy[cE,e8O˫҉:l.SëgPM|<]vo%ΏM~ 'VU$P pդ>kp(7V!ipK <ڭb\QN;Nc qpTԒGˀ27wꭥM?dK1hZ @INcީJ>P?DN8WĻ`lEi>.`e)#] @zFVi=wBT#-.Pʶ#nՠzBsCF!WMx,ӱ=7Qzc2 z|FmTT)>d?Ts?V?X:~[J(JitSӗOB:pR{0zJށ`_{(On=XB=`1Us#20;b59q\d u2 c B.*-015Y9-^j-+ Y >q֓ V߃!Yym䗈i{4짼LOY`cx_7* au珞YESɧR2+֜Ac3-ʮʠ 43aʫ),7EmU2A֙qv/hG>>E0_fL]wF$eVF"ŀ ծ"=pL_ߕ^N^fxyWlYQ濄@gޚd CcyRP{-%tY^)N3 *dF)*v[j(?ċq @>G_dl97Hu]f])p' Zl\J8B;Ž DٹĪmW$mtOqw+ 5N%]|ƾ`@TvyO3Km]g-͏trtŸ"Z l CHBQ7v6R3d6o*KT/'23j{41ƜAT: rMv0x6-}N?n62o:$`LPI">șuW\1-<DŽNN t8ہQ+ʋvҾl҅wjy.ScpnOcKHwXW0RT@jiL \AE[P K/xם2pB s#Md6wI{uczugMؑ|+ ucwVvH{͌ZuBjL<{d;ZxM/N nUF^dIפ^s~bK`XrXvww S!ljeD Ԑ6|/֝.OlPwgs]K$ 2)S=i 8PL#w"(P;}Wt+< }f9#/>/t ^bn[Y09G\1sӾ3dδ_Rfؾʖ̄[GdS75ZQE$5Xҧ<8i[d8 %Aѥp? qx3FR2thUe6b]nQ5Byx#~ܲt$2ť 2M m j@9_Dլ@lE8c+iVJ|7(KBۦM|}I^8>饐VUKԙ{B>5\u!f%:P :=S!-I1#Ὁkf}5 N5.}+ilϩZ,wO~PF{}x Bɶ3SY9[G??h̴ Z^N6\"| 1#`rUbnt}q^]({/l:;bPYujG6ѯ!b*r7A*}C(c2KEN M[{ Ə4x-@K.PPF# DĔޥ=0>E2o.^G;ZVankMPzsOAtFR s2@LX8fЂ SJUFPAoJl >E6W|CBѾw1'Ў|QP㒖pmC )F WSI Z+cHת _0pЍ0_Iۑxc2uYvӔiH퇜eY -֐>LJ<)c;E1A`Lؑ3s3k<ѿ41(Dz#-%-aw]yj pߙ8U)p2'ғ/o} [brOѱ0'#L0`JM䘕 @{osKf4,K P~۸sJib"ff.ŠY)dGgyUWVeӻM-< )Q\>1PȬ赏]H‘X : GfykF Lj,;H˙ES˔y~U79 7<ϺUArs)4p g,Ԩ/!i.VAmЊ\:e\Ό'Rp}Xa~[q%"z& }pH 6Hϼ-+}VH &U(5X";-TCLjӕv(\LqڡqYttnGNXTˇ M~}!7e:$Lj y! zBO@EG7F(=*qƶLeh'.`=ZfRdմ7<]Aw 'bT"g|:ӣ)W&D aSLEqsAnEhlhwE *Ӂ>3 AKpIO|+SHUX"> ,*Mp'Z \,]ʳ#:!.k%Zњ 36NgdZA5MFU10'| ]p_:{{Z|;cJ:XtU)E3c.tHʎdsQ2+@`7(aZ :n>JB s"EG9rwpЙ*RSSDY#z$.%!6'ɿgO]cupj4:d)+bhQeQfSy 2Mt@Lv/ @"˵.GFvؒ(Ղq9Ch/\l2d!Q#bȣ\Sh%h鴘qxpXr7Sθy]1tE-Lxk(ƦC]7FAO&y-+ hKu/W)^@oq*zʒu5Ȫ0{2Hc]48t%!( d=k-ŌveP j ͫ04$XAݓ4Ab|Vl.b 1\١B.9 7.&WRpemOKl_._cwiz6&~_)lIV05(mrlvj`\wpT-7_RExQQ%,904 ׬q:wypG%0Tu5*+&*˺<\R+C j| DTqexu[Zr% )#{T.>4.>Wࢠ pᣉßa;츂Rĝ)U`Z{܍Ba0/>$ܸ5WC(IseN8ɀ?PLXaa-<8 CBx'C/zH8suIB`b%_Ь'QmBܮt+_h"=2hΪvq5TBc.@.R4N^z olj_# ^^iJaJdN(qoħMzoo E.%4NQ6^8BOE)(%F28%ȏƼ=/c*/"t8³`A LHU';'HI34x(OR=h,hu15d2~ ;ES<-1燍HNeS=goj>9\O^BR*g:J ̤oq##LۨBn*hqJlp5mQ4HrרkFΊ3,%_؊CJ'Mz+a'뼖%&@񽙅@'v% V؂K˧u^{A`b*L8\sF4kRS|ݳhXF-\Jڵ>'a# 8d>Do\ ŒL>r)& [<32HfAQ]CS6 @g;Dw`v0OEpv~6S]L$HJzo:&25;X;P`+)$񦒅'hDy Ϗ{ w#,?fv\T^bZʸGJ&Y: ,A>Go-.ss4 LMN6U5Rea 'NkHBfvrӑa7sׁ!tuhGI讆7x%qR aV'*/ka?bw, Z"E֑X󢏻_.Dg}H2ڹp Ȣ y RRs7XwKj 5ۺITzAL[?f9yڞ4%o@2xWRەL5պ]H]}[┿Q9e,눌˗X?+ByaiO&3u'HGJEdϊn(?>Lc'oUIf1MBk7SDF1 FU`xCoPR9hE)Utξ /9DX4:-–Kn"Tm`Vᙥ2SdҵՄcV'b8!j\zi3^R.rYTq Ů$X?JI/ύڼ#,pa&EDw^$I6v5Xul5L ߶.>KlWϠC˫]24p¦aJ A&ʕX-c<敕_W)z_/~սcQC ذ~Y Yk]XQؒN ]ؠ> ۏ9R&f^L$poashKki֫GF[SKyr+Hp3'3!nkۗͲ`p̥59d~ N<*(d29"8tqXܫcONcMJxU/Z8u,\@u+Q?JX/ev䞋:e%/ 5,p6-@{RA|X0c H5CiP+: h+_q &,^=:X(;hf昱 '< g?ޭC^3)B' v;xs|04F:pQFxes(ķ$;9Qoo}㛮&)X$y@AJ)╚IXLD?_L$˲)؁^'B2/7''J/{ n*DC\^w;Rz&[ |e` \<9 _۸ 꽧cj90e5t'ڢ,.+bO7*)? ԃm(5䟠$` عb 10"$Z6y1L]ةYkM% Ā?/tA4$ɬnD)CCj+ .L^D![<1@K}SHPPkdt5\n2rl.7!Ψ~wD{"^Nv~ob\FƄM.0YeQ#gqDLP)'scQj6S5vjL }ɿjFp[ (E-XR֒ս2_.S$f M}bk^НQ[PaPj|$~W }vħ哋࿂4dފ@//Me{bUF8FO<>Xρe0?]Hꕁ~d hcX[)Ic"RlsecduF_" 0Dg 9yOZp9IO;.lߟu#E iMeBGPG]LQ"1lpJ(QR32yO:>uF6BariDte \Ty8^nDq$Y^ 񢶩9vNt:K~LY' 4t.f@xd̉65$ɋ6e9 6}IR9Br)# 7ݤFrn$@Fo5U}WNk.?61T3s^ăCP#F=eO>*&bvl%ΦB%X2XM:P.D"P AP2@.7"q;;3m 칾Pke;zw؁RnzfvCJqF!STU}H-s^mw$p )cf;6c$t5B$/T %IMCQĊ==i 7!VR${*2(cyO,FZuն_ c4 W8I:eV [1d&m#OY]yV*.hNcT5 bA H7I$TmrMaɕ Co,_iU"#U\rxBoʆ>+nY~W̵!xV& I± :RJ,2` m,+o7^弋HZ!Je1dȧ60(G~'=Uͺ*!lct"hVGAo)NAUsɲQoݝ,iz4!ȳp~"N$WOmuDFEn"BX>.;0Aatm^gC<6VJy Y%N!I=BRڕBZ-.k j׮3I% VIϱjjvq,>A;gzރL"0UkLEP C-u[_:*<4< KC;Cpp5oU66@;G,"|Q'nV(dēU ThT*"8 ~x,hyXO~}j`_ } >a~r㓦{!Ξpq[ NWE+e}\[SnTEc}ٜjSѩJؔ1)vSQ0d-.a n 8fxVpRWT/ lML/cPX!& `x%rO@晇GJkJM,mlUZl ظ'4UƇ< "ż=r4+[4>WxO9ٍ@uOt~ybV屉|qӈ|}~9v.g^iX%k~\WQӚQmo[>>|( ㍵jKj=(VAb7ı)l94(Sz0RvԔy9j̪xM.q.0.~f*Eda :萒A|%g&QNGx_ *XS83 E*[:sF`btg*Gs6c*ȷ'5Z(zhbԾaVAo^xr\^ mIy!u5EH.tSb,+\u&e~Z G}AmXLped>cMpAswq W*>:c!;v`1?;LՁx?[}Z3Ae`8MclwHO/+ A2l]HvCۈsL8{( Z0j.Uz0ceίH5{s;9r~,;g!`l>Ʊѐp940Iئ_ڒךgTtׯ^ iɐDM.yDz\g)Ovcw[$؁x&$OjI [4Tvw?['^@ y0=tLtAivYX뀡f <H00 ;'W)Us@~x eu(C ̯ IͰ}rܗ8[I2YWiM6܄jSqY֢h/j><h>J$xRC7?vݗ ~Xö_1v`\?~ Tϴ)4)-ʪAkS:~~eЭq@Mmͽ:Q}QLha1t[f`hPNjaX/ot:x|NAN%^=a,n)$؝1 QXyKT}IL '>bҙF[,ǢxsJp|0tZq&? o0wo{Ġ;:֪AeehȸOf:<)&Qp?^&̈Rz}aΏpfT)+d5cpr0]r:S1r4ze|{MBols@.@2F^ T>:#<Rƕ`¶$2e !ѧ*6r7'ihKɪB>;@u\7xV?,IvN0h°kr8v] )m`jwE6T  ehpzgEr0|=V9FHl>*)*d;p A] _ŏ2fղϘaV$U Ňcw-{)za^S,(/, e#VA DQF(bZqlw&=% :(jtSqOS;orsNDdPbld]'MAO!!!%C Ӹj Ţ0fA-R ҜVK=9mj+iM̘OJo9ٵ\dt\\:o&HF| SQؒ|/QU8)Vh/T ;:96Hh'$PQ&%eglj%uRpP;@%̔f `+򊕨g- =l6*0tBm }G"/.y&w2 QZQ;_j1?Rj7Hn`\*ТYuÏUeZ\. }VY%ǓxboU?*r2_YxoѺ]u͉?MP6B3jfVռ^_gٿ-m͍ttkqoI5=V+žxHLΌܰZ[e=  Q65z}+Tș&A#wOElv 6 uʰ^ԳzF()'HBLm/:bONЖVxGa - FZjZv]bLܥ=j ZHZ[na^zw_5;̢AM(UҨM0SPk%>߄↤*/L.S s NC?Ok Z(HǸO]9KWC%\kzᐱWA>Ę ǒi o ~,T:U^SK1a/$o~C!l=CvjBh'c I,D2awBSo{=zd*Cy: WrXS"Bi<s -D?^Zx25*ɞ慥w|\RiɜKP-1tWby]|b-58/GkB4W-bOi@Qn{VPf{?fJGl.L]Ķ>4WBEz?n7EpJ)@_>z Qbl3̘~m,HQGNK^| ĖǙL;FR ;ANT#_y!-=Z5/wiVLyV[D9TD?Bl7SH'J(ʁC5QX'T!ab;ac~.mM'N|J ft{G) KNG9?ђ敫Z:e+!9L(EM zZ8kҭ)g0/#mZ~\Q J.! w?ຮletǘ~6[_:@M"8>6ǀ&h)c_ׁ(Q4޴! .x (Z0=L\}TY#~Z)\BT]k)j\5 W_Yd1xyiEuQ6wه鯣M4Y"y[Ĝ -0]GNw55q0#x0BqNLջ.sKgVx1J !?,֪Q+18Pu*`:"QZa}QI:Il7jáHث?X="UT?r$aZ?Z@:C> {a**$1B8 ]}hZ)i_5<1EE݂}XA nKޛHj^LBjOMq.*’o h1&~Ly+ Am~ sñc-!qh,d<juy}G 4*CY[p [TI*.(v"/LvOhڄ+jM%[fUh陽,SQq=8Xu/L_!1i"7VI΢^HEJ>)Z)KZ\FkEh3P%_IQ駏*%Vc{&qHhoF$IX&'FRA뜎PO%3Ԩ$2~i,ĨÈ+Q+]bUE&yA3$ nT݄)^L=c-SWG(7! 4C!䱜!1pmCWT7P^y$˜$,TEl إ0f4̗AYb졿n=C#㾣$nR?C՞19keO'shl~-8N{;_:Z*'i+eJ< xBcڄSaYB5@s Vqׁ}:eoo pk OH/[HEwLЎŕ'v9x ;S\Y}*?j~a9'gH"N ( 31UQ/.l=U,`)h98۶̂h5;itSB|q/H{=++NM!vtl;< jr>h(q->::D$ ՘P&<5(Ȧ*&0Vx(lնǏz+)< B:l0WBn0cI54 3^nk2ǟS5Ғ 3&2 "/ijV "=>ZJ}5NPUlz!Lq0APڶԀ|N%%.1pH2BWD.+4xUϛZe>b}4ʇ&J6qE1;..n#6<83eߙ0rXq8βg^jlq8h N=_SAw j0[#ކ u^y)ufps2c 2m+1-lK{1l b8E`=m۲7 O)k|t9B;PFc0|Oa2*OfTiEm jPữxjߔbn4 D>aQsLs={U M:nHԃjNv0GHW϶5tCG1a峁sN<UC|>!f a%/C{= y:^Tf.ҔpU^7Ul@-_b>n qZ0Ȓn.k_ohU%k#-LQHچPPЀӉ4)(dpiE2Juk)yh i{zZ sFҝ֥K@ncqb|ɳ'bz BNLIIOxKߥ.n%Hx;Yꖾ:^ Xqao7Ab:oRݼ*Hsu[ z~ CeIQ}8= S:2uI7Kw0՞}e_WNE@baxy*Zh ˉ,"/ZR}6473Lw0T+]o]KR`[VƔ0{e9`59 4m)_1d Uwq"3B@{li! ˆfˍ,ݯʽl`-u͂EqH<y*eKTJAW 6ֻcg]/3EBy,>tڄsH?S`y3uQ`4zZG)ʸ)u`]o˜91M4]xj5Ihژ_j ߴz`3V0ZzmBϋ8,LVw co4]I|xrsPOŁT<{džir"OJ -\)̈n1dq[X++?md8E%7}tcQ=-`r_|%ޛy?)4AY]TwR-ƒ~QH FWnu Y7~72N~>C=P< cC}?Ikaϧ njE#{e H܋+iZ= e5S^|%}T'فNhnC t —<=I歎@=ZKV:LI nRe;˝HHf3tpYo#1V 3ir&p?NBWS?ÒSI/^P؞FYrTrxr$lG7©"QmWԟ/,z4*M2w4Uo~TPCQӁ(/o&楳.!GH?;sG,` wNGC"/RF@LhrbM4ل9N'm_YGl;gr ˸U&Zmx `>` TIfdѷXg% ;xjloM:^'W5b*.2hIJ<>p 4!WTmf_vs`L+84JUprr` Wpha$҆IsLK]ED0D/mK¹uwev `G3DhMʻ͖+`Os# Wq`K q<8!]ȈKmU>@?W;Dbc [nTx y@_GMקOodŚ&#cT= -OPQ{U/[9K\t]lj'`٪ qww?!*bE$v|> gGu6PBʪ!nѬl|`j`?̒$HךPc?5€+ dÞuod&V̫1X_+mLѳ+JDg6/*KA7/ـLl N<_XCy48 (K`-*G C bvpDzbQKs9*k3sln:vF`;З#.Ao)*j/$Yap!ݿh5pA]Y'L C ^|(C@ LiIOMdaIr8O;W)j*@R"L)!LU`kBFR[{hN{|wd"HȹX*zK=شsfKzS rbtf'Q5 D(NgeO7B0@+hCX59?Mߔխd0AL+;k O|'HZz69 󖅉{a!;vt K5>O}[ͽU8l0B& NJzQ ɚ:o2H63J`T[/^SCjHЖWM\ YK$rw?[佒$LhE>Y5V'~%o|1Us G6܌oM"-¾ds(R罒}ص6*I+rݥBfrt'vqW Ui:Ѱ?H3p Nz4ќiqQ!$ۦ>*Y5) r v"o'a/@wAm^[f~͠3vU}^g&&Sdi⎗1Dd!|1͛Lo$wb\x!?/Oh.k\0vawt$=oB@Mѧn2P^ǺV>_x1<>mؐz-V5<2YB&\֒0}-f`>z[{5MC'n m@ ~wmty.єLxSqt?VZ|a4-'o*C#%8<ɪWEXbd[ϛ{+%^C譳wktV<~(&/bC֘o>UO^?L(b M5fпaM20m'.>L]ua(^Bþ{2߂;1̔g|fΎZFR3L5Ny1s EM-9Aԫ3Eipyz‡S]2$i`4Ƹֺ: ~sew`CiHWUJ1ݪTpr5MjQ66GK B(Kn8Kvkn]dK~M'٠bvc[= և1#ᴜ1vr2=k/:t ^5# hcn@ W88V{ATq3Aؾ̛fO4IH䌼@\F x!nЙJudr61RO+nL_&%+gÓQ^:nѷ|{VCnPPi9*ɢ5I5Txv%"^k>k- Ewr[yi@{qZn"JK-FJk5 @^.%ꍜOE }cK6K@Z& r'$<2^SZ8OFr(D82r,WM@hL`7\?Zq>Xz((8ݺ-r0S>P .OT!IV])bK T7ƤÁJ0?~fhHmrxH4 tzCYjz{&MWYF9^;͡ 7 j iyBsyl8>GMl+oKpۢG['~V@ ^ᒡ="6v2^4.nڼ}[PkIF*Yd@_ 䒣WmH'7cS*/KXHg1 d b%bnC8-MuKS/jEB]u|̎x%Ka{>qc J7G sڔk%[qGwrigI7K"GJL kAD`;zѯia3PgشE Ƣ'g($r)8L\j7Yٻ T.t!tQ!a(|N:VW3wEdhֵ *~ b+s =r$`8l$nVԋ8[ [BT.@;otJ R=~s싱,e`[GV?xR&p^h,,S^! 0ɩ(#8mlL(;J^8[Fgy4<Ա a}rKLTvfMF;7aj-j^,~.MsRii&UO\ 0QFCdRTgcřgoօܣ#:jX׬'`d>WTL_Vx)lNqs0kGg ۣZMaq~i12 rU/3Djjk4GK B(Kn8Ev׹׉}z32s[eK8Ө޴E@[֍Εn[ jpO%^ 4]{U1K91,;>>lDL@-KŐ OH+gfye>p!/m)0Pk-)qeBX;3蚀Йn)bVwR|;'QP->Qm5dp/u[}`}j@]ݎB-R 0oIaPgsxkh;*;S-,hAeպPL"HYrwB.ny+M[!U[/:q4|?8'0 VRE0IpN=Ɓ39M)%BzDl,9 ,e> Xԏ k-lsa}R.⶝S7}Km*%u.n[)>z Tn%~of6|Z7GD P`!~m ̈́ۓKQ X6hI9y`R{|E#6H}MN֠hpW%ɯ˭oF>9y *Yhi͋- +}3\!Ƶ"JS{2~5?i?#\ QFှ| m iſYd7c'WșBd(_5>Fqt^~AIAHDD09Wij;M|] Ë@mcjG+WJȻʞ97ۅY=(";arwwiC"FF >s$ %}9gyPQ;RV[e7Zf$XZSyګϧ|{ˇXҍ6Bc 6wA: P$vcnvt׶^ݷ!S Bȱs2*R@HS{* 8]a9`DVfER$N6THȾCM@ uK>,!a g>h΢`Ŋ}7<\U7>MNO[#^;,d^k1PFpL/I5+]dx4@jϐa%S?DQ*NכFa hé`hYDt0]1T~_;;]XHd؎5#K l]Dl-#5u:]uLN{G.ĕ(;vgqѯAh(*E^bh,9jGm4:!OAO+-bzY2d3m",0C3Mp x=&ٰr79Csnh"^4X*{V?D(\Ni/oFoT_n{En.d64xXL08A Dyˆ$i$-*nVVd>!KP [k ml/£1|8r%1Zz3n܋[ r }߿{qs~[ LD#+^xNA5.^2NBozo,Ւ5+tw{(@@ۡέwu%jכ{_^H IbhF4ϔ3cڱ7tJ6m{9R^Hp2 f὚ty>s%^ 3+o.< vP 0EĐӢN(͛m;wdJq0xZԅ& d~޼ܘ]E>NF7hQ88i ^`С*FD\~;ya_$[4à53õsƏJ!/ 1ߍPçRsg#=(B2g_5 Ẃ3Afa s>ܰ/J[/>4^, u8e?<\@ˁg)&pE}IkЏyYb`]Xc\y.ye{E٠s u"GmYy^Oڻyxja(!v $6] GҰdxye~!*d{^AȦѶY=׃\-n/Yk9\e֦YP"f |НLՒC*U0%1~Z}$/>PaO|F^g|eHΦe} 30S0W%m\I]  $y?n XoSc7TdO' ®UwLFȖL.jⱘ ZSHJ'̖F=Ԙf-t/xwxr^:i3؀B.,SNW,QRŁS11,kj~Y=8OXЮjQf4`G*#JI!d{pb~^!]ғ >..QHƬQνԊEpI@A#kWE,T")c I̷w_ _ظNDPr`rs:FИ3ʻ K ֬57lezv3%@*W3^]X ydn:}_AB, =6х\,{%}2`kf;!+¬xmYjvHh%-4KzN2(KGQ'ÀU{YCKŀCx’@SLGPS }=u ,ĥԶ{n8Xk $ڄ ]&d^.,S~+@.E^p'áj4lO@,tzi;@y s E$0ns_7ha.ZC|z$Z$Z9x` ()cնd5@@;s7 .j\s0|i9k գdfsc; ,8}= 4Ō bsEFӺ \6*ABhk)N*fqG5(Na+> @@H֏AY2ÌXODx?ŁyU. KXOF ~@Y`)aBZ|y 27ܗ'P Ǎ ]-;E4$S>";9N"GI`B>yʺaC9hRR/g/\WJ 7Z!w`Ԣp( a# +qEёVq&dqWL\j+\lUFJQHB+`,ʱLޫPƹSX)'6.5A> rm60.{,/E;L2b/ u4) tB\c_x=| C4KJD)%5Nǀ0p.g=,_̋B <*;Ӕ3[ɚq }6QqB@ ^]ӅUx1":( 愃~R5T-Ķ4"BSze&,!T@X4"ۡ IJ^jmj<5<m6h 󉂢܄ujnpT Z!6Tӫ7s&AX@M($R^wPWb[N?ayfNhFalHg3C>rE|cxmܚXRbQr8khmfZGɑ9ЎIVR)wvk8y] d<g~t V\ gx'H8ꕇlA8ލSDݝB[ɚ>xan!~5 #L^? 7LZn6}Lx$rC>5wzV3Uv.։ lU HgPCУo@5؁xgg4l w'ՙcNNzA% p8S(3!qȰ kN6I} yD[nBbQP4Gmrj('- rx@U5V5cIh;~{ys-zc^73zR([?DxR^Q]W~7?6>X; e=F{@Q~>Nސ!Kr4CGႿD㬝*Gp$L߈de +k>b.^nW"p4oDԧC g2,jjRw) Ýg,Ol(5Ò{F&qZIyQ51"ȂGw36`~}pli ~_%tbcX%E0;J*3)~kj%ɍ%W[!%`l0)z zLdMn#?& RWNY/WS+#u*e] רڛI `s(=iNyD@$ |DZ9aٶgMc+2/IONJ~M9OC-ں^n]:.?{xB&Hym'N+PS׊VҨs" P넧BrN1>R7\vqy2 )Km.z젊6…vˠ#ʜdD?qtǨ݉ew)(1V§3"1u5:eg) V " xъu3G U$h! P+EaMT`ޓeXA/`LubJGc~VJ <Hg\|\B~:έ,gfҿ^c9H\(/uy<|Q7Ɠ 8/ /|p0WFl#PY$ރ dM%:?vԈ>  SgEw7ʍD1T(vCOg(Tώl=ۧ Rd?狞`cJ>CYb78y<&>YvƓxWOrN;&x(eo_J&3MDYԦpU*ůGY<R\N<++r!ms%<4}c W>^yk{,܂?,C"Mяmhh,c8s6]fnrTk k0[@&Q ΰ~9"X/XP^3a:4y|a3u!#xl?V?>oO)מH*+fN$ BERVCe S K$*5c]_q7cmc&'._Fxk^ҔqRQ˫BNEwiEKy@l s'T( D&6&9h+A8A~R,nuPwVhhE*0d^-;h;S{_nWFLEҒ\v oȰzYZ !f 7.Uya,:nE'/H@B`TQJJZc-tqѱWt^_)Mb- aQ G&-fEJȹʢS<[kx!|H 4KEo TecVݱj@.ތլDack%` z<;::,ٻi_Ҹv iF;.eNVߨ뿄%aVXWʥAm:R۵irzHbPۗ!y(nՊoˇ{IZA,' [mJ#WtwQ{CM*c)4 q* k &{|_1ǶY 9scacC*`oA `Mm*9{j=bC2w>'i5])C0A@[R[w-jb9WPi}Os!~^/qV/M=-9d +ݛ3CSK'_:(P>ݏ$r1vhx1uz?9&N2UZa]Pl-9 Dm_T]SB_va6f2 m%+C{Q~C_?Eӯ:9|+3*o1tSۙt4@«TI;?ztp3 {`ў<ۂhWL`plI1yR &I\dZu LU4FR5fO0B^,i `cFiqx%!ȳl/bV.LjU\B,A}SF@gn{c3u a"j^?Bab]Cucg͌nh"*Bz zT*Q H<1}gpOZѥ,}S;к(+ O V \r>m9J01xC?{>|ʝOȝڛ3 P̔"@i5 ANsSrZ&  ev0^ InZDry;!cSule&.m"(0z: E#>Cme?Y?Z}GHL5 %Hf cľ.;]4~LV/ M R U,ɊR)[Ùċ(q}C35'([s:BF7Љ9_f"wz :ڞmj&@tO!Goi_oKqB0Jx! 1柛}4ϒo H02]Y!m̙F ݱVvjUUJzZ/G ѲYYM0]szI^2{i$|˸Ĕgp`5SUUXqf=jpr(##~aXzd@ӠmOw<_l1W>x|Z] 3 D؟ ˳-=Q 1 7<\>X6ܾ{ΆQ[ ~ &7 :.x)O 14ݫuy;[:sK)JgJk(BI86[A^W 49̴&tvU.3]Y <^ޞD;B p(IRcə>4@^>G}6"ҵ䠯X 4Hq0M@"Y–MC ѥjE|5uJ&bmz'0Pyvp{ ( ;L~E#DCiDx)UmN w6mKǭbf^SfXK)Vyc.:T}ѸiŢRo%?9+q=M6A/mZ4ăY7 嚩*H4;ΆR! 1XP} a 4o:1\!Fr&Q)lUjh*6 L*P!PΛ!.ym1&~čE h|IKpģ+f/'ӫzSuN=ue):Ղ$=PPwHL~>)8@5?:Ԧ)ΰڅԶH.r_F |j- !\"-kRC^=֤;uѻ}^ E2(z7U9^WT5@HLu"w{ĎX6? ?FJ ;m>7Me)w0fٱ-Ru/ɫIzyjȾm$e3=?M&v~Kżɝq9M6FuKb.ain#~ <{evj#;} &h}B͐ l6kFw,N5a)f/'⸉y)4 ߕP{c^*Aȕ8ނMr62k޸/ʌ#菲Ҝx~YsT@_.Z#z=-JCeG3it"{Uz1O*}|X Rgf>.tf8p#p-0v™i(,䕭v7B;㳎!?hn&ZҜ$ AĬzT$jhgCN)Lʬ(>Jްrz7L\ף9兀甶t|q4FsgvS(zl?d@MgMxռ\CQSVg鎿BbF"ƴ>$8GbQOILiս)R'uLb2Kj@(;27 KĘ?,1 |߽qpDy*[iF'e|O0v:*'՛ञ.l]IduSnDy>bRiN`[ܽ80ZF;iQGs )@̥#n {6 | N5` OP=?A Ē̦qyw$uso~@83'#IFhtt[l--:D~2\wWzKS᫉W]a2N#N5@Ppz0#Vb7>oXB.pK;gN I?19գi)uf+uEq+H7ɶo'hĄ4+oPXrA @ʓy9^?E?oS@%I {#ۗH[|YJD!kfT[׶Xk6eqтi"v} cȣW֠ɡAgv/%iy\^[^'8RX=Uk[x'k(leP^j]kJǣHQs)*G14tC"9(1R`ޠA)(ɸےa3G3YCjq {1G}NbMY*Y5Cr8Ttng!p#%>kWZ74=R^AuY1WG&#MU.e̞wUȕ ?OA;yy[B,PD%~8R*šuX.bOT۱ Ytnys>aenRtj8 )h5*ft\Y5CbxAKpz }Adi }7WP{c^*Aȕ8ނMr62k޸/ʌ#菲Ҝx~YsT@_.Z#z=-JCeG3it"{Uz1O*}|X Rgf>.tf8p#p-0瘒C<&eqy?[Ҝ$ AĬz۷I {#ۂ,̷㕠GRrX%/l.qy)6c0#;)srۅ<&HJMN$5nY(0w۷]`5逮*6 .Z@,<}0*`eL/9?#eKN40ASх3laj}8?s;ҧmY/{U"w5A|>5 9 S">+>БLh*:GmM@]_/uT411橇^!0jw$Ւ ߹03T1 ( ÅGFf~b r8h>X̂^馵u1#zC%]Strl4uR\qNU\R;-Wn "u xOˤIW1"ZUV$%I J:bF78}&\Jm.F~c_ȍҐFQ2niEŐ3T1 !ȋEKϾ>'v4&\ύWH۹'p,6SC!5dQr3Mr@'5朞57DZDD.\TIhg>3*FJsflƅuQ˦G^ |=t~F JJH8W{j vE΍Ϛ"Gɒ[T~:2LaLYP)5hq #8¼&QѶR =LwJAx$͉1V[;|J\%,ɎT0Uy:3b\.ךo1`zF"Ofj X=t d_ȍ&i$dL "G9ĺdyȭ* 0egUGHtrc -+j/3"no~j m旟ZϤn" CM_v*W`꜆a#Ĉz3g{WtN$5nT?~3a4&Pz|iTJmzDhO؜L-I%Tra fb;P {Z".p|MjBbF懡K. 69$iӌ.;#v['p5/4 ݴ+hE񠟗H$b*EX.ҬI#\J{v0t !ۮ0nq4Lm۪\.RA eLӚ. fb;O\;i`\0(>X̂^"2_g>̠bN9/'ӫzSzH8r\^YMM,i%" 9Uf3-D:IT۴dQ.aw%vlP.%1ˇ"H# `UWnD9_Xͦ4 $];BVصJ=*+fj%0JdT)(AJqb{ ysnrl<\:٪Lۊh2ڽEه"IB?Y*'wB>1397h!{r9"J̛S1~7a>3,0KeW~2Q +7QN̵ew oN俅g9ˬHi>xXT VQ,mG63z'*Idae(cDX(X8@ 2dr]gWD[ր .NM`ރwBm&*_20<()庾 蚡Ơ b@rNL+;!nA{Ʌl?o> Y7G 2Nͥ!2:.o\+fVOvvt1K{\I$'cowPOK{$e`-ұbTGH\,F:ǭ5PψjfI}oI{ ?h $u0Y53_= g|{:f~UYRj ) bO=tP}8=,%EhPu0wj~a_jm(G&iCֆ, #z#Nͮ?z4݌!&ݍd,3h1 \Q zZK\Y(I oߎKGDZ XOyss-O[>/+2 p H0?S7Y)0bĂ2U!s;D[vǷw}m&aF6f[#x$ߘޑ:`~S:YhK ya&- ǴOtՍ=8P0ePi}K4{E  lдFÈ߽K~5+Uu(c([Pdq|Cha'E8Z"f()ӎ|t#RNNU lx$zĜwuKJ@YY;4ǗD &O1]dqaL !b(ZE8Z"f(b)l2|%™CŌIcs)RÑMm'9>E(GiKA>)ii_ܝ3*wsK77UsGq|ʾ|4[@ilc쳅91)s$NF9]L\r'[Ƅ3T}V)0 vnh3mV>y 4"'.%};']aq哦`_ |[[l wK@ncqb u Ag  s'qZ/Q̡1WUgTCDL5Ņ4h6wb8FpL_F%4 )NBj*c~n⤌0XU{nJYxΊr3X/J&jny.!qeb41c(L3YMGP/D( 愃%/a&@$ЈFNSYŢ0Jc/j*x6 K&#e8[}Jn1^ugAdA[s;U<34d=m V̇]AvS S&a&4`iY.ܦʇ%91"uÎ_2vDϞz/SdKǛKvqp\SX[j99h䋩7[ۓR#8}MSd㯂'i KV.0@91݋aikLmqfW6߇1:ReYHH:׃(He kՑ1XA7NsDBlTH߁d>Bwn,-|l_řI9٦ ߽G+s,pl1@,KvN&[K[3aY0@.%il{j;1V%јLs/|jkIT|&\r1>5JQ|(;0 ),>0 8 +V&UY\qy#_a:qb-:tl@!Z@Tw9cR33ܑ)]\L oMwwDTHӇ9N 0G')? X־QW-΀"rkxwJO;*|7~!4 dFɤkGqĬ>Pv+ [x9ܰhǻ(a=<ǚtܫ]?F; SG yHt^OII,IB"?1!Rd4}?2yc DmR!7(5X 2f܇w i>`"`F7m@p3r4eCC R0G2,jvN Iϔv= Yea^'w#:S0l30 5l_hMsNZg f]J!yɰ;~dBgE#h2O ̒ ɰ;~dBgE_ 3_ʒn_@X޻Iq+Q^>cC)`r.156Yc8}6|:#5QdɱPT}m^'^3tjMC(|?Mzc %5(V0 z i8]$CΔ:u} !Gy<4Z!H 5?a{ZjC# d\_k15Ĉo#lc쳅4 O3>Ɣ# '8ԛz"+VI -vUL0ǏҖZ|y Et^r\8`KE@Kep(gzSoCQKB+8A:H\HbìTSop,-VBłI/0w85qj`>EזɦۨKCC((h@ič_aC2i84o~S! G+A&|*8"dX5%ϯk}3y@BWOAaCW-N "(G$ thIEKqqYZiS*g`l8$2|-`UY*ʼne=9ó#!1:4+ 3K 3R)Ѳ@T7XÙK=P<6fu5m>| M2PD;tTC ^9!9KU,(H+cv[ ~#E muEiWef޲a~w0Z;;~ t{ɪ [szj3;ϗRWc+]XZOQFz lo$TipIz M\<G]`,o}4!@.bTm!UT{$Jh_\Ķ4l@@&_nS;_$@Cھsw,eui`i".>>~#OAd"O;Q>K멉sSa|I!7`fNA@3ѸFdʔ.Mއe >Vmahw̜̿ c?tPXwFN7dL o)b}aoLyFc!$I_6Lurɽ 8ɒs#~4+(M62)~2 Rw&  1-f!L0.LSJ2J4NC4II Ɔ!W3rVI^|'i3)>o"rrBV \pQ ,Q~w!szw,^%f?VBiOCv_`+]MӜXҞH9̕}v~h\oD3U3C6M]V!<`x]_c!v%.ۭv"v3١CbTiphA7zpmj~@sىa_7mV8uȠP-}{xM%؀ ! )=i <8EuC 5¯,ޅ$@TW񳠣 ?ep ")a'=r.0%޶| :̩* }KQ{KUnsOG97)C)Z<͋S#''d?b7<80@p.&3VA-yb*;xpz& u[!c9q(xwMZá&Yp ano?E Ҵ1;}D~2 Ku䮵J/H﹎-oJ':VQl }2G9-50J7rB܌nn?RQaBiߪuV{ܣQvg[vX[PL+K똕"ɗҗ1٣-?':6}OE`AWss4dL#Pr6v}eqnOꌄ hG= p%ͷY>. "!Ka8gi>t/~)9̅GhTGmҦvp XC@Ϻ0M8^AiY/Z"9xG9{N^BS@Eo8bk#o͉"56TYlffIOu}+&;λWi_)|,<|k5>?WRp03Ia]H\JiwU=s`E4穀! 5?=^K>01O`b!ai_ _],Z M'2wr=B5vX-_CY_~|.1>-M3-q~we~22߰,ɌZM! 3+ t\%܊awCMĦ3'Iv/s_n'O/2sξn`P1WU Slb&!ܓԠRkznApd'#CEQO3IFK={x6v99CMqLRl Eՠb47߼ˢ5/佌Y|g`X=|AjDlz$w}L6'+t6PU]& NQar*9rĬ̱XMϗ7H[ J 3m:}ݩC\4R.-wk*6λ?z$(ݒ\G )ʨ_2?x؂pgCgW6,{N$ôi2 'BĖo&s KpEwp p"C?gh Oq0O j 13XaX!z7PJ r@kҷWF^.r1sdqٞO/e:,! ^"=ql!0hWyycvJ1% IŞ \^}mrCoL|0_xEWk pj~&e"#^H3lk~,J^|&.0p({5H- utzֻV`]oʳ4|I`/34~iF`V ]ɶ?(IUB,_Х]}TfM 30Z}W2ޓNJA[8_]ȒhyRIcwYc>>=d2oAP Å$sfZ论AH%et R9BNK3gzv(G‚K0 gAntr#/,,[if|V/tDʈ=\o諿n ~ǘEk[B[cVXR'2(ifW.?`̢qPKrV1g;*Ej~>t2;y `֕y|9%` QL<6a; l1Z쟨U5YT,v(BBדk?X̯q痮7A>Wܮ_JtӡkK}0ܘ{!C1L `VFD=JsnÛ=M%"코>N`w4V vh0)v_ɝҤQ}ց-'N;N\Eo"U:&WDaiZĿ!V =iT'3HÜRK@%f (Y R}9q_|Trxn+>׉+iXT#o5qzV!}qYq ^(%<Kݙmfu'WPTuB}ͩ~rՔa{+X`u=N.TՄ`[S(<+2Xf %!LSV`}a, wA8We(kfcrp5U񔐾~Fu ϟWK#StF}yŧY/شجٮd\wWͼI/ <_I5}w/sM_%ԟ*wBY据7A[z7"éM:~x+=[, %F 1E3C&aL ג%&i&'RpСjMPG#؜^YҖyvtn=NQ럸N:1wc?Zus_)jf@nU LP~lW W0:/ K;ys\.ä.-d۠3 曕^ r{L8N?ҴE&8]@It%/nz!\{>9bĜԝ:#qeڸsss޸Po?>`M/lMaĎ.@0nG)FJ Z*/׫ I֖/{#&iFߠjjc}%f{g%]jd[4;,SE؇;ԗ0GCys'rnitW rGMnH*U 6+Q,/8||#IQz2eރV H|/m.g]t3ڑKkd96nz5@\D0 Z/ג K@Aزeʌ>Y^*{y ߰V;M;>8I`/O:g#林JaΞXAB0o8t"l̕m{LMu^Ǩm)% 8\Hc8'~ޣ 5+L Vh+_^'ǽ|o .պ Vϻƹ4#3h‹զ{4J3xP+IH-3 6v՗:Wm<QoCTc^(.̩ۚ?.\ I粳) \ d ej_Q_p*}( O!%q8ųo& 5G$.Qa p!|^7SV'i8&(;1{P/ummu=ⷳqpFgvm ό;ČY.)>LwK7Bx&,Q,E{qI{kuVDaO"E:_- D3g3xBYA7@/+|8+8drlg9nPxX]IŽ$TɥDm$:{)$(Dڢd>oe(jWA]I u杼?VUp֚]'݉.!xy_A­W"jpzKa:r?[]Da#x 5&Mſh`o!kF/u ,#p\$H}R!(3v~e$J7PQ~\ @`>8,שoܩb/McId*hLW[B,لELkk遘nʫLJO@..|)H]W2ρ8fݵ; L?uIhU  Kj!hQ6j!"wqٽ-o̊MR8&N(;%~}9OW邂ӚQLHr"u [ڝ)U` T鏘5t yOiH>=tDʈvf+N'4mppx+dsV*ҋQ|ֹnŊX#ɑs8IyئW2aDQ2('hhm&n;d_shɑr#\u_"6ԙq9!HZYh<7Bt[|wQm#̔oVsZH[>!\V7}Pb+6E%l"jq9[8$45> aw0,~0:*jaqZ-QMޙ,Aօ%(J15;>c? n[@0 ^Y+B/4qQ,,剴Rj)!|,j \ɟ>rG;.O_GiY\ȹ}|.:ayj_NJAlxj_E}y]䚾0K?5(U!7S\$n0D>nEmR t"W@{ɲY J,)c؊g(HMkNeG$JL:QGLLNBԚaGo8 v-,9Ɇ.z?q>(t 3c8 13.Df*_L\r  7n]LeX)^C8G$ЇtꨖM]j ԘS@usJ,`Ft֕$]ȹ|.Kª+-:t$$0vcT,,E=hvOդ&^ ՞E+=+웅RrF$Cfi}!o_<ʌ>\ybMh!I^xF~a:{`OhbPLϡo;p}ޓy˶Zd\(GoKi!:bbYBohmQ.0@Lsv8Qj/l3C[?s5CAݼu0MR_ ?[2!ڛ@9.qXŜ5i+rzB VYrJesqeNsxLbx/$>Pt2+mzDg1PhF_F^rv74_dD՟Euϲ`E {t cm֞J2 5 6"X߮I7aDz׉+iXT#o5qzV!}qYq ^(%<Kݙmfu'WPTuB}ͩ~rՔa{+X`u=N.TՄ`[S(<+2Xf %!LSV`}a, wA8We(kfcrp5U񔐾~Fu ϟWK#StF}yŧY/شجٮd\wWͼI/ <_I5}w/sM_%ԟ*wBY据7A[z7"éM:~x+=[, %F 1E3C&aL ג%&i&'RpСjMPG#؜^YҖyvtn=NQ럸N:1I٢HTr/GJ9c.&EU2!#DNC:uTK&.HSSL)^ :%cs#:kJrB.d\TO% VV+ɕg {/:)'B @(ndA̯FXf׭ a"wYʍ]QՂ,Owlw)I!WízA lLQJV2gϪb' =9w. ?KP`Iw˶ LTzL}a?.rP]h{!6EsQP<܈ '߈1]EB'9V,AL_Tn?jZ^@Y(Ed:۔xp],5Kvoe`1O7M?wɕuOGG1RE0ypf|ɈŹa}}Dq6.=Y)nB>t#P{C\(hqǶ" $N) )5ʏtHlZRxG }.v?4 F`ϲD)|; L|Ti%b!)}'uo-} 9Hu.B 3> T*LC drx),Nݽd gώڲ@o%`~`jpUB6K|8F˯1EkG "kVX#~|9xa"@Pbm/=1Ar#75khjj 2FRzwz6=,mޑjXy΢D g RpSspM;]'FȳcN/ Rb$ d4_BJ 4 xh'iw>3ip'T1NUn ɛr ,;@yXKTk*ЋKrLo=f 5esZm(r'xH02.⠛U g\GF&5{ĠvznFAs0u|s?S0Mj1$I@MO疇 Rwx"`7,WReaYX-)\\$Onv0 p+g \۸}Hƚ.-zԙHQ2BExg Uʮ!&,2꘺unIkcbFYWM(OºP ־X^Gj0 | ?Rt;$Ɏ['hr]k~g(jAՇ'81OLʝ(jРd~Ĩ%J>fbӁc旻& ޾:}I<D4Y^>`F=Tcf0fW@?e+G|UBނj:MZyQlDxJR4;ƈqa ୟrp/Fp(W PYl\%?k=ػ5E*!f#y$~nQQq4dcq,U(4g 峥Nv ^kǨ Vnҵ4 ),by,9@cDCGR qV'㨙3h̋X=5#bSe{7a/ƯWCdeu#wn{zoٔJ!k_8qT2@5Sy#N/)4éO)7jd0lc@"9zWE[wb 5gi3;xmfULs`{+9S`` 0Io_s_>P ^Mk^!2l e]:\<w[SBj̍OX;v]z?ʹjhY::b4'o7K{S q(ڊ7%[IKGbK+ԩG>dTt18 r\"yz0UR`"0&v>m+}χBZx!"}rK7[RCJJ1H=d%N {o2\PzƖK:!/P6DJ4xhFXɇ0ENis4!Zb1̔f2w8>t>hzY1wS.a(9$QD3`Gp  \`zh_| D6Akǁ~Qyz&O$* pW&ojKf.if)\NIs )f L TԄ@}=#z#Nė̙S㯽YT-H e7CDsnI*uO;}wדcYnUoT%6*k0TA?ߐ,hK#M;dOsޡ' J·GzA)pXV]%~$YU xCz(L|>̵J^k*m7 5=ˑi{E00+ -2/F>4d7\0mCS8ŃIBw0b#6ŲDʟ=<F"'u+)6pZKt DXF_*h8274;KKŗx|E\2Ǔm)0 u_\!Lo3|k8>T7  # 5icذJkI54[D2Y;lWnzij &6Kز]}ESje+`9Z9g Ӿ)/d|"b'yQoZhИiz`TsTN.l2u(:qN|wux3sVکg9=e@ s?<*l퍎;~O/Xך*c)LC[oΊ4##J٦l":`R@4}ȩf`r49TM}iJǢMWk2SHÊJM/ygdhԲ2 ӯs2hNu@j=čǿM_;T}yYgIܗC!d֞|yZP6 lK_>ոUQ9 fyqf-Ƨ&Y3~i0d]O o+cJnE8|߅J5U " 3ϡ|_f5A}SjŮ7N'r*^/q=F5P€H YQUY z!ô?ې.{2%嫻ӆ9ݶ &>q:Y?w_-x8b*x/v,Z!zJUzR~O&{Ȋ9{Dq5GE̷+-'6{`}IW zhD8N-MٷK'VເPyl-~xPcէ@?2. ؕ Z=W#8mg7>O0# \[>.32!) =9f1'Ju׫ Gt5`vGk+`qn/= +HO%oKl^3?JJ!B&N 0[oAKQ:ƿU-o!V^gOg^~+ccww6=6xAqtOְ/C3td =|?]3< )~-aW5֐2O񕲌 pg*,mPƟ}c̈́9YlVK+ R[KEsߟQ@1/θܦfkr; +Yמ 2<sܗv;"ʌ`;2˥׎)5"./0FQ!U?ב$6Z 䤛r&(Qz3J;0ntZK.~|W+ز9l@\Ѫ$cUoA X)Q|C',<Ė7vS=L@ҷԎM*9cQA s)(Dޒ4`'Gώ=3`Y( ®">ZӚu^9o2^uc ?$S5rtudО7Ҳ pMA"zoT?-C~X/xpcNespܵg~d+5]8$m69/ CVleӶ X Jh!,`A ?s嗟iQ, _aFO?|iFPa ٮ a@u"1&0%H" ߫IM Ty }02N\Q9!w ݤsisYހFG[cH xq 9>TĥFEdغn,F>G _Fl^']`JX=>_r1,ԗ2$0)Z #nB<_q7El,>5UbWfX|RqkA?RXlQO:-Gq|b鍤mZY#Xґ3iݿ^;"BF=ĵ %u,8|=juQԻ5'oi~P "$w@ rKBΝ"aj2کԹOA7w :HtU( (ȿo:4AIK-w"L:Y4RSg;-=ߕ$3kp]ߩSOd֦޳=5M|!Xür㞖C1  Q@AO%H`%2KHsǫinP{[_~`3ucq 5ʘzN;Nh/qod#fjȊPI*nuӚ.?az>E} oO+cf"7Q,I-su61{;k27(U*knnUcKVjUId[߿RSh7MpG,M.!==&;m+L. #+ms׫Šۛ"NG,{wD o"z DMކxG!9wEwt6* 2ICP+#guf^%?$mDž%Rxpx8^ܼPL p)]lJ: btl.]݉nBϥ0~%fP!YDENR'3PɻԆH\>Ob*:-Z [`۠nK֠ٮ5ۓ1Qh΀RQIB? ׍+iF|#&˻wZ/]Pa#|7]fI!Mޱ xzt7s-B{XA[J{EY ;/i\=6cBX-z^nf/!N GJR Tg]_UҪvG1|z"tԀ L2?ph97-H-%L̂X@Fjǵ!X$` *01271 2\ʻ뼘2p`kJi(ѫo@Ao !4hL s [bí|{!`m'%w8ةX.]ZWbW7) &P߄/|BG|dSD⃚iGi7_ %M" ~LΖFKz~XMcd^dm6`ΩF*Fl<7J)|,MH _ƍ_b%@fu=nVG8eVɧg+зXbQ2TbHE^7c eRC=/f|Nk4֑i2Arԋ[t5]7:Zcաo 2#?2SjF[1"l5yFIpQ0kXo@OhF K8C:O_NɈ%g>8%!n>E8l$im,r4F=yX@2AO{|OvTN6흃# ~o,[Vd~JS>Kۄ,KCKcҾbh~VXvv̈́yֽ}!$Eqs `V-sDņ*T"|]1yU4(j8OԜIV^y蹹Fwހ4}h| 2$8T`j#eBݯZ5U<C8hmҺ6T%ڋݖf0 T0 3"p/?ԟ6>|y"6=9?Ec51vJ,5VQ9]U(ԃ)k+䠽8bQLن٫m;Zo⇆{KK>'k> X|k ?!ƘA~)d,~ŠI9]|F)($z@(/ bŽ8؇e d c*N9aIX_ƿ[1ԚOv)o~f.P ^mUmlxDJ]mD.Y{N= DDn?%8л/a]uh~w&}wÏԏFqJkCb>Ͱշ ˦q_}ĨנGðE9P]qуmN-S&L#};99lyX\hGf9swmF27Ecu-p8z&O+^BJ'Xi ;HDIaV[f2ߌp,1ƭӠ6‚o54,8+S#)V7q崞'} 1VTnؼV?NLu{]Sbgc慉YBLj.WL+ŵIgb`Km~V v`tQ>`FAXiPgoB־ ͍MdYi\jBje\,1IʎPC,52WIJ^,-=÷n1FgaOՄ#j}l .uO8bև_~/.%SX j mz nO=ÊFr{4 ny- ТK;Uemf}׾qD;5À6tQ`B>,r$!Z̗|?;{ tM !"d0b>K1&sSqsO3Ft|xOwh12+31vx$_ ܣ_>ENLbGj9]-itؙc2cPANRN&V,  wv#[Z+YYؒAbT΁'yݍL2,NM3$!x b_F81Aۏbْo'SUm7*c!ۋl{NM;ؐ{5=ĉ fٯI,3R R]?#[ MX 1 A , 0 ,J)ȥ9.>- з9HovDyɏokhr,wLV ^<-a;TLph=iD/jWH;O>%ҷfU?NCm[^bXܶOyR[JKHv@]>iT=aB#jTGy(&Hq뎗]f-_$I]AJMMdk Lˏwmz8}&F޾:o@s?P $i^0Ř0oe(D.ͪzHZBN.]e.og M,{xÀnij'7N}-X]EKșW| uގb+%p;} eqѤa7ٶTJVx)=@{W7_ʣQɚjBf5\&* $oxlB$BUP+5a"xƙ^/z$niN_O4q:=7tp7hLՌXo)#hie9< *+gqf{ÔF-:uDTo_gȩ)eYG4e6/c9+{nU8Aǵ2/iծ_Z!$pJ)v^y DfD ؼ*C,=iCO\U߿q@ I DmBT'i'Jr5-!x\դҘn-DZOj@ M}h?AiSYvqbY5Cc 7P#mg)I/6IvK<&N,]30Z<8c )E\?'XVu @*nȊsTíarDfq!obN묿O?O  S;M ! 3Q$ 2xV&V9'n7T(oakeUxZ߾U*~q= rtV>iׁON$sxMƺP6Z䷞΍]`fK+[uDq%0g;CNY>",4卨[ [`n*8p_A:'VR}|p1ǁz%WE&th[jcpTM~?ևQ=LRT{Ѣ{,R`띎!=d}︬6ڕK~aeoF' v뙵^HFJ<^HrSX$KFH/z -2(C7AKuGSA3 ϒGklZbthЄaf bAvZu;*+pS]@:ʀV.k~eȽox1h`VH!f#U3WZ_O) ǥLDjvܟr@GݢD{TzYY`?EA[r 7a Q. uUh⵳G{]<zÇ3CH ܴ\$uꐎq 8e_4XS59'_ ν OyHLVfE)iWܵ=g_q>Nd+]9}[h}/L`YDܱIVL& -Oy+s_3w"H>&IyXr)YtlLD!TSt{\f PyV jc1v:f Dq$| 8nP+7paC{WH x53t5V'?\;eIM mTc {Xiˍ (Yt2H a<ԛGbbv+D3k疼RA1 [f`i2 ^*M%xOo5xg e,qbX'2]L/yz­%UT]Xv!C_JF@;Lr[F%t{eseTulbiguQ.r,TAT?AxXO*_"łK26ie u"xj6RVr|Q@9rF9}>?~uӛzW$%kze[^(w/H:OAv+ur_ g2m}d"VCrNLϊ䷜5vbIICjy¶ٹ/#8~73gbt4OhA+ww:kWlrT97$8;'F7Dl{~aCiECs d3 $%mg[z@+؜t3"$ %zM{0Sc-fl^_8ti.)z-n΃mf =, 6B zMwE/g Pf^c xLCX,v}ҤaC2IE$C50WS= {x""E|D#dUJ{?t"y\ ;;yO)]*cOU Ǫum=6Q!eg* uAbXTIZ}av8oBf?<5T[nʻPy&Aoc@&)mm@hنl$iH= \*oJ(fqw'3|55U+ü$iDK`}O}~q5 x7u.R96cuSYz#(O\{NuSH\䘣9cӃ'G- YNefH (n*wxWx ͳkpwC2 |/՞~z|t0r/u@VCYb$ wR}U[-V\D?'2;v8_aÂXu5-QF@(pP^v()WN O4 ޔ D44T2z ru[;ʖ]_i2, |%ЅJQM{#ԅ]Sp 1Tc%f/P*g96J(v;Bq+}C8-LU\C`{rFB u'=P)uou^#V1 Ep;}E.NkA_pʜr9wFi #@$Ojg/|‹脶pR1`3>o^Uwc6YxmTO |@ֹs;e2P<;l4BDexMO&5bW7* YQ l<(jhN![cڥH :VM!? Qb\kP`[g n^tѓE{uqP,[ߊROҘOmoM A@w$Qᵿ>BZ4R$Eĭ1YSWR>?⮺ G(ԛM2JQH %7$P5mw춬᧧nz>Cump7:f\'v +`~ pNˍ;H(:|˅ngEAa|s7Δ6$dcv{=cH;*Hl׈μpc,RfS[ ^aq5p f b{C}B].? [5m,dPX,E.s5-Wj,y"ڱ}oh_LlKAe_}{RPAi#:Df1/J ?DȲ'fCHAk ?vNreԦj$yzU4irC^.fed9wl+G`%_ms9(틳\IϷc5OɜS$1w3=U 81:t?+[Ȇj :<`k̓ e)xu=xĪ-\m֪);jPJ;Y]*LCZܗ d+ud(F|RC# !@)a9ΐ?pC~n>IhaCFv ~}A(3"I_0 S $6fCKAPjPx,L'r`y +֟,ϒߠې+(N,m,g;ޗnZK SAwv-9w@kT6=KwSo: $0_~ aA݄ʶ;WT1"8se|Z]^g? P q{' WϥӖQ`?-{FXwD@ZTPw)ȘLl-3tiIr.i)xkzCNkOmj3. sL;eŪ@viK%΃d/7:MB&,Pq|g,_jڈE87ʉ3c2Y̑y ߔE v:gQY!yR4vMnU1Qm$@|:릋%J:2^hMTj/b-}YތI#BsL^țjVrXr}XGvD\yxE@rϱV>ʥЏQEN6G{mOfbQP#LE>ʫH8Y@y/ ]nnqf҅Qwi/zq=J|Bl"NiiPΒEj #zQTөw2ܭ>w?uFɵ`F68:] }L6n/|e4VB`~)0(glAmY ,*Vuf|^ץWg`\c$$pZu+ qj^,ޅ,@ ns oUjP~ae/m~_p9P`Ҵ@z1Q*>' e5X(@ʀYoFbdx|֒m˚'z7 |dEt|Rv<{z)$q6 xKv&~z>$*tV,ĀgJ})հ}Ƨ:kD?]8635SŸ|YA_;6$ɰ3! $ݵʱ@2 ;~ mԷS.KK|Xt.hNR|KN+TIej4mh316Y_P' v[|!iz)J b.I(çɡe`,+s*JC1rbw)7@)HNKr;@-s*JC1x,砨\%[ʺX}4Tt2^Ou+h[$U VFw8f,`ʄ-ĝuXQD{N8WyISN#,g{Yǁoboۿ.h0Sf&vqG|'"3'@UN꠨2Sf mxv{a!4vF9PhK$CO "P\dw.+< !tQT$$/,5SyBQԯw{,PP7:œMX 1 A A$%m#EZH klv;e \'Cڠ߂6ʩQeT\lݑ{}Y`9姡J2b:e7$Kq B[ԕZ7fHUv@$.D>z.]dz9<>`j*I`dXoڟUbлJBQ;2זyD!KӻT Sg-8=i4 AZx[ lgVzG( kd9)Bw*ci5UcDҾwҝ%g>g6˜w#e+s豘*2Of8՜aGt63II{2z6jIfVE%2ZZMُԶRGa};DY-n$mxɏ†IJȶA 83T*b%wa!rkV?O>XRw GUz4HK+kUXA~[8o#>>$e^Vyu5D/Iģ{j}ع~RwW^K^w\(s_[KDZr-fڼA~i"{Ԓu.C˓ovLj_iw7粍WW-2M{h0G|D0QhQtzqg%_S*L3 >/aѽO;&t(QAsvLX;^Dю=#삘N{ҧ.JN:A\#8`B4#b!j6o>+ T2!>Z*@] $MA#2iT/ZT @m6zb"3M&zub Vl & Z}%SM-mACZؒ nutlݔ=P L<[9] dݝ99Gu4.6: 뮣|PP0D&`GRʆ⵿IvVsூAgJ^deI +=5*jxv`Y,4-&WIR'Z aQPN|C:z$yQ+({Nσ@g#ꏿz4݌!&|Wi C0MV|I;IzdA@RB)GG㚔{tY BLn;H%JV>gK{Ijco-qgpF>Ϻn aNzq ceI&|F u5})v NVF4wOo|qD Ꮽ3C BNZI% jw7KMdك0AyT~`4h4~ʕmڝ/aMv} 4: o زcSS4nXn5^ =z[IۧbITe;R~TF.`Fr OGoE云Hd+4M@xW6Vv|- B4r&H]u,e[Yn8LʨB2DX2.tMf;I40D 8n6? ÔW]L$iXL#6ypݕcj†LHݖH|؝1fmXG|Z ZAF 7UUcTed‰ΰ9 Xt)v s4!j}˹63hOq+2U1]lve\Nk K5B\|Vx!a֤˃K 㬳{E^"d  ;$✗~AfLa3!)$܁@CpXp~N >{xs&s:B G3 Vɓ;G@[FH%5} 4ZHS']&8GSuK~!D_yXTPdw%fXOrädS<^J^{/Tg ĄȈ]?p%rQ^Bl*O[FCµ@uRDۥys }?5b}Y:N78Y *PNgQb/[ [3&py->C\3.ȜZ?6dU}8m.|<s\otrx6FL\vELIc(5b\4YJ8߸Φi-jO[fXdlj]2]zmI6/bu2$"צz\>\rz٫KvQih䀱ҢV>g&^kzUWnf8DQgl8Q`MS4^+z%les[R>WNUuq5JuPRR!Ğ&S2FFk5r=}d23}a#thBFsa? \kM橋TvD@Zi$ػP.;z׹ĐӇVvɣuV+l'ٿ#]y8@Uz`, Exd3ed\(K~"Xی{?Ė \4)Q]7 *(7v\{X}Ξ;]osо fŬy˙Jf tWђLLSf'E?DX. _4@*:MՈ]T2 H԰j iln ziET+OT[koHʤ]YIJo"5Kc'd)Q}?#1'87pqŲ¶LV\34P O5 j>!mV/kͪ$7eÙ$"{Z3sl"+_JDƿ QA*V_+ؑ# X<-a(i4Rr?V $!g7)!KOe[wy*P~67< 7H,~(,-ܚ4q ,L[$pԦwp;W(胏:j 052} a82F}M)9YԸxoƞ(CI0媬hCioS2GjTt$w-tmV}/@^Ƞ\|ܦ4%EӯW tߢ9U\I=>ƙ0/H_X\NAi64/![H{%]-\u痡0aS1y #;!,O)e LR&q/[7axVY"݌Gӎe }6 jd6أeȡ-`vT=`$ڔ CҢ<50{i`:[ U7;\|z6p[]3O%}]V@b9pi' e⯞Bq4r+w LVjV1e_X%[Rvg:ʚ GYq0$/KqYZ},SQ&AVX%"ق3:P|aEDoE?U({ Ayfn1,I+<\(=mc\2'-E_QƁ"i%_dDk{烾U] =C2ʼnQ^-"ݦ*vO%=&v-Rx{}t]we{+G'Q7wMSqM0kbpcv t<ۇ%P/PVBV>h[Հcbb<@caPm`gZO ȎM`ޭLP_دOF͚ xiWu_8%:5ce;!**o(3 y`tI+⍼ &DYz%lM D>pW =TૢY9" ;ݡ&Ыa'5ZRiw1FrFX {e6JzGN$Aqvr-xl`Q ěq5_!烀[kɀf.˨X$d|-s[pQQy\ e‚^35@es3Q1* =ՠofش/LV`h^Y^nZ,ey۹ "ncMXEg'i6-끠@nF1 aDn^ JD^ {@Uiض Kc[ AŁCbxѝM64pVhmF[2+dʘ0n]qzAU/hc.%vm %F?n)RʳZeN;=f ZD~UFs |1djK<a`- ͠s,[#(Ժ˗KlNhyRPAcxyV,M0 L L+)+P'? W*;*zYU*. %c.#{u'ONJ1i$/{q_u'QOצl]V/8EʰdnhN^#鎅k*fg⻘H:ȓ k  +4 ]JtȮoN=5T:SҔ&Zw 8ѻFe2ه(@ʗP7S-%9oi3d;\ 720Sޭ0"v ϐ7:풸Km0yu:%"7H#]aa2E?*l mO+WKaw<zxCn-U3W^%IJ+9Y$٢~mt/n)vMB*f͎7N۴z1qrPkskާ 0x3{A2,gچzȨ!X Vձ'fKP ^/Ə%ˆq/va&t`ڒ?de~V06 6X%&JsMaݲY=|%c;`\x^h)qn/Ҷ6>vy(I@E Z7,@, 5@iMjpv:uŲ@*{ `j~7˒t8p׉3*G]Щ#bw54@4t¨CyB)>܊~Į'wyP&8$\~pl0x/*eF8&'|89E5Zгs\؄:X8.TNdA9Y{+Ac&¦b (.x:!W$|eC`=;N;,| 3ڢJݬI̸?Cq!*"iD3-:ݮT|VƐDqiL W#yF-?D +:M& r&w0lhP#7}< K޸JyfQHHEM[ RMS=,ˀoTi%''o܆"ݹ+Vb&@qIh&Y'oA #M>_Pg팞N'1 Op_ēdzԩDA`?ܰv@G><[7tX`ZUl>fQ ғ,\y`YaOm6WWj[=9G@ *b~|J"gjq5aY_rZp3y;J >-Rb;wܫ5AXhy8Pb'KhhIf74 %?iIt-/*Hnbq8^y0aҴexO0 Z //[z6 |  I, zo M*0R&i  SuK h]sۂUv^)S@W`j"9Ut(Q-VsYl4ዛ0$sXOGnk)G0S8Uw*K|ajN]ݙS"wY˯Z(ybSt=g\@r]~b2aՕ! ~7q@U76JYճXt0qۛڊ<"Ibn%N'sCǓBa<̰(QN@Q#/WI_> |b* 71hzoJBi]e\̴&%OHƟ1ZZ{6NظNM"LXcA.{PXk ћC 6(n2:K0[Mk/, ~")d/rK()N`#+ O,_1:tuǽj5G2\#W ~V'̨끏4ށ{h,W=wMaRnD;n1C7ZNW[J[X@$8U^2;||Ύ~~PFjĵR1X+͋\~@v&@3v~:xMc;(2{5B#Ya%`ɓ4h-nq3ӻB@G&YY)3a:6зkw_(OF<ưk}l 7*G F̑`ře !K'[Dߍb`iAG4ȏHЏFT7jbglkK(.FyX✵"݉b~ұg"֒Ai%uN8c"A0DŏE%tn Rq`Ax86usA5$uY,t>:ͻqhg˝nDǕAn"DC%U^s89W+:K2,&¿ @:n1"9Tk5k-l>͢Bt򸈸=> +k_A98$RHp PH?>ل$3q"Z& '=,_x9Ut"?=c$Jjc ]&%@f)Rn|q]a=^66bT9low_L5 :G"s\2 PwB.Z[CdTGjLQ;oH!Ddcq;֒2?;' 43WVhL4'?NK9Jwy̿?+`xAs~=NˁΞ0-0K w+@-T@&de5kcy\䃋-CCM3KFqNc*(/dc $ J_O 2L p(I2݋Ea^SLن!P-MqZЯNFcd p@FiXD@XQ͎*JL[D<$0stlO",gԈ/ͮ2#xXVQ2֎yhf*1\~&VF[ XNV\Bbg3(⺂9MRTɽ,:^"qu(iVE.[ۜ]CO᭿&ЛXAi` RfJH|f_2ˠ'qAGe /O)IEwVl o9sA43?*h5$HX^<7#Qke@B!. xFXuf|NkS Lg t8eM8gvGhnJU[S{l)O}ڽ]r#;_Õ7!i{,WssphŸٔ'G {H(sJPOJ,1QoQ^cVr YPL"IHŷ /,)\ҜODAm5ŵLpHd]*™wrK ɀN7r+Cŀx\O[Ft <7wwz>G|ԯ,(+\A@ ':*&/Aal/uuY:L~N||VE!?tB1kQKwT|`Lkkk<)Q|$o_mNkGm2,6A9-o&\9,Y$euX!KB|̫?Ejݬ.yO g9HDKWR1/#$فicqaJtt¡5Ym+bT^ihӮgκ3E(i&(Lx"i3pnocZ-$ɵr90i+*Vd6tI{XLS2ƫ-*w0sݡӐU.6PE-vł -*JC1rbw)7@sͼ쥐+(s ^6gOUnO 4=)[IV)Bβ80̜>GSt2(9гd/GtXTHJ\9rzt{ək~Ǒҭ`%=F=P\ õ("0PDFO Ϊ@:2i ^;awkz2}MQ+Vހ8>LKT`8@"6a*Π5$$Gӊ0![Z#fY NDxp&8rUn;S^u_i.LjV^DxnAR Y/|і:oQLU]M"dK :1+4ϫ}Uʯ |7 ӻH|0L$@kygf>.tXkLkYۮqY13rd,w3lZ !&?7nE/14k5,H8q91C"`z-Z?H:oH7 %!KLVJ6'Rֲ!&O|k"P7݂c^o^ Wi$v )c4Z8d? q97m9j{}LVVC9)>:§;T)aPL갉"* .ʤ.m@crUd%D >qr1)y)P~p' ޳(p. 䙯H{T*7=r0Xy6_b˭c)}AL2B|w)6^rmRkiܰNss;{I H1~a @<i7#E&Y RlN6!@zQhݿ{}eC,mNȝʷSKkr3כcj#n\GPD(Yma˘Jipth6i*_|rDE$z5f(3p_[#lq$uꄿg3+$f (Z&r٠-tH"Xj}6XxuܡMkMJՆ{E5--$\<{&ȻXC3| آ{8fWƾ{F=S|QrSwmVl۩^T_񫫖 iD% d' N@Ѐ`Զ{¸14U XѪrRK2OMA}Θh "|R7fQcN9: hkEH:?д77.鈇V zۘt^6F'YY \!Bf ĿoM1u'Ӛ$$1wN|pCJY 0c1UWB'$eLAe0}bZHK_bM=^\R0|ڻ.PV} sZ:9Qe`|:ڲT&ތ#݀j.>ũӚ_ASl6iW^L #718׷O ,ic'lD:P_dInMd¬!XaVFF4#,©iX5Ria o|vxuZ(G -@&=9uAB 7!YVv"[L-)#EǕSp-eyYDxpa(BXq̪P pu-yc,Fc(  s{г JTj.fr^[ޚ}jf\T|M?Pށ)EPCMcP?Bab]d3jP zG"}ЈO {rl^sRx 3~c@9߾qU)Pk v_+CIP哅>|Z(=B@)JCpH @|}wuî an)!ؾ$C}|&+q3X~IkOV,dgΨ@YR1Jy捸2b\`t1!؞;ŌMI6IivXߴγ!-'sSVDrm7_:@%k7d(QK1{+5( H-6~⥊E_qF^VD{F/Q=|jd~n~YsVͧ񯖾}J~߈iyA0t&mHC ɷfZq] l,Ers{ ϛJT4)2vb)QITi>Qf6TPe_2GsPt3 F[haS!z.kv'? p6o4QWWHU?U-`AW-]O:6փ9f|il0{[YmfW#=ښIc;?:"kHz}'d/AGɻ=Hv#f٠xE ߪ(4qs'v^BSlrmз{0NIxQߕ! <6,r*2APe@=7}Gz} %0PMO2M"lvNzxI}MaaC(iWxxCj~ | Gi%'D?qz  ݺ nr5 aOhͲhɁH:!*5Tՠazі Cwb2c#1٣;hlw>tƈhV<տ|߲UI2u=uZ -Hic %JGA%ba_Fp }1$h3<FPsL"hbM'mVɑ~^"1"6bESsQbp'v7|4LJpbSL1ag bf 6am,gNTn@cnmXܭ1Erw]Jh_^OYdX/9+*xJñO2jM Irޡ VCQwwy.jF& .oFSA~!Ai9E@°ǘ#g`DaWb%TbYAF**Mʰ.Bz;N1keτih>u <d]8uL\孛&,?M/SeѳQ5C̹N8W}DvkxSI¤cN}fGIUb-V탩Чԗmo)V0k>γjˋRDpOԬ´T k rEGi/cQ)*\DEϾ/KP<$>hO0n.0o`Op̂$/@Dr[P FFu P,abtIGim4܅̄qt.+~{A>1=,oOlW2yP&F+ʪ s2;oZ5UNFkP~;% 4舶"WZ h&sYK!L 3 x7^ɤJr9>ʺLh^K954"لAd Tp'tD+[99ĵ+/}S*wų$]@ԬjN)N;+Y_Y0i䕽y!tE \X9V5aߝˮ %%Kφsܛ (,<dzyQ%لg `~q3bevd5 @]AEu]X9ǹ 6`>IAN3}q@ÀOER`Ҍ 9ԷәCʄUHCqP +K!y O'agG4౲;}wbrsntDޯ$p8E<:o4/"GTNWgFEk(2tR*f^E'}4?f&1U hc'B,_yK P1'u(i ll2n4@N,.aTRG-e%Ot k@QRM)T?NJ@t@|ONm֦`&4UGj|K1]t9 3N Ûq4#؞x 69z|>|TQ NW"J+3xH'aj$d_G Zpݟ`kUYy͢1v`\B˳?6Nq<,|`uN ~9XFw|kJ>' `2*MΡ>}sـ4}̯+t+hµͧy/`.a*qocq5ěL0.LSJ1e2͵@ == [!KӻT2rBܺ?Kyh^s~ >$4?Xc."HR*C3?l$Ȫs/²a;IJ ? ' &p-,΋M~镍vӖ (%TH;%`\'|f(J4]_>HI]I$5n O7R [>AyJgWbC1REMԵ+&0(UgG)_ؔNAX CqU^ǽ#W/9O4lLv%uym`{²Hhg䝟<ǚtܫ]S=Zl$Ctsqs2Fžt'G[WPk-j 8ƁqF;d,]Mfl2{27ciQH(3SK q_xa? &bڜb6嶝tS~s e |T|G}hۺRt;$ɎF{cxD4Fz0Qwk Km>}NPNݨa.@fxcf0fW@?ž"? #$iې^ֵ nu@6#xh2z`fXm(8~9Cd%  Wւr!i*Dp ; yy4o 8`[-qVT Ns;wEKZ%x6"W "4*l]i7;x: N[up85 ZB2ԌdwAs7Z[Æqpq)T?BւI#2i Hw X,ξ0ǣt;2`oƂV[:F^&j'uilLقh cXzҁDDGpӛDHG~d*= [4s8 2q;9(HΥ M۹ܤ3}߅`J9mWFU=x8 64[lK"6y!/-,; hb*2b8(r2y`i#yڣT?o#`w#<.r66Q ]Yԡ&qq!6gfFg25zDOd pqI%!fApw,p=aOesŜ;ĵõ™4ݧ`?Fԙ08Z2yK%ΌVZP_R8Yȃ>8r `7gnH11VY"=KJ"Fp0ʊbFw  KM"4إaY 5Gv^gi gm(Ldq,N:XbAO*U}簱cRrQb&%f=%NafWV1*AXr.{$V{!MH󱫹#!GlA#׭l70P;b ,1۴wȁTV໠ZR + Gv-rIqK` ЌkPvtAi^ځ ugG1Q 2a&tx 2 A6~ZkVS }wHKHF =<}bB#<N7]T+=F _'x3Ap#8EV5v>.Y1ZԂtR<4bAanEnI3N)l7ZMcΟh-+3{P!N`<=J1],E4ǻ|p`)JjZ|fobo :>D =+l!Iz^ T[`/:8$+-wW|6F.ܺ۔'f0vkepxS3 NaYEe ?#М<:  .]ۛ)"D(Zaw߽%hEλ!K TP s\2>#} Z^tp̸2(Q-Rg@}cpH)ڸssg߃I6 ͔L u Zd|@FQ)OuEda,-xM%eDYӸ0FAcJ*Ͻ~޽)aWwYİz;gLcM?T#4oŏK E@3/,:߾GPCFy.g{E;B[眈Gф,wLseP՝Jf4sI1-g@ɿ .:r'2ƯNzqC sfli ٦Dm+"PB _ZXw_+Ue p&Pe -h/ӌGg50F*>߾GPEFy.]N llJ2BL,&BmseRg8kJ8a %!.y k'!V4PZoŷAK>C}Z%qWX -Y%+z%qUЄ/ɜF/҄0\O?QuHy%Փ IV c,撥CKyFZי vU5 Kt` 0ZFOcp{94O|kg0  ;vMCΝɈɉ!0 C2­5g-$\zNEhN vLCVr@?Yd *aqapaLRaJXjqs ":,pm%'p6Uu ]2p!Óz`%FBbHL6&>$@،Ԝ;&hhV/Pǎ YRWAD?L ̣j4>+kH gFJh2"5s{{{>TK= 2(60yks sn〉H.P#R~F/a­5Z)X߱y8?x`%)XBgyù :2K @}rB۝m^;2Sh1)켥XE`"fd&(54T!(z mU\%Tz>R@rF4=T0LI/ACr*)nМ-| U UbW5srbȳPX(@&]3FI͎W^>_yz{8>ƳduG0^YS{Kj,NS@ ޙDOtCԴHɲeC+(煺Ŭ|}q0R>oDwմY_^@^D0#y8z26IMOh|a)=ŷlmW.~>TNWU-g1[L"wE̯,E=1 3{S g 3jn[)GwQ I-KC)f $b*zItc`*ΐ(]| \66̭t.>X_GeFS֚Ү|yȇp;rz8Ʃb&,;Tۏ!@qXXF}ҳyOQ8pK1Uwv1!Y\ Dgq;պe^ Fêo?~I7k9<7N,5S`7kUZi^qQt:`)ݍЬAc& s1P꒻D$:pc)6C40\S}=p@:;cyU+KZ1y_EtL܊ ZB-s }Y~(HNDzPۤ@pzȪ[T\H9;#,{`2 .Q۷yMkI.,;Fɒ7Wt Z``EjJVʇTMĩdث ԲBYƵd+HzJ\8Sq̚8]i -&twi,„o !bHM\^0u__8\=Ɛ7Ac,{ xlaud/Bdy/fЗti@)Uj{Yl#YC}NZXN#fjצ>`ג0`%)mu44 ChǮb޸Q ݝDYpe$ )Zz][ga `qPHir@$ (K@ XJtd+-XC0' ϑ`?)g.Q E MG>*Z"oha1~wE0HiKʒ#`O(¿(|V֐ ˍ,Ii gדnwնʃ}x+fF-I 3~d4fa'h8޷h"8=8eM.餫~"O$z!b 8 ]z@%vpU@ tj] *4 bd(a])4ayntkI'mx&  ;-c,!\C%a+L5'υ Z1%Ad2IQ.lG03rgPk?15} +и(3%q>O9*581d䶵+xtD/(#8[q?QCn?-²"oQpY"&~LG@،@[w;ST0Gn17w&\c8M@c&H[ ]Ѐ3Ui*[*S7gbR gS| })sO}L s2hu28%9҆r6ގ <# c4#%4Xs=q{a}$Zߙro@({kbcs2aw+ S C}'v>B~eP2W'–:P̘S_kE,2" c+K9btGF~x`,!S v,aV_{:VϲJagٝ@L+egSwǣz][ga `qPHir@$ (K@ XJtd+-XC0' ϑ`?)g.Q E MG>*Z"oha1~wE0HiKʒ#`O(¿(|V֐ ˍ,Ii gדnwնʃ}x+fF-I 3~d4fa'h8޷h"8=8eM.餫~"O$z!b 8 xड़TH`iw»9(s|.\`Ҟٗ* ɩt0* H%b| 'TmN/$oDk%!MŢh6~Vs>Z7b(cF̦iAؑuRap7b \6o3=_IN"XM4#*U8L0sBH峄wz[a9<z@l*|ΉIr cy+_apU "DL.R!*MggQS%Y ĉa]ؠ8Gai.B^ U;ρjg`ҾOFXO2w:JT=3E%CK8Q)Xևza_X]8(Y&|:vC#k3|Ej@Vv, 1zS~6-;?nӒE/KXM=dY`_!pmU=-hSqrvټkcљ=<mSNSzGjD+]zEx:9 t"~ǪMw5x,jQL&$@Ѽ%k΋i̡ ?@w.ѣދP%j鵎kNJ]"%d# 0 Ri7Y<6㳂ŭk/~O~ٞ@Γ15׳x\Ep__#h1 iN/eR@MK4a[m+)B'ʗGp)~_#c#'@`1JvwP6Ջrt.Hf 352iA0Z̤9Kj\U@**? vEiXġSNa@$yְ %\ZY>V~3:n-dP4R>v8\VQsmjH0We[6V(+ <1Q9(7 S9:+? \P,@[Mfps}Hya'H40b >6w?Q[`R(^]zF]΢# I3b,N@}4c͞VAXhpl6e2dSMR2&`$UsoT#ۄȆj Bi/1>XrKF9A@xXibOr ^ʄ}6NjӒQ MsMn銡q VR;_hصVTnxS6;o ײP/DEV܇^`*ȃEѐ7Zg"Z9Tlg ȠlL+ 8UVt^u,{?'jwL {l6q,A( Ϊ*orw_1Zł0hH9!<7#Qke@BdxiWxߣeqz]e+w8FuĿnd^)Aq<@j"MCX}`cK|х3ću#qIyX^n}Z'=8 9k'm ]3F|Adz 6[aK )HSIw|m \K'~i EOzgH4( Yyuo i4{wQ0ݜ=wΦv#Vsy9ğRe|<,>f[YC ׌a,-81REa?$p\>g{Y֮.CJI)t"!u.<7rkϨ=O1/pZצ%,k߮&-Uf[2IlX,$=_S7Dem9 {\%Z~#b o@O5/` tN`-%N) 3NHuE72 ۄdwHDs~vŶ-t^=gj%$mꀞVx%h S{#Zȼc`>- -0k+AEv"+ =]՛_sfBB-%BQER n&4mVM oe ǝ T< qXp| h4?;2De13k0 4J\ Q'7oyܲ矬 6Q &Ch|qYp$<;}'HCݥ2|wIVl[w!3Un?1.z"W Gz({ # a ۓV ď~$Ć CҫГ)JHBX4Ҍ-5|ϥe*y exxX{LU*O)әq8/n7>JjY6'sIq,DK de";`D'2%- j\wз%ߵ>#fVؿit9€s#h\QϽC-tԄ9)RoR+5*aUP1R g2oվ2$m#}מTkc!:#qnw@}}ūq6SWo!X~\GvMi~#"R49UF4`eqz_$t0#F 4b5#ɊN`q 19\Lz/f'$ЊR{,e  AN<7te;ף T8iBOԞ"I: I7&{c*?3 Z-z䙵zmXSD"^¬>}GWn=~RP0ډ? j}kI S޼}łSLxpCҡs[c5r/5BRCWv&+p>V q Yw>cC$ŒzTI7<4@s[3V BIzCbsB' 6 #&Q9Mxs>v$U@W! mda3IjMhnRHg0}Auq'5WxΜCT+g_7Qwqr;3v@+LeJ-]귺\:8A͙Cj8"a'@IV.|k^*?` j"2e5*UdegM^`sDvF968\_#J1uü:Ss;F&qZIyQ51"ȁԔ%ZMdxE8wk^v4,#؏wg7:hU@7.vr&GNL͠6.t:GRq95`vgG&Ȧw".0g"< /n34@k ],JpJ0@Q}suYFbT)#^Cל("Œ$FxIbPBp'wߗ*A;1eACP1/wkBXDO>&,`ӊ'"s#_ND.T_񫫖ՀF֋W|'21 uz@>CNum %W6k[Ca9/-t> O#{2΀#i^~4At@f]xcmmB$2}BEᥰ7ѹyX_KS;Ktj(b!ng  A+XYKpAXhy8Pb'O4_37>N` nsFf'$X *ܸtB 0eQ8$  qbiD!?nKz"ߧ s !@DeryZRsP Q*"}@Y9 -NqAꝍhBl)[4L"E3)KT mWk|8 r͡{\~>XtYIN,Vhҧ Xt ;0Cמ5 Wiهm wۖ Lh+X~JI`y=_i~#>&We\b9 pd 5c_Tf^(#a'Ji\2@r g|HG RFC/iCs'֠Xkum+ׇBA)[P.?q Ҭ` ߮!ӡ yq@J7׃$UrA_;HZ(^0hmo{C{JOD5\i`h<*e4WYEwN Ͷ#l Xƥ_ےItvw5Q9x܉du&8{؏@3Q=O|]3C ]/zt~\|/y a#='ʙ 8]իGϭ\zq]ܼw(`H[+w3\3HT]oe>뱣U(ɵbA--|a-|dR@y.ߗ";-_Nti}@XMg 7,H,5A8*nYlb\ٗ4++ƍLBPi8ǖ2|*u>S:ʩ~ P$:b꿞},!"ƿ;qWAyfrzގhܺhk6y斁 vQ<C)@wr׃M;K*=?eu/] a@e^T%Kt W撛G1<`t׭d/#RD$ٱqswp9WAg8tyIث ICo%rTMȠ.yʪ4sr -eK~ݞzbi9^ '|Zn7"؛4{7EDtg,=#Br4 Ն'o?\`FC5)@+ yMI H͢QC-5eF|NIzGTQ ?DP{aمVD_Z'oj"[هak2Kъc嗬wq]Bb;8 iͫIK-`Gs+$҉4&<HZ8O@ָ ̓m4(0J[4qQwEDz&/!ɦ!9SuM;@oGqujQC>ͅ׷% V⬌xEȦiEU[|PbO/Z]Zpc&ÌX87\]CzكfͬPEy/"h OPN@n^q"D`./1#2X}2osܔr:`Ofu]m0@^FD&s3ݯe!?W;ٕR k)L?yijƕ;#CϋS@e4ʖ-kH'[W8@##ꏿz4݌!&|Wi C0MV|I;u`c2i.(BE \u H[Zw"g$rCYu-qh+4'U X_6xI2ɻ57uKUaqP6{>@@ϼZz'BbW1p]cW|H0nSjsS d [mf/2KM[F[=1dLNc\Z3Ͼ96 nvyd%bٗnv>˗Jjpة=1U.`G,2m/ih#8鼂 2.o'2/avU̸4'Bc^q%5'_s악az EGdDF3JNMN,ȳAUQPy^ ۓ-l‹"6Ox"߾蘆Pf 4Y6e_2H6qY}ZUg&d<=#Y҂b25hUp69ƊA*5 Fڸ9\.#9|t Ѭ%v#}Ei%w6N‘K耇zԈԌ MrbֻFa۪إ*F짨(d̀<=LLaxQKeH>:aP dABa>ʂ m3}Vw#M:WQf2dNHvvRghA#Ei6Y[:#6.wGr#FƑJ0EZ6[8h=X64G*md+` k.BWتl\v\c{E2M}Pw Zjv\ q]cHl]oGgW쳍vz53 b5gb5h:s9\Q#P*89Aģ*etQ-[|In,}rUɨmZvY耍5wrFNQYA'v.Q3%h73FF~Q'ֵq yyMmy<.tNN2jtsb3Q> ua:ܝ WXSK~ ;GHp8>+XX1 Ce޵bԻB`Z,Uo}L@=K=ll2̄I"gFWZ@Qk70M6a/4+b?7z@PK<L}0mʖٿ ?V/Wi8 |;ZsM'mذ]m%VU>V ~3P7RBbj_s S0xmaTnX*%Z.xu~!obj:r"gc6{laki?̌Tv%5ľS' ]wF)9>!$p!ɐBFSPR &=Gn 9}&Be)|iEnM.ʖ%hQs28M.zNpq ω!7jh!^5| a><,OF)b{r=8.0ʇ<ߍ4vHA[ lA-3&!5IS[QBsyj36-koFB@/NG Ar+qJh4T.l"8weZok2z[z= @ٗ#-v5h V 1kÄ%?0mjz $^2?8vy[OpZ~i)%$z*7K/GW |Jndfŀ>^m:b-ª$կX4C]GZކ"<Hb])Y!`hӏf #Y"d~Eɹ)"X 5|Cdyjb 8 .St xTȥoImǞsMŮOC8NJMc#7^Ll}vt7G1`oi +whٌ*4MG[˦~ٚzGoB2#G@M6 K$؄N$lgyXhiHcپ⬿SZ_\<}xL`\'Hސ\=:F4] iO5I9ֈCЄ)f(_VsNqU0J 8A@**Yn}G@Ϳ:ǀ/hݠD_A`^ȿ|xsq_s¢c1ȍoY s/QPG(\;%MS Q \@l-3}>V %lѕf󨈿c7ȇf } ۻ+ h)lY_cLr!m;_-Zsŧ)Lַ_Ou>UdwuL}M"v8ve̩qBv}n.e } 'GM%Rhר:u /Zʻ:F,d .dB"h.4ʐ|<܈ )ь[Q{Ӳcɉ۝Fa4e`cIB(b۫E_"gҐeWS[qkPҸ:XdtdͶBv? :F^՚vB;&E]"o'QR^`}=xcHV??m YGVZԇZvv7rI\X4EBu_|n@5~ CqF8ʫ_#\nLO*H&ƉD3& 0'Hwds.?O3:ua4ConX#$ amʟsXeµ͖VO63tT^I_dI%^_$+ O(mQꕎ !:*ǮƵl&-hW-Ly ,&񉞑彥%EQ|sfAropwPΛ(̊w_w!9,XPT/f5rw W"q ttW $$mr{P /nosY0]|~ո'g'z, X24u~BL|taljb\h-N(]'R77|9Y0CQH/w PPPzMɹfz03suojp?HP̅Vw$07&'GEPQ!B4Ƀ6xg+:Õ#~GJT@vGJP3gs/d. AC~.\:\y"Iqd3;\b<[(\ "0څDg.u!<EtE{:}Ұe1#=%:h'ϟ1'bq30C.W>7/ Y~ '6K[ S`˴@H~mXCs~KlVD+Hq>nOk!fSd)e;;AYgӫjtYVs""|{Cz'@ٻd-V=E'Zt,Da9%f퐵Z2iTikuX W 0&YsJAޣ=pԒ|[e)傒EVWŭhmP,EOY9ӴmpbX9Ч+jɤU;ZgG?ʒ/COЂ"J"(=r $>ͼ>rׯ"u(-Tm8䊜Pr\]}FD ix.8x0/qv7"2nzuѢ# btD?j':rV'd0e#ӗ`"Յ:?vfI6U]Ũ> 0M1"2 HAy @gӍÅ6}yt)ܔ z8H皂y*tO= :HGl :y_4iu:g?'xBhէYR}MbĚ iH8][<ѐVʉkr1셫d@i}6pp{ _>Ss>-O%`#ر,Uiz6]ueގK<(qGSemZѧ;bhEҘm F-5:-j7~Zԃ&YV 1|C;f'Iön"и螙8f K1G{3hԲ,֋ݍoq5./B4SR8 fct7,hUXZb5 !ĩ9FnVm<ص/lJFQG)t QJ9Dr2 0fbx+0vD#GK2pOTNVQBvEM5M W \Fw^?g+nT- y%M|9&SxBby8 5Sh ׀hN/ f=шF<F,Xc%}e xYhGDdY`*eheR/L>2۝Aƥdi3m;k+JnLk营-r7S&t'۫aĭݭu̷$銖bap2 Iq קGn;S+,oyDGȱN .^5_qIhM_7z\GX} %#-Ua,Rl'쉭5eUM{2QoAl;ǀ`<@=x1Zng)Kd~jwߞ bE6B҈6%j'6VF1G)mi)by)qlS7X+,6/*(o5F X4Dh YF{g>Wd[+Ayok40](~+L*Ai1MO$ao*M8AI֝zCyϝ|`f5mGTMa+`BCmT-%\S kE{`r<=HgPŞxfn,!+in(?ćCQ;_љzvBN5&0\h5 J]~oexO)^ʶGj`!ت}Z? D}3)+alXvs>'y^Hc[;=hhNۭl"%|lЙ<7wN½9+fQ¹b?60>Ǿcq?Q2GYq_J!Hfm6FmeNb^98'@@n&6T/,ð|7Pn:mYhk+.]e dxL8!Oڈm9nB޸XNb-1m(ݦoٙfx3~nk@Y7)hYdlp<NDf}^uVmh8Hmf߼YB76;5*zD4zHyjL;%1\VIlDg1#!< :Kbrcyc$j*Ф%_tI֨8$*LZ1p$BQ;>{4¢ +dfM.:љ9"R&/VFh/3HKΓt'mLwsp>@o{\6͂%ުf Unq/WYW3f@ <_Ӎ^2+r9#~ x7@}z9 l(jD4*kI MN4ϭ>衻?4NlLfd!P\^c1BG=:V݋Fc@r#yphu =y #|SYMx܇IUc4Ƽ{g7Sad/Vù#cF+k[މ-":Cg[ؓN^A7`*٫Ӹ' NNjG?%MC fkQ^H2ǽ h^>\ϷxW/X9v޵s$0 `!z!Etb#gS>Q-`x{$G,Y1`[>)_ɾ60\HM&'@V℠ddBۅ`>i#B}(c~R0'OUǥLc7_If@ᦨzm,~B4twsd&rʸ}m_?@oz'%k ?in F3o/hHqrQ^[W%@uR}bcưqE5a~/ڔIe ͙=xNpZ~ZمD.j$Zq q~ʹ^DY†%{>x)j >qSEdA(n&99Vk*̨rZ/Ȧ„-2VHws+N]@y ޼cIOd(&mmљ/Ace]tvS^;+1g$}m=W%Hӛ6y^99g.Hiqҵ V^x6-Tʓ~QVI+)49sotW!Ⲹ֍6KB;:F| Õ OEuoZ;HSTQ8+Il \Wo^D?qx'C: +wv.x[=y"]&%؜p :;xS0AЯ&A&n`+bo74/-%7yB+KM0t0wZɟCYO.Fqx. ;tacō١ǐ=UX#ѡ)2Zx5N9?e: 3 ֍Πi8@I:?\Ybi=, id\aoZ 6IaYCj}NqD IYxF ⓿JSܕJ'XnSz/rhh&ӫ FIb#G+@b$vo63>)CsreM`Hw Yp#a5p`}d#C3lDc-- >= 0Ӵ8kiJTWй 2WqsNd[|5knvso]wTs||5ݶ nElť ϛM7{RIr:x^d&pzQ\w{dOKfІ'LEvU`h+.cvxA@ n C/ kRރZ"ʵh;['qI/.8O|RɅ^S?Ư$d);5%X$?*QZbL DOqDPE+]o&QB kb`OEQLgZ_?ݧϥ.V ɗX(fcݪݎƆ@jvU_?MfN H<$"/? -T!kQll'[`}пx'lEl[ KTR +}wSDɞd$mܑ$9 z vC+O,͓A mTp?(뉑L~Zߤ]WO Əj KOiYSqY8zFK U68I7%Jl&]0"ף m^^r?ZkUovVľ}X";7rRpElޜV75❗ Ruu!$Ds{_5/ۏSe1 Js; H/ "TDOɱBΫ FaY9)DG mo.㙙R\'fe1(MܝYz'p廞L/J}u^DՌ~8̺Q~Ƙ~Q%,* ;@+c|NǞ,2]YJ[D~MoBN~}u7q!l$ng (t{o:Zt??>\d1r?q#%tl $wAG.桟n/Ze#eђyDS mOm^mEHTy_[|f/RtyR/:\@ ̱Yf|,i;k+mhEphZ[/+̓ @˺z&OTpz/~rD͇] eVu!;7Ό̗gcdFªx^|AEbL-}`xpchSDhɦOg|nA-'?pk ֑A 4}pM D7d0;,upa@! l?}G~(E$H 5#?3:ӭ2.FdL{PX 6Հէ&[xB;f}Uv*Ό4; ~;l/Bz\g&D8. }51O݄[/..TP c- b:WkQXMj&yn+\ŋ Vi9&t!?iZn="31 W"$<1O׏"SHN7%aOp^F~lO2rtuQ<3Xw6L'k3TRMs4^". y(&fxyx<_Ew9m`u0X 6 dЁ]]Ey)"B5F H~6.0,aV2m}l$rrʏ'c#lfA:lD[zp"h"i(.X_yd}oD~HXT(7}l|Wu9\*b ,3LǗN̑xd#ҿCT2I5Tn fј5sb]Yf A-#[*(ﺷ;Xa7mxMT?ӣ7VUWmaZeFT_[7%QRKL{OC+d_]__ځfebzEF C #mq $R^F/N4oC5a1rNm5W-ULggO˦z7p&KoEwHGiH/;ݐG{V).x<4QsgN fq`Gb 4؇ݢC,fǏ U>}F_FND]X sW !(d+VN+ljVuد!آC]"rKؽ2,2nR2:r|y!`8'Y]1m{tQ $V \ӽ"iR.\JIn}8H\@3BrY"X.Y9ǽr935;17*Z E`%%_fz4Ut u?eJ+d`?8zL 8o_P<'<6Pڃ|ܰv|:48QLJj\ s(a-q`Jċ{Ǩp${F4u=@(s ˾&0 >yV5eoG!pm8 ;C!t_4AbCN[g756I4Lg ,$|:~4R*v jҼ{Qr!O9 xl1?^rn5^7n񫟤':ȿakwevo.?9o@cFٵ9#4XJx+lbsnۘaKDq]nqa+?|XQCG̅8xkM?"| A{m^H/FC~In،q7?9QYjv12Vg&.N)jO,k s$ZX_t8D݇B6X4Ƶnxd񖍊'𹚫 zξyp =M#{-#H5_%w# 6A)[F]-`n~v?L-7,}%PanLodҳ4O?ԆJ8_~}Rv)t-Wpw @N^9SX10Vay+r`& HL.:qP}"ʚ~̊?._G%KTXs$xDCX~Eg0NO)M2_n55 I_aRw@mx6CcBw> i6RUieRlݿ<'pPFY %SsvU1aŞ2ag]XYU3IaGExv|G*F[[jULGdxqx fsɫJz".>od6PEZ2&o1nʆ/ 0&4^Dwx^\r|ѵܷJg=/Ɇ?9u)5VBNb \`Xq̣=;aB|,4r]\*:oa' }ڗ!n 08>?4.!꼼RgӱTx>9RKIg`fTcӛ8d3D zffv%H}WG$t#qB픏@9p)82˴ *̠!z|U pLauLdJ/Ŋm4{svR-ƂVw6bNH㗧Roš ƒH}BY6l ޔuc1*|zV6S Ɣl;] ~AUl!XrH *d a1.1Gu,/؞qy{ʛsl҇rtB`Qhz WOxaD2B)Sd?nCV+e _R%JjM!۬ CZ%lb]^Atm)5n;!K4H᠝vU1{uCt~υ!H`M*k|}&2KEG˼ >h[,$g2"#˯mH_"woMh9$y~#&õCB]n}GRޤkA zq;E58זF΂U_P\y}n^(/t~<˚V@mqi$w4eզ7 ZtMe>kJ [r4*FU-$N*AhNn$/`/-r'{ǯOWoysPT<N`&( 9k/ 4Znͬ*buI^9ha;\7穾^tg !4Z `h ۥ~ 9+?=Mf7F9BS|>g{2 ]=¤wf2- 6"Z`.3xe mD9T\2)A\EtO %Sz,đ2]Q^tbai 2/fES!{KUFn>o#XN`4uy"EP>]ϙ0h^r'y.~^.8|d^<s:u= S;og X5t")zл0 2k"~pWGCh 9AYM{ ʉF@Tڏ l&(S ޖ'Ɠ+2u01Y`>OH!k*hpg``C-!vf 6->6>[}ݓsTz)NDX`cbL= hGD.J]e~8W2XI3F{|BSԝL RIH5 㹘K)wAD%2 wejKTlLNg;:By~6rp%8 AmoE-}#W$|+@&k@ݞ`̯F;nq,*B`_z[`Jm6I$"!!48 gn;˫zħAuHogÍ\~ ٛL[~|b#ȗ/~`HNYOO_?wP}+?Xypq/??Eg_'QzKgŸOڏx_ 5s}/?9_Ooo?(_Piso俺I_r3ޟ_迼u;jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS|Nʎw' S)nP|wy35DC[K MJx>m\DCc5-J>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:izhy@Hǻ[UC1ߺ/q˞]%HblORXΥ/XTӮA)ֱw{I,YwfQvt=^C"DxU@F5N쉻>_2~LyH7R 3u+t, oϖX(L߲`^6I_l{3gX7y: sSi)(kG,A LcF 59!J> &Ȓ8Ai\6Vn in:9陃k!&< |6E>ۥlq6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫg<@dּٻ`u#rcVNs6͹L n6"p|gE[N.L6sBINtUWNzUOB͓lm#XlG/aTto".!,hO[b :3x>CA:Ӊ5VUu v MF5,2+*?xK :8?5$|H'hGʍ`j?T7-#O*}SdΡO mU!ڙ{"pKm+ &NS,ro(r΀_˛^]Xe.१/>}':1LQ/RZ3!ȃYVrɟO]2^ I.aB˯Sg`wEA8P}b.lPwRj"د]>1O5;ZS&"t!@#>=gd~t6{.u /mäG1Yi.L9Ͽ٠-C!RFat8WF5D-,rdWَc4-U:t=DO>НDq^ G|F뢥OyW:)>jZ%=>m\DGͫhhs6uS}δJzg覙) 7 m͚MJ9:*%0T^GѴ$ZgaT <<*EB{4NkSTq{Ϸ8kpy_V訌C]$l>ZUoTۤN/\:i2.Z(G|_ub ̏?zp'!;n("Z_ n2}-7?+*o8aZ7F<Â& Ӝ냚&b;82$0e=Mjʷ*h?dhV܇,+1@[ U\rF0Sro?`>+$ݭ逿8hIܸ0pGw}>4+~Ƃ3Z3r#ǫb#;uAޤ=%_yQ1%/^a#F hs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\D{#^@Y@2  aEC-aKhȂzG;Sl31KBӏRZ)sbv\s8yK7>gAX@'l.$gF LTjhRdLͫw *:_^q!AΛ0dXaV34l"K5S}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhsNzrpjGT<-jȪ{Ul&$x.L^ݰ;P?9fB1qn料)@"p.[D* *WpzVZaδJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}%esɆQ,oYoByts$InREA_'ˊno,Xc )BmYse4Mj__LN!o\Mo{%_:э܂SZ]Y$Jat "QLm APE rjZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>5Œ5yiWh5vD0iF# p*ÅP+~,j P(t!+}9@c}!^ץ4 ɱU w2i ar[""4PzQ~ rÇnjdw*f 0pEe17h(_$: [`м]EY&Bɾv0SBκ{Z3fQ?[IpEEw_\F/R+>k#CǤ57 G|fL6Ql;z?=PBT! {ʕ!DOaML+VVH]s6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uJUZkL:[ÊlK /oB=P>*ʼa}+lGPRK_Ub勞lVu͙fpjK6+='B$LAM`|}u"ԃCV~<_3xbLK=~MM mG4ٱ"7)4Ke8ִ% BU8Ĥd h"Ab_zC*sRke~muMRb*'*g諎9Hhh³~!TX=f%n҅4жP([2fefIy1v#S(0ıl:U?@"6]뮫6B|:=nX&ۮɐ 36uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}H*GTVх[U{Gͫ|v[Ej 'δQ>HKZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhsy'!@^r;ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhsSJ<{^lPm\D5 )]?\"oHTVh~u=>m\;uS}Rm ``\>mKp76uND V-TCL 3eq1{2q8YM(= gc,&eq^ܚ]Nx:ڽ8(W:$hs#PDhsOB}ΈLkDG͚o։OyqxbEj։ +XLhs-?P`\9Z%=1#84C 9rτH"v[m0-p=윍{ԬB1ADhj86uzQW:)/lNFl~DhmþQ>>m\3]um\D=J{ϴ|څ5IW'GͪH<\9BR=PT+ВB{⬬Xy"J4SQs$;(, WQoHI/az-@bů2,w">rry=DQ7=%<󄕚yoXB]HjB_3dnmGͫ^jJ{ϴ|ڹ{}6uS}δJ{ϴ{H`'GͫgqZ%=Q>>m\^2L OyW5 |4|ڹs+UδJzb$G X 3?oaî|"9U.Uzl2* ; 5CCp_ˌSi_Ƈ *1vs>`ix?ĕM֞-,Mo6uN& uS}zW+UδJ{΁Z%=v,δJ{ϙ %=>m\̡%|ڹ։D>m\D%;6uŠ|4|hY։UBytuS}DZ=90``\D1IδJ{ϘY2sZ%6~OVD"j&kZ%<#0\D-ΐ8}ΫxDO~**&+[s_\eS +ץf"(rPXC9 ,#ViB/2NՌjj!풙@(*VnB3*H4{ϋ.Gu( 6"%``\uxδJ{ϴy_-{ϴ|ڹը[GͫhuS}@6uSoF"w'GP )>EwK(h_Vg<}'i*`.Q|0t&F*,프D[|/0-1ΫxC0z'i~v$7B,+Din\x#\K Zp+䱄Q/y:+΄AҳRV3T /g7@,7F*WY RjE hGEa^u1!ӧdZbcQx7.V|6J*SwgQjy7(UX Y YQ(>m\C>jViADhs_0QZuUzQ>>m\{]>m\?59jYحW:)鈑Wg0_~/,g)&ޛW;pUn*U<4u_`zKb>m͆%4``\Dmc6s]ֶw'GͫemП 6u 6uGs>m\D͞Q>>mZg 6u*3^}δ=Oz3>B|%^s Ifj-4PRbp(7;ڡ>jZ%։OyZδJ{ϴoW)>j@։Oyhh( 6ug+hhj;ADhs%{Gͫh{1h( 42bީK}gp'ai!@pNATv =v֧O|TA+p%͠Pq3WEZȶל-O1FlJqme_4zz@Lx`=DF$1V"n h+C=R܇M`H2 b^88o >cf?cFfͰP^>w PM-4/h~af!`ՖfWyA^.KD+Oyw!%δJzBhsbj̿uS}$%=>mZJ'Gͫ@ 3yW:=Զßhs*w+h( 6Es^jK͗Z%+UδJ{frhsభ>m\C ||4|ڶMHxOW BnW:)\``\ߺx3Ta"`v`8?q`Zђ)X+u-w 9<X1AԂ oS5iHKM["rT1ACJ ngA; dnS9JqJ!URW:)m\TcTOkDW4M#4т֊ث_Vh=QZuSFX`(W:)Ȭ VgԕADhs hsz>m\Di%Z%=>^A7_GͫhR\ajZ%= V}!(Oơ6CB*clwكmӨ2Ǿ$rZ4gkzQ;m換{57>jZ%=>vhr)Xw'!( tO]hh/HX(W:;'Gͫ|ڹ։OxCz9 'Gͫt;՜8˴KD0Dhs!(W:bQ>>id`;(%֘":ylnP?))9a6"2UJVhOζ>jW(W:&uS}҈@W:)/~BҞ66uS׵jZ&S}s:+`Ejֈm\0\D /[+UδJ{%qW'GͫcZ+J{ϴ|ڹk_^=>m\%V[>m\D\8Vh~Y(W:)Uuqhӌ(W:T%a gc,G;ƯJTP)X E+4Mgc,3eΰDz`*(mE VOZ 'GͫOJQ6ʕZ%=(W:)+J{ϴ|ڹR"Ej։OY BOW;16|ڹ։OyHQΞ6Es^N!>>m\sDhsjX :/|ڹ|_)jwGͫg)+wZuS| ]3UδJ{ϡRc`s䴴J{ϴzU6uS :^hhhafOy;9s։OyTW:)>-ZZ%=<$>nVUS;{8(H6 =>m\%=>m4mst;ϴ|ڹ>&W:)jZ%=٤s'GͫS|ڹ։OhsG OW:!C鳭Pwc L|-sR[".!o6ヴCt/<6nl/Ld8=kr!)@|"/:z(#$dXm]BvJ4H~X;*\ qr|m[t?ߤM  Xu(JHKݛyBo z6vA"c?O.sS'pr+$4xh [Ԙ%#C}6 HS.bx]Lj@_2)dĶTUjI/3N_z.4rpb+[BA1 &KP)+'w@1;ؐU/zV0bҁ^}]K[,™t/^;$yZbmwbu]D<'CJ !̯ZNWWR6]V2;gƊ#fr|Imh+b]9vwZ`\#ۅNw;<^Jw±2\!HQJ鳌4oǙ}I AiDbqVCoj>SxvEV050hC0X& LnUּ_'׆I~A$w Fb>!*`Ʈ{fjݞCY !7ߡ:*5"d@3)Vz#b /ɚ߱p)9,#zT IjjUex77~J0jF"rh;!,b_ :H+~|V#_2}(~KJvc1gw,0=ԟ,crQq+:|kʉRH 'osS(H ?Fbγ^_'EHOWF"^YδJC%@6r| 6knkI!}sP Z+;NUiEӫQ0`\֌100QZu51j@:6}Q˝)ihfޭC;8'">m\Cً"%~ !NfxHW:)>|Z%Ə|ڹ։HOcJf{Q>>m\O2)>dpX(W::.sʹsG 2LOyW/{GͫhoOyW"QS+9_ĸZ|,O 6u0QZuO|Iܢ|4|ڹϔAhs.W:\hho)>]4_[kC|ڹ։BSsEeݣȾ}AJ Vhە)n;յfwkQδJ{ϴ@פl>]PAdBB|4|ڹց.Bj !tOW';҃#uS}בk6uCd Vh'3hs}*jY~ A,LœeS}CQ>>m\&1rb)>v(L_]*BTaδI,Z%=>IbAs-}ɟJsw4ʇy^jZg||4|]DhsW_hsϴ|?fo--W>jZ 1Nh$lXSr7`}df[;m\V1( 66thslkDG͑O'δFE!|δJ{ϴ| ``$jG=N[GͫhVhZwδJ{@(+hhAs*i{--K EjvYZ%=scZ#i} V?bduZ%=ߤ'Gͫ_cOUuބ:fs".J1hh9,;Ԯd:Bj~w'GwX4؞δJ{ϴ{i>m\\(uS}W(W:)p 6uLz%As3DHV"Z7~w'G ׏¤`!"8%y Jh:hf>m\?6u2V6uS}Ҩs jY։Oy ؖOyUs[XȅZ%9&o\Cu#]00QZuEyp( 6uyugZ%=<$,HGHоD+hh6uSYm:)>쳳6uP~>J{ϴy%jZ"P``\DtZ%=uAuS}cڹ։Oy!}柊X2up|4|ڹҾw'Gͪ9ǼGͫVs04juҿ=}ŗsѡsBIP|ڹ։Oy&o 'Gͫ*kUδJ{Ϫ뗏Q>>mDWp|4|ڹj"-6uy= 'Gͫ! 6rOnF8V6tʽrjYsM#}δ=H~>s} ||4@BB|4|ڹ}W:)O‰jM} UδJ{ϲ;ϴ|ڹ7}ί<₉j>m\Dp 6u@UδJ{Ǥ_¼G?bdDhsP5UZ%=RW( 6Ek" upzW:)wZ%=sӯo>jYs;J?||4|ړ 'Gͫheȿ|ڹ։@A}δC2_>m\D)0|4|ڹQ CrDU]J{ϴ|ڋ\sW:)>>Γ{Gͫh L\w+$u p\3huW:)>˺{ +UγߺB'Gͫר >m\Drwhs:Fyj։OyTb)>^5$6u[aδI뢈<\DxuS@ihhZ%=>l'(W:& It>>m\>CFQZuSkF jWJߝpB6Gͫcb5S|ڹ։MuS}Ph+hhlOKX3)>œ?ASR 6u)hsckQ>>@Uoshr)X||4|DEj։6pZZ%=$|ڹ։N(  W:\DK6uMtJJ{ϴ|ڹ.>m\D"ͫy 'Gͫ }hhhsV([䝝ўM ,G=N[Gͫh(W:)9/M5δJ{ϴp`חܠ觼Gͫ+pf VhTqFJ{ϴ|68Ί|4PQ>>m\u{{whsyǣh=>䇵hh(ўjZ%< δJi%Gׅ*"{VKOyV VgADhKδB&-7 |4|ڹևf։OyS}Ջ\cδJ{Ռ VhIOy'@1whs* 'GͫOW9ca*O`@AW|%J{ϴ|Џ+X \#>Z%=ɢڰͫhdzj( 5K`>j>>jX]OLhsQ+W6(W:&{xG ɥC3kfδJ{)>_ѥuS}J 4$tq--^I|4|ڹ0>jsz*(Z%7&JDT|ڹ։Nh+UδJ{J{ϴ|ڷj VarS}p)'Gͫܿy>m\lNrEW:Tuy>m\Dh(W:~b YU+_Vh&I0QZuS|KVh`x0QZuPS >jY_k;ϴ|ڹ!Y(( 6jw( 6tFRcZ%=>luΐûGͫ|+X bZ%=QGͫhSi Q>>mWA#ԓAN3@T〛=a -N-ת7|*{8,4R$Inmz_eda^*A,5«合;2:=$fjdrHsC$DS3n"-t)1rJnb=)6G8eY:e#+ ͅm(EUMS!lxM"EGl"+(@n{Ž Sǝyٗ$pg C4(3UoR IA0"G2VD5H,rی?.2czS8¬K5mvLM"GBV!66_JR bU!ibGtl$3&‚̔ c&f9T^X0G o9sW,o>bX5l`fgR^ y[-~eBLȌ XЮwwH2P@ap%gbG#  0cX 4Ql;زn/)~(A٠8cdrE Jn,_#:r´.A_bPڹrPND=8;o_J4")_d̸h1 fvh"2{ReC,䬨EY L> !<*:0 }ZOؐp`$!dC1QPjN"l }qk QTnuc-$;'(C\̄KTp O@泻2.(=%PIF>~jC':ܚp"=Xd3BPL\ QNAŀ7%o) ?iFpR 3Ev :ΆOPnV7}n!FP~σȗ_[$:2P7ד+@2dך%$C@}/ s047 Ia"áu&Wֽ|aqo1uc淰8r1)^<3e@=ݦ'a޶d!}:3KHQ:~VIoH(D~0~DhE n(r&+)bGk\ڗkֻ~Ey{u_t6M @Ԏ)ΰ_(3*c ~]*WٹOR.ik5#wf%?\"sy돫ֺ!dI+ okW^E}\"p$L+W߼mL|]˸+UȥciFjZxM\|4|ڹ/Ն8XX(W:)oJj%t||4|dGͫh3=( 6P5(FN>mW1 a;ϴ|ڱER)>i YS}#`Ss2/%FUGBTaδI5 +UδIu1whs->W:)>` Vhu;OW*W:)>ZWV +UγLEjx&0QZujh&W9jن]hs%2UδJ{ϴQ1^ٲ)>~i-nE]ʮlCגh 6uti^jZ"δ"J{ϴ{$͢uZ%=>Q>>l>%pjZ%#ͧGͫb)>27fhg~hZ%=4O:u@P|4|ء}Q>>m\.dXJ{ϴ|گ䃭߉ė_jcQ>>mIX(W:ĝ +UδGƜ{-\DoBj/<0QZu/,W'Gͫz/aڣδJyU)>ѯAk&@id(us(DOQjڅZ%=R:)>^0MxDVJ;kOV+dJ{ϴ|ځjhhZY%{GͫhL"jX ȇyW:ž|w'GͨҧYDG6ul+UZ(>iu\0_TyW:Џq_ 6uT<: s '6uS% )>]NhhüGͫ`3 Z%=ٞ Z%=>kOθwQ>>mErjKhrҙDZhfY<7e4 rROz/S9sP`cY؟ˆn`#(Fsd(W:)`>j;mQ_Z$:jZ%=!iOyW?( &{uS}ٴ+>m\D+*>m\D 6uSDh|}]!fZ$Fj;Gz0Z%=˓U27 U%j։Oybm)>jdQ>>mM:/hs}U(W:(KDFZ%=>lO9uS}!ޢ\Dtͫxh_@N R 'Ɇ{>m\D6D/|ڹs(AX =p*IHe%DGͫ:A00QZuqVZ%==p0QZusgOzG>\D+ jZ$DX(W:JsfOVߌ;^ }dBm-ayClhs\D p\z}ӋhδJ{ϴ]֞s矛y jZ ։OyW00QZup|ڹ։OwNճ,{GͫhuS|hGͫ`٭f\f\dqGi}hh`gQͫhg00^}γtGͫh$hhJQZuSYj6_sv+DGo jZ!2iZ%==v}Gͫh4SsQ\] qoډ_+AsdF +UγXy=VmT@EZ+U~^δJ{CjZ%=xtEjօׁEj։OZZδGK:)>As8Ej։M|ڹ։Otك6ubɢ&{GY(h=N-sX+Z#E/jDGZ¼! 7ϱWZ%=j6uSg1܏(W:)sδJ{ϱťyW: -gOyW(-}A}TgOI*՟ 4SsQbjp؝Ci4#+UδJzwKX։Oy[8n\X~^δJ{*>jZFKW:) %SDG曤0QZuSU4pJ{ϴ|w3uGͫhHW(W:)v0(  HDh[cLQ><(o=dFJB J!Q^ҺWry +UδJ{ޞW։Oy+ItI{ m\6`½δEJ{ϴ|ڻhhւj;rDhQ(0QZuϴ|ڹNDh (BfET"U%_0OQjTИpsNTH֢l=н(\D4<< V`+sE?U=>m\i#W:)LzZuS};`>jExoC( 5Y V^4P`\@ U }as:2+LVpaJ'G'G|(W:~bFdNȉN-QOyؿ\DB䧼Gͫ 5ÇyW:J{ϴX#dB\DU8hlg,S}TC)As։OM:z]}D`6UJjn6uP6 }hhqOyV5S}4OyY#)Tr^'_jaTZ)O^%;Q3uwAδ=FДUIlAuo~m\V1( 6(Z%==gДMj։Oy] z0QZuPi@'Gͫ1(( 6{r?A}δG\q?ihhyEjֆEj։F$A։0lȾr(c6}(vkP3RB^w} pF1qO\G"&ڞvB5` {ϴ|ڹ|& +UδA)>dbjаas٧9ADhsFU{s8;yW:@>>m\?k]>m\AC>{AE AJ.MukM<ڹ@/FڃMkDI= k҉ a? # >LHd tmmD /.gŢѨINú/ѭbyp/V8P˷xOLJgB,)_WVd GX!)J2f^l~O򦣋#tQ?;YtZwoq)< Su`e_4\_ >4Wh OPZEf5RSR_BDIY ]`mj]9?[t_ЩIX+lj^m:"q3_CZ3!"AKrca؄CeyFx@YZz/^ _dBȸLCwI18+HrŘN&FV;kAå5}5ċG~Y C~{4 hg/S"C=͒&XŻ\ CA 8Sz,יÿBJWmw-O:a_ӫOTq*i,궧oU„sTsfrhnoezkrFIs'GǬS& {vME7495[5H\e㎁qۨ\6/E24qu@@*,_R6&D|&B2mXĚ''$ƺSЊF[ccO)ZߢPڏq;WbPŦ$łCI4^b9/+M*nQgQ_f/ʮ| 5e 6L,bq--Ķ|WGWjwh],(n1B Dߊϔ.?XO- WN> ۰4> "/= *5yܕbb%L)VfsraK~NGJkHB։OyZ։Oy!hsrġDhsZ%=#K~w'GWaUδJ{ϲͫhxGͫZP`\=9gG? օKSM@fECs  NB>r5~i,n;f|%J{ϴ|XRUδJ{ϴfh.OW&3Z%=>l(gs" QjZ"̮+ZuS}R'δJw 'GͫDy}Ϊ;Fmͅ\De 2$JT@>OCt„\o:Ϭ\DNe2+g{?qOyݫ>m\DR9։Oy{| VhAH9OW:{ϴ|ڹtDGͩ]s|ڹ։Nz;v^QkDAU"4X-,3aEUp^R=á9&~~Zj@hg/7;/> !!>>m\?5сsZ%?}ί(<\Dϴ|ڹ0J{ϴz{Gͫh%z%=>mjh_F:m\ _֞#yAJ5s>8Ϲ-J"1vHB̶@P^lTχ4d[z,ͫhFuS}K6uSo鳌R6uϹ-s&6OyOC%S}˔OW95δJ{ϴzA>>iu\5+>@]V;F+ډbw!)5Fj aEjO2xw'Gͫ]Rwrj>Z(T|ڹ։NDhl scZ"sU\Dhh5_hs24h5 ۃ^F{;h>/|L K(,3aB/'W"T;&G`6E+OQ,ЁhskOW:I{Gͫh?MW||4|R]--ʧH--Ѻꢵ\DU)16kxO'XͨzqFO,X?ܞ. E+@%HCh@cctꠒϭlH2#)'\zZ%=Vh}D_ 6uSx00QZu-ihsل%6 jY'p( 6s>|4|ڹ։SU*DbGF?{F  if@^ _S(&jxe!U v9EethlZ%=lk?hs}iδGk1Z%=-ZcδJ{Z )>jG=( 6tE YA|4wq,kj GͫW Dv5oGcKf{VpQj7: fm )7Z:y (:>1fXV*TqjcQ>>mMIaDhsҷ+։Oy?Hdb jWm#bEj{1Dh#hs{D[-;Il'Ozqso ^R0BOf}_fgO R SƊjt(g7Y{>$D$vzu)uPE&3P99m6uSmMsIu~w'GoCW:) ί( 6PծS+UδJt-nXg~c4J{ϴ|п|5vScDfԯM>g76N? :6Nj y0f+2M$։Ovz`0`"uO|*0jZ$сDGͫ|$Gͫ-# )>i+%yW:/D1jhstj jS. j PAF`O6ENCAJ5f -kWѢZX(Z1RR#F-5}V#cQ>>m\W:)>ѹ'fCδJ{ƈ&K:{ϴ|ڹjSZZ%=3)(UU$׵,+UδJt-h( Y3:{ϴA:mQ5Oz6&'O!*i'@1ɂuPI g։OLdcy&NT.B=Ej|ڹ։Ov3Z%: ՐQ>>m\̖( 5^j3P:/K$(䃭,R7paEElʿ;$hOW:S]jZ%=3VhE~~߸Z%=3='KDGa'ڷ{ϴ|ڹUb1&W E6Wz6~)bAeM^L5ogtF'F#\=s4T@hg/{B2Iγ^QȉOyV=QZuS0T(W: ||4|ڈ=>m\}%}.*uSx *E+Cs#60[Tp"  inSM@i|m@MO}]̰0QD*b~1;̂ E =$/ hhwYĄͫh5hs,||4|ڷ}KKDG_A_К!F4ﮇD;ϴxP |kW\`C-s4m^.!DrEI'㰻kĔ}>_xOy^$D$oAt (ruD$'Gͫh=VvuS}&%=>mM=>m\ǹOo20QZusdΈܗG07(kׯ 6oBJf։OMzO5I DvG5y0 ,}Eogu@Qdoym >m\AC؉Qo >_VhVuS}=Y(W:И3:syEj։NH4Wz> ֝ȕ %lG|h];hv;B 5<ڹWo5&VW fKBm-az'%|L4D^Wu+Sd!|.g Vbc)KD[2BIc_'dv7$̚>QZQZuSl6 !>>m\OJ{ϴ|ڞ;J?||4|ړ 'GͣTՍro+!Vp{̝o%,*>a{G͢Nym|k9 06/*W"X'ɝ'"|4A:m4=rg7>u}D`6FCL3YنOêH gb%w \GJīːո]$l\=lD6a#pRv]6S{An Js]u>5,DA-<]gNښRD$M PZ1)kO9ܴM)B}^sSE@FM?3,%6;0;P .w&Л{;D|$s$VLz{UsQ(P ^j7 i-OJF~^YGO~u !"|$XV!^ImX0&iiz6c<2e9_\eFyKgFpǧ1h AE b5}TQ_Apbjj qՕNIpؤ{#l M> V>\``\6F( 6tRA0ڹ։Oy,tr.lUe/4~Pӿa?|LsH}!'ˈ(xQa2WY>OL'iBHsbYy3O# q t^ADr h?1D1ryr/pojMF{yNTMF{Œ  {`O NCj'#W>>ihb7ADhs Vh^OCŐz~ug۩vјӉFL=(sFV]||9;X$ 4S2W>fJ E] Vj:ğUg+ NV?T7-_((Z?KU;6"F|2S)uR28laA_MO NBPZThth0| c6}-Zؕ?y[ϴ|ڹcQ>>m@+Q>>W +UδJz#( ~3gm/NAu̵(D:$6Y/Jam)M:zSf Qj\I)J6Dci1~wNܐ {0}++0=J͚qJ{2͓R|i+![ˉuٰ''U!GzeSdhKL`6]Q)1d= Zus4|ڹ։EjYQZuS0d:>m\󛛸SY~1ζ8j6,:v,!>æ-2,f#[㴺QSΨOQ>mWOԵLڔFakW:)$`VXn5IG0$ƭ=aF`rFZ!/_*ȱթS !- е42 KgXYf5b`z>֬PHGk]*,B4"sqbiGrU|=Wȉ,|>UC)DrEt;@3AКz c6(HmDthޫ\0t)BkP7J+sPJ66uS79 '[A&6l͕Eg 6g)yQC*#t`/8Kx~Fu d/׽ZO4} u2_CL}zv jC RI9?T _$9]?Ļoq [Fh6EyDWDŖF{h`åhh>C8%я>(1™uUFa׫V5=C89M!bA<$"V9B& 婵 ?۰T=7r_ |ie`O NCgXF@TOSEg!'@Z c6&NjkxRsa;ќ +UδJ{Oy h;]٣C#}nqLV;G6TBOfm{_߿Amغ밬iFR[AEv內|J}c'A\wPJ9dCLB45>m:ĠEb))ѝb :5dU }Hp"ant1/yw8&kZY )O׻к /ȱn?:LB4 rGMEq[8V0j``~<1 !xu%dW^&dlouq[dD%i%Rf~'}E0 )]G)DrEpo tg& L~ESo!uf'l\3R=Ա^]$UHf'lā:Z%=Vg<$+hh%(q}ԿQ/p$uR͂aAۋI-Tcp&~:v+C6p2Ê-[,^|Nk %*;OdM$ "#ˢttIy$r!?y>4'%mxU%恔y&'k`lN \_I\K1ݨ7ȸP陛;|޺p~;-CmdE\66;ql?lk!'ijj@a'\QE)bGF;''U!u+C"IM7xgr 4ۨtS:e^!Ejδ=y&jW1݂r/Qh+mkFN]p?"%a @0kXܭd aU4*֋9ɣ޴2t낱Rk")J `ݨƒ ]s6A,fOSBpf%x4 }XXBLٲZD`7FY1jAR^bÇHTAGիXt2&!c .Ij/BzR :LE8Yg @uR1m''U!C/YBlY(@_ ŔoW:Q><Usp Q<Bx/REޕŨ ֺ4;ˏgc'vu:M-y:$zD (F}&P 0xR0qf\wmaKTr@?m^߉ȳК?c㊌YH]G=_Q U,T_6B?1Cq(f'v PR_Ef'lXQu+@ *z^݄ljas= Zus4|ڹ։Fmt`VHwY牿`Vey̖q;<&$屩rT6.]Vp 5I]w$ MG B@$!CttJ/X͠ŒP :mK#Wf@TR:iu\6'O Mu'\ô\!'X0շI3n V>\``\6OSJ{ϴ9Yﹼ06V|^+ Y]BOY%^Y۞'EN!$0sULuNڽ.(N&w`k5cFTw!%%*[ПÎupaLArSnbr =v˳ Xz.=L| }/gx dzM 7{x:gY?a0&yv敪u_1ߦglZjpN-.3jWc2 ͠œ?hGV$_)1do0 7Ĥڽ :l"Q?s5S}δ00QZP4-&/p3k 4b(+6KQ'RF#WܵZ%CjBDiUń~0tfQ^rЂ3v2'_qFib^l3hzT{ !ncQH[i =5'cxp09"9.s1ɢ&m=C=& ',Zah `ܲgC\=0p#RZ9h{\h[)C/L[2jy7hjۤbGG0˷tg7 زgA!; 3jWѢ\u\<ҞWhhsqgOy(E{W."KuGrḦ́S1%68.&2kX6efd%stS? 3Sie(Xʶ} LGYDw^5(qN$Xc.   =NY?jA,01DɐJğ>Q,>bFޙ% ZzjU6jd()+ssw0S\!!=80o) lTtJ/Xͩ_I'XͩdzM=biPշ:~W _F@d3RyF;<5oBN[2$ޑC/qX((;ϴ|گ|ڹ։M~u(kUΩ\e 8KI\7!! 4B8LB4>,f|\X9a VcZ>uZYJ>@XW;t٫dʍCw]bW'eֹvt8=2q|X/C&BC c ]'V^WqCg\ dh}?>2hS6OX<.+mq߹l F `GyF5R(c6I@w{1dZa'[-wԀ :mJ4\GF\6'QS7rm?=jۤ +.00QZqOyGC oƅA^.F--, yd.BLU/h郆/r)# Dpd1tOJJCܱ`*8^J~E{k ^tl*0V bHE-KDV%B;>  Y 6p'ptGlf"@@o>d&zø03BGK՟z6%bnM.@z>f&LkDR_S=uRQ?9J|L ̎|ɼblY(sji'U!yz_زh$M-@]" ϴ|}ȥc)WJq)p dgQVC0ZYk{vb3{4z"g]$Ȋ ]*ھ( y~ _lV,Fٛ?t,t[KEd"|f71|H}$Je8&k<)Hz$4[;ܢ`dfl**g#O~ji4Q ~$ KV V4AeO ѣuҧk],9fKL~ t&aC@!Ƴ^͓r{_J!&Ayu+9@^ړ3VTJ&tw: _9$#z!r#7F]{$hfv'.j Q +Q0QXcJU6ElY+Gp_MO ;OPOQ֋d1ZR$DÄI >l 9Xy dyP  u<ϭJg AI?sL{: ڼYĩOCvX|p >]r o͒GNmz`>tOCD l?UC9;yvw{D3)g@ys.畳!L|'R 1\{LxPͶh篥MN=bɟ4&J/X͠œ \y6}W:D ^SJ5kI|:T5¿A0bcδJi%yA bG:AƾhwN,Q2颜$a 1{2˛: am-ۚ69-|J^+6YwwH|pׅ\/T%Iwy0/glQQ\cI RP&W|zgdE1%*ɗCW<$#d#a^wk&xGU|?.a VVc搿mKX+w.%E۔ }.S@zVER% /P7 (<(BeP9"ن ELșo؇4@v?L4ȿS /r=ĭ2u/.qq˂a˄ig]gF:iw8FtD;ӝ̈tcmj:W"_0Y:_RT9'@?9tל4U1Ծ˹sF09KM=fh ޮ?n1yVZ/(/JS(FsYEYth4U1i%mI:5ńW#/A.>mX||4|ڹhs jZ%=>]Z%=gɆ%&m\Dss;(dGͪŋ&g9sbሇ{Luͫh3߁{y:A GͫS@ZlgtZ%=;"#hs-dJ{ϴ|t؂UQys2QT%=>mW"LX#GͫTbxBŨq!BJEj։OyW:)>jV!yf(kpJEjƞADn"Q(%0~ O}tVhXÚs2BE) ]h;H06hs0=r\AT?4!L>m\DmM&Z}Я~1ʧ  w:P t9r^M=>mp/W]Y7AEj։-HCPmF 6ug1,ݙ~"4څ _5i@sdܢ|4|#"\Dhs6uSjts VDz#mh *j9†Ǻ7δJ{3 e$ +UΪS+[p !Q--4bF{BKD|$SŔ6;/mEGdϘ4C 'C@[ 3ᷡߵ6uSvI_Oj܍Mc=vn6=I"a71sywBH2/W<tXZ$~]>^C-Ej։OyW:)>hs",`ԇOˉ+Xp(W:)NūjZ cOy>JZ3>jYmŶ|Y?hsˆ;s6uO %B OB|Žz`%0XRӣ`>j0@ZZuS/!4ަ'6Cd֮ T^/W*L=?!` &%2uZjQ}0_??_ ;%6a|V6uS}δJ{ϴ76.E58ѣd^pNmFDh (s\q" RCAEj։*wIǥ&{ϴ|pnܶIkA~}sDhqA$aQjYd{wI@?OW8եUu 6u\Q:JLVg =hڹIȦjZ%=Yb*\*"K%dδI[񥑲_+"FjYD֤kLm|q@ ~\D`1ߦojCOmYX||4|ڹ։OyW:)4۟}s̊mG q&( 5Pm8H}>G"ZZ%=qArpvh ))> #;?ۛ&d02j։OEniDҮ֠6p8o2ꌂ{'yW:)>j񂐮`0>iS}P֝n9):`5Ξ6dUёRrbB98hs4'zGYju5 9ϴ|ڹ,;"DžU{DG|5Yh3dN5*Dhs6uS|)`Ur3S$Dc (%(W:Ķ3 K$DGN %<U'Gͫ/ZH]h@*DC)℄W:)>jZ%=:K )||4|El^hAQ H/@ZBh)>ƒhtRiWR e4Q>>m_ i6o|%v+ F]MB"Dhd@z>\!z62[O(W:)>jZ%=>mW8*7y4J{ϴ|reC>jZ!n36uKTGͫh}5!@>m\DGͫSA3UUvyi( 49_j ڦt},s2G͡=Tr sA6MKptW:)5 }v>4bBaCRhsObonӈ:VeISCms$)>jZ%=>m\D >,Ȭ7ه )s6u*jZ%=>m\DGͫhhsTw(@ V7Q>>=pZ)с]%+=f-W!δJgrHDh X!oJ8bu0QZu#jb}ēvuS}δJ{ϴ|ڹ։, ERI>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyWx Ӥ)>hqDGͫ||4|ں17'Gwn_g˛B_AQGCᆏW:)>jZ%=>m\DGͫOyW:)>jZ%=>m\DGͫhhs6uS}δJ>m\DGͫhhqј,ꭞ*s6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m-G`>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW8İU \Q71Z& /.~L1,=kև)m/t%)7Fl-}Pg 9K( 6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZj;USJ٨^ƶN *bʜV8{EC^'B1I}Jc$ FC pQ'9%"(YJ-g q`K ?4K&Op6P)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6q+z`h6{ KCjZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\Dr}-%=$~jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫhhs6uS}δJ{ϴ|ڹ։OyW:)>jZ%=>m\DGͫh ,V5(>N+W7߯ PM9"*j_ +m%v1OȌ3gܳWn'&+sY*)^Ԛ\ʜ^સvef0Eӆ|Q]RS.`Tv+W.wƦ;\29ym=/o7=܋Qk;q/xzdq)E{LQ]?|o!ÉU;A,Kj1Rܦ@,?<+W~..I3 @8e`hudEwn>fZ8TF< =y9`i&u4 ^:fk$WY[6AYN- dQ0iw~3Ga[a%+AN>Lur2c!r ң3&?lVsagj zEobs-ъ+m5q&d"3_=a(-1 Tfr ̽xIƑ1ڃ fw~u `YuFgaؼg)vMc(˹+57ѲcXM; R}&jg%!h.C5ŪKFNP^(J%?Q )n_P&c6K, *ORzuL1$nVV>wήCF-}_֏] ulY84Rc]h >c6ƾ AWҘP?O#qN, 'mTa&goG>ȼBJ~2mуW?%xF<04CsN_gLF55F\!Mv Yg„*dǺjF%RPCpוRڽK+C`*,i`k-s-nZU ! KY\/||\m dãT?ǢzfA*TK#+vy7t&_0DCPO2!bOBp` JdcaPp/Źub1Mb*{YENx7j"t"B@l ԶG<!צ%Yg(4Y $j]imFLYBd"t.|=h) ;++tt<6 ?`;nUWH\8KS`NY7|PNMږ;*/y,n^ Zk0Plb mTAX"i# ~2WֲPإ{aXZ? &b + Z l3 0O˹|KSԝ0v"_1 6l0Kr͌~Գivf}C%vҴ]'NG33pb&M`V*G1 .*|lK\SR{&F(0^­PSx8kV=u8rДQ_1DS=JtӢ?tBr ._,_g=ׯll"RL6>T4 zGBTMNŹAV2dϒk'|@ rFׯXXs]d2;&gZɛ$8 t] I=7'M321rvfEmTRK-T@^;eSkK&ImN*Bg]\E{S\'0$$:}!SεJQcZɕF?Tf\mu4\r4/:K«XYBw ^7pӽ4/ے%1s= 5 ^ʕK):POP9v9ٶ^,R :LJim&f=icI>ړ+"fLO+! "r~Ef+1NA{t'EP!4ԟlx."JٮQÛ&&MyW 2I$(c^˕ 5 ^E ׷AA{C<tSO懋*I NU[Q6^Wu5_TÂ- I񖳟_x/sROVG@t]YXl):> F{A"INM"EM!࠵"L' F|!Say6$DE+-k-N$|5> nsh2~Ά##^ˈۄ5 .іf6΍н_hdȦdm'gHT`Sa 'x)0.>Jld),{ +ѣY߮΄S,,UȷNR77O;m[RY[ @MNsUTUԸ= NXNF|w/>Es(^5 j/ )j#gjf$ɋc*ΪOmhuxv4oI#@u{>nBݰqrbfZ`^gJTTYF!"BL#$6~Ed.B Qߣvo4vbqս#蓄dڻP'/h>cλk{z*˹=he-}9tv7J~= "7̣ jX`u!ފ@%MH#QxAҌZb5muZp9/Mwi$`apalS|%$0KJgIy4 ;FJ@{O[ɭGBL٢>LfX"Z5An"(lX' c˪(41uYeFCXڟ4L"=Z!ІЇh'c_=whޕ=tcx3g ݄ _~~8{њ^bH2Y Fپ 1H\)Xɛ8-YqiJ7r^F`R#V<̛)m< ɕ$GWM7E2p,>\_)crF1 2joL@;;o`LG[ًko "v˱ : gWi([+^6t~K1 mB$?ABZ=؄Q  !L4wIp6R*[݁X<1 5SVp xQO/}\iWuy\kU(RZ4ѯ)i =LTbeznz̓^doq!&+bШEYS{'FmgePQǚe MDM^qjmFȮdŀo˨;Gt%BIΗZj}36fba)P8 Ap<1,x,ͥ|{`( XpA^ ^Qhre`64{ӬDaՄ K!zRΧ8^s=mw ΩtI-{ټkM&=AѺjIY̹cy''#^F0)P#S6 yMz;G֠u^F_eY/Uǣ,]4| x1Ҋ hYBpgl@mڡ൸Ae+cba+C𙌅(^$=^q!Dž֫ˎBB؁UeH*E:L}th?@8O2uW2)褔_V6UNȡQّX{蝹Hx@M/x3pW+ȍYO9("Z \:#y@=Э/$q'/@Ӊ2/+)n-%igbLץ"(ЕcBl{Bi_;`0b(궒0jcю2'Xu>g{5΋}7<Z8)[sE4գDPA(yMi}a[{GL|q1{OT(=p"`zzݎS\C~78,mE))A!*S3:#FpwjbydcD-f.~*954r"~l|r%cY5Lᙞ?Bp)!q`2MvMRFS}B\|;!i#4,1;P@4[$,y?}3m_vojwIAcQ)^.Rf3)ˆ yK^@ma:qƓGrHYxJ컆R AҁylWJ,%d|OD\cI%܉8Zx8 Z-ŨPRZ *ֶ`7.2!eIQSemCp/‚UŒ K/HfsPL;TۃAXYL5Ÿzye`w Jx %?-uTGO*5K_&#uƯѶ`(k)_:~ 6:uge 6ZwEVS<1O QWGnػo|ĸ_<XY|NЦNM5$>\ jFsѹD64=N醳BD1Ril2度c-lVzeLCw{`O=E) ntNJ@puIlE% 5VπF/=n-tpA \r>EݥsLr/AvB#elHK > |j{)/H<¯sH=GPVtrA;4I4i:40ۆڻU Z"spqXTlð;=J}t:-T" g/+aEGildA۶5r-xSٿ\#4LPV4boyzATP[R* lɞD5 `h&f꺢4X5-Z:lmX讑IyJ82GSl29lX6krBлA%Og?}3 `yU^ (&#R֟!cG_=H`z/.{Ð [X)qHA#茲S4Ҝ #Wp 7xݚ xJ1hW L.Wʤ43SWGy%7j]Q lFqvϥW)UܑBUi8 hB}9 3`>U,9 [#nLw$?< ߎ;]0(U[] f6574 H}q[A%@î9׷oskw`Qb!hً* (RI1MWb ʯo2,?s4VDG 'Y+,R8^̨#2V|!}jOED^ט̯ D(v}拥`»77,nņ٩F h4HOS>䜜E|и$=͆4K\[&b佑k ;mTDBn }vЧgGq`ůH1zמV c Gpv@uinSJ:ZMŎ};I4TDB~_-(ªZoRZmMdYe6:l+ ݻ0tI%e@%)qq}R*qR;.4D91@+Vdc| #K-'r/~aqKkְL{盗!k>SA4kMx^a o?B ǒEFTfUIF.G,O I|h aefu\lsq4M6#h=Q4ю$vwS@,Ǿte2j`"e@As  ˄whRQC48c ?>/qVF=m&0x-g+t*$\Z \i|%~vW">Q^ٕd"cz;})>?kE%|cijSٙg; wx+)IzxOD|/9S^s>I@~%C5*M_,p6J~Eѩ!gi4e: $J'3({G= r•g٧߳D J,em͕f_2i~)c5賨Hqmrfu'_~"&$3gxdRm0ְ,$|rieCj*ezQOm+SՉtІjT;qmcuZܼ20W&7D|9ʵ!8)ƖANT©s6-EVI,D"ǿV'v r3#EU}ph[O+9.CX`\0 ^t p(s. 9lMеJi4Udh"?UpO *l D('v%01j7,Џ *Jw:rt`XP6i?؛(|]E9ft c+& :9uvIwᅨ:MN1s1+`vTajlyhHG;ȾS|TZ&/w1 z.f~\:+u"/!QxQΝBX~t 6G=?I*np}eXυx96|~(}JX#k_q.ئ 琊S~݁u XjllOdվgtV:]yĠKY#_p,d] R8.\ZɧH4#sXP&3ac|\N% -8JM?zLEw陁Ԋ&;\V׭:# $LcrU4IW=0|H3?EC T%%ޏ:_suV%5esE!={P6/$@4ӆE$B:&.R`" =َ/:ˣ GJ1tf9㳶D6DgMԡ'+|UtyY`= Og>tqGmgd,i⫅NM?_F<: v<8`:<BFg$pzV:~Zpf|m%xOYosyB<>NxW:#Y }bjʔbQy߫qCm;#FsG5  w9OρW$|Efڧ,Ty!9ڿg`o_r;t6K]I h]-kCc6/S0`.UCU_EHQnߪx"mL߫q&㮢HƀuO^0fX-ξae@4ѷ{'^ZT$~[l e5MOYW8'Lz{ST@u I&mզ_2+ 0:D,?rHכ 4Lc4@ gED&IKgТBFA !_@L ԛ눓 lf(2hT#I_>I_.k9I"먣F&7!G9|$GID^T<(;q12H+ [Rq(+GwwM "TtZp&jkB* E>ηsQ7֩hf5eRm d!/ʼnL]kO!sʨMFjnX/YN1d2SQ`-Dn>$N|X^32f1/8R-ntB-gg}.Lfۍ9M`ב?%5&RiA  %S_bEJǯp耘}U^b&- ݞAJ.ˈkj:<5XXB:I7zF#  Yc> mVgS*KXV'kț4K71yJnՐn')XZ'H> C56p0ݬ/O\4]@e^6]"aW ۧ|KDV=&Q Pp?21Ŕ ʬfgjB|Tj}Á``rȉCי0 )&> #j-sj m~0}Yᾳ0:o=gi\&Z50M xޯ[8gAJո1 iɣW.2 D h^T+z5CuդzG!FtinؖUr#* w 5 dMoI=@]e)dlkir1}ْp>Hmi/}%oG❘3YWo|lou,:c~;j}"S\qzuFwҌ5Yg" 8voēxkPZ&ZjMav YN[#& 9EFRŒNБ:HݏЊ2Q0*"tyX'qͺSzfW4ۇl >] T/  JHN$kJK.Y$lgLy( SwᥠX:N&}bI +5)ig@ O e'm=󚣨jA[=f=XpR5Դe"c0PX 럩͞#lh W/Қ=mnaU]RH-ML|2%Nps`LxN yNshd F ;:)kbN5JL.(!dyҺ Jnk[uhAtUYx_P7QM?L}o0DE,7uH༷;NײuފU$}GBrR$ٙAQA' d`{V;<OBZgVL /oB>:򎔨b+yqO&Hʑ*M7d'ѰQ25A;")-@EQ5,CV{MhϏDÓӂ6:MDߋ'gsY#K &X)+5Yf" I ?-1xց`nNɒ 2岬:ݚ]6hշ=@30~uJ"Q&%ld< 1YXʁ }9;vW5#4z$UYx,$%!s2f ZG;#H WAZcK}|ֳxoBq]$H^59GnS㿔Q뒓}o?gifB[c]G_W+dX:gc9kV,nct W CW޷-5-%wпIHusb,/qZO]K<{'y*"APQӯ>9ωV:ٝocH&i#`>q w<4*K :XOEda+w nog~\]_)ʆkp2-V^3ӕC+g.y`tFl'< 󵽶Vv[0D'nX ~b61r#qbq׿XT5vU DI^c.0ɋE:KLZ?HFU 7́UGqTك-.YkE ѼCT]6Uj ,(#bX[qJ&Ǜ(%ALaa_w]`ZҽYRyyNŵV0WR>|u &ٞIlD$3KQ2T+bs(?6{Pn}swkDJnӁ*pS~Hↅ)[r3nvhe]Ud:\ATץzSac{a5&Z=)H9IʝwAsP]pCغŋ {pJ9rt11ʄlJ~xT%-pSQRm}h' ,z?L?GK=S?YVDEB ZwTG)^ 1O/P]ȅTxI UbI"!.uD@! r<8P^9MAќrv6r˔U"$_4q#qʀ_xPjFT߽Dv/Vn8^6:SH|`qo&G,_Zr#OCoxzE|%ZT\Da̍U'(pEI\5P_s{hxU嵴tr;kTt\rj<ʅ9@,԰P~-*Xr!Oul'nPj2O#^%rҶ> rOfi̯nk'UO_5"@#$&U~D{ =󹶨2ÏvNw·{^\뼼^9@}b{X#F#b]Q0=.*ɌwV8WBT1} 6ـ}6x."E3Lv3P'bt)Dv\9u6G877RJi4du}ki/> -YZ)6`3( fw~j}B,?8U0ԌӰj}3LաhLS5Fɔ-fE".ZT{Pg5 ]Կy}>Q?y@(ņފfW&{X(h7/IDO' P('Vȁ9Hi?I}nѪ!\@B` Ý_8t)^bgLixg+P|'[ o]_-I{(w4\*!&uL'dwXLL3a#q2en> zߎ\*hmA }088Q * ~c~}lY  N'*@hF'팤 $KW}f'th4K6yh[#^Ϡѩd䩚:KeD$Idڽ:8~v l0wyԖmxzj &&qE~hI&XrVg4Vn3lUk%_`|FXi~Nsаwbt[pu~,1U_N 0mh$X(PL(ZG^z}3]] I- /pTt 5'1 0S3UO`]h?бgꦈMI\\QÛ&&M:sUX"0'f;eynZ]M4L\A!$>`g2j.28,ǰs`mVtj"mYKu%ŷx5(#'GNq9igUuOAvT֚Rbenf735 x_f3EņK'inI +G>x</ۀõ\EjJm:QY";sb框EO2z.f8l>6Vz$o!b3뀔Vnh(lhJ ɷ#uo&g^(tGB04*gѫG{37$Hg:H>2L/< gU}myHTWviT*^Vt(zz,J]@-w6zQxk.ڜQtLfuS>( S,Y5Ș7N9^MÝ` lhBZ_jջz]y.EBA`MF%I qO^ LÑHP<)?;Z\^.?{^-sXZʸOmk.Heҷ#=yҸCk ~VF2IJ"Ј9pOSMU "tp)F|M@>?COu㺹% ;!R $?}qۨxYŊEnD-{eM;@mr.ơm2HRlf$lk+Uq5dyIwo̱pnDή%Mj~ #NZ壟1I4!7 J0!0͢C2A,4d hH>E:GvOg*iRO(;V" iNv"xoD<*zȁjWz'(ɷKWARzj/+Q/+eWpdr~Et$a.d#R8A3y9^U欽L,괮֛"nSba"WrS4*l95<1>đ7XNsbHԏD`gˁ/b*ްQ[wcU[SGPt)262d3/[MSCYG\12$X꼿̚OBj%xAAҔg>LG'-{G9U/pWY]ȨEir hpĘ6~ek9cQO8wo+ أzc:Cp8pɈx8EQqb.Pp[- 6!lH`)*Q1,Aϣ{NHePͭԅ($i'n\Q h͈ǕŪ4 `Wźs8U0H!篃&AG/36821k}İkVCgs[v'S<=][>[V<:U>i!>!>fyh6 AOO.4,"J(b + «{VR_5@&)(;<Su^;qDu~)ePҙE46fҰFx)VyvϬ삵gY[TεmPRB, Yc{_AX5`\@Di![m)0z } ʗh'I dW-Z٤-)f&gsμ=ҟt"LVM9 BC"1⸎/l~L9/r]iXJCQV]kۀG å;ds\RR # ɤ#IAFb)pY{ drIk$<4WdZntt.oI=Yձ8qzc[<m>q?I!?A+ic< QxcPyX1|ڙhN!.?\uR f (6uKrc}O \Ů pzCBY Sߒ y~Q*ыK?_KFH,{ JZ|ؐ2a5GgjAs~MW>]`1H3,[ⶤx*+p[s;F{WcShFV4QN4U:E]`s{Sf/?jTw-A[t9\%!"" [q}[g ʛ8/ p͛A')͌^6P !fX*~7@օ(U ,%LZV~^fn1͇AɝⰫbd @jݎhaOUWqca΀OGNY'ExA~"36y f9pjVۄ\Ზb0)>{ɴ՛"Iu|#T֒|-7tW8OvMBnkk׳s5f3o[86Sݏ{sȝ^ms8{ Ѧ_a!W1.P +foszw8Ab"z kf:w^ )KA,wX=3#d *a>wQ}ȖUj"`Wy_ƃWWM}Yݗx_4mPDD%9RK:W!gxB^n$v0H^݅<5C8]ז}OtX_⏊U/]$ZH_} vA2;ĭ+5d.:ռe_".zϩĽsYzK @eT~gPڶ@vf#󪖏:QEnFa@ B7b!8 "agsTi%V#IB&tEGd=q)w:19rfl17#hf'mCR5ڱl Q&"r`1haՍH=CypUT3a1{x.Ӂ `iGC qy1x.RU$5]jk_'wŸ=DbrFA 'ncef?)-l  diu&rNα?e?qFCͷeS _tt&a,5^Vg$c%u2o=iu'mܺ+*;.@75"ljV>rD|J1;8c[71dM #l|y6F,V-Gwdyj}QI%I;|+yG]AĔ0qrjԮ۵TXϚolf|@쭸=&!;ω;3ǝO ^PJJּ ta!f8)œ/!!Š7AUy0qP`oc $j C~gv(a"Kepm167}2:5dH`LO.~ ,p&NS -Zz\nai gNF렏FGtu*_䵳PnDjp w|ſd0%0{}3d6 ٢1:ޗ̹j { E˨.9xE+*_eQhGgyh ꡬ ƈ.Wge˼D?r\]EIv:*.ꗕ~{y{E /Ço:o)_xԔΜ|U ajk/XEG n|_:Z:B@730m{lQ-!5(܃]֙巴OljNiYB EK/b{@5[tR>7Uᛣq4XEW^%tyb\YW?`Whv0W3ii&QSza4x9%&u1,, &VRﮎOGPͻ>H캇c mY!-?D]YNf@|V!Sa{?L݌`q D\UW矆go-ƽ1)^ /Ǣݣ5"oI ߙ5sr7$$Y#h^R ifXJ<8o| vju/Mf5ᫍj4 [PM:Tv~llۋ08 ߔty)'Z1{SrpsR! vV2X49h[Bg*q(o2:p9a7C٦G<@Ƒ![l5Dmd?oڈp{C9ЅV(; 5ZNJ.Ǥ< _W1e!hZܫ5-HXC2BhfRy7@7ãtT %[- ls)$Dn9$7`s.=a"@̾37OL?oCqb3-wyƚBq"e j$JJ+cFp6u3'@J^0N I7&w㹍Ԫ,u"l`|,[y}^zd sӓkduQJ@#c1#OڃqVǖi"2]&4^Uꛮ V_"l`=U:uO}@S 9c&sy/s8{$=~=3^gdpǦ 7~Zk(oȧ1Q~EAnJ2PS&X8MCI G%@SW_^\ X@ v^.PvM@y}ozsRN:Ѓ@;)jܶmP]10 7p52,|9&̶LP.e_a>yG<׉89ѩ͘D)' L#m5JJ,֑;SrB CRO NQj\dsD[X睰QHˬT3@H.usZ1m>rRhQ$aHvMl 푯 im`;1I;R_ߊ9-мFWu#mùSV7e\GGsV0Yy뱲$C9p8K $%pc`YE܇2"ZgN͞ʼNor?@UYgjٿZkPQmz +h#|)Pfa( ^"؊S YnWn>fwyABʃ>5?Yh&(052[T L@dԺ.8̓s^}8TBP(v{Gv6Yc׷ g -f$az"?ѳ1V-ީ &}YHY*IJwWdBcmymvU%^U f!™ue3q2GB0f3?:3&ޅm6/w?"M #gZe6O8^}m '٧f 9A+:݆_/4/uBꝁ8Α )v39%=}8B>džT`9 a cÅ^OADr:9y -5$Z̗]j"DpXe [6;ݛM!{\1u^lZMѾԜAf7@'8K dE/] QknI 'h;o|`qXFʋ\^IS%8_m8R]943p'#u;~xhT˓[b#>gҔ/Vx.k/TUۉm$0hYy|Ǹ}>D7w*N LU%sm=4;eΜy] Ġ˿و,%==ɬV:ncmz"f4 nce'1WxvjJV'9՛}M]r/Q e/ g܃|OV,`_@Cbe)%_wMZ%?C/]n)ĜViE;4{qTNt߉HRFV ֯o//OIr1P}n YFX'˻q I$"t17٣7XazJf;DN8n"EЦjUtӌǃ?wb!C^8~S)wұªuziF̯#-rR~S Uq<.X^Kr`g1|~P{D6cpNc`Oy!Z/Eh!fm 1gSNmQC,I_39QNPȿk,~{é_&YI/*2]GthDZRF{įT^O䨛!xMv0bI.s=7儥=\gXѓ)mh7)s5䤏`mU(fU˕6~jBnXWTյ;u;ΓhR e!L#,5bU>jߤpf^"-3/&{?C|nj%ZγjW#wf=O dI#G߰pWRYK6`ڜ`|[$P@oT>#Bqv:` Y x`E"hvb`pe yu }x/#pK6΀`TݡHS8HcY0j}ItS|8ET PTi}Ѐ3_bj6BTس(kݟ5$^|ۗǵk'*MaDtAR@O{oKU`p~w"'-$Qm%zqha(b^Ti S*&GX|{?U Mݼ0 pPZl>!RH1!w?܍Hh'b B#g H=Srso {& xsHjyKGKDjRi`R5'ǀI("zG(bD" Mf&_H8z 2)ߓx" IeEvFΔbkCA>.bKWsuJB# AZE0=;\ޤv7\<.i\&[~{hur{hKXK+`1(`"Pp}X";ymOВ3@SYfʃ7" dz E-U]1g3c[s_CxL\C=%LBOV) pخ5Ve9+ť!en/-I rSi\èF%'ZcY[O8ST+7%}* J8iߚfvnâVM ;[g=[YO*B \ɲD AYcu[C*[n2 i[g}4m E 5d%zn4M8n1e*n1&j4ܥpOD&v?F] UISbM.75tm?HNpZjUE4|'j` M # k*gf(E/fnih_(KNObnNS '4QwmKGh"} Iޜ{/CŹ!GBGkL QUB buXY,zga0KӮcu'D#vrgII@ac"5vD#XjtElCӨ{{ { &l\dOO:|v wKȪxFj!ma؉ <ׇ64/ߎsz+fc J35}UWIC%BMԙ:v`UMT9rSss6wI\1e&Ƽf 8uS*$|PmucISeky\^I87=@vMSU%{[=@C|^S7 },X*4'J;ZO dl̮I~6Vo,OXVdҙ2iҾ3ͯN֧k+a*sNz%a+ ]pvU2BE7(is%xf1n8ƆBԎA''!%6Z*j!&z O6t& %&="*Ħ(:'md+Xd7SK /wv馀%2*nl)ۡS*~^?ڑsy,g5j+P^r32 \*$=ݛ6]ĝNٽ^ɏHb[ GYHT/.T< g!$&A1_o)][C(Y(Lv^}9m7Qr(ή8-tQ+鄘lIYMXV ≜݆pL&aVJ;؏.OmB$(%/5IIHc)S 1٘8V% mR"WV=pznjoMjUD\f٨ E 38 Uu#nfrL_2,E:h6}уiASır2 vDbGBoHF8]gU&*~;'Z#^8JzS7/|D'u4vE<"arh@I;yy!68ڵEחh.Mб7LbA^\,(Ufk?hK"(UPb*|<9 /0h{!54uqr%(y)SrHHnF6 Oq7^~v?d%opXHK\n Նkނ?"j]Yȴ} zºwlnU\x-A_8egcT':b-SQxz}["sQKiuuFNqsm49fzA80v#WenrJ>;q}f/]OmB3d'Vnǟ#%*Se4!kJ^ ¼(dz>if8W23[pŽ0ohE^@?Po%V Yxu|@&6% D [n'igA~kXNX~=@ (5aijr-5Bms(B+tBT00DirBiA@^fn79x{~h kPw ]yfX " ҕj!bSGp6L;6 y{}Rx#y؆ )v~HjǩV RbJZ  xJ_,tDbr:F8)AKmQ91,AnPe"CzrmҌ7*PA(R##R-rQGh+<]aj;Qiϫя6OǕ \(x[uGl=cA*pioQAa#O޶˜dgB²j,YK=mf 9Uye^™u{Nou$ MR1#;@G76Ϯ i$ ȅApD f*c<.LVp fD04@;8#O H>Dt(=ZL dW>{dL}z(xXV{sp(~L_ʿh'w3XjOhQ!~6dˌV N1DV(oT_{[~å|-tǺVyt=sT!@#UKɾyBlnJ6vFG(۫9?y;ۯL4 1[)ή QP4֠}7f`' OٞKF9;[g/$`"E!88(FEqv6Og]wH csDlG(?vmBqZcXl/ãyurCX>u%`v|Dږ24akQ@~\Y] >u ^g١ї%|* RݦÍ߬%c`ۀud_$kM JMo%kzFYĐZl.s$:N^V~ LAKY=ݩ(8Z٢D2mYh|Ńҟ Z&;{ &wI*gH=5 >&jnhJC ~Ga;C S:t XZjb0MYa$dG3(/rpMt?(֓͝=Zc~ܯ&3φ}61'V=I>}P`1, vh 'LRNKOL: n-tA}pߎK4ھouϧ@eM}F)IpC}XeXtj+7lU:s0|y]0HgzZʫHe %l=&UF ٌkEm;pdS߶>%x%iQGVKwj{K))V﬽}.}`1 y )U\g)ʔ _(HS;4JFE$7Jv7~_>O+>%SרEę` 2*x^(a`E;>0;Abhv" 1f ]ͅ1,źU#fl b/}!spqT{M',]&NI):nrlXp10̛ _$}(d 0ea-X +CsK0@.*Tb:'t V]>}n)0XW>(MX +Ǭ`˚ltl;PU_a# d]LB0P %0<o<) 䠏6#&CsƢѳB{6z]uשu /$n;]׸jb232%riPB-?qq@1@RXIߦoi:?"+{`Ҥ2$Sn->+Ͷ k˦U羖m`̅Bd `  2 T @AwN#Bvv43(r)wH14?mݶ$_Ukj^* l축bPrfMzc.pZNCq\WRӕI2h6͢WG P:D/R3 ,[l(G.`ᢝeʡcqLzaD<<6`u'bK6ҶŵbV}uOYR>N*!;aHQi-L5&n:1'& pu|Ph;wa 7zsDg R+ )6TW$,CRpu+8]di%{P7)Ѐ> V&|ϱoeIV5n9>.BLja3g6^}guT)lbfpJ1|HPw xOkک vRDvt J{Sv7>۾3WB A(5x],Q;g>r%{n;ނ|[or"Fh&ALp?YrnP9g#N?~'jvt;K[D t9(>ama7.Bq^aiSt(0I71C쵪{6l908VlÏhL@X5s v"B][D ˴ }N1xpH.X5z]HYvųvS|IiC!.De :ޠ+59ȜN.' l2:i])a^'فpf0= U$ q}sVB35| 9߱V-mV4FdׇA)C wRו򞱤/qBPyC;VQ tÒݸ \GWNYRyUm-OBWCRҥQ;rûx)g4Z"˲}%YE!O gtm ֱՂG2#îr@{w Z`u28.*dxL|MTnA@9fEX)ꁳ|k%@O/gh22A &L9<~hIBH7g?<<ݭc"u%vRmt[Gq}W!.C C6Ti1K?K 4G><%W$U+O~k> ih$LÞ~&n TL+FbgoH E>? JmQ4p,pKW Ź=9dI ?t 18?>;*N蝢r,YM(S%xb>X}.( SڣŸ7)yEu:IT=rd3e{`[vp3փZ3I 1㞜L`9CSeVN`!6/WCz8=Wmi:_2Y/hu+=+<|kom udw Iz哯nfq8*!T^Thq:Gl5L^?OȌtRGc`w%[y9:?g|3TU(`G8Y*_NY'A(,V'[pca]U+5f8<+]zTqB2FjYa՛#4^8bt4=yGm8Pg; U9#gk#t/d8T?xNt'e;98v]D ]oBCtE#ބZ94AjJCBGOtKr|C+2Aomʄk+v/ DaLrژr`IpFj>(lP,x6hǏ;\c_#xnM1J \.l)P-}"~t6SdPSWŞwL%H+2T SX4GY>`l'&i볊FevFadsD  -a.@{I#Ǎ"Nc|M/S}bwgˌ旆=%G9M"9h7qP7?8q8k cUڗAclI)Ym6qK[t>4K7_JxYhrgTu XOE1k+(mOv9 SG; ޢMC$VƱFMMw/4B&4^Zۄs* 1qȰ|iW0_썅u:Oߌ}5iM\׷x?P|)کjm@r07n^M[2pqErc.ʵn™eeZ&?a-. Fb O*ͱT-)ak>%@ -%|#>jK&Oo0IsJ*F:90K*.g4}i/lcg8;#kNXnz5>}x] XIv ;;n(} u}]acK&s< \4J7Y5-Ƨ(\;iC=K^91[M_f`hO*rUm|3eKe?PW.szJP*g4:jZ0lcZ\8ߋn?`d)1M,.ku ڝN>iO禕BU)w>#'\z Oc2ҩ3d2tjʹ;߱ـR%ȭS_s@X99W׌iLT3J}y{;>)wQgw9aV5ӵs!HhT݂ ](z#ץ<̘J%o1Ğ?rA_D// _su! Cmw)_nIvU1SpdkQߕB'Jz05h8\A!`b`v {% Ҟ a: $KNY7ZnEEiyk!*ppwNxʗ>?i0*\ᨲ-5iHe"';xɜzr|O딠!o{|CX@RGKgS50'Tρ+1Xz5; |̔`9 [5 /[^%D;p߉HpW' X/L):?tX!s\koRWLF%Yί: n}?݌ _*Ϥ(RuPo\#|p7> \Sd`X1_W_ @<;91֯W舋HE*J ]URʝ#e4w>1Hx.- ʮ݄ClhEc9ކ,3`ijوi獬 yo걔jr+~=H" 1/  (} s.!]FYŀWUPf@>Ѝ_2@Y,R :@5=v}Ns, iE&ݩAxUTɘ%U'?DIijx`94S\Q ^pC;rmXLzxw)?QdQӰ31g{me@Hhr!0*ja;=N1ܰV) \ d{AǑ􋩃Ajkdĝcuct=䦮6x}~ڿFȪ(b$t&Uh1џc9{$uf#Ќ=p&Dypɽfj 8Ej+foRy:yt8Bg5ԭ8B؈!Y0e_) AÏ "j {N&iUlN]~kG?+@qB 1XpK;600 سSM K򄪦~}F0R>'ɡ OgzH2 Œ&T08kkftTO~Ta"9}^Nvs2;UA`#VYͼyK&?RZqWT6g'~E1cNEYT2x &['u*|F&7߅D@WG/7Pl> mLa8MqeJZS9kR57pWgs3Mh ``F)-19ۇNX|17a{+LΫg[َW ڌjD0&%9ua|6l#{4hI IZ/̨ M3+HQtϜ:y{XTH[DKT'փ@ҩ(98tP9m !.nqg{pbj$#rƀ+m+z\?Cp~h*,1S6y—Lș0 a{b,W@mki0Ce*.톇TT;W B&cձ9fUy )x HeҪ%fݳ󙽘ĺVD8)B~$K[Med0FWյ {.{:lr\gS*csI-arǚ\UiUV`e$A য়Gi90ܣ^p./c,NWL߾ k1ɫBJuz*ŘK*,}Ǯ>0'Z~d QȽ  uHv97)\:xD>Վ-tFJ6 [w8K2A`% 8nMA9Nj2882vHCݔ_P-)˼m\ͼ`u% K` 83*1" ` m)G}(vx#k.V=z Pxa"$˫ī,?y[#($sf@Cli#VZ2No~:5]eqnц'uv[+rHoqe/2L'X4pfIEѶ*#[ӌW&{xśkhCpQލ5v\t Qˍ^BᇟHN~v-p$j}SPAڕ9Q幄^?2u zj6e|'>]Ox5>XbJٙoL;MGNJ7|z6k~3A)MCq~ulڎ:}:hbŮ!ĵͭ!x|>"=5MgtTY=<1El[ca\2o&Fp']bJrK׎uis좜Rqb$.'l!7(ܹf_4]*,ÓRV\i{;~{7xB@\$=Rocr<,oP?@Y1׌+Qݰ xz&y,,AJS-!}ZE9quGXӆy.݆$*j,wK(ĕ ~OUzT_K4Nܾ3cоxXף{&"yna|*)1~ۨgӁ &75asGIU+-~ܙ.Yf_rz䗻Hnp4W ЛXy% dDgÚW۩ײQ(euϪQ:N/aS Crw7X8zN&b35/%OFϚ~D}H#u-OMP~ܗǏ/t;9Ze?׷$;iU "l_k/Oz\LZraO`t?k]imjX \ [2Fh\Mi׺c9`1_Gc,] *$&OJ=ljJDS8_lϱ/mDv2ht%=+ :F0"QQ P@pXDfԿd{&Z%E̍r"Ack>$"fb| v|3 nP>gNUg-'_ m0ϱj֖vޚԂdWƖSH6P ÿ|պ˫~ $&٠jB?=(nVo;*mA5[yϪ;.\S*[W 9!_O`I=x0"A, HAM$xcsT!_KJ8ڣu[Ȉ{Iߐ4X"tϚpS8Rg$ S~u`Xz s8_y\:-,dQ3N>vZh%;oiZ<+ b- Og7lP~ß(i-_~لI: GŝK>Z{Zu_T{GMuR?')16eFWF|` %H4 <+2(E ?gv#}ݳ!%[:{KF9e]@Xե[ۍV-QӬھkkM$)hw';zk߸7PP/~d \![%tY<@?ܾHo!0՟dYXg U=ξ'.fAt'lc_XeDt{tλ zOS:IQO*˽4tHyG)]=(}lEn&65'oNJPp!b&q;d"Mì)Ak`wAi. @DOEl.ݎLUj iZU>6nT%n8'9&߈ oiWK~N. {S80%RœDW(YZ .ũ`IF7iGuo^ $r j@"UW1϶kpv7o!vSNb߆-qq,oU-;ĻΌu3>(ߔ@A_LEcT` R.`BY2dmusЅ Ɍ\C3c-%vԔvɇdN.a‘²+{Wyzdž;(Y)=>*w?3z ƱpTS"Fgkv0^}*MEQ"xLbB&G;-rh5l a,iBw97Y&IeHfr}h/hVX\B}J ԣuUO 1k JA Ia0QP>AbRsU@aa2d+Bw\}["Ͽb LbBRG*asB8@Vv-@<Lˆ$?0HFXeEuy#+9YiKCBw! _QoH-/T :9Z-PAWVI1iJDχcI-T$G?}< hm60#Oدs 섉F +I~CH+8l%YgR,טi(7?ۨ Qm-/E,oƸ1# sזs ?*T"َ:wLMQ5ݵe$!=SwJx1I 測GL( ؤZ1l^zL_[nIN[iK@4=縭S 1rIv5|_s44J\,wjYM8ʾP.7kE13?kHwDW׷L[(#4p@\W3Yx  ,y e4,˂P{!ܫ$wHGE=moJI7 kP,d`SJլBJASL]ZID,R9U)e RuTlbP3DYX;*ѪX˟CC:A?]5!oTnR)tDP9zIҷ PIa8XŝY4VI64.%Q'J؀:8=5a{$+q:^Dj\-fbډP/XU+0¼a-j[CYM5` v<>y2g=w>rM!^=TܶX;Ȼ* ^2OL3FkA%\ތuH'ϕ7G瓻(Q٩ W˥J/N$ƒ m{㜀+T]0GC{€3۫z =8'>2 ȃ2c#kRK+q)֢|`S)*Yhf)=>U#ZA+FcB^DVtm-5.{ȷ=)M`C -7H8;[HO$9a)L~,cm9~$ ¥Co+Ki5SʬP hko{[.]FPXDs\ ҄ a|2[`.߄0v#86^>0-Be:e. /PNݏ?NFKl aixTqO F-W | =:$甗B?Sn>za^lכ@oh l܆8ɨsݝx'ApD\2Q J#h i.HdvC0 ;&O ._܈hDŽZw>i#d(y'=.bǑ,SNlV(QK;ru+D>hD@U #vpeޘzN3>pacжJ6VXfN}:ȕ"&O%@=lǥn56VZs~: HI4 !3Q=awI%~N١+lB dZc+orx\3!AH^AU'H08gf;8vPȌ!Pr`jɌ[WWlIajI@SԉԾU37 h.njd 3+#T)v}-$N 0HK~_qxe? ~Iea3ʵGwB}@d5V"%Wk"VV6@ԣf!E5ew s}T+OuzaBjbChq}kXϳӺ3aIcf@^dH-\ z?>s.G_oqҝf(thа T 6x愩>-\ huH[l[#ڳ!-i3%/]{:pdDp`:"4%QEyAdS?B{{bUfg-~ !0 v1i(kRͭBEtc࡙s^cI%^ ٿ`z'( zMPYe p(:ؼ]\4}ѳx'Ń^/'+dgOP0Eyb; +⼀jc6T9fH}U{}hJ:^B3#_(ԥD)ct$0&!u<*JO;^9mЩBܤ{]|VSzQAb榘j)Hng 7"):0It~PKWfUsi*y ݴ~ǴS)~Զ8G eu'Q*4XJE~˜Y%^aӼRBܳ!:-QL]> Po$jW$inĔd3ۀ g0tY'Dxz{-?W3EtW M mQX1˽Pe[!P7he]w\%zwjֆT񍽭{owEMʿ)R_y ihPfA`1Tӳ@һ.kd ,ca'* \O(S#Ԑ[N6g4w>2U@a%OoMpԔm+Gk *iRǾ%9-&k+ⲏ QJh]KyQw JrcyO> ɢ ̲GLatszv}T;emDXCij#GM᛿@ډXN+`S?rނUpif2zqXm=ߎ sFY#H'$Mlo>ccYwcs<+7 jyq$,rRRSy4FX楶>l1)@Ⲛ[9X8 A{#AL; {~뫂(<6\*hl.D6(QgnU#"B li:bϐ\Iă2.,\@;*aLp[mb8 Q z*]kLۺri,?y:SPeSz-aYC5# ۞jf-592ICIF "1I8ȍ/#@u&M -ě-&b[>_Zmi>̏RYKk4l]dAE-[k@,hW 5uWBxbPêݿm@c^iJ '1PG fu؉.HZFO=4zؾ#Tz'&#Hx;wHReqÐ֗lz " ixüTF%Ќ;~Ӡon@z&U|m鱱O/Ήԧb6cAzF*qpzQ6k;<}](o#e/4vLIc@ι?Wxha6}LyS&`,vVFHHi/?Y@{M}ysi@9zҐȏLT8y2v0'Dkn qMGIik2Tk~Y] !뙹4iɼxfmU $_Nz"@W OM|T)Xmh&sy+ G5NX~Up~$&  %q+U e&/(+:JsQT!iUjWqfl6k)anH{%*ރA6 ̎iQۣ>W{ћ>+X]*^xИ6N՜+ѩK LKꆦ[Ifc( 68WT<;ZD|3(z}GYfK mXKw>i/If)jNJmgݠ2cm:9-Fj+4h91T(G: L q3gkzؗƝLk)oc}%Yc S+!/%ZaXx HCXIqf{cIp /٘bXkMp lgdwRWY9Kg Qp,/> `|/ۡщ_2mX4Gc:  ٓ%m+K2,gՈ ,O$g>y tUwq|  2c1efxE*@«kId\S)ޡzZq}*t gogO[LiuIJ"L{MOC]# /)#d@2#ē6ceY:";[5=K#+|m@5u+ntVFF9B8~/-EΞ{dVs!5~–@WA[q5v8"x&#JRxz))B|"tNƱӈcjX}[crC(̩H|VSc' V 6F_ت; <%OMҒr1\ܽ @)Z[DTB[}fI 7[ p>n` ~B!dm#z.bP3ӓ-k;! ~ TavX]E}$S1pPvdaOдU3j9]wRr ݎw xpD VM} Ha.sMsruKѺV}Ņ}x,j = su p6jSoS+<*%cY4W)坤E7?G2b=%M1QB9+*hwݢ-ȞvVWTڝnvTrAT F%Z@:ΤxV֧_)3JvD">?蘩p$|$ŵyG`p6[=}G @={h7Q9 @@X݆ R=\_vI 3]5pd_ů`b͓ _ SUw ev4卻X[]}gt-NhdӶ Q qvY,+> ]Vk 9QOъalՉǣ99c⸱]i _}Sׁj1b16^8yd4Rp@]K;l68Ȯ/ S۬FNd‘1YQevY҆``v IpQY )q {=u'D+?L |Y>r[ Z\"Km;(ܝ2j#6*O6s /ܽ[pt/-IŮR Tz|>^)S5v|Q>GOjTQyQ#DyG҇^K]m} bXrzr FPXlp.Ÿ\zLc%&Q^4Ziz;ӇOy^S%TRԇ9RhC^ll=ht *ΈjǾ6ݜbxg*6H+ZLDIXNCaɀT:}ľxO.uvS{wXE4֐);!EF$ g&= 3H0E_"&¸zY7qhgV [n72&8HMjhA4nǡM^)]̷XҝAGz Ysym7w9=CXL{Vi@=`Ǝy)',Tq <Ŗ2^|_1,@6-9.W@ 13d%4'+&0h`&b`ҩ_JɏbA+K&v1cBByS}ś~׳NEB;Ք*f [6RZ\r# {(e3dZǰ[h]_K`TA!/83IWxK3EEoq!M|_͂s_8Kwe3jI)ɧJ؍x#d;vMVZb blp1\+c }G"7^=23,GurRbNp~5j+B㋠?ݩG54BP:tPDiZ~5jbk PW!) HՊ5wnR2ToT ( Bo 6jP]H ɒߢ;@,vm&E&@z Aw@yDEc#)maH^ ͙7Cc2m fVt7f;"}Rׁ͐PkyS=OuJ f&Q2H 6&ȸ^3#ϼo q R5pLW~:e_l= ]16r#Yi.PYKT8CbM@7 pa5!e쐡YG&-yB fKk` sb3v mC( _z@w̨)mD 5zq-A.MvY X}M;GΜyxO<vY=t M`z` gӷa 1NK0J4}rŒ@\'L.3GcDlH@|pP}VS+@iv QC`Hml JyPeյ @9ޝޏ~J;VlުۜӢg:l~2x*|쾶_UT⛣~HR/@]E#L4*nׄv:s^~c)b!f@3amueD&;G;Ŀµ6ިUsGRǹ8Z 5 ;k0@tէkc/ 1ߖEKh6**lkR3ةs @n g::!]?o&<6G [ oz}^!8lf*nYx 9!ǫ9s6~Z3D6Q,Tkʡv`| ţ$ ? {{]{0blL ?;9_\vz9P.'TnF:ɒUհ#P @#[n mP LTʏc4j~_z\Q zF AV7p$"H1MM^}}^7yZeZyUz_)9U%u3]Yy{t9jQ;z`a[]-Un{ .BF VwWՙ8":F@w?D3fq@ő)H7i'.Cג(Pۚho];ru+DƜP9h  =ՙo/U N)74Mc?il2v]*0 eU. 7Prb`-}HB~;y2D+,>C<,uEޱFȩ[8ETu1xwV4DLKSY$i٪SLbm8]Ʊ_G畃Qjfx,c:m.Q] Su~yw=c*}w8]v |y8kϪzv)瑆-\狂?і)q~`҆`GmgGp0ֺvӮ>d}^4(ETe^f%wo@ \_,F3>炀&+oxG+35IjC mCON_<0J "*_FږtѯTRᮕ'εɾ3 9r옏 q9[47{jq%Ŕw^gv%EY _Inye8}'vfIҼ*"+imϟnLSGsЯk1YM&jHX$K'ruԝ0NC<͈: Eq+* 0Zg٤( åx!0ojЯ'IG;v06U/Z`pګW+qb)j oQ`iu,2% MB2C@Rl|tNdȱD|,aER_{P|z1fZR3z*=Vno& gJd٣ :4v:Hr#^|zпa4Vdєhemu[]Y9ރr%Iu ,d>jD@z zu~֮Mlꘖmݲ)teKX|ez b: ELO2)6s{y'C^3@kVh-DžTE(zZfD i>{ym7r¼vto\dz<Xyi!ĄTBᷥ ^ܟSڰj&23T—t) tk\BL2 p,U[xKV]c`hΊM_āNK2 HQ L ]e=T_xf2SVD/뼨-}8D 72uJhCbiwO uT_f;!%1ue~/N-t:ꐟ5@:7 >A/RpQXܱ- #<8Vc G{h-ZgA@2O#z,Mtf_&ۤdWy"aueE4YRE*hgtcD]jCS-m}U_jnhJ#峨2jq 0 =8`Od%ZY_ Hz<^.Ns|LNy ΨdX7>! }&uY⳺0na x#Fg(8NJrPM2nf ~s7Q3WS씟/h2tCͫ+6|n#$ YQ69guDiБ !j},iY-~D;dI>ӟdUa+zY'!\8Bm.g%l(|'{*hD9m,+iAZ/V G񪅂rҽ! cΩٓ`0,حTq1V"rbO!mtcȁ//fۧ=D[#BB,ȿLz`*m1ϙ34hN$7%΀8z a*I`_wMmD6 rm9ܺH60@s`se+"ID5v3 %~]C4"Mg͆5tԽ"’f =xzN0gۏ7oet rjޘ| idyF!_/A,V7`)[o "v)G+sy~c =ĉ0(53%c@$`ڃ&Y[-.=/mcz;7ܽ"E[xBEFemUNGmy~A|4M \@Yq <áHte Bӑ󧤺ڷGi}Zh|gY:px ԋP,p-dF?!'Bɡ9")sD/%~6Y/gϒ`nf .NirTS4UїKSs *IDAie/8.mүa֎e|12tMb:ӈzh %AKs:z&֩Y[.XX\K 7Nȗhk#HJr Շ}懾 & {58ٿ}S QvݸփSvusF}6}߉JyE.Wo͓DCCGAEV#us-vckmBHiy^˳Yjė` `@ S&D0`iuoC#J,HWxgnM<̓ J/*g&̹zmo;FE϶9uǬ[ jܥD,& kX DfǏRryVC$fZ`MǭnIpu_iy*M1N'sAbFe{0Y7hN}9װv~ga^/+MgA ucǃo&L*$lrwhCtS;OT/0ٙ];푎$F$Ҭ0#;{^5:yY膔N'^:Tr_P7nYGX4]#r:%qݝdlkF7]&n@T!h]^{7 P?.bS x[,wZ>pkZt ݩ=G?B^@ѝ~^#aPlWMaHi3=Kq*|Qɝ)pJc͗b>P\T;,$Fvg臱ɀ!53:R'5мZbmm!v;ɸ̆2[62σN6)c3]g!(]+ᥒ!g~}%.,L ;}>1I݅z3Π|n[3 #S 8ɣ zސ3飠 W@L/hl B_>n ,s5!8rم+F.B8jgKz!"sSFcAPWJq`X> nz)|P2tJ-\ȮKtuhΏ#1?B!]\nQ ?,{kZ`V.w^f-x]͐9DS.(Q%N4AסI 9(d ]婝(o0(^c <# inUsў X^S48&W'BϾ~^Tt܉iMh Ϧ7~lzXO0kЛt!0DO7yHpw1ܨ.zAc!4 5'LfU`zxŤX~TԮnԅ1 nS;YS3%sP tBuV5N7:a\6ېdg$a%$@C֍fyk g=ѺQ[-:M ^/[N{68CqĈK|P%'RE*ꓻ֞d(Ax}IW;rLD\8/H(WB DE54q?Xy}lL~S?[=Kg%@ ϓEuA:K#6'eu}(_Bg<̥ ϟ| I+]/' CLی(x Yi*6Oq0~ӍU6IC]n6t- s*b{YSb0(fC#jV_3ZՎЬ"{B*+ 1cstmK8T3׽jtԓvWgK?JF͵*HW9YJ.qٖfQ瀏ąT[lⰁ̢m}.L[#-&̬:- UŷчmWP( &+P͕S#RUG'yiKnp@I2A Ǹ0~ӣlVKmtg۷|w hR!݋~cnVC]oQ82g}S qٸG1pO\&h˂HAw9 ӭ (6^&Z1)A}:P|LŇn4APON 1:$W=ePA7ίWнE^)l,ļxQ Ztp8N6tb#ťKEeq'v4"GFEtD!3G{mZ}LZ) 22@JL6#&2s ETF}{Yxlhi-mhIQڅM[Sʝv-cǦD<\RXdMfL[-Ot:8 o10s"C%^{RX+{cՅOT[M,Aw1ÍUdm6N'O^>H:[j)Rѻcb!ⰂqxFx]Eo؏(7\*_4Űk:Ɇ G1*`*u8*Z=o,$gG\X' kn ь;Ra;)I|L?s4 Yv1E7 v ӆO#k92|FsiNJ_]I?DSPmu+8Mҝ@m< (3sHOKD>us1a|#S5ZJ9Zt/U3rOid]VRVՖ2#DS(mP&7]αf$d) CT[m시䚰e`U.jOhL>0ϋGqt/>jK1@t[#_絞+8˸J Gq o9X]y_y7cjHԠsfl[|bshT]MTA p;K~-|RNY!{ q%ͭїJF]Hp#_EM ./$,A~VDM뵈b*y$oAz b/<ܣ9~Rņqp\.m6a2diD'(TXx=>W{^;%H?qxo N@0 qFk"zT1(}~ ! *Ԓ  \46 DCIy$/H> !)GdQk1%(T^E3P4!薝x4oឰ~8G\h?=yn~#.OJ=05stwk; BEo2zҽ[Wr#,u9ǾT hpgW3l 2a&DJ}hPpիjjSVGnyoLBFCav sGIp+\#0} #@6Ug&l+/<%'orgBa9-O-s$5 j]@/ulL41nQ{8 Q!Sy۵6:'ʽ͖w 4Y;뉞V%;xǝxQ=reBYYjz ވ6$^0xl,&G` + 0M;v*6fN ňqē!l_Q yS)xKQlL\~ƷքQ~χhЩui$65g V%rTIA㲋kg%#760+Q&KP<yLC6NK(,nGTт큜63s9b?azQqc w䩸B QM"9*qg Si%ZUYpiF2d?[#OP!Y]k w.rYaP_nlJ, 7NA)юsi"mOUrZ)!4ζ,jzi0gHkZ\LȴAd_=r u\VIEC0j8Ku,t>Piw4 2E_PH<39CxNmIᢰ=⯭ Z_>W Zg\$]n=@~S@bFcum\93D3)6 tފ.ų8aT'tcN_pszA'׆8-f$ @_UzH錜XƿBFhLGDBnѠFJ249}TWwVt+>7Y(q-4Vkܖ<]~SH!hxeTPң %]2<}*MK[w"]9V2]`)SE Ÿ ߮h$GLͷ4}(9ⳝK8̣`*{lt:h^o j{dnW]nnD)]#ۥ΍ٞn1'=miXhss&3!:uH8'w#3́=/Y/1cW)F9'͈snWȖZYn0r Y2Cűb7LHeP{x1JñVuq6T<31-A O;C@#ld_?bJcv⨾yW_$CđASU>ӱtLb.Ն0J6 sNobO˛QY5uJ]"nY@~]IB&raI>1qYRKY%}o#n6  CǰJgF:FZo 8c FŚrf--0!^lF2 1H3"wii=(mp4Zb&@]>,hła@Y&=w‘eђh23xRu-a]P3 3QGqĮZk-Jx+l]8g)A K/Bn-!7K|;hͼt?e¢ّ^q5X=3Zz)N.!B9*>`Or#]1.V0_FoAI" 6lOt =x.ߕzIm {o\pH#_9X ͉TC]yʚ+SDz!|ގ4A*7Yz^UgD ;1p њp.FolU;UZ-CكaYc=/-Cԟar[%sT0u:K%y[h&(Y:h,uF#yt>ϘjRfw8 #'POPdK8LHBhw#'c0 om³d۴49k.L*~،ZBm&4H3r3+/'|-{]7`A>G4*n?cA'ЪB|򶁞42])KZ6xp_- =n[!42Z붌Fvq+'3{Hpy!%>2{P P :4Uw';ɘ!Dw`Xm9g  (iQC$p**;%:' -sHw Jw%we[ZZz7W w A ~#qQ`x< j J$Pf Dxc4uAp7DiȒ_5Ui:E6ԉa`miJ>-:],ʱ4ȴ&rFH'B b:Eդx<93gF*%Ja7G|$1jp#2"]Q%$,kz Oҹfޣեjn |7pŗ#a|*|%yD1״$gٞQ!(? a3S׿мL'Xm Hy}'%JϮCA6DhK..T>@S6sk&9q4XӼ4( E vt r&KL%;@9z+9~4X0go* 1#@7l`vO_RZKK1Qֈ&˰~.Ik}^G0Ӹd 7-ZR9s <@?KTzC;7g7b9b҈0C9h-3Œ%SK-ZVRJY;Ϡ ^z1MH05[Œ%SIk8ZTj;MMIZvX: (([҆J`IWobc_D`9ҏHɟ?Q6L,H(KRLh3+czWŴU=PF`yCEl\hKĆO%%y[EESv2rte>h:X~/YxmhB(#ףՈwDaAf?x$>m_񰅬] uJum00WϾ=@o13L5v4"7+1 F> |_3۾GAH&S*ЊK'b2ҽmF'v!zV# z=u޼uCq[] `۳I}+Il{'!wrrr> 1T.t}XC3sPTP4!2 D_p'#]]ObE/E*Y *>.Bkm⯍0'tz1H1=͛f8}z/RS4x=hK9<#7u`E;a7&Cv=ϸ`H*:|@LS9ϕe3Y[[L{ENĵ>t K.mt Rm&zV`m"G%75gnىU@/%O v!` q"P>> zх$R؏\ɄRrBwcV ljz.'0Fi5ԥ3 BqVOCծ)H֛m6ULiD:9a\gޘ^*!Q#~'Xz. =~v9#!hD#JƷX ҡ6Oڂ6@h4B&ie͎,%4ŷeDZM@^J";@~1I$%x  XkD|}>)+ ljÁ_C (I* ms0L@Ț{,PЀ{L9/.x},OjjtITԊy#TKLRpS!k`∖׃~Im!*b_Q[c9*q$coPtHOD-$M)LZ|uH. _\;,#noN(Zc6B}t<b@,aL,. M h.)#pyOU adV@ۡmGLxU" Nj`3znaSPnﰫ;luin!l,>?4ן&\ۀ2V 5=q] ( Ɛp+ NY!mD@I. ”0\[\#,kEΫi N*=2H&&OAHCrAQCWZ_ Q9"-]A)FIA'7|%\zeSvaGҺ` tNH'!T=g,wvCR)/*Rxȕv-yP(oRJ;Tp~!Vv_Hҹ `?[wmeS! "`{)V7EQ=T)\VϤ4@, #)]#pG5e!PڍzNah 0n-UBb#8D`o;⋼tik3:8~!02r̓-)%vǙ^YxvZ_0x q6aI|a2ݑYq:%/J޺-()WETPf:qMjٚۡ@_,B g)C8&nų|3u1m¼^3j&PD(9' vOw1xQEyH(P}˙ 0q_z<O3 +g{ xa| 7}myTCDͫ;JCTϼ:h5!?5tYOt}xa| ꄖ? )Tv-`<@KKa,f=Ov:L6/k7MrMIb탬$"%ohx2|Ɲ_V_ ?aᮭLAUlUR A9 ܠ:9;ׅD Hg%W27gܘ{'j)#xqJNAu? tܫ1S;<4>( RqfLG=OL·FE=hT"zĠsȑ[I?YfI}G" bTG%بO')~2|@e5&I({ŠvH8K"'7ĀD;v?C2AE5V n4%'FTČ.g^fF  eFR>S|@Hb0xN#?ܷdBr~2(p,=MExfx5'{$RfYV E[ub_RV| 1(= A+Spj)SH~ט(`,MVcra, DMR M&"x_3De?{y_ hKmP'?[U+:9P@H̽$RA{.]9-xS@ +b;Fnv,B[loM@jlrlᅀ/=w: eӒG4XR`Px:.}>\n [.8m;vjBy`KѤsޘBf ]Ru]n⃑Z {1Qd&d҈#ڼҷ)W*e8K gl3oM:׭iHֲ9JbĽ&YgN~*aCKv7"M[1 |x:}o|;':8)+shm hn黒G9ouBXphQ"*y'Z/}6m,BuN~H *%pQ_ ҂u\3 G.Ni3TߕT@- M↊QT{LlCg[C/'b BgWAڪb>'SWlCK*P;MĐ"n{F{Rx~DziN' }.\~gk6=l)J8q9Kϱ%\&I{Do{?q4stw5wѡe+%]-#qGˊ%OxG$ޛjo1$7: @_OeFhXjqdjB6l]a+JzokN'dDAYٰ̯BCw?=z|٬R{Jgs$7ߪ}q)ೳ>/d(h-6czּ|[FOywCJsom?`(ҐFrvl*.գ#gPxoKȣ@&&p)0_|A{Bzj^o i#Fr:Hb,xGd%w: irWVQY)2$x6Aqwۓjl} D3$PaUFH؆l2K@4l@}'AoQ=]b2&, yQ؜~,C$N~唍LR͎K9ja|_~- >3k(Xubv3x:,Nq8ʅ 0FԜ|?7{\4y,a\oJQwƲ7EQ<XImyue^cUu@=s?ȉo|X< =0zWaѓ7E1K4ޡq 3(űcP|9ɈKgq(SmmOj3&.}Sİ>u:h-[W~8Cr6'ZS'SyP,B -)wa.HeW[>#Ћ_nG:B!ʚ}k80G<qty֔j{GCŁf)Y4&UR I+3_ 0zH&G], ^׻& J%.A<޽MHrLѽV97 qh¬NI S] i OT1ZlŘ#~`UQucyX)%lk)uܚos%̌)2=1Z!Â"iQüXk.oQvp3KHZIm=J``[h]ZAơ d7w3P\Ⱦ.^E,1"Tr&IM7mcVkN,=5TZWP_p JF3*TOTɩ ,6,;4ўic!Cac$Ֆg' /g 5 ݏ3y(!\gF1]z' plfpD.V, ~d`YđЄ7CNc'șJa̲ ޡ?ثnϢZ{7Ǽx1;+wt=zܦ+ledF4ނW@Ӈ\DAj.=g2$L>:Ա tNGc'!^؂hK shs,JGnƜ`sCa(NU/~>V|=Fl- G-0n>LpVYҌz:+gNNÄ4JACaӅ[= M~ƄH0/Wd&TD[=Å%)J$:mŢrս92k3#DKM\f,k swz>*deB7mU Ӟzh ,:#0o0.-;,ⵑ_E̠ik{ H[n_ \Df7kEc_.vd,%{C`/-ܗ5$X?f>}ۊi/M: G\@C۾ɉHvy|E(Wwef1g7/cdMC(^\BFW!*FЏ2 `l`TE<ծsX"E ЧI>x 4a!l|,8g靛Gk`Pw,|~-Yb v02$~]tfJ}ynVA;ZwG@C$:e.ܔ‰jg?ޥP@?Nq~,&TU%5;Fܰnlo:ۃ]c$dCAM<7EKdcA܉,r{6[4شVrtd;."@v|z6h!E+}4 *s|pWI}ˋ5ڛ{yUNkIn.k񜟳u-S2 ޓ>=7zXm7/᩠MαP,rN ]/ }5ߓ;R7L!BbYΡJ@_rWR5nhdu9@9s|㠐!BY67+a愗2}r' ̺IC( 0O%뤅z Y1pW`@>`1tsC&yݐ~U$Bh.LeFlBeIt* P_ {<39,Aԥ!ʁREOqt3ԛ}9[ac`U -FGw' :ZԱ*ݥh#2ك޵;icro_hGƲ/-Uǧ Wvyc5Ab@MIl ^.H@#!3QzI$kW2$rW#RNy\L4X*UFk3<$f#MrI땞tJ̀EcիDӏLT|G& IOfL hSh.DWe-tɸО®e#p8ab['KMÏf1-jT;~' )17gk,50ry t%(oo(2"ߟrqNLo YM 1pW]懏}ŽJpfa2xsj4 8.TI3O'7$)ŗZԱmc290ryqQZ:.t ~}=H \ꏹ/Ŵ} qϠ=.c@ȗ(3ș V^ѩ m˺a0;{.}OzIa+؉0B?M `j `J[C\<1n8^UCj0Vܺ҉i.4&'k&oy;LS3>׉2. )G/ ȉ(~[n Vҕ?ҙ4Wi-4wX{BLsPkkUWh,l, #uF`"C:q¯\O*0f?|]{a qh{  Ioͬ'Rz0< =0zW{w vUU]hPq}8CgNN A #ޙ* ="-?7[fIMmJ'!E'n-TA\QIc(v_l`j. Ud/r 30'5Ĝx}߂" ;WHMZ :|0/ͤ9(hEiyk?H$uEKe*=ؤQbZOW/Pإ'(,Vp6 ʗNboˆ7IqפtBPٟp0~J^ }CNN_y[v+zg*eqE8AQ%9-20TEA?k Ceg9-5rEm\czo`]Yf$(υ1t({9S 1hYb7i5&FDsa{)>.@;[KZyT](Lr>Zex_f^A!>K>@äT{yD!ɯ7}j"aN~jy7/kc5Ab@YGeՠ ]y#r+ v$BWZݖgzY+3ޑԙ䍀m66V,B_8h.6d")^[h4 4K%1@8`] %ɵ@8!(+zsdKh~wǪ-(7yY_5Ebu۪(4,yrc.-ICrN*Ku4WKt} (&BKZxSbXqII5ّYֽ%˸F, v";{!v zڶ/GB(Am ;J#^GNF96YdL?dDH^ ڒ"Iփ|E30<;Mg\ybgh8I_lI:יxE#'+$~aMRӷ᫠%Q#6gn*XaV}htAfL#@M:`%{]5dxSiWt+[&}Ϙ&sGiY۴ʿsFy\vu{6p^,׊M"oYسެYZtE車odÆ^͑'Hz$\f,sgĬuz]«,j3N Ig^@9ϊ_sz+̏ok f 2p'z;>Lu dU HYgS-IlA\C`)F51n0GeG@ ov>(ZǖAŕx"B+Z%; `udV><dƦq Vq PL&t\4Ѥg shb_ܑ(PלI/c[q,@ZE*{R땒pb:״󶧚+|l^i"Z㔅/$TTI]U{?y yftgn9p^@~o]/v< @<&>ÆHXҠ^F @dkYjL֯CzbR$YKV"134t/ F3)[o I8A&K"w-WKʰ - ֣|=:vfQ 7DJJؼՁ~DmenVVt$. ?|¥*HIa1w(匔Bek.uqiq{~̌WંbYũ;FMC,]E jrdVq^ IoM~g|#Cj06c`*iӧXn ;E8Jlh3B,H3\C%:OndoYDܱ ^!L>Q/q>2)Ou9M'YM@{*c4%#0}{Bcր<|!JC ~۝ʗç#a95j^GU[|U='ZyM,k}VVJU w H2LLIƇDg x[ d"OpMA"Hm̭8Hs8SGvɞم1r1p*&#fv>j 2ػ.y"kI4Ii^&׿ZSlaw* իP#<{LNH 1R`R:flWcks=!h/)lBͧzwΜBytVap 19 8tNFmzm'.~CrPdў߹W"Z tvui)BᗐԷ-_MaOP-e#K9/yVPi@IʬQx_%LjAl51+^o4Y%t L lujJN[#i}?/&V픆[A^z+Uxky2UVv)>]6.`R?L) Ó6?E U0IP=Xq쫓>pc^΍ț%nܝgdJu=DK0 88+xn1s!ǏOx.2:x-ri"aLEFes|<]MݏDzݡڞ3*(!]E x:znGa5׾pZ9;p7][8ozv%!0P3gLDr@B$>scE+]BOcз͚`1bO_,Nm#v^8My9[AD# q >~ri\D1~Oq1ݩyvD`{큇:V7(ePZdv;]QI2Tڲ[j*<')_7sS026y.u_$j,h` XǸ$SLNXʳJ+=J;S=|S9-FVb ACZ# !'<[@_)qVR^$(s/3mǭ]F(+eM;=B(h SdrE`D뱰_eVNiZ%b;mAEaEɀe J$8fc{2z)+P2I\QSGP6y %9^ hVvQ8X*eR8XB"-hVսv\tTaaRI3u+U/Cf|]g{r ndM{8Y1|e%4}*lEe4*&9~GT )3mh0( E/:J}(ݩt2ϷK/,-!^K=SKQe4rAK\ezDsZiw+;.S xLJTFbːѓ`^u2 e)$Gʷ}ħQ7D{,{E oc^J5˾Lt;-V0@<ԪgҾ"р'HFԞ@/Y_o=Ǿ+gf.B|{T<*ԗ79-8HE2}$|Pv3EԦTͿ@j5sG-²Wi1J׊|!."%y:XߏJx)sV&,M^d=kneo<;Cc3׼煠a ma=?FB#N"p l_ Qs7)j7)LlT(UpQM=U%26iD9^1`)yG8eC:]s^—:Yi`1DjK h I[>Y7`Cϩ31b"Feih Kc"q١}=(J*Bxh00*zcDZ]ڝID41E]R"IËޜD1N3 V+ HʛPS0'CbI E.{^$ &)[?^DH>~ΛsqڦzvZ{Jǒɠ-PW5b;h'jǚKl0/p\8P=&"-0OROA87%Vʠٵwiޜ]i<s"-jZ}4ِ(f" ٨=QjwQU,vП2 ξ`i!M)z?% rh:=L7c(7Vk E_h6]u<]" @1F*,""ybsi`]/9ߕiꞟڨk å|ubg*1!]v5G;@giR=s nSG1'N̳^H҆D;8șnڼo6;޷ϕ'1V)蠅s$]аv"I؄$M`+tp L`#^\ Ɯ ^ɲ9,OOϧnwA>Ækzy˖mU Ö g.e#smL8s[d:gGi3dEjj.( ]E>^=I!HW}k㽻{K䡚1K-qtf?"8%)MJLF=B-3;fLcviP(rv+L|qEcZ ФCgow*P\ ү2H0Fь@@2uk1@}g:rM,SY;`cg"B!TPxZ3)ÎYAR} {~ǛeE 9PWw9s SC\*w^~y 14_6FL .!|фZ]bjvH}kpILlt5 Lc;Wi3?Z #z.IW@5fuZaՍ`H!?|'h&x*{Đvzޅle.7? {"Jm5TP0-7 rz1=/ ҘׯrUmavsz>`ťZ9@³7&_`d7vP~vr6r %r 9 mc @T@|noa)*ljiG;4&Lʔ9OƘa)׬U*;^F#p*2r\ \yRGδ{^4b;'[Ŕb܏RLcIʿOWz9$[\uE{'Jxf|j R{uE?-?X; PTB ı`W NjOqgsvHJKHqem-PToA^Ÿމ7JDpȳ> ah+47]}D#eȶΚi*uA]C"l1Q0Cj L 4$XZ x ݀\'>>ʎQ$ˍѽyw+C۱EcZpK/!pgjF^)  f(NkiXvj!G."N 1?cfڎRBvDfuyp1;)o!/x)D!M sBUZ/D*M8[~ZsY&1!c4bkd9 J!â`1&F#-{g@=H6*4Q @ 7Eq|ֱa@S{od|XUìȬOё]%я!T^ ŗKe1s 3Z# _3J%?"8%S>˗,oo딗VS!τVB=$)D/x29j8^bSwF_? x3".# 7(EKcxT{BN?ߘpK/'{D-N# &ti6y|,6&,QSԣ͜;|*)p޻S sMZ7Un۵]AzD5E5,IMZPֲ`&p»W6:Ӎ'( ֕縎rD0Ubv=YDVd # /YJ{Jyace1{<>򸯵GiB~a] beoC6~r{!>+C6x&!+ 7# :L(mHy si=2MZZ5[|?Oac a]>]=iUM g]A[!4 5FuQ$ Z_ mLbR"93Tn "X{))TbYb}lss#~z`Kv ELHw\EQv!DGI}. k:J0*^aZT4GpJFxCT. }Lc#xʓJs4tnf'\CwS,]x/A1iYDLsQD +lk>5]{XcWm@#%L *(zR|7r ~ToG1@$;2&+BD<'[x5__TCOu+X??+z2 )% ڌy{R$ ;;CtRMFR2큂5fk{UYܖC=qZL?q;H39P L$ FV%kiHe@qknX!T}#:/^4 7*\}Eɐ|FwDJŋ\#PZ@'/ʦ:n,Xhwi_}~6rU0TYOz6;5.q>c@l>tw0GPٵp&!O݃ IgFUI6̝D36|ZX^rz Kʫ3B1܈R%cZ,Siuڼ!#v,#n^q)zY [.ۀ/v+ O/X8_v_ƘLyAc&OvH 0 |Ŀ^&{ִ,t<=r1F]X#/%ĩJAASVSc56AG!;%`["%w 9x ddeVy AmdlQo6[a()S7Xuc]Y֓YZJ☨#FZZc?I Qv^TTQ +RbQ%hh5E]:}P3GJ06D]( G{*:jJ=۱uŖW;Ww\rZ UhG{﹢ ۵])줗ydL|U)`-}D M_+ ZL>H1TF> t rWL sk5/KF t9dnϼ<}w;S!At!5\q"-*'܈ (&=LA-]{V ͤe/vLe/Rڬ$ŕԞzDa+} `9dO)yNYI 7+r̈#A9,W{&!b~6L_iu͕ڏhwoCj/gBUCWU/&OD<I4Q@&esw8M}Ui1q;8xQ2  hR/A 1Ȁd9`5F= 1WydbW/%vtTxPUBnp4{}))B{і- :ZcbU-'tmD߁^@#si\[o >)Bh"=-VTCbqY\<.V3^%<7фuVjVg%qg!. UOZݫ㛲p+mj iH$ۏYܵ mx%6\1kzyy. kU\:8jm+䂦5VsB*q\T1֋3ܓ<8{^֑yɏbPQalo-“p*>!MʮbG;E!Hl4 ;F֧4[cdl!C'E<嗉ٝ_[๷цeJ 0gsSε kt&Cd`֡gڨA{& q[i4 4Kʏ.u1$aB*~B2))H̽3z3t$."ZvMGXqjlr9;ѡRKn[ZdlȽB*#~nU>tU%6O`$ӳ\nVٖ1I8ȍ/#@u&M -ě-&b[>_Zmi>߁ұcGPJl<`LbC z-76Q&cCmS \[WތeKYNC_6RYbg3~9X#F4eGˁKe Ti#(\6J|CVZ=e.(xM췔姼e 'I5j)Wg!( #&\VSdP2(Z48e4@P1lfv|- R\>_L)7rDWA'E#ol)Hz4蒣N)L 3\Z:tb{uD5KO=fDK5b#|}:TnIQ;75!Խ\.R;P/:G)=}1Z6Oϣ 3+ď XŰ:6u/V`pGvBD!67LR4  g~xX3fLJa[q^UUŇƋ<Ʋ}+e'dnE+9b𯍒W{J(S?{nXh=>x;?GwM7bވ#~⯙:sԹi]!Dn@?*6}nӔvOkhɓ6 J-QFZF>gr V< hg̙I;Y%x#[5ji/  ak0Sܬo!sD78f~ [0Ҥ g4Ql]x6-@vIu*|<%hwux.#{+RgKl'6 /0TzH*c-|?C0tu7=[,gfl,=,|122%adsO . b/BG؆ g׽+Ҧxտ"Րdhk_,7i9\^=Y[ Ď^7Y!" :d?#K}hXY$ry̩OrK5Ga#Cѫgڿt"qkCjp_~ݫzWpd*y.$E8M ;:hWK{GgiB-Rc d֔O~u>A?ѓilp2#Z=GTbGc$ȩ*-@gUИD@O}mHHU!W 뾭xE>eQA.+?͢ԣLܪZ?Ĥh>ԭr$|}W;aSGz]nHmM]3EP/ p\=e"8LEy qnd ,Aئt q92L)ʟ Aɩ$bXޅnN6uĭ S_Z6{㢅6uœ %&7ÍlT55k2%:Zߗe7-2UsĆ"T hVE<9SF Km.-0 $|Yifw(G6C-~RArKG \W 1(ax(# h MwQ )F( j3"^',W) TCrJ!;GDd玽?K}D[cir9pl#8lN:+Z*\H0%`VY Sf6̲ ѳb4uƱU[rvXM/bLS^~8 %ޙ*Dӝ:_T[U&ƅSb$*swjjr}OKoɝK|Zy TH ImLDS]NVMh^IƮA>y<^mk 'hE.|2LutH6II̮9Kv$CXpĮ3:hnjqnA~u*VGQ$ʓ^cz!%؛*ќB_tMxU*/@"3IR^džj(L\qQ=<-HdiKİa*Vnbt9I>t^ åUbø(Pk!FKv+,(7|[ o7[q~ 5d=J[я+:,EMXb{_ei3#Ev](,ۛ|:rl9E?pfg{HxQ܋.JMDڸwD܋7OGd^ `HF /bK޹9+|z͸ )61Zp B|&W#>ۯ F@SQ|F3%eZͥkN9@XyKMݩ誹ouO$OpؠbUSCY";)QP>Snѹw@^ӑ>ࣩc^QQp @EMmMAV$K@+eR4S*H n; <6fE^7v^kxƕ&neY 0n{^Aq>7>8:W}ƑB˂nС$V4TÓGM[?Q}iyN2\dDb V˼އ~Cx$\%d1yRI4  MMQeE󴄑l(N`{p˱ !:|`8)1IfM%UR`7I`@-b}& 4'1x}Ke{C?I]WE>V T_{m[di֓vWs*A~rbgu/Gz)wubqg!c29;($Tl#ւ>~d/2AƣY5o@ EX*8Fw/&&, pnl?q- \Ͳ5&,*aeAO&3Nk(ld֍ꭑ:&2udLM G@,Ew_Ll}3MbT)4U'ag  OMȅ g}KFwh⩵:*J~#j[\pVjYU脇y<-n^Yq%TEp c`j\Sβ1!> MuSD_q_T!Y[(&Yi?Vë7M*˘T#@ %%q?ĥwt'݀gK@^?w90Ŷ)9RYD(rbEa[4CSMr'M`,Ɇ6=IQKt>h7=ߙ.lmx#q|ZfS/(uk)A͞ Xx%\ K 3PPCQdW<.L Kx͝MR@1 fZ|bCdAk]CZ e%GNS+;wf L?_IOY6;%=+FR! 4|fZym*;T'9m-^OPۖqQSYL{P%sُW:]bǠw9Zף@[@ L$ HiWD(ˊU3-~/bmYpP>2xcT"Vs HaVwIB0?92<*@wxJYҰQyrND4lqXr8"W|-˕  c5-w0dwX3;}b0ȩkQZPkk>l L|gM*>l6 ]ˈಹ0PM!Z g5,6ɓ!Qoo'\,l)X''mnt'u055<w(KQQ7{bIN!"(4sQlbJ 6+<٦ |\>O\ m\(NJ&?!{NdvѶ]g;pe4B3x$3/qv*H+AVj/{Xm1Ev-˗'OreCW$U 5 X E-Xc '#:/n?6K5N͸mpޙSš^are=#qA1)gk}T] )D_8_{rDniS79}m@A( kn(ɿoE}cwwv;Ӆ]eۦVXg#/,4N.tU At )[ SӾA{8%ālTQqļ`HOLߏɠhRPtāM#U3)D +&F#yFˆ2y۷d+Uއck/d`zgQ%nuLkl8g̛=% WQsxف+N^7A/n)~IJ j̶T 7N ADeYΓ-6+[i8Q:X hݿ&W;XhJzpߊ%Xcxy$R$iD#]@qgM݋s6 jJحGboC5-.z@0COV ƥ4Q7C"6)? mX'aԮaa c2z=Y=0?WP;Z5XejqrܬEM`v_VJz.8 q鴢a:i_Gn{]@30C ſثooچ4NZT 2qrV=ha J"@ATUe%O\`CN",f'VWx)\?-IUF`@.6'("DFV [ֵM=!H{ab9}N,5ԗA忬m+wrtvˈgbQwi=֙qx*;\ih䈟| @:(㲀0dO80{ԂI- -51loegSsO@:y,}K4"%W4"%z&9p(ilDZK F2e ȫԋT<PBBT <@#%ϷA/njR8xƣB,JJO2k,dr%Z^8L~ͱZ; {BQM!OrGQÈ=3P`#+܁%stQ[bNmM77ZP_)3u`,s j?RitTp߮!k7,t% jw B-%1#!9,F:Tx 'q$7VbW4|6#kKeZL ;nڞ&,VMiǴ疠GCfe>yn漶ʢ\dY|1Ѥ`BG2`knOqcJ P~Uعf6ʦ1R퇍u!}jD|ftvכIUfܚͥo'\3ߤR? b7DZf"m)̇n#Qg#6|J/h8ZM2m/`8&j-iA׏,^@h:).Œ*nvh`ywe`s2]KSv.8*7]T/r~)aZCP?Bě/?DtrfZO[GZc;olyhmػ2+Y 7&[LlL"G-`&`Pg(-[ ACGb!8fb3۰0. g35sR3/=#"m` r_ONf8:.+a a:j)6vCa.MS KXgWؼh?ʊ7ȷc1}4o!AMߢVԶlJ^!%n[8SGPW?[6pb x;eUF֍0aCjN95lQ˼],#y )װh#uU9lK,;sK=s/“mǃ0 |z}VyU*Fb%xU Ͱ6ȼV+[(?o T>WڳNX(E+߆A +AQGQbSA[5C %_ g 8ڭ)ypl鸶U0H&Y,iČ<9RD=_eo,7HWK0 5?m zx{EcBo!ÉLp+zQ9qyyR.s=bF?.ԺUd ⸐;!^ ))] *Aw SvI::'`HZDx侟YUqʪՔp G)Ng]7BI|?K.MS KXgWؼh?ʊ7f͐ Fva?uSqAytRJzRQwYz4uu6F5&ɓQϩETQs#H'qvR|k"t|bakp34Ƌw_jg Mta7\-IE1. R )0\9?q^HVMlP:뫴 YTJ^lEGHrgmT`$@o af/"3􄾨KF eh \\^(mOo m #/P3j//DЂHM<$FJũG!$滺CDNoq'q"$Ј\Ј ^hac*ð-Z]GN ~.$ F%l F]v^I L&[Re}_/@{}gxS?~02+HrwfjPb1^EHNlub9FZ582]kD{*Cub ;A9~\Oc[2-3qzHu8eA-Cb"ʧX @}_O3*q^P1JqTHjL3 6 196zFߔt!*z E~xX4,O/f&ЈR)Ը_M3\d P?"TbL'FPZW8`Cb=Ȍ ' A|+Zw{Mer$VoZ* ji,ڑT%ԗfJU>kd)Q R`X4#WYiu_B~D\-vjTbsv#Hd6j=-IȾ9SB0gvc: #\LkZX $Tb4[kz(ꀮ)n7v&W2_ϸ3Y$rxX:z{6ˮI@)[TX&)\՘n)):}Ҫ ?N*YlLa$|w`y]n(؈4)~g2F[o*:\Cd/s ZK_'UΚ ~} 4yvNȏ>u;Rw'&`*_/-J野E_mؤ ˚^ S!qyHRdcSg, A0_w4uGS[ (YN<$ދܠ>'rNe\[31\B~GrCbIʮɰEL-ެi* m`"QgH5^4 󏡑90n%Ej H~ohFӠOr+ .(J,+皟#& зd|כ<˅6t,.&Ѳm(Owe.n5fCcZjO-iNOkuD (Mkp߯@4^ڣ8boy\O3]t+MQ0>)쐆*ܼCyx V B>q3u,$JpGsi>5k釷$'t^VI @}_8E!.k۪7 b<}eaFzNqץq j4]ҡjߤ6=bqb&7_KjoŽʧuhR<kW $KAT$Tw^0/(B[m%MV')W)v \f戥!G~I;ΟO*NPTdToؒH*\sNTwcp"}L_M]%Gy{x(- 'Liƙ׽Jq, +/  3~x.gG g]H,î5 wc̪&|tHJ/T 5 U3k˼Vs6[nGh0bNn,}'Kp 5?˲D J4t"a5x:5wqЉ͡&튫G(-kZ{DTkTM|m…AP7HZ~Afe+MvY ORgPc|Kscg%@eraKck!#B$?6ĜM#CJnڣ)e]orka#*򲀰0,u_ŰJX݂![H2#sEW@nsIm6 nItS?䱌? r4G #EJGdpJNXzC{&Y;6< }oLk:Tvw;Oa&wGDVڕT1 ; Wf^S9$sܑ6]RBG+lRa2-1H SǍ (<3s*d%0| 1 Swt7 ('(.B.xrH;SvHgiEմDt>;D+ۻ;v#VL^Ӂ3`E ՟B5AhIӰ&VxZX-}6@ >F4Mٚr&3v/&*q3},)d<`< H4܂[HE@k"D0 Y5z)!ssL WQw4GoEsmQX,+ཙ(:t7<5X gB./͖ gv3[HW?o 6RvLL GEb?[wj>'_A*#bnfZ/fL5>(3 ۗsJH%ٺ5M2%Ώv!a~4]Sew.Qfͺ`I ,i͞rq7 =>w=\R8ǚ^BQ۬,ԦvmM?pl+Ѣ)GGhrzVn6K ԑ%g%ٹwrq3{IϼғBKq$p O9*:MJ{SX&ZhTeVC5ILW /!1FjWjRi91p߮1"ǙrZ2@>JGh42{:XdMDN`Z232Ps_3>D%LVU M(ЕERc`hy|v3>Dɥ4e.F uB[h]!FVV $bS(qX,=׻Ë/M}x{ouT!}.Ȣmd_@#\*xbli*\*q0cL L9aާ`YXPoHl$P&(阤Ʋ-}y6>A+؈ 7hǬnCa"-akTwtDrg<}DˬNB֍~׵J5Nc{{䚧zW<  |߶LD%oC-zumľ`H@ NCvW;T*ƍ<m^sH@ w@Ip*dk`OONTKC ,K|tt]XzT"ZڧVoD pה$z(~}bXc+k ڔhR;Z!g؂*l=򸯵GiB~a]>C\eG(-kZ{DTkTU_@f' L"Yc'C)JreSvir<"_% \V[4kl_u8Mf͸IIяXo0Ė1  &!1rQmD<:~s >VR(Y]?90c@*[=Cz4aԈ҄4<."|s5_+L[(iO.ԫf/X|H@'X߉wsc3vc\# 6D|?!{Ut%qSD@-,.inͽ6(4z LǤ u wꙓ(H>8 ݇eR$6Fk3"8BKݣ#S;hg.6 iÝ94QǺnl}Ҹ@R^n:fq 9Ʊ3^>27pIl Ak]P*疷 iAr)*5ǃ9 w1S-rX^sD0D!CQM6![8)fjoS8yy`2A[||SHH[ct]e{歛 R]{N_QERMQcew`1%̪T zGɀV㎖rhҩBS*!wU*ֻ}ޤo - {"Yv}]c~U, itl_ @ wLj8R+s0BQ_u1GL$$ݣ$÷]@=?隯Rێbo$_>|4ƊAw`t+dc<.`="Y lpW`QuJs! $G y6Rc2E]`{^Z)q?xnhĘ{-?$@'nغ D0e"2d (jӭVg]54BcشE½͉C;Mp"4wǕ-+!hRXE&u=q[Kw Nu!Oa.!-`{ؑ`.aVǢw Qxtđ̶/K+LG1;:oa. jݎtE:Tϋ/փTe"@j ˦`lxlX=`stQ=vI 麢?\)"HUE ^ڡ0D#KEdߏ* Tj;K8Ei%}3jWquɭDh^rg4qҞP_z_1U〴};P9Yj2AA'-®oɨGXȃ (QO &aچP'q-\swXhQU1*f9M\cxnøV!Sc c{jQ?0 FAL=t.ԤisPѡ"LlpKe Ӊ%"2IT]>w I "rV`L$G\tM'-n-$x ld_ꕊeP[;#20!Wvˈgb5«"q Y|k"/+ao!(hvE(ͰuVESd' "(4+%O ,RfBiM˔EWZtvbKyG,d6jzXdpլy]`B{eǶS#AtJL8 ݰG6\Y0˧#:1V2rٮ/D{=Ak]Eƻ֬f:|.ȟ [H\K~v[$Ay욚X{ɽ3[_ցv+Lr%IQa.=0Q~t'Psbs՝vdn,w}9!BmNώxcT7A(DS|U+$r~i+Vz|.n 8w^, i%U0=ܔέ]-Im+_lt .ߔy w c /b4O, }V >QkgR*pK/Aߌt.i-r$]pedHz fǤHv`*zMXomeͦ@Jx9zԐA줌b,V<=Ӻ[dt@plVyY9"{6ɐ()k;6?KB8X]Eùe2_z&7+U|'@?r]lܬ0yv4O\F#DjOg܄jy:]EhH`-*xZ.rK=0<&$Z{I7!Zה8UjzG\݃jiX=Ĩ8W.V:QL@f|\ iM(OB[G>ϏcFdaPzxE{Z+5$/=v4sted56bZ6=j^Շ$"d)}\PZB )`!D$o<&iy^.xSЛsof\A|b \AgI@2Qv˖;ryBlN3M.O՛ ;%jms6i9>w. ^ )jNs9@Fp29CZu qGɈA{6$!()T%Es:.a2#ga CR*lVjKb`IFOO,Sv#GH7q+'K<_8;= ^H>e5p11QBcs5" &^;k%?wro݊[T'da !fE&??e9RKGŷ))Vr:UGtoᄉz./]b7!!xJe2ĸV.ĚJǫJ9ݬf`+!AImZ_ygủ,EGz8@eMZE3Ƞ:w@'-uO +h#oj,(lI˼V~A\l=?q*v#DVT6${އbIP[%.p\-AM~|f| ;rtǎ~O2%lx؝m:1]H*) 0汁'I(a2$}L))_;f:E)N¨CdeiHfDzgzΧ&z~ XڔjƇ00F(hÓm )\ƅ'3/)h_ @7G˵I e|acYQ¯p@*0AHeAKx?RPmwH*>ȵn#vMLڞA~Oo!(hvE8e#?/קR_Xm&+kXw#HCzi!m?0 3h:|45ys 0%Աjy}LM 3\R]ubν'TY:'.tiPl6192 57`]9lWOI=ywaxsf81C^ 6ol}9& n~MOOfCjj%}$9+AH}a4fJ#Xc.ʁ(OwxA@E60 "Mehs*٭2lI/Ggs-CxniTK1$ovj \E*7{]x}ܺht nk93 N(REh ~:DŽwњ LŞ|4:oD z1/s 0&|BLI=;h$Kn(-kv?BD$U7$U7EW F)R^}k6;  )84,0(gg>njƵa+7Fć *w}PxQ vf\ah5pDXkD_T/qfIЃȬ ^[-n"tOXrJj ǀ]b= salO:xFi@S"Fma+6!ɋ{ID9Ô5yۮy Wcc/# 3ѪKh (HDRFL=2cR,q)Dυr܏k1 >/y %!q<9竁 |,8 yS*%hXL͜D^4fZMZXoē_A25S"v%y|8g@LLUR_SSW;?яk}Ƈ0<0*)A:bQevz1|4!*%wuf[&^ºZ&P0NAH߬ P^?8؟͈oEGYniS!9(W&?Y\]7e@Ȧvp($XR;XNZt8M#n@Y29Ti!{<߷|ތЂy5Nɀa+jBPC!9U Mг, G9ScP35Ho`'ʩo,GltVSs9@Fp29CZu vR9Gs΢@_ʺ5A.ߖH>b_ e,S" =ڲllqJx nrֈr3 3R$aɇ`G=):A2+h~' 2`ivL8-nYUb; ӬFg`zbwFo!};hSoWI%$f2i# a[ڬ 6ϵ1?ћ3i ~ \}!k:AB "/מ i ?$s%l+qe3FYtU2& %ʬdlӋs&chJkl[cث} Ά\tNh${38dXMVJ?=V/wA5۬Y>vo®r&*!c&+3&<Y!H6AOqtNUR,Eep|v`ÏQ2Jٍק>QdX a]W8(1 7cq|A˃w\a2*_+%O…,2&j[0|(:1[#Þ8\QJz q7q2uWbkI!v+u0l@wƪHonvÞzdTb#txMدkgMτ,*RC!7Bi!&W#YǞ`A|? `)`SV,94,ºmZ][J0(6hͅ`鑗Z 79ErLǂ40V0V39cM\+k`#))b+=_TC(^d==ݭ?Ч$bT F:ۻ4emZܬ+a a:j)_6+˃v,V yyFe?FO]Π< `Z9 'lV=!jfO%J2?T[pp|',#|KQT` PܜcxrH; 7LHL):bE QؐWԾ2)΃Vh+}.RΥJ!@>( Gu5Z w`cby-gL?` UAdyfQɫa`A6}N9NbH| E6]ÒmNBb -Y{wsѡug&y8! ʤB\X-=mg[}$R)řADrF`ȥLR!D "&6EK04^.uZ1tSyڅVo%=FbܜIvC*g;@,qi郉uK[hD}Z*q);&o!޲ N>ol!ތsgKj@Ƈ,n<"jijѾXF Cd Sg++_'2/ Gp+$.5VԚww%JT$~@7;;o_rs`IˆWA|N {JʯVz$>JtqTUB j"/ Gbʼn&"Ec ǒȺ!4q{+Ƕe}?mNI2nN0~3G "f,-.!W9s;!7WpV)C+k1 ΝOteZ4_Gb|ڳ\6q]*Iž<@9쵱cS-Q06L'ME8ЕGh FǮ P%A~VEFu~!'QIB@0! l>;brK[uqGYMW= #>q,|lǀ~f[Di(ia?U~/Rσs9\ 2o^&1dZ6@,0m(eV=hdlo*8U4p굥bMw4>7NOVpÌj5--dqfOv!;hSo ߏdB WeB.DߐZ_û[JOW[P6}Q,E8d*[Է;xlIZ8(ic!i'h7d̑Y{];)5SKO 0i47חؑ){9wnxOa+1-Fwوjg!& &eW]{ J(CJn#z0 \LC'"Z B@OVY ,__#EAW_ 7x6R"z!@_\ 'y9)S;Z7<-X1rc T31L(dًC~oT2)/:gq S7tJZ) O_G c%2B~Lkj4|[w?ciI{(XF2|䮄HBOߺܭk֤Pگ7ub6qY.c6f JI)wFkhGUh7Q:oL) 5m%OUP$Lj#~Kw[4Pqͣt:&?BU q{Œ鋾G*ֽ=ٯ/ea7TbM%c-^$t N[k[+L).]?aaxd+kDtQ04 zv#DV wٹ`+*8IJrvvL8%_I^݌xYB]` uF{8v=R9-$v VS_0ɨJ0!fBmũE.u=twތ'#d<&hôYVcx`Q2 ?nʷ 8pVu# ve̕jUEoLt٥DTn#vP5ۭ1l˃A~1~M㶭=#'.5 <:H!<4w$CG1Z; &rEF*<$Nt-7 s!et8qz2 ѹp)3/< ;"'b!In339}71j ۓ]79zG'S~&އc#1_ڔ2špuޱuBL [z"l NA=:ms>w%R )fR ~ kGYD|=,,qQ/NNxmxfʑ/6b|@0OvPz}XP3eld.<洿@pי}}ȟӉ-?ȵo^si 'Z>et1چ%+\*҈hǑJ]ZiS@*Nk roJ`FW(qX,9gMy^[Q2ztRyFЏUZޞJ# UdT /LzGv4S6 >1vA0NR$nu{YG/4=?-XcϓGQbyؙ1LUG[ |r{-Nk.ڥj82騧DJ+ FwjZX:5dMQS8}[m,{씞1`pܾ=o} z oN[nDU$dZ9 &#V`ciq+1/$ޫBXPE/hQQ$乖-1G# 4qXݛ68{喺x`Z2_,ǣ {:LIeտؙE>L 0 ƓV+bnzv{'ʻYpy Pm-԰? u(N6p4v59,7r\1cz2xf#ŨX"ww{`cz }9vuk]"to_j;;cVOujp V{[zq_-!\/X/ڈrغJט f|W# ۣDC,366iH1cޠwHCÎB$Ma[*zk".?o2OA:FPSWfHI=Ov}>?37S5I?fԳ]۟zm¸9f e3иp# "!>5\Co?L(v@31_ 0Nإan]?>UuJ?|Lc;\O(QݶWK T%h5Moⱆ\HIڷ?$ٴYvAࣁYw3-~\C[GQR/ |A2t!;~=|$}/kYczsiK/!1\F#@>/>@' TrGƞ B];_!>2)iqfG cAġ7%=~jvd6zβ/OLZ**c׏v GDiRޥ㏎(>gѲK>Jc'~ȵ :(W&; QB 4L+M7ފCO8xz˞>X\+og}`,vo!޲o!޲ k !\JTϐ|aהiZx-p@c4Pg`;cVOuj'ן!g[OTO4dOFF9^|%E sv/!!*%]AXeQ dV> e:^j|{5TO4[ư_w20} I@A`j{vt zAVjvm2510F!(41hv]g³T`d=Ġ{ qۺ(zF1 BK+eJ#OL"){ l K0W67xr^2̾䶭.F1tǹ94lvU,yS*CxXpk$"d@?7?+`(5DHZmo.7(]2સrbKJ׵7H( $(i9N] f=Zq^ԋ&$3gwt-M5f 0X5;RvJ. LcyLtlUujx_ORѲGmY𙟞I Y-Vw j6输XϏeM Ia9qwƛٴ#\a^}卑,6;bA%cOڎK|sSܢcV--ahc6\Ay2rٖ(Xw{ot/FU ˸ɉ"nqWsFمإC+V'g`7?%rybt?U BYkvb}^եVk*~Z gqԡ`>܇CѸCrm''6ꦇxeW'W",o!tBپGQbHKfa>$,nץ?-CE[^'G zhr~yRQV:<_1fURP\󒫖fe֘ X\adBPJ4DN5-9CR_dh#V XgO3s; dgsأ:VϾ__{ WV5cI`_ r49k6[$/)2ڕi=Q#2ֽJQTӲ$)VZq0 867hu&-^5lCM>{PKOm PZZ3m׶$Hɞ KFBQHbǿ@/_:gw½Y}M2U (Z:tRIdsܙ;?!=qc'=Aj||Wx87)#ަYM} JE6w{fu.1R00,ɬ]>yLc\*wGn@lD H$1sdh 6=s/V'X)TV7:|eXO|*7='WĹ{b |tuj ,6V49AtIb HIQ)qJ j8'Kw}F>5k釐H~ohy1//WN >.˃{P$*w#'Qr4=y6ep0<ͧ˄V<B/?Wr#ZjVPU3ifDZP…Edt]6[S!dho9lGIkD͎ԍ52K&ecitUF0dN8 v.BBP=x=ЀT TSZ8>ŁՂ# V>Z'NY+TV>2 pqc{+||c/;F ^ULJ6ҵFz'6ꦇxb pEJg2&U\"of͐n9Eh;׼Pһ!9 v^pX^P nbϕ-S2XD#?3&FPZש~!:Hn5Hn&پ6mVm=>Pt["bfIGa_RV;"U lV%Ehk.ddV-xA@`qq"ߚ@M՚ :+D{wD;K::zLw"0d_HJo&!>4T\i<b@r@$F f*nqtrnRaXIpz@1r^Wcׂ,~/N߅68r=g. xS1B.NktcOy D6M ՗X4-&6]wT qQ6A }6K콋dezoj59uٗ 3gl1ASn/6Z S>/REm3re2xqܞȶLdu+WG~QoX Y'VHcJ^uXUk֢ b:ol[nN!;C$YAv:F/ZT/թ 75e (."UpgOG>]or7وj0M-|p SN@ ;(cn^`X,ּ5x$8LӔ˗ -|K!'\:N%%CTmX?]k?hES߽r10I]H;hC-t0_&Nvj'&1j-4 8mm#j A)I0)zmڤ諬k%Bb m:UAG c%2B~Lkj4|[x)\;+g$Do!! '3QEla(QݷF=bὠ;IPXn [N:|$bSXۗ;#Ks}lt%(Oŋ%+%~qQ_hjܲmܢ:&G;6<ʣhϖ۰D'W",]֪^C >>)$:.=V͐ K).Ч/Mߟot)N ]ƹԂK9t(VP 6[JOj@|F;~ Y-AlMlU+ q ZyzcO?.Q!=ق^1-!́kjj+&h-)ȝxEDzI;*./iqĕN;D G c%2B~Le EX<99h(;1bdnPt,*:i'IGO< i//C4<˨Q-)"ԫN@HBt7ygZ]4o!~HN輭@$ p86>bo$_>}846jPi}!ㇻ\"hY lp^&{~T!u|ta&F*t-}2U VlP2R*'Wr1и p 5QcQ~e{slTlCz哘*B}c'MJ8! YzCH ٨xT*ڠ_G0G1.V^* g2'?G-t=' OO|O6N@ 0">agpw[6cbte"JiyO,`(Smm3;8U Ǹ wwwU0=yCd9pgu$?¦DzgzΧ&ϛE>8`,A.ܻhRYХh^1+`q#v U>Bb 0fЄA]>}GtC],j eg%%\T\rHk&NJNLT yEY6lZ:"L `g.}x_紺Tl8jt5Arr xfPSW&!:^m8{rDWim}w7T%e .k۪7 b<}eaFzHxjɧ0ӹxVz#.W# Z% &e ^SA}ng.1uJ1jZxww4IK vZ9?$ 9g/T9a}zy% IS]큛H$"Gb%]oG]@ T<$'a"+(<~ԅbhT9،Ǵor=GV6]'2N̟Q;ق #NJ7@1zRU *\6V:QLKj5q">pQԠmj|r̮Fzڲ-84kYP!gkDk TrA@BypLLe"~l58@f)E#~$ p9c}@lk}@lߓ5 G+3' `(x3OΧ&=-KvkKbm\@u< -d,.ft[t]fն6~xQVcڹ:P@$q~TP_An]>i9lXY.v72ֺ7wC"5qKa僄J=PM cla6x &h;_zNbXm>A+؈ t Mf~Wϻ)[1wyHY~zJSc564Lf:OHc@Z1BIh3/)h_! =C<=lEB1'Zc|J()e"21/cۡ+ꔻ h|z_jIJ]j$Ԙba߀:%!iXZF;P܂83d 3W߬YGOSc8:Pd 4Z[gnCLՍ,e"iezGYcIߔ\)"G&k ךL4;6&H/Ϥy@δdmlm@=TNm]S39NL94 c-{`pG bHq^OA8_:Y:lIB6)Lշ?esf7@DӺN `)^~\)CKe'e}!wW)=p|Bz!;&ϱ/+/?QaZ,)ˇ6tt.ԤisPѡ"L+?\e6WE81̓\jRBaο,{9Qrh.z|+ :Ҧ_@ `o){xCO8NԮa]u0PS2n€D>yp\2\zq[I NĖ 5])XyG 5ÔשxEF"*886HT8I PeRc= 7:c_.rI ӾVzˌ`>ȵo!~HN輭قcrQyݕyH\LM)k=lORQAZLg֩o׉ [ct#τlL͛Snf˩gc bݻ "=y^ΥOm.̈3M0 >R=: jς)F-ӕeLro,=_8 CS(Iw@u#:ɰcҊOqNe 9teOvHO\:义ЖWj- n!mP M' py=^!肬TCFAk K;NSb-Sbf`R_d Z0/+ČB9A)'Bc"(0 1g- 0>FZy F[aEvVzOI1Kh4 ` >dl0m̉;\YNg(^Sп_&Bz?hy ]{Elri)87:^R?,Psyd W!RobZl4JLB}J@$`EۺcSӯcO7995pۢ%82Ft*:<_XʅPO3jjķEdu1A%?7!4c ʳJb#$z*p5:W$\&Qh#Rm #.]xH@ &Fݥ'07U*7χƱ_M5૯yN^RYSEl@ %qqAHkCNO⣅PJ2sD f4"UbHu72# B5IUqZ2Tz#ЩFݓ!t_e=jԪj+ooLV‰鴢a:j)6;FZ5>HC7+55"?G>55ş"rhO6~E2׭嘩?=7Xa Uu_q e!6QL\߁vUK%' c T@A>VB8MlU)y6GpY~, 0^Dt;&iIO:1{ĈAEQV0 6+hwn )\ƀ8h_}'pJAPd!RWxSoBelQ(!(WA9B-mJD=.Qƀ ۽Hr aâ)S=[DMD|9 .[E%/Y8j:kZ{@x,G7H\љI-r,z<_ft.%`]IM"+[96jHQsBQY%X|iQU?B4DeAqFT0 6$/=kaj#bpa==%:{>o,~z~jf 蜈28❭\?kua[oCDxNe\$"e9bU3qB ľOr mheZt0 8'4u`ZRC7yU"< o/ZQ1ql ?M+֨a г7;롇|99»9bӗj[w((3~x.gx}sz0%wD񉹉szD4P&4L #5w:}r®pᄉ똳~ Lӏ2 t{ߒK(-kv?BD$U7$U7EW%c.wgI5N[k&D?*=mNtu )^Ts'`bmWzEݕ+v?j h~a{0c2Y1R(3#9=Ը_fb#< dsϭ]Dװr= #߽EْB1i$]R_48ɓf ՉS׽/^H!Tg5dߐ/7M %O^jpj`U>"DA?v#KФrE> a y#fS6 FuV^h=beeh;+;~㔥  >8KI& _6PvYž49Sl oR&Pc:*0:돣moA:'vji=9"N>5k mlrAp ,6#v8/!m09vVaPҪ:1?@䣅x%Cꜵ.vتoI)w<3j0yZ!ԠVhi^)c2Rt$%V XLx_2Foa/9l}KC~ yَƻ!?8yEl_G7\ǿ?`Z@/7b̃CG]:M(O~[1vLi$B L a>}@Hd3gV 4sqI^ϠC+ ;i/SoN.O1Cq mL X"fOD0SvV%"X+MBH*G _OlL 731d~tT LxLTKcjZ|7o\2c4ZVvd+7bY_]8 _"6NܰMYQQdmWZan}ս P~SL@2 ~vn% s -ef -#v)!*|"'3]f[B}}> Yp~*z-L4Jlݐr;DTݘuDIl-M 4j_T:e&#!+AapW6![_ &҉騧!h*0:M(;KWj1EGئwDbx_ORѲT:? j OvZ9?$ BGżA4*+9ziҀS{_D*#\wvc|7),cFpnJӎ7Z8t6eY?[ 38')ԇz/OZhGV ,_Ϥ>hՌPj9DǯX=nP4Q~-j9Ya.za g`7Ur@$0>-/uW> O,S߈q<;\%xYQ{u`Fl qŎ6;U4$qLo$++>u/(ZsˆRnv#5G]-GU0=HjKYn &36tx(WKSރIuěQ%(9j6Ko,Pt6#Lߑ!ZMh~ :}#]0TZ`Ak8:19(2eai~.>@Lq-EGeE2®2Q*b:Kf)w cFd˼V #u:!4|}'n,o2|BF=Zf a U'; vC6L'D`5_(3I`= Vh5 ^P0ɖ)GCDįȚӔdDŽ`rI L"Ag4fʴNh/T.#rzó,ADt|{ЩGZ`s=[k}(vꐦ8G]>-b۽f"$vAl1>}76%TC/Fl.;&AWˏ!X Iq AvJmb{d=tezGYcHX⨪2bQE8i"fmq]|cy2 f}eiq[Pe QN1%Q5BѭU`íjުWOQP?UP5U@# y5o ~%_[_ʍѹձjNPSjref0 k|y2M0jn;OG$}LDoPws%Q8 ܑ'E3ky1#Hx9*kma(B?,2(IeHK͕#gӾVfNkad({ l K0B)qB1-<%Kqw+,a#>hwjC(D<FƬr-qQeͥ3Y0򝡦5^hӱDI"ս㪰>:Jk~D5 a]lhlLǤF:ń!> Nd#HC.vнN94` :I#. )l'Tf % 8a-}|,/t[Hٷc4v?K|sRu$Zypok{dB+)a*l \BL'M QN1%Q5Bѭ2if![_wZ"_MZZE&DO_9-G#қ{`'ĔhE;_;! *[%@8%lN8v k2-ZW;$v aҔwO>0OyǑuQfffk8ߢ7l N])w1- DLMS9kD*!_dۿ1`C&K=E'*Ğ5ġDgv#cVGLll)B@g?lv)iqpnтW\>.,SߣīWIbI :!N&,6;b% {$"bru{YG1Qu@0Ymcu"_\Jɀ ]&0 ;_]2.mLG1JczByثH@ l}& њU8 ng\sJ[?'ޒs۟k7b*lgN鄸W7iLPא;c1,+$.=53F(Rɯ+I˧L,=O\ m\(NJ&chJkl[cث}7d>A m$rSΞc7b}jI8oi-nv?`Lߧ}8J@4 7BFR(1b(9D:z2Ϫ͌O$A*+&$'ﮥH1z۹(QYj x/{ke'S$jr{LÓG-1hb+C{ )mu4/WɄ2A`<Caԍ{,%$-DWw^i9 њxRx:kh|{b(|1*#kmx f2_a3 &҉騧~\ج . ەkcpCM3% $_[dhY<?D_mwR+1oj`RNc'N#R:Q`c^[bcstQp+$5vD# Xe3dOFE"\BnX鑊lnO?>HH%f/J^+h G {%4ڈ%20wƮR'bVLViKWy!JP!(qXlL0d53u>OsPN=ӃPQx{: \gOkDʼn'+*(X5]+zR/ b6O_ACGk  JZU ڮv G_]shۋ/#0A(Yɚa cc4D'_.Uv Px6rfV6ysC%4ڋ F7O ?x!c#y"]XmMj4KE`"h$*ς"$cKFԫM?rRH( 5`{?b5Y1YQ;$v8ԾEb殯ߟJt:b鰐xH1Pezlg\.m?Ug邋s BT}wU;nNG錊lYFlu:dzsbOK[_)eХ_? ]ȱV@&(#rZjQdıpr`uQ:*̖ @BrP̃orP4TF'P/"&RKIzZpHliʵ"H`b}{j~d|9~`8U\(&PM~.ELLp 8|l7@|sffGr0!La]`SyYI0D:cFRe]G&40Kٺ%μc<ѿDn K/+Vl;1l+#=]\9}Չ>^n8UVvI!c iUe#=bD:JE!5^>nAh!b_g+ ǕIHQV(hlbHnrfXZpU6 nè(}r+0"KY |<LlWxX6i(_qԟ6Bb-,ɽz}fGAH /HC3(/'3хJn>ǵf_&r&tOe0:cτ!a^G+A*U;tC*^]uӫ8I@3y֯BTtuk%J~icuJLa(ľ{  b|)VO^LR\@K/Ao"tiTBUZuN4Q@Xkz}R pw@LcQҙE՚JQ rHnnocH~> `ZCϝM3sb`3~wW(I62BY*by&sQfWk8qbkOvH\~樣 aRxX'DA#wM7?~[bHdq2:h'!$滺CDNXC^xui"NxOx,ѧ[аd8Y]L}dߵAG5kvJ!ס2cnG~"RXmQDUA;R[tJHyS A9|'de3kipjLR?Ulդi4?MP=-HDtk[;=j2㻬'( h}ܥH528HU{~`DS2ۢFzn`(JkVk֡R͌-cp*jVt"Zvdj_'5P;.g+&c's{#pVpAJN6GJgy( vHݶDUR\-]o:~61fye=` 'X}D*)~5:RR hE] AY%n,ڳ0QTD9}? ׈o@%vUzJ`"Z.p R+/IFk-%ӟ/siu/`H$Wp}0%3&DO!w;I9sMWQiPhj?ѻMɆƿr_5Bop9Ng4G\M@Xlv*jUB23jI@'YLi ý O|;_1J]t| %Ig1򣊊KK3rKG4) &vaILsbCЧ}YC&i.,%6iQCc5\%,Zޫ7 a*PPXXq_kfB醒tBU|$N|pgՓ8>STQG`[*v, aN.˚mҼVA.(3=pYD’uF3c{z֮3n ,a΁ d12sJ~v0­P@yIۢ:J6JV`Zqch:&#njZ6e E6y>ofDXmUQA²kbuJJSE o˔W (gc@QXvԊ=gX_8 ۦ%6v:`2ku3ZUYX6}q0RNLn]W]Wʛv2tD}qU\'3 u}"eC0FIW*7x Z%((- {2 a ACen:([I*!hI9∢ݚLC<+FwSG ҥ('A2Aӻ[wp'wWfBEbfUAwA宎V^ L4%x=BbC_HIz uOd%lW[v'S ˯-I>c/>}k!fp LlpP@3I alZKh]3~Fƽ E'ӱ`NW ܍$pR$ۘ<:vE>D3qalYEl*[]瓸ӑ[򨀮>/NxD ԣ {;cgln8dfIOvQZrpc\ײȣBLg7J;y}x?]J8{>>Rq~GwMQzagmbnn=77/T]A#_w{D.>䓡g@0a`1J'y0|jv cos4}r! ɿFg2Uox9Lhm i!#ϘnT-`X(l%ԭ#sh,|k0!w<[@)=ZO!5xSq[iQGl%ԭq@3'Uk_V~Aq@!Aه+bͻi%:'wm\*Fg@R˷;DԵNwy󸫰;Ln*F` 7p֤9U{Jml_jsMTnϜh %;_3 W`V>. tYf# ͌6[7 *z L)JbohYcz&,j! +#/UC@5Jy8km~!+J8Os\vQY&PBM]=m7 W$7z2qq~"÷3eA$i7wC_D'(uON' ճpnέW!zZ[]uɛ[i%Y7lxp,;6ǻBHR$[G,]S*X%6/ isdyV.Mbo\R55'r=iC76c ,d`?',dM2cU&M2esAX=g5[OL1E;3u҂9p֩gO4I/qؚ:.TВM6i/z]E@~";)b#jusmst[Bb,V|o|)~"Y:רNO@ izS"zAJ;q\N%JoIy*:#K+6fhQV xeP[T׆.Kk`ap#C6?n, Khi'zqcGbS:*@JjJ_ ^mFζ 1y3XqΪI(B(W53C@Ϩ@E#owu(>:ڀwWCj"^:+_P8r] Y?dǀzR2qg00ɖؼaF$0tz35s/? &O>33c*Sԅ~1)l2CBЛRGlm~*z7}5Wq` )i)OMRR=eFoG%]A?-!~< Ҽ Wo1>n~z+SDM}Qx{&>b͉U7<>>s7Uq.ntp+[FRto.zNT%yD635 <Y'3w9~~e#6od 3rγnγ7]R6CMf8*Asn0i8_#!zo g x$)nr9z^ZBQ ABʹu' zRUԇW'iޡy m/w$)u n[`\&jek@Os ǢЧ+QaιCJ A1eGo9~47++p<50wv %H\cQA T'DkR_;n检oH_pVOu(Tv]v^*m.ruK@UDeN7ZtSMf%;FzFTOeQbuT}.Ϯ~|qp+XåF8o(Y#aN%*$eҝF!o_x8)ÓZ'|i3*}z(DsMfN,6ß]xguP/dcb$&es.rEFX8xy2O3FX( xU2 /2mLU$*쵦W]EgАU{i֙ Hé峌tk{ܯRf]YɠO *y_-ސTdTxQX6k2ŏU@aĎ&Nᄧ ng~wa8#oh$sH.ʚȲ~u9.[Y3"7\6äGK(,-L=f[areOva]r!_dj2ÍN+Ws-Vs &vr<71ێ]stdKXbQ:N( IЇ{"`|HŰNq%aCt< *a֫SxD<}wRB@q[94KP1O#rw\D5D-}R 7xCt[;F[/=eÏ}+'/'dY)Dw"u+ڣQm7/Rje QVN~(yݫAI_ /{"fxsM9D}gA&/}g!IO0\8Jy!p? ` `6> --YI6wcjy1DIE)Z"b\>H$=NNfx_g=Wfv5[sKԢvTGf B`q(&ԣx)3>.ZKe k[).: v簟 SAo@K3nbpY4A"?a_ʦߤ(MZI۶P,U)8RCmMR|a ="gG="%9\= etṈ-`u՘~>zkCy\haB1Uc HX'Hڑc!JHDIO?ֿU"n;kmVOn" 7ig\A?V 6w>b'>8AX|vY2 9hoI~4WI/ ~>~$+D;DZ~]ίeyW)VJR0{b@10PBe#8C\2(:Fz=r(* X NȯcO48{]lo$k4G MSݥ:ݙU8mA{f+/Ua $}geʷv ^<7TV#ԗQz@Eoy,Vk2 ܷ)~z]`/. prj>VQޱ?4eslvGӛCy/mOQTɖ㡯p';7aT67c8XY (,'N{h[?Jh;DJ*WkjtkMei./K{PԘq:Ve^Ejzo)#)fq~&*' qqxh #"٘ 0 dgE"/~TKkro_n@|\FT NVyf?)34X| ኑ6Ĩ=-Ltj;_'J27$N4>I&'gX [zaÖO-sg R`EP5M^oMx3YugƋ#$~ЂXWA̽I^>$"NwZ#[i_L%$d\*_i@AkUKo܋QlQXEh)Cy͐)a̤Va#=w5է}TKbG3͉jo'aÕOSx~Hpy{85z Y2zpy.;–liUCI=wq)bEYWmwn$)B( ):*!c˨ A*cADwфme Hx3Cj @ovUDm&~:ҢPY4cU@UE6W{wݖɫ?J+ڞ[cl7>3׵VGv8 @mXSn, Se|j@z3Lf&Ln1BHJ'6c٨M? 8719rdj}2J'Ժ2x$B P g'D-1F0eussR¤F\tO0yd8YvЏ(b)|g5ԑ7'G䑨dwzX@Az"~K_~0uȃ=;jJm׀׋,f7Sg PX"NXғ͸@P<~,`T[ ڋ/w'2W6dyf6Bkb#țwFxK{%t ֈ\Rx"2C%ͮl\<.1aV䤤FޖceAT'<[- GOd ) uӊz5}Y q6m򯂚@p݇97<iZ`h؄@)ڝ\1J8>;HP1D;a۫&w)RDT 3K-b{?j Qk.kc)C!FȴxqJT)[mte1 Ķn?SҘ./l KJ{r?ϥ.9`_wmi#0Sg! Dr{)PGRնk?`3A.SԮå 2y"*곊m^-.B 7hݴ4>\09?U`T O=OTbֺmfUaEokr?4U+mӔhlY'c5HA<^bԨ% D·ҷbXcD)~Ҁ<IŒOlkXJz[^.%b]n]~цBt߲?<.CJU9$pD@mM:P}HId/1r% 5W Z_ކG") [x:IuW|_ތ$8?.tB@ޮ'kl "~@.Ͱp>\dQ7З4^mUBJTeԓ,'%:gѬ}vHFO;=@ݡvyW~dVS>vNsOOc/5ͺ YX:gr,77Y7m\dg .,2ot:Gh-aK3o ɱۑD`; ?1x2i/WmRe< NB(s #^8/I{_1UwWW8MC % 䯌68Ӄx ?UyY6]b(鉌F0o[PIfM8u1 C?2zZrPn|cKK]ܦ3ǏNh틳OFc++1`p]|/ )h3g&j jP֋ ր׫]mBH},ZMI1a^NtJZ=*A#{ybm˒W7}e\"v6(?J)QMoX^mI;EҜ: ,N 7_OS><V*qm$,(_b2lsl=,;ۛ'9aH2]>.gɗw# u}Rq^qXGG^e* _S UG^}=[n}ٛZ=Vjr*l \6&ea'AZC Y():7#jzQ{H1dJn~cc7mUr,Fk5БgNG)) ZOvy!NBYL!hF1ZT()&B>w[ i[*E!I}"$/#8wgriEcCpvG]ٳLS2P96!#fI<{@2 `u'gd,-=%QHؖh|@Dp_u4]1{͘H'kY-$M_@3)ka\'5n>Y3 xg(дE}WPPLjW sFp~W!.E3z SPh"C7.ŶPw tAtwT-B,PXs6| e2KvS 'rih3gdK939).??nxd_Ҧ .dUMin[96y0Ů%_n@I\z# PiN]QF2N4-yN4 Kܐ_n=q.s&T_ [ j ,QZ z[`=uon$M}sV8)Z, `/5+MD yتZ,@xrWEJ9~ ^ b}\4G8x~j vhD)\a:e !3F{xPLp+# 'K*z%rLO>04o*Te̴>vS'lL*%<;BT]2w'g]S oK2A't2՞Ԏ q%#?7bt9Lp%lrD`(qCE_ w>fdD;k~a." T6ilVҰxK&U].ň[}aLt}r )w"𜠔#s3/=< E{;L =em3&BN(R'Zڊ@Uyvabڬ\DHܡ!u>_Nj$2FpI uWu(JJu'Ō^`LbIՀ;jQKT%Y}X|Bd{\D/flԪş~bJ)$1Y8~{~@gi^ `x lU,BvO+VFQp >Th({A!i)q[?z%Y g8>djR `p6)FyqNH% rew) UU 6F.p ŸjZKtʦ:hVzuCj[; )MILXtDPX0ž>[C>Qn*q--wn^[)G j"z.f3],4S}!oCOZVZWZICj_X3pMH? (|:I tE!+ճs>^ )^Z5=ryn_R?I9Y sx$f@E[~찍x|nzbm5Fm)P" -/+T7^xGtIO605 ʮ balJ(dЭ=lvԳ3dyi&_qӅv5 6ZXr$ˠ~K)#>k_JsA@m13^;>Ӿ3;ag:,QUu!R(sitqO$[rhhCcG#]&koN5ÉSȸ@qR0HaN;MÌdRQ2<`w:$B3 +q򠸇? Ge"sgcKSz4Kx2;5 VZcӂ=iH݆_C}&wN*j[sYYur^m:j]qȅ'㈜1a~lyN-qGPu!L 31!zvYٜAOp?'a^xٗn(tEVM4F_IF|=QԏDl}׮-Su+ ę|K~8OzQgpwOmQ2r[\s'0B ֡SU. 40kJ: ݭy3% >5z9gʤ[r5!`#nbh2Qĺ p ւ~5f@|JwPt`'B(C$d?u(5dv슂"='g*HȿE?6._xuah%D#v g)%ߨFuV]4aT Sa6AWĉp=D4$sN-zL_ AdJױcQ0D {`0ǒa.40tJpvRZol17lnkc+t=K܏{_2CaŒ#LPR!i~Y?Mo?J ; FN99 #t==lslƨq'D%:UlLrq P4TU[Y`BBOG8Vvr,i=e FbA bM?]ɳS\j((q }.#贞xGaSG" >VMQ0l8KP?v z fc/Tn qT&q|ҷi-D<4h dAWܣI?hM-Ā ,xӼ@j y8`f[jʡo#ijm2_}rpPgHT/pQ:enhdxp$_8Et~x]{4VK6CTDGgF1I1֪Oo^K&tly빙xyX3DS& !٣!Û"5(g2cfKPu A(pIfNmj']Ѕڨf9!7ws2s @d)#܁Ȯ?&"ky+;x+cR-|F6TS_Cwa_Woң | .qDbVf :SMikG͈;ɖٮ џhI% Z+ ռPJJСa~s}SDxJahc8"f({L9[˼l?k?&0Fj|mr؞q c%A(ľPٍ.ly !2x1jlY4'JQ2( 4ˍ iWw$!H31jJDebFa4n%3t}g}w1NzK{瀈DZ\ѣ:pM;_ bw2V)i.+KC䁕t^l$WB `bAʠ|01|:uG{#|=.1ݪj=l|ZLwgf M+( >R4pݸخbNGMHKz=iՎj7P+1R{5f̄pdaOpDdj"zY61.Xΐ9;]<Ȟ h;kI?3!unQC`ۙ1q<LH 9ߜ+&= 6ޗ%>$La$ę0lvΣw_tIh}-nAO&4:E۷\}t@Rk)xb{mL sR?NƠaͫ:F.]58k,+%D(9țekX%XAyw,*d 9u#8*z m<qNÕdGW?a&=ׯAg;1 dl* *ȉ^%; qirtje86x#D05 t=p;WLewZ!^K.2{=YrD!9S^}4E+ӔfYMkQRŝU5'^fk+qO*32Qy#ׅ7&#ysw:ud>M/+tM9o3W4kpz#H y< ^jbxROv.|bz#[r!$Ymc}l)W_e04M'no02; +U [n3Rw6ÈcI%C f@:5OD4u.Go%HJdɶb$yﷲZ c/:t{b&Ek)ĮS\Cl8WТT#Zr~L}xmu;=+ :5Ŭ&*Vgws >ةls5+K,e9A|wܪ*k+3glTk/6^M_:zRľ[Oz$o^'0L<Q tO&§$CVw"UgL.s]ořocLG z)_ЍUx/0!@ȦnogF{&[hUf^sd@ȁpiZ*dl̆h[u*ӧap^185 !ّR@ jEū]9@oyfq':w Z[߀mسmeFO46]Wҵ gP H?$5e`vMwۧ]]EQF{gvC^dtי};2ɞZzS'۝~k4,0 O{Qh,Ak#$niykzkpd`-6+7| @8 0P^ίHFMe8.s@Nk׀n sq|/.,Ĥ&ƧymW$hz f+),0n8ڀ(^ˤ:F@`Ģ&._*NH9DgIgf[RC4YJSHR VV~꽫wN@UȤe.iO5hK|Mht6iw:_X7 data.txt **List archive information:** zxc -l data.txt.zxc **Compress with a custom block size (64 KB):** zxc -B 64K data.bin data.zxc **Compress with maximum block size (2 MB):** zxc -5 -B 2M data.bin data.zxc **Run a benchmark for 10 seconds:** zxc -b 10 data.txt ## AUTHORS Written by Bertrand Lebonnois & contributors. ## LICENSE BSD 3-Clause License. zxc-0.10.0/include/000077500000000000000000000000001517010557200140225ustar00rootroot00000000000000zxc-0.10.0/include/zxc.h000066400000000000000000000006251517010557200150020ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ #ifndef ZXC_H #define ZXC_H #include "zxc_buffer.h" // IWYU pragma: keep #include "zxc_constants.h" // IWYU pragma: keep #include "zxc_error.h" // IWYU pragma: keep #include "zxc_stream.h" // IWYU pragma: keep #endif // ZXC_Hzxc-0.10.0/include/zxc_buffer.h000066400000000000000000000325561517010557200163430ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_buffer.h * @brief Buffer-based (single-shot) compression and decompression API. * * This header exposes the simplest way to use ZXC: pass an entire input buffer * and receive the result in a single output buffer. All functions in this * header are single-threaded and blocking. * * @par Typical usage * @code * // Compress * size_t bound = zxc_compress_bound(src_size); * void *dst = malloc(bound); * zxc_compress_opts_t opts = { .level = ZXC_LEVEL_DEFAULT, .checksum = 1 }; * int64_t csize = zxc_compress(src, src_size, dst, bound, &opts); * * // Decompress * uint64_t orig = zxc_get_decompressed_size(dst, csize); * void *out = malloc(orig); * zxc_decompress_opts_t dopts = { .checksum = 1 }; * int64_t dsize = zxc_decompress(dst, csize, out, orig, &dopts); * @endcode * * @see zxc_stream.h for the streaming (multi-threaded) API. * @see zxc_sans_io.h for the low-level sans-I/O building blocks. */ #ifndef ZXC_BUFFER_H #define ZXC_BUFFER_H #include #include #include "zxc_export.h" #include "zxc_stream.h" /* zxc_compress_opts_t, zxc_decompress_opts_t */ #ifdef __cplusplus extern "C" { #endif /** * @defgroup library_info Library Information * @brief Runtime-queryable library metadata. * * These functions allow callers (including filesystem integrations) * to discover the supported compression level range and library version at * runtime, without relying on compile-time constants alone. * @{ */ /** * @brief Returns the minimum supported compression level. * * Currently returns @ref ZXC_LEVEL_FASTEST (1). * * @return Minimum compression level value. */ ZXC_EXPORT int zxc_min_level(void); /** * @brief Returns the maximum supported compression level. * * Currently returns @ref ZXC_LEVEL_COMPACT (5). * * @return Maximum compression level value. */ ZXC_EXPORT int zxc_max_level(void); /** * @brief Returns the default compression level. * * Currently returns @ref ZXC_LEVEL_DEFAULT (3). * * @return Default compression level value. */ ZXC_EXPORT int zxc_default_level(void); /** * @brief Returns the human-readable library version string. * * The returned pointer is a compile-time constant and must not be freed. * Example: "0.9.1". * * @return Null-terminated version string. */ ZXC_EXPORT const char* zxc_version_string(void); /** @} */ /* end of library_info */ /** * @defgroup buffer_api Buffer API * @brief Single-shot, buffer-based compression and decompression. * @{ */ /** * @brief Calculates the maximum theoretical compressed size for a given input. * * Useful for allocating output buffers before compression. * Accounts for file headers, block headers, and potential expansion * of incompressible data. * * @param[in] input_size Size of the input data in bytes. * * @return Maximum required buffer size in bytes. */ ZXC_EXPORT uint64_t zxc_compress_bound(const size_t input_size); /** * @brief Compresses a data buffer using the ZXC algorithm. * * This version uses standard size_t types and void pointers. * It executes in a single thread (blocking operation). * It writes the ZXC file header followed by compressed blocks. * * @param[in] src Pointer to the source buffer. * @param[in] src_size Size of the source data in bytes. * @param[out] dst Pointer to the destination buffer. * @param[in] dst_capacity Maximum capacity of the destination buffer. * @param[in] opts Compression options (NULL uses all defaults). * Only @c level, @c block_size, and @c checksum are used. * * @return The number of bytes written to dst (>0 on success), * or a negative zxc_error_t code (e.g., ZXC_ERROR_DST_TOO_SMALL) on failure. */ ZXC_EXPORT int64_t zxc_compress(const void* src, const size_t src_size, void* dst, const size_t dst_capacity, const zxc_compress_opts_t* opts); /** * @brief Decompresses a ZXC compressed buffer. * * This version uses standard size_t types and void pointers. * It executes in a single thread (blocking operation). * It expects a valid ZXC file header followed by compressed blocks. * * @param[in] src Pointer to the source buffer containing compressed data. * @param[in] src_size Size of the compressed data in bytes. * @param[out] dst Pointer to the destination buffer. * @param[in] dst_capacity Capacity of the destination buffer. * @param[in] opts Decompression options (NULL uses all defaults). * Only @c checksum is used. * * @return The number of bytes written to dst (>0 on success), * or a negative zxc_error_t code (e.g., ZXC_ERROR_CORRUPT_DATA) on failure. */ ZXC_EXPORT int64_t zxc_decompress(const void* src, const size_t src_size, void* dst, const size_t dst_capacity, const zxc_decompress_opts_t* opts); /** * @brief Returns the decompressed size stored in a ZXC compressed buffer. * * This function reads the file footer to extract the original uncompressed size * without performing any decompression. Useful for allocating output buffers. * * @param[in] src Pointer to the compressed data buffer. * @param[in] src_size Size of the compressed data in bytes. * * @return The original uncompressed size in bytes, or 0 if the buffer is invalid * or too small to contain a valid ZXC archive. */ ZXC_EXPORT uint64_t zxc_get_decompressed_size(const void* src, const size_t src_size); /* ========================================================================= */ /* Block-Level API (no file framing) */ /* ========================================================================= */ /** * @defgroup block_api Block API * @brief Single-block compression/decompression without file framing. * * These functions compress or decompress a single independent block, producing * only the block header (8 bytes) + compressed payload + optional checksum (4 bytes). * No file header, EOF block, or footer is written. * * This API is designed for filesystem integrations where the filesystem manages its own block * indexing and each block is compressed independently. * * @par Typical usage * @code * // Compress a single filesystem block * zxc_cctx* cctx = zxc_create_cctx(NULL); * zxc_compress_opts_t opts = { .level = 3 }; * size_t bound = zxc_compress_block_bound(block_size); * void *dst = malloc(bound); * int64_t csize = zxc_compress_block(cctx, block, block_size, dst, bound, &opts); * * // Decompress * zxc_dctx* dctx = zxc_create_dctx(); * int64_t dsize = zxc_decompress_block(dctx, dst, csize, out, block_size, NULL); * * zxc_free_cctx(cctx); * zxc_free_dctx(dctx); * @endcode * @{ */ /* Forward declarations for context types (defined below). */ typedef struct zxc_cctx_s zxc_cctx; typedef struct zxc_dctx_s zxc_dctx; /** * @brief Returns the maximum compressed size for a single block. * * Unlike zxc_compress_bound(), this does NOT include file header, * EOF block, or footer overhead. Use this to size the destination * buffer for zxc_compress_block(). * * @param[in] input_size Size of the uncompressed block in bytes. * @return Upper bound on compressed block size, or 0 on overflow. */ ZXC_EXPORT uint64_t zxc_compress_block_bound(size_t input_size); /** * @brief Compresses a single block without file framing. * * Output format: @c block_header(8B) + payload + optional @c checksum(4B). * The output can be decompressed with zxc_decompress_block(). * * @param[in,out] cctx Reusable compression context. * @param[in] src Source data. * @param[in] src_size Source data size in bytes. * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of the destination buffer * (use zxc_compress_block_bound() to size). * @param[in] opts Compression options, or NULL for defaults. * Only @c level, @c block_size, and * @c checksum_enabled are used. * * @return Compressed block size in bytes (> 0) on success, * or a negative @ref zxc_error_t code on failure. */ ZXC_EXPORT int64_t zxc_compress_block(zxc_cctx* cctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_compress_opts_t* opts); /** * @brief Decompresses a single block produced by zxc_compress_block(). * * @param[in,out] dctx Reusable decompression context. * @param[in] src Compressed block data. * @param[in] src_size Compressed data size in bytes. * @param[out] dst Destination buffer for decompressed data. * @param[in] dst_capacity Capacity of the destination buffer (must be * at least the original uncompressed size). * @param[in] opts Decompression options (NULL for defaults). * Only @c checksum_enabled is used. * * @return Decompressed size in bytes (> 0) on success, * or a negative @ref zxc_error_t code on failure. */ ZXC_EXPORT int64_t zxc_decompress_block(zxc_dctx* dctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_decompress_opts_t* opts); /** @} */ /* end of block_api */ /* ========================================================================= */ /* Reusable Context API (opaque, heap-allocated) */ /* ========================================================================= */ /** * @defgroup context_api Reusable Context API * @brief Opaque, reusable compression / decompression contexts. * * This API eliminates per-call allocation overhead by letting callers retain * a context across multiple operations. The internal layout is hidden behind * an opaque pointer. * * @{ */ /* --- Compression context ------------------------------------------------- */ /** * @brief Creates a new reusable compression context. * * When @p opts is non-NULL the context pre-allocates all internal buffers * using the supplied level, block_size, and checksum_enabled settings. * When @p opts is NULL, allocation is deferred to the first call to * zxc_compress_cctx(). * * The returned context must be freed with zxc_free_cctx(). * * @param[in] opts Compression options for eager init, or NULL for lazy init. * @return Pointer to the new context, or @c NULL on allocation failure. */ ZXC_EXPORT zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts); /** * @brief Frees a compression context and all associated resources. * * It is safe to pass @c NULL; the call is a no-op in that case. * * @param[in] cctx Context to free. */ ZXC_EXPORT void zxc_free_cctx(zxc_cctx* cctx); /** * @brief Compresses data using a reusable context. * * Identical to zxc_compress() but reuses the internal buffers from @p cctx, * avoiding per-call malloc/free overhead. The context automatically * re-initializes when block_size or level changes between calls. * * Options are **sticky**: settings passed via @p opts are remembered and * reused on subsequent calls where @p opts is NULL. The initial sticky * values come from the @p opts passed to zxc_create_cctx(). * * @param[in,out] cctx Reusable compression context. * @param[in] src Source data. * @param[in] src_size Source data size in bytes. * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of the destination buffer. * @param[in] opts Compression options, or NULL to reuse * settings from create / last call. * * @return Compressed size in bytes (> 0) on success, * or a negative @ref zxc_error_t code on failure. */ ZXC_EXPORT int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_compress_opts_t* opts); /* --- Decompression context ----------------------------------------------- */ /** * @brief Creates a new reusable decompression context. * * @return Pointer to the new context, or @c NULL on allocation failure. */ ZXC_EXPORT zxc_dctx* zxc_create_dctx(void); /** * @brief Frees a decompression context and all associated resources. * * It is safe to pass @c NULL. * * @param[in] dctx Context to free. */ ZXC_EXPORT void zxc_free_dctx(zxc_dctx* dctx); /** * @brief Decompresses data using a reusable context. * * Identical to zxc_decompress() but reuses buffers from @p dctx. * * @param[in,out] dctx Reusable decompression context. * @param[in] src Compressed data. * @param[in] src_size Compressed data size in bytes. * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of the destination buffer. * @param[in] opts Decompression options (NULL for defaults). * * @return Decompressed size in bytes (> 0) on success, * or a negative @ref zxc_error_t code on failure. */ ZXC_EXPORT int64_t zxc_decompress_dctx(zxc_dctx* dctx, const void* src, size_t src_size, void* dst, size_t dst_capacity, const zxc_decompress_opts_t* opts); /** @} */ /* end of context_api */ /** @} */ /* end of buffer_api */ #ifdef __cplusplus } #endif #endif // ZXC_BUFFER_Hzxc-0.10.0/include/zxc_constants.h000066400000000000000000000054631517010557200171030ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_constants.h * @brief Public constants: library version and compression levels. * * Include this header to query the library version at compile time or to * reference the predefined compression-level constants used throughout the API. */ #ifndef ZXC_CONSTANTS_H #define ZXC_CONSTANTS_H /** * @defgroup version Library Version * @brief Compile-time version information. * @{ */ /** @brief Major version number. */ #define ZXC_VERSION_MAJOR 0 /** @brief Minor version number. */ #define ZXC_VERSION_MINOR 10 /** @brief Patch version number. */ #define ZXC_VERSION_PATCH 0 /** @cond INTERNAL */ #define ZXC_STR_HELPER(x) #x #define ZXC_STR(x) ZXC_STR_HELPER(x) /** @endcond */ /** * @brief Human-readable version string (e.g. "0.7.2"). */ #define ZXC_LIB_VERSION_STR \ ZXC_STR(ZXC_VERSION_MAJOR) \ "." ZXC_STR(ZXC_VERSION_MINOR) "." ZXC_STR(ZXC_VERSION_PATCH) /** @} */ /* end of version */ /** * @defgroup block_size Block Size * @brief Block size constraints for compression. * * Block size must be a power of two in range * [@ref ZXC_BLOCK_SIZE_MIN, @ref ZXC_BLOCK_SIZE_MAX]. * Pass 0 to any API to use @ref ZXC_BLOCK_SIZE_DEFAULT. * @{ */ /** @brief log2(ZXC_BLOCK_SIZE_MIN) - exponent code for minimum block size. */ #define ZXC_BLOCK_SIZE_MIN_LOG2 12 /** @brief log2(ZXC_BLOCK_SIZE_MAX) - exponent code for maximum block size. */ #define ZXC_BLOCK_SIZE_MAX_LOG2 21 /** @brief Default block size (256 KB). */ #define ZXC_BLOCK_SIZE_DEFAULT (256 * 1024) /** @brief Minimum allowed block size (4 KB = 2^12). */ #define ZXC_BLOCK_SIZE_MIN (1U << ZXC_BLOCK_SIZE_MIN_LOG2) /** @brief Maximum allowed block size (2 MB = 2^21). */ #define ZXC_BLOCK_SIZE_MAX (1U << ZXC_BLOCK_SIZE_MAX_LOG2) /** @} */ /* end of block_size */ /** * @defgroup levels Compression Levels * @brief Predefined compression levels for the ZXC library. * * Higher levels trade encoding speed for better compression ratio. * All levels produce data that can be decompressed at the same speed. * @{ */ /** * @brief Enumeration of ZXC compression levels. * * Use one of these constants as the @p level parameter of * zxc_compress() or zxc_stream_compress(). */ typedef enum { ZXC_LEVEL_FASTEST = 1, /**< Fastest compression, best for real-time applications. */ ZXC_LEVEL_FAST = 2, /**< Fast compression, good for real-time applications. */ ZXC_LEVEL_DEFAULT = 3, /**< Recommended: ratio > LZ4, decode speed > LZ4. */ ZXC_LEVEL_BALANCED = 4, /**< Good ratio, good decode speed. */ ZXC_LEVEL_COMPACT = 5 /**< High density. Best for storage/firmware/assets. */ } zxc_compression_level_t; /** @} */ /* end of levels */ #endif // ZXC_CONSTANTS_H zxc-0.10.0/include/zxc_error.h000066400000000000000000000052061517010557200162130ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_error.h * @brief Error codes and error-name lookup for the ZXC library. * * Every public function that can fail returns a value from @ref zxc_error_t. * A return value < 0 indicates an error; use zxc_error_name() to convert * any code to a human-readable string. */ #ifndef ZXC_ERROR_H #define ZXC_ERROR_H #include "zxc_export.h" #ifdef __cplusplus extern "C" { #endif /** * @defgroup error Error Handling * @brief Error codes returned by ZXC library functions. * @{ */ /** * @brief Error codes returned by ZXC library functions. * * All error codes are negative integers. Functions that return int or int64_t * will return these codes on failure. Check with `result < 0` for errors. * * Use zxc_error_name() to get a human-readable string for any error code. */ typedef enum { ZXC_OK = 0, /**< Success (no error). */ /* Memory errors */ ZXC_ERROR_MEMORY = -1, /**< Memory allocation failure. */ /* Buffer/capacity errors */ ZXC_ERROR_DST_TOO_SMALL = -2, /**< Destination buffer too small. */ ZXC_ERROR_SRC_TOO_SMALL = -3, /**< Source buffer too small or truncated input. */ /* Format/header errors */ ZXC_ERROR_BAD_MAGIC = -4, /**< Invalid magic word in file header. */ ZXC_ERROR_BAD_VERSION = -5, /**< Unsupported file format version. */ ZXC_ERROR_BAD_HEADER = -6, /**< Corrupted or invalid header (CRC mismatch). */ ZXC_ERROR_BAD_CHECKSUM = -7, /**< Block or global checksum verification failed. */ /* Data integrity errors */ ZXC_ERROR_CORRUPT_DATA = -8, /**< Corrupted compressed data. */ ZXC_ERROR_BAD_OFFSET = -9, /**< Invalid match offset during decompression. */ ZXC_ERROR_OVERFLOW = -10, /**< Buffer overflow detected during processing. */ /* I/O errors */ ZXC_ERROR_IO = -11, /**< Read/write/seek failure on file. */ ZXC_ERROR_NULL_INPUT = -12, /**< Required input pointer is NULL. */ /* Block errors */ ZXC_ERROR_BAD_BLOCK_TYPE = -13, /**< Unknown or unexpected block type. */ ZXC_ERROR_BAD_BLOCK_SIZE = -14, /**< Invalid block size. */ } zxc_error_t; /** * @brief Returns a human-readable name for the given error code. * * @param[in] code An error code from zxc_error_t (or any integer). * @return A constant string such as "ZXC_OK" or "ZXC_ERROR_MEMORY". * Returns "ZXC_UNKNOWN_ERROR" for unrecognized codes. */ ZXC_EXPORT const char* zxc_error_name(const int code); /** @} */ /* end of error */ #ifdef __cplusplus } #endif #endif /* ZXC_ERROR_H */ zxc-0.10.0/include/zxc_export.h000066400000000000000000000057111517010557200164040ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_export.h * @brief Platform-specific symbol visibility macros. * * This header defines the `ZXC_EXPORT`, `ZXC_NO_EXPORT`, and `ZXC_DEPRECATED` * macros that control which symbols are exported from the shared library. * * - Define @c ZXC_STATIC_DEFINE when building or consuming ZXC as a **static** * library to disable import/export annotations. * - When building the shared library the CMake target defines * @c zxc_lib_EXPORTS automatically, selecting `dllexport` / `visibility("default")`. * - When consuming the shared library neither macro is defined, so the header * selects `dllimport` / `visibility("default")`. */ #ifndef ZXC_EXPORT_H #define ZXC_EXPORT_H /** * @defgroup export Symbol Visibility * @brief Macros controlling DLL export/import and deprecation attributes. * @{ */ #ifdef ZXC_STATIC_DEFINE /** * @def ZXC_EXPORT * @brief Marks a symbol as part of the public shared-library API. * * Expands to nothing when building a static library (@c ZXC_STATIC_DEFINE), * to `__declspec(dllexport)` or `__declspec(dllimport)` on Windows, or * to `__attribute__((visibility("default")))` on GCC/Clang. */ #define ZXC_EXPORT /** * @def ZXC_NO_EXPORT * @brief Marks a symbol as hidden (not exported from the shared library). * * Expands to nothing for static builds or Windows, and to * `__attribute__((visibility("hidden")))` on GCC/Clang. */ #define ZXC_NO_EXPORT #else /* shared library */ #ifndef ZXC_EXPORT #ifdef zxc_lib_EXPORTS /* Building the library */ #ifdef _WIN32 #define ZXC_EXPORT __declspec(dllexport) #else #define ZXC_EXPORT __attribute__((visibility("default"))) #endif #else /* Consuming the library */ #ifdef _WIN32 #define ZXC_EXPORT __declspec(dllimport) #else #define ZXC_EXPORT __attribute__((visibility("default"))) #endif #endif #endif #ifndef ZXC_NO_EXPORT #ifdef _WIN32 #define ZXC_NO_EXPORT #else #define ZXC_NO_EXPORT __attribute__((visibility("hidden"))) #endif #endif #endif /* ZXC_STATIC_DEFINE */ #ifndef ZXC_DEPRECATED /** * @def ZXC_DEPRECATED * @brief Marks a symbol as deprecated. * * The compiler will emit a warning when a deprecated symbol is referenced. * Expands to `__declspec(deprecated)` on MSVC or * `__attribute__((__deprecated__))` on GCC/Clang. */ #ifdef _WIN32 #define ZXC_DEPRECATED __declspec(deprecated) #else #define ZXC_DEPRECATED __attribute__((__deprecated__)) #endif #endif /** * @def ZXC_DEPRECATED_EXPORT * @brief Combines `ZXC_EXPORT` with `ZXC_DEPRECATED`. */ #ifndef ZXC_DEPRECATED_EXPORT #define ZXC_DEPRECATED_EXPORT ZXC_EXPORT ZXC_DEPRECATED #endif /** * @def ZXC_DEPRECATED_NO_EXPORT * @brief Combines `ZXC_NO_EXPORT` with `ZXC_DEPRECATED`. */ #ifndef ZXC_DEPRECATED_NO_EXPORT #define ZXC_DEPRECATED_NO_EXPORT ZXC_NO_EXPORT ZXC_DEPRECATED #endif /** @} */ /* end of export */ #endif /* ZXC_EXPORT_H */ zxc-0.10.0/include/zxc_sans_io.h000066400000000000000000000257741517010557200165310ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_sans_io.h * @brief Low-level, sans-I/O compression primitives. * * This header exposes the building blocks used by the higher-level buffer and * streaming APIs. It is intended for callers who need to drive compression or * decompression themselves - for example, to integrate ZXC into a custom I/O * pipeline or an event loop. * * A typical usage pattern is: * -# Allocate and initialise a context with zxc_cctx_init(). * -# Write the file header with zxc_write_file_header(). * -# Compress or decompress individual blocks via the chunk wrappers. * -# Write the footer with zxc_write_file_footer(). * -# Release the context with zxc_cctx_free(). * * @see zxc_buffer.h for the simple one-shot API. * @see zxc_stream.h for the multi-threaded streaming API. */ #ifndef ZXC_SANS_IO_H #define ZXC_SANS_IO_H #include #include #include "zxc_export.h" #ifdef __cplusplus extern "C" { #endif /** * @defgroup sans_io Sans-IO API * @brief Low-level primitives for building custom compression drivers. * @{ */ /** * @struct zxc_cctx_t * @brief Compression Context structure. * * This structure holds the state and buffers required for the compression * process. It is designed to be reused across multiple blocks or calls to avoid * the overhead of repeated memory allocations. * * **Key Fields:** * - `hash_table`: Stores epoch-tagged positions (`ZXC_LZ_HASH_SIZE` * 4 bytes). * - `hash_tags`: Stores 8-bit tags for fast rejection (`ZXC_LZ_HASH_SIZE` * 1 byte). * - `chain_table`: Handles collisions by storing the *previous* occurrence of a * hash. This forms a linked list for each hash bucket, allowing us to * traverse history. * - `epoch`: Used for "Lazy Hash Table Invalidation". Instead of * `ZXC_MEMSET`ing the entire hash table (which is slow) for every block, we * store `(epoch << 16) | offset`. If the stored epoch doesn't match the current * `ctx->epoch`, the entry is considered invalid/empty. * * hash_table Pointer to the hash table used for LZ77 match finding. * chain_table Pointer to the chain table for collision resolution. * memory_block Pointer to the single allocation block containing all buffers. * epoch Current epoch counter for lazy hash table invalidation. * buf_extras Pointer to the buffer for extra lengths (LL >= 15 or ML >= 15). * buf_offsets Pointer to the buffer for offsets. * buf_tokens Pointer to the buffer for token sequences. * literals Pointer to the buffer for raw literal bytes. * lit_buffer Pointer to a scratch buffer for literal processing (e.g., * RLE decoding). * lit_buffer_cap Current capacity of the literal scratch buffer. * checksum_enabled Flag indicating if checksums should be computed. * compression_level The configured compression level. */ typedef struct { /* Hot zone: random access / high frequency. * Kept at the start to ensure they reside in the first cache line (64 bytes). */ uint32_t* hash_table; /**< Hash table for LZ77 match positions (epoch|pos). */ uint8_t* hash_tags; /**< Split tag table for fast match rejection (8-bit tags). */ uint16_t* chain_table; /**< Chain table for collision resolution. */ void* memory_block; /**< Single allocation block owner. */ uint32_t epoch; /**< Current epoch for lazy hash table invalidation. */ /* Warm zone: sequential access per sequence. */ uint32_t* buf_sequences; /**< Buffer for sequence records (packed: LL(8)|ML(8)|Offset(16)). */ uint8_t* buf_tokens; /**< Buffer for token sequences. */ uint16_t* buf_offsets; /**< Buffer for offsets. */ uint8_t* buf_extras; /**< Buffer for extra lengths (vbytes for LL/ML). */ uint8_t* literals; /**< Buffer for literal bytes. */ /* Cold zone: configuration / scratch / resizeable. */ uint8_t* lit_buffer; /**< Scratch buffer for literals (RLE). */ size_t lit_buffer_cap; /**< Current capacity of the scratch buffer. */ uint8_t* work_buf; /**< Padded scratch buffer for buffer-API decompression. */ size_t work_buf_cap; /**< Capacity of the work buffer. */ int checksum_enabled; /**< 1 if checksum calculation/verification is enabled. */ int compression_level; /**< Compression level. */ /* Block-size derived parameters (computed once at init). */ size_t chunk_size; /**< Effective block size in bytes. */ uint32_t offset_bits; /**< log2(chunk_size) - governs epoch_mark shift. */ uint32_t offset_mask; /**< (1U << offset_bits) - 1 */ uint32_t max_epoch; /**< 1U << (32 - offset_bits) */ } zxc_cctx_t; /** * @brief Initializes a ZXC compression context. * * Sets up the internal state required for compression operations, allocating * necessary buffers based on the chunk size and compression level. * * @param[out] ctx Pointer to the ZXC compression context structure to initialize. * @param[in] chunk_size The size of the data chunk to be compressed. This * determines the allocation size for various internal buffers. * @param[in] mode The operation mode (1 for compression, 0 for decompression). * @param[in] level The desired compression level to be stored in the context. * @param[in] checksum_enabled 1 to enable checksums, 0 to disable. * @return ZXC_OK on success, or a negative zxc_error_t code (e.g., ZXC_ERROR_MEMORY) if memory * allocation fails. */ ZXC_EXPORT int zxc_cctx_init(zxc_cctx_t* ctx, const size_t chunk_size, const int mode, const int level, const int checksum_enabled); /** * @brief Frees resources associated with a ZXC compression context. * * This function releases all internal buffers and tables associated with the * given ZXC compression context structure. It does not free the context pointer * itself, only its members. * * @param[in,out] ctx Pointer to the compression context to clean up. */ ZXC_EXPORT void zxc_cctx_free(zxc_cctx_t* ctx); /** * @brief Writes the standard ZXC file header to a destination buffer. * * This function stores the magic word (little-endian) and the version number * into the provided buffer. It ensures the buffer has sufficient capacity * before writing. * * @param[out] dst The destination buffer where the header will be written. * @param[in] dst_capacity The total capacity of the destination buffer in bytes. * @param[in] chunk_size The block size to encode in the header. * @param[in] has_checksum Flag indicating whether the checksum bit should be set. * @return The number of bytes written (ZXC_FILE_HEADER_SIZE) on success, * or ZXC_ERROR_DST_TOO_SMALL if the destination capacity is insufficient. */ ZXC_EXPORT int zxc_write_file_header(uint8_t* dst, const size_t dst_capacity, const size_t chunk_size, const int has_checksum); /** * @brief Validates and reads the ZXC file header from a source buffer. * * This function checks if the provided source buffer is large enough to contain * a ZXC file header and verifies that the magic word and version number match * the expected ZXC format specifications. * * @param[in] src Pointer to the source buffer containing the file data. * @param[in] src_size Size of the source buffer in bytes. * @param[out] out_block_size Optional pointer to receive the recommended block size. * @param[out] out_has_checksum Optional pointer to receive the checksum flag. * @return ZXC_OK on success, or a negative error code (e.g., ZXC_ERROR_SRC_TOO_SMALL, * ZXC_ERROR_BAD_MAGIC, ZXC_ERROR_BAD_VERSION). */ ZXC_EXPORT int zxc_read_file_header(const uint8_t* src, const size_t src_size, size_t* out_block_size, int* out_has_checksum); /** * @struct zxc_block_header_t * @brief Represents the on-disk header structure for a ZXC block (8 bytes). * * This structure contains metadata required to parse and decompress a block. * Note: raw_size is not stored in the header; decoders derive it from Section * Descriptors within the compressed payload. */ typedef struct { uint8_t block_type; /**< Block type (see @ref zxc_block_type_t). */ uint8_t block_flags; /**< Flags (e.g., checksum presence). */ uint8_t reserved; /**< Reserved for future protocol extensions. */ uint8_t header_crc; /**< Header integrity checksum (1 byte). */ uint32_t comp_size; /**< Compressed size excluding this header. */ } zxc_block_header_t; /** * @brief Encodes a block header into the destination buffer. * * This function serializes the contents of a `zxc_block_header_t` structure * into a byte array in little-endian format. It ensures the destination buffer * has sufficient capacity before writing. * * @param[out] dst Pointer to the destination buffer where the header will be * written. * @param[in] dst_capacity The total size of the destination buffer in bytes. * @param[in] bh Pointer to the source block header structure containing the data to * write. * * @return The number of bytes written (ZXC_BLOCK_HEADER_SIZE) on success, * or ZXC_ERROR_DST_TOO_SMALL if the destination buffer capacity is insufficient. */ ZXC_EXPORT int zxc_write_block_header(uint8_t* dst, const size_t dst_capacity, const zxc_block_header_t* bh); /** * @brief Reads and parses a ZXC block header from a source buffer. * * This function extracts the block type, flags, reserved fields, compressed * size, and raw size from the first `ZXC_BLOCK_HEADER_SIZE` bytes of the source * buffer. It handles endianness conversion for multi-byte fields (Little * Endian). * * @param[in] src Pointer to the source buffer containing the block data. * @param[in] src_size The size of the source buffer in bytes. * @param[out] bh Pointer to a `zxc_block_header_t` structure where the parsed * header information will be stored. * * @return ZXC_OK on success, or ZXC_ERROR_SRC_TOO_SMALL if the source buffer is smaller * than the required block header size. */ ZXC_EXPORT int zxc_read_block_header(const uint8_t* src, const size_t src_size, zxc_block_header_t* bh); /** * @brief Writes the ZXC file footer. * * The footer stores the original uncompressed size and an optional global * checksum. It is always @c ZXC_FILE_FOOTER_SIZE (12) bytes. * * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of destination buffer. * @param[in] src_size Original uncompressed size of the data. * @param[in] global_hash Global checksum hash (if enabled). * @param[in] checksum_enabled Flag indicating if checksum is enabled. * @return Number of bytes written (12) on success, or ZXC_ERROR_DST_TOO_SMALL on failure. */ ZXC_EXPORT int zxc_write_file_footer(uint8_t* dst, const size_t dst_capacity, const uint64_t src_size, const uint32_t global_hash, const int checksum_enabled); /** @} */ /* end of sans_io */ #ifdef __cplusplus } #endif #endif // ZXC_SANS_IO_Hzxc-0.10.0/include/zxc_seekable.h000066400000000000000000000200321517010557200166270ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_seekable.h * @brief Seekable compression and random-access decompression API. * * This header provides functions to produce seekable ZXC archives and to * decompress arbitrary byte ranges without reading the entire file. * * A seekable archive embeds a Seek Table block (block_type = @c ZXC_BLOCK_SEK) * after the EOF block, recording the compressed size of every block. * The table is detected at read time by deriving @c num_blocks from the file * footer's total decompressed size and the header's block size, then seeking * backward to validate the SEK block header. * Standard (non-seekable) decompressors ignore the seek table entirely. * * @par Creating a seekable archive * @code * zxc_compress_opts_t opts = { .level = 3, .seekable = 1 }; * int64_t csize = zxc_compress(src, src_size, dst, dst_cap, &opts); * @endcode * * @par Random-access decompression * @code * zxc_seekable* s = zxc_seekable_open(compressed, csize); * int64_t n = zxc_seekable_decompress_range(s, out, out_cap, offset, len); * zxc_seekable_free(s); * @endcode * * @see zxc_buffer.h for the standard one-shot API. * @see zxc_stream.h for multi-threaded streaming. */ #ifndef ZXC_SEEKABLE_H #define ZXC_SEEKABLE_H #include #include #include #include "zxc_export.h" #ifdef __cplusplus extern "C" { #endif /** * @defgroup seekable_api Seekable API * @brief Random-access compression and decompression. * @{ */ /* ========================================================================= */ /* Seekable Reader (Random-Access Decompression) */ /* ========================================================================= */ /** * @brief Opaque handle for a seekable ZXC archive. * * Created by zxc_seekable_open() or zxc_seekable_open_file(). * Must be freed with zxc_seekable_free(). */ typedef struct zxc_seekable_s zxc_seekable; /** * @brief Opens a seekable archive from a memory buffer. * * Parses the seek table from the end of the buffer and builds the internal * block index. The buffer must remain valid for the lifetime of the handle. * * @param[in] src Pointer to the compressed data. * @param[in] src_size Size of the compressed data in bytes. * @return Handle on success, or @c NULL if the buffer does not contain a * valid seekable archive (e.g. missing seek block, bad block type). */ ZXC_EXPORT zxc_seekable* zxc_seekable_open(const void* src, const size_t src_size); /** * @brief Opens a seekable archive from a FILE*. * * The file must be seekable (not stdin/pipe). The current file position * is saved and restored after parsing the seek table. The FILE* must * remain open for the lifetime of the handle. * * @param[in] f File opened in "rb" mode. * @return Handle on success, or @c NULL on error. */ ZXC_EXPORT zxc_seekable* zxc_seekable_open_file(FILE* f); /** * @brief Returns the total number of blocks in the seekable archive. * * @param[in] s Seekable handle. * @return Number of data blocks (excluding EOF). */ ZXC_EXPORT uint32_t zxc_seekable_get_num_blocks(const zxc_seekable* s); /** * @brief Returns the total decompressed size of the seekable archive. * * @param[in] s Seekable handle. * @return Total decompressed size in bytes. */ ZXC_EXPORT uint64_t zxc_seekable_get_decompressed_size(const zxc_seekable* s); /** * @brief Returns the compressed size of a specific block. * * This is the "on-disk" size including block header, payload, and optional * per-block checksum. * * @param[in] s Seekable handle. * @param[in] block_idx Zero-based block index. * @return Compressed block size, or 0 if @p block_idx is out of range. */ ZXC_EXPORT uint32_t zxc_seekable_get_block_comp_size(const zxc_seekable* s, const uint32_t block_idx); /** * @brief Returns the decompressed size of a specific block. * * @param[in] s Seekable handle. * @param[in] block_idx Zero-based block index. * @return Decompressed block size, or 0 if @p block_idx is out of range. */ ZXC_EXPORT uint32_t zxc_seekable_get_block_decomp_size(const zxc_seekable* s, const uint32_t block_idx); /** * @brief Decompresses an arbitrary byte range from the original data. * * Only the blocks overlapping [@p offset, @p offset + @p len) are read and * decompressed. This is the core random-access primitive. * * @param[in,out] s Seekable handle. * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of @p dst (must be >= @p len). * @param[in] offset Byte offset into the original uncompressed data. * @param[in] len Number of bytes to decompress. * @return Number of bytes written to @p dst (== @p len on success), * or a negative @ref zxc_error_t code on failure. */ ZXC_EXPORT int64_t zxc_seekable_decompress_range(zxc_seekable* s, void* dst, const size_t dst_capacity, const uint64_t offset, const size_t len); /** * @brief Multi-threaded variant of zxc_seekable_decompress_range(). * * Decompresses blocks in parallel using a fork-join thread pool. Each worker * thread owns its own decompression context and reads compressed data via * @c pread() (POSIX) or @c ReadFile() (Windows) for lock-free concurrent I/O. * * Falls back to single-threaded mode when @p n_threads <= 1 or when the * requested range spans a single block. * * @param[in,out] s Seekable handle. * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of @p dst (must be >= @p len). * @param[in] offset Byte offset into the original uncompressed data. * @param[in] len Number of bytes to decompress. * @param[in] n_threads Number of worker threads (0 = auto-detect CPU cores). * @return Number of bytes written to @p dst (== @p len on success), * or a negative @ref zxc_error_t code on failure. */ ZXC_EXPORT int64_t zxc_seekable_decompress_range_mt(zxc_seekable* s, void* dst, const size_t dst_capacity, const uint64_t offset, const size_t len, int n_threads); /** * @brief Frees a seekable handle and all associated resources. * * Safe to call with @c NULL. * * @param[in] s Handle to free. */ ZXC_EXPORT void zxc_seekable_free(zxc_seekable* s); /* ========================================================================= */ /* Seek Table Writer (low-level) */ /* ========================================================================= */ /** * @brief Writes a seek table to the destination buffer. * * This is a low-level helper used internally by the seekable compression * paths. It writes: block_header(8) + N entries(4 each). * Each entry stores only @c comp_size; decompressed sizes are derived at * read time from the file header's block_size. * * @param[out] dst Destination buffer. * @param[in] dst_capacity Capacity of @p dst in bytes. * @param[in] comp_sizes Array of compressed block sizes. * @param[in] num_blocks Number of blocks. * @return Number of bytes written, or a negative @ref zxc_error_t on failure. */ ZXC_EXPORT int64_t zxc_write_seek_table(uint8_t* dst, const size_t dst_capacity, const uint32_t* comp_sizes, const uint32_t num_blocks); /** * @brief Returns the encoded size of a seek table for the given block count. * * @param[in] num_blocks Number of blocks. * @return Total byte size of the seek table. */ ZXC_EXPORT size_t zxc_seek_table_size(const uint32_t num_blocks); /** @} */ /* end of seekable_api */ #ifdef __cplusplus } #endif #endif /* ZXC_SEEKABLE_H */ zxc-0.10.0/include/zxc_stream.h000066400000000000000000000130121517010557200163470ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_stream.h * @brief Multi-threaded streaming compression and decompression API. * * This header provides the streaming driver that reads from a @c FILE* input and * writes compressed (or decompressed) output to a @c FILE*. Internally the * driver uses an asynchronous Producer-Consumer pipeline via a ring buffer to * separate I/O from CPU-intensive work: * * 1. **Reader thread** - reads chunks from `f_in`. * 2. **Worker threads** - compress/decompress chunks in parallel. * 3. **Writer thread** - orders the results and writes them to `f_out`. * * @see zxc_buffer.h for the simple one-shot buffer API. * @see zxc_sans_io.h for low-level sans-I/O building blocks. */ #ifndef ZXC_STREAM_H #define ZXC_STREAM_H #include #include #include #include "zxc_export.h" #ifdef __cplusplus extern "C" { #endif /** * @defgroup stream_api Streaming API * @brief Multi-threaded, FILE*-based compression and decompression. * @{ */ /** * @brief Progress callback function type. * * This callback is invoked periodically during compression/decompression to report * progress. It is called from the writer thread after each block is processed. * * @param[in] bytes_processed Total input bytes processed so far. * @param[in] bytes_total Total input bytes to process (0 if unknown, e.g., stdin). * @param[in] user_data User-provided context pointer (passed through from API call). * * @note The callback should be fast and non-blocking. Avoid heavy I/O or mutex locks. */ typedef void (*zxc_progress_callback_t)(uint64_t bytes_processed, uint64_t bytes_total, const void* user_data); /** * @brief Options for streaming compression. * * Zero-initialise for safe defaults: level 0 maps to ZXC_LEVEL_DEFAULT (3), * block_size 0 maps to ZXC_BLOCK_SIZE_DEFAULT, n_threads 0 means * auto-detect, and all other fields are disabled. * * @code * zxc_compress_opts_t opts = { .level = ZXC_LEVEL_COMPACT }; * zxc_stream_compress(f_in, f_out, &opts); * @endcode */ typedef struct { int n_threads; /**< Worker thread count (0 = auto-detect CPU cores). */ int level; /**< Compression level 1-5 (0 = default, ZXC_LEVEL_DEFAULT). */ size_t block_size; /**< Block size in bytes (0 = default ZXC_BLOCK_SIZE_DEFAULT). Must be power of 2, [4KB - 2MB]. */ int checksum_enabled; /**< 1 to enable per-block and global checksums, 0 to disable. */ int seekable; /**< 1 to append a seek table for random-access decompression. */ zxc_progress_callback_t progress_cb; /**< Optional progress callback (NULL to disable). */ void* user_data; /**< User context pointer passed to progress_cb. */ } zxc_compress_opts_t; /** * @brief Options for streaming decompression. * * Zero-initialise for safe defaults. * * @code * zxc_decompress_opts_t opts = { .checksum = 1 }; * zxc_stream_decompress(f_in, f_out, &opts); * @endcode */ typedef struct { int n_threads; /**< Worker thread count (0 = auto-detect CPU cores). */ int checksum_enabled; /**< 1 to verify per-block and global checksums, 0 to skip. */ zxc_progress_callback_t progress_cb; /**< Optional progress callback (NULL to disable). */ void* user_data; /**< User context pointer passed to progress_cb. */ } zxc_decompress_opts_t; /** * @brief Compresses data from an input stream to an output stream. * * This function sets up a multi-threaded pipeline: * 1. Reader Thread: Reads chunks from f_in. * 2. Worker Threads: Compress chunks in parallel (LZ77 + Bitpacking). * 3. Writer Thread: Orders the processed chunks and writes them to f_out. * * @param[in] f_in Input file stream (must be opened in "rb" mode). * @param[out] f_out Output file stream (must be opened in "wb" mode). * @param[in] opts Compression options (NULL uses all defaults). * * @return Total compressed bytes written, or a negative zxc_error_t code (e.g., * ZXC_ERROR_IO) if an error occurred. */ ZXC_EXPORT int64_t zxc_stream_compress(FILE* f_in, FILE* f_out, const zxc_compress_opts_t* opts); /** * @brief Decompresses data from an input stream to an output stream. * * Uses the same pipeline architecture as compression to maximize throughput. * * @param[in] f_in Input file stream (must be opened in "rb" mode). * @param[out] f_out Output file stream (must be opened in "wb" mode). * @param[in] opts Decompression options (NULL uses all defaults). * * @return Total decompressed bytes written, or a negative zxc_error_t code (e.g., * ZXC_ERROR_BAD_HEADER) if an error occurred. */ ZXC_EXPORT int64_t zxc_stream_decompress(FILE* f_in, FILE* f_out, const zxc_decompress_opts_t* opts); /** * @brief Returns the decompressed size stored in a ZXC compressed file. * * This function reads the file footer to extract the original uncompressed size * without performing any decompression. The file position is restored after reading. * * @param[in] f_in Input file stream (must be opened in "rb" mode). * * @return The original uncompressed size in bytes, or a negative zxc_error_t code (e.g., * ZXC_ERROR_BAD_MAGIC) if the file is invalid or an I/O error occurred. */ ZXC_EXPORT int64_t zxc_stream_get_decompressed_size(FILE* f_in); /** @} */ /* end of stream_api */ #ifdef __cplusplus } #endif #endif // ZXC_STREAM_Hzxc-0.10.0/libzxc.pc.in000066400000000000000000000005171517010557200146260ustar00rootroot00000000000000prefix=@CMAKE_INSTALL_PREFIX@ exec_prefix=${prefix} libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ Name: libzxc Description: High-performance asymmetric lossless compression License: BSD 3-Clause Version: @PROJECT_VERSION@ Libs: -L${libdir} -lzxc Libs.private: -pthread Cflags: -I${includedir}zxc-0.10.0/src/000077500000000000000000000000001517010557200131665ustar00rootroot00000000000000zxc-0.10.0/src/cli/000077500000000000000000000000001517010557200137355ustar00rootroot00000000000000zxc-0.10.0/src/cli/main.c000066400000000000000000001453701517010557200150370ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file main.c * @brief Command Line Interface (CLI) entry point for the ZXC compression tool. * * This file handles argument parsing, file I/O setup, platform-specific * compatibility layers (specifically for Windows), and the execution of * compression, decompression, or benchmarking modes. */ #include #include #include #include #include #include "../../include/zxc_buffer.h" #include "../../include/zxc_constants.h" #include "../../include/zxc_stream.h" #include "../lib/zxc_internal.h" #if defined(_WIN32) #define ZXC_OS "windows" #elif defined(__APPLE__) #define ZXC_OS "darwin" #elif defined(__linux__) #define ZXC_OS "linux" #else #define ZXC_OS "unknown" #endif #if defined(__x86_64__) || defined(_M_AMD64) #define ZXC_ARCH "x86_64" #elif defined(__aarch64__) || defined(_M_ARM64) #define ZXC_ARCH "arm64" #else #define ZXC_ARCH "unknown" #endif #ifdef _WIN32 // Windows Implementation #include #include #include #include // Map POSIX macros to MSVC equivalents #define F_OK 0 #define access _access #define isatty _isatty #define fileno _fileno #define unlink _unlink #define fseeko _fseeki64 #define ftello _ftelli64 /** * @brief Returns the current monotonic time in seconds using Windows * Performance Counter. * @return double Time in seconds. */ static double zxc_now(void) { LARGE_INTEGER frequency; LARGE_INTEGER count; QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&count); return (double)count.QuadPart / frequency.QuadPart; } struct option { const char* name; int has_arg; int* flag; int val; }; #define no_argument 0 #define required_argument 1 #define optional_argument 2 char* optarg = NULL; int optind = 1; int optopt = 0; /** * @brief Minimal implementation of getopt_long for Windows. * Handles long options (--option) and short options (-o). */ static int getopt_long(int argc, char* const argv[], const char* optstring, const struct option* longopts, int* longindex) { if (optind >= argc) return -1; char* curr = argv[optind]; if (curr[0] == '-' && curr[1] == '-') { char* name_end = strchr(curr + 2, '='); const size_t name_len = name_end ? (size_t)(name_end - (curr + 2)) : strlen(curr + 2); const struct option* p = longopts; while (p && p->name) { const size_t opt_len = strlen(p->name); if (name_len == opt_len && strncmp(curr + 2, p->name, name_len) == 0) { optind++; if (p->has_arg == required_argument) { if (name_end) optarg = name_end + 1; else if (optind < argc) optarg = argv[optind++]; else return '?'; } else if (p->has_arg == optional_argument) { if (name_end) optarg = name_end + 1; else optarg = NULL; } if (p->flag) { *p->flag = p->val; return 0; } return p->val; } p++; } return '?'; } if (curr[0] == '-') { char c = curr[1]; optind++; const char* os = strchr(optstring, c); if (!os) return '?'; if (os[1] == ':') { if (os[2] == ':') { // Optional argument (::) if (curr[2] != '\0') optarg = curr + 2; else optarg = NULL; } else { // Required argument (:) if (curr[2] != '\0') optarg = curr + 2; else if (optind < argc) optarg = argv[optind++]; else return '?'; } } return c; } return -1; } #else // POSIX / Linux / macOS Implementation #include #include #include #include #include #include #include #include #include #include #include #include /** * @brief Returns the current monotonic time in seconds using clock_gettime. * @return double Time in seconds. */ static double zxc_now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } #endif /** * @brief Validates and resolves the input file path to prevent directory traversal * and ensure it is a regular file. * * @param[in] path The raw input path from command line. * @param[out] resolved_buffer Buffer to store resolved path (needs sufficient size). * @param[in] buffer_size Size of the resolved_buffer. * @return 0 on success, -1 on error. */ static int zxc_validate_input_path(const char* path, char* resolved_buffer, size_t buffer_size) { #ifdef _WIN32 if (!_fullpath(resolved_buffer, path, buffer_size)) { return -1; } DWORD attr = GetFileAttributesA(resolved_buffer); if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) { // Not a valid file or is a directory errno = (attr == INVALID_FILE_ATTRIBUTES) ? ENOENT : EISDIR; return -1; } return 0; #else char* const res = realpath(path, NULL); if (!res) { // realpath failed (e.g. file does not exist) return -1; } struct stat st; if (stat(res, &st) != 0) { free(res); return -1; } if (!S_ISREG(st.st_mode)) { free(res); errno = EISDIR; // Generic error for non-regular file return -1; } const size_t len = strlen(res); if (len >= buffer_size) { free(res); errno = ENAMETOOLONG; return -1; } memcpy(resolved_buffer, res, len + 1); free(res); return 0; #endif } /** * @brief Validates and resolves the output file path. * * @param[in] path The raw output path. * @param[out] resolved_buffer Buffer to store resolved path. * @param[in] buffer_size Size of the resolved_buffer. * @return 0 on success, -1 on error. */ static int zxc_validate_output_path(const char* path, char* resolved_buffer, size_t buffer_size) { #ifdef _WIN32 if (!_fullpath(resolved_buffer, path, buffer_size)) return -1; DWORD attr = GetFileAttributesA(resolved_buffer); if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) { errno = EISDIR; return -1; } return 0; #else // POSIX output path validation char* const temp_path = strdup(path); if (!temp_path) return -1; char* const temp_path2 = strdup(path); if (!temp_path2) { free(temp_path); return -1; } // Split into dir and base char* const dir = dirname(temp_path); // Note: dirname may modify string or return static const char* const base = basename(temp_path2); char* const resolved_dir = realpath(dir, NULL); if (!resolved_dir) { // Parent directory must exist free(temp_path); free(temp_path2); return -1; } struct stat st; if (stat(resolved_dir, &st) != 0 || !S_ISDIR(st.st_mode)) { free(resolved_dir); free(temp_path); free(temp_path2); errno = EISDIR; return -1; } // Reconstruct valid path: resolved_dir / base // Ensure we don't overflow buffer const int written = snprintf(resolved_buffer, buffer_size, "%s/%s", resolved_dir, base); free(resolved_dir); free(temp_path); free(temp_path2); if (written < 0 || (size_t)written >= buffer_size) { errno = ENAMETOOLONG; return -1; } return 0; #endif } // CLI Logging Helpers static int g_quiet = 0; static int g_verbose = 0; /** * @brief Standard logging function. Respects the global quiet flag. */ static void zxc_log(const char* fmt, ...) { if (g_quiet) return; va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); } /** * @brief Verbose logging function. Only prints if verbose is enabled and quiet * is disabled. */ static void zxc_log_v(const char* fmt, ...) { if (!g_verbose || g_quiet) return; va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); } // OS-specific helpers for directory checks #ifdef _WIN32 static int zxc_is_directory(const char* path) { DWORD attr = GetFileAttributesA(path); return (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)); } #else static int zxc_is_directory(const char* path) { struct stat st; if (stat(path, &st) == 0) { return S_ISDIR(st.st_mode); } return 0; } #endif typedef enum { MODE_COMPRESS, MODE_DECOMPRESS, MODE_BENCHMARK, MODE_INTEGRITY, MODE_LIST } zxc_mode_t; enum { OPT_VERSION = 1000, OPT_HELP }; // Forward declaration for recursive mode static int process_single_file(const char* in_path, const char* out_path_override, zxc_mode_t mode, int num_threads, int keep_input, int force, int to_stdout, int checksum, int level, size_t block_size, int json_output, int seekable); // Forward declaration for processing directory static int process_directory(const char* dir_path, zxc_mode_t mode, int num_threads, int keep_input, int force, int to_stdout, int checksum, int level, size_t block_size, int json_output, int seekable); // OS-specific implementation of directory processing static int process_directory(const char* dir_path, zxc_mode_t mode, int num_threads, int keep_input, int force, int to_stdout, int checksum, int level, size_t block_size, int json_output, int seekable) { int overall_ret = 0; #ifdef _WIN32 char search_path[MAX_PATH]; snprintf(search_path, sizeof(search_path), "%s\\*", dir_path); WIN32_FIND_DATAA find_data; HANDLE hFind = FindFirstFileA(search_path, &find_data); if (hFind == INVALID_HANDLE_VALUE) { zxc_log("Error opening directory '%s'\n", dir_path); return 1; } do { if (strcmp(find_data.cFileName, ".") == 0 || strcmp(find_data.cFileName, "..") == 0) { continue; } char full_path[MAX_PATH]; snprintf(full_path, sizeof(full_path), "%s\\%s", dir_path, find_data.cFileName); if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { overall_ret |= process_directory(full_path, mode, num_threads, keep_input, force, to_stdout, checksum, level, block_size, json_output, seekable); } else { // Check if it ends with .zxc to skip if compressing to avoid double compression if (mode == MODE_COMPRESS) { const size_t len = strlen(full_path); if (len >= 4 && strcmp(full_path + len - 4, ".zxc") == 0) { continue; // Skip already compressed files in recursive compression } } overall_ret |= process_single_file(full_path, NULL, mode, num_threads, keep_input, force, to_stdout, checksum, level, block_size, json_output, seekable); } } while (FindNextFileA(hFind, &find_data) != 0); FindClose(hFind); #else DIR* const dir = opendir(dir_path); if (!dir) { zxc_log("Error opening directory '%s': %s\n", dir_path, strerror(errno)); return 1; } const struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } const size_t path_len = strlen(dir_path) + 1 + strlen(entry->d_name) + 1; char* const full_path = malloc(path_len); if (!full_path) { zxc_log("Error allocating memory for path in directory '%s'\n", dir_path); continue; } const int n = snprintf(full_path, path_len, "%s/%s", dir_path, entry->d_name); if (n < 0 || (size_t)n >= path_len) { zxc_log("Error: path too long in directory '%s'\n", dir_path); free(full_path); continue; } struct stat st; if (stat(full_path, &st) == 0) { if (S_ISDIR(st.st_mode)) { overall_ret |= process_directory(full_path, mode, num_threads, keep_input, force, to_stdout, checksum, level, block_size, json_output, seekable); } else if (S_ISREG(st.st_mode)) { // Check if it ends with .zxc to skip if compressing to avoid double compression if (mode == MODE_COMPRESS) { const size_t len = strlen(full_path); if (len >= 4 && strcmp(full_path + len - 4, ".zxc") == 0) { free(full_path); continue; // Skip already compressed files in recursive compression } } overall_ret |= process_single_file(full_path, NULL, mode, num_threads, keep_input, force, to_stdout, checksum, level, block_size, json_output, seekable); } } free(full_path); } closedir(dir); #endif return overall_ret; } void print_help(const char* app) { printf("Usage: %s [] []...\n\n", app); printf( "Standard Modes:\n" " -z, --compress Compress FILE {default}\n" " -d, --decompress Decompress FILE (or stdin -> stdout)\n" " -l, --list List archive information\n" " -t, --test Test compressed FILE integrity\n" " -b, --bench [N] Benchmark in-memory (N=seconds, default 5)\n\n" "Batch Processing:\n" " -m, --multiple Multiple input files\n" " -r, --recursive Operate recursively on directories\n\n" "Special Options:\n" " -V, --version Show version information\n" " -h, --help Show this help message\n\n" "Options:\n" " -1..-5 Compression level {3}\n" " -B, --block-size Block size: 4K..2M, power of 2 {256K}\n" " -T, --threads N Number of threads (0=auto)\n" " -C, --checksum Enable checksum {default}\n" " -N, --no-checksum Disable checksum\n" " -S, --seekable Append seek table for random-access decompression\n" " -k, --keep Keep input file\n" " -f, --force Force overwrite\n" " -c, --stdout Write to stdout\n" " -v, --verbose Verbose mode\n" " -q, --quiet Quiet mode\n" " -j, --json JSON output (benchmark mode)\n"); } void print_version(void) { char sys_info[256]; #ifdef _WIN32 snprintf(sys_info, sizeof(sys_info), "%s-%s", ZXC_ARCH, ZXC_OS); #else struct utsname buffer; if (uname(&buffer) == 0) snprintf(sys_info, sizeof(sys_info), "%s-%s-%s", ZXC_ARCH, ZXC_OS, buffer.release); else snprintf(sys_info, sizeof(sys_info), "%s-%s", ZXC_ARCH, ZXC_OS); #endif printf("zxc v%s (%s) by Bertrand Lebonnois & al.\nBSD 3-Clause License\n", ZXC_LIB_VERSION_STR, sys_info); } /** * @brief Formats a byte size into human-readable TB/GB/MB/KB/B format (Base 1000). */ static void format_size_decimal(uint64_t bytes, char* buf, size_t buf_size) { const double TB = 1000.0 * 1000.0 * 1000.0 * 1000.0; const double GB = 1000.0 * 1000.0 * 1000.0; const double MB = 1000.0 * 1000.0; const double KB = 1000.0; if ((double)bytes >= TB) snprintf(buf, buf_size, "%.1f TB", (double)bytes / TB); else if ((double)bytes >= GB) snprintf(buf, buf_size, "%.1f GB", (double)bytes / GB); else if ((double)bytes >= MB) snprintf(buf, buf_size, "%.1f MB", (double)bytes / MB); else if ((double)bytes >= KB) snprintf(buf, buf_size, "%.1f KB", (double)bytes / KB); else snprintf(buf, buf_size, "%llu B", (unsigned long long)bytes); } /** * @brief Progress context for CLI progress bar display. */ typedef struct { double start_time; const char* operation; // "Compressing" or "Decompressing" uint64_t total_size; // Pre-determined total size (0 if unknown) } progress_ctx_t; /** * @brief Progress callback for CLI progress bar. * * Displays a real-time progress bar during compression/decompression. * Shows percentage, processed/total size, and throughput speed. * * Format: [==========> ] 45% | 4.5 GB/10.0 GB | 156 MB/s */ static void cli_progress_callback(uint64_t bytes_processed, uint64_t bytes_total, const void* user_data) { const progress_ctx_t* pctx = (const progress_ctx_t*)user_data; if (!pctx) return; // Use pre-determined total size from context (not the parameter) const uint64_t total = pctx->total_size; const double now = zxc_now(); const double elapsed = now - pctx->start_time; // Calculate throughput speed double speed_mbps = 0.0; if (elapsed > 0.1) // Avoid division by zero for very fast operations speed_mbps = (double)bytes_processed / (1000.0 * 1000.0) / elapsed; // Clear line and move cursor to beginning fprintf(stderr, "\r\033[K"); if (total > 0) { // Known size: show percentage bar int percent = (int)((bytes_processed * 100) / total); if (percent > 100) percent = 100; const int bar_width = 20; int filled = (percent * bar_width) / 100; fprintf(stderr, "%s [", pctx->operation); for (int i = 0; i < bar_width; i++) { if (i < filled) fprintf(stderr, "="); else if (i == filled) fprintf(stderr, ">"); else fprintf(stderr, " "); } fprintf(stderr, "] %d%% | ", percent); char proc_str[32], total_str[32]; format_size_decimal(bytes_processed, proc_str, sizeof(proc_str)); format_size_decimal(total, total_str, sizeof(total_str)); fprintf(stderr, "%s/%s | %.1f MB/s", proc_str, total_str, speed_mbps); } else { // Unknown size (stdin): just show bytes processed char proc_str[32]; format_size_decimal(bytes_processed, proc_str, sizeof(proc_str)); fprintf(stderr, "%s [Processing...] %s | %.1f MB/s", pctx->operation, proc_str, speed_mbps); } fflush(stderr); } /** * @brief Lists the contents of a ZXC archive. * * Reads the file header and footer to display: * - Compressed size * - Uncompressed size * - Compression ratio * - Checksum method * - Filename * * In verbose mode, displays additional header information. * * @param[in] path Path to the ZXC archive file. * @param[in] json_output If 1, output JSON format. * @return 0 on success, 1 on error. */ static int zxc_list_archive(const char* path, int json_output) { char resolved_path[4096]; if (zxc_validate_input_path(path, resolved_path, sizeof(resolved_path)) != 0) { fprintf(stderr, "Error: Invalid input file '%s': %s\n", path, strerror(errno)); return 1; } FILE* f = fopen(resolved_path, "rb"); if (!f) { fprintf(stderr, "Error: Cannot open '%s': %s\n", path, strerror(errno)); return 1; } // Get file size if (fseeko(f, 0, SEEK_END) != 0) { fclose(f); fprintf(stderr, "Error: Cannot seek in file\n"); return 1; } const long long file_size = ftello(f); // Use public API to get decompressed size const int64_t uncompressed_size = zxc_stream_get_decompressed_size(f); if (uncompressed_size < 0) { fclose(f); fprintf(stderr, "Error: Not a valid ZXC archive\n"); return 1; } // Read header for format info (rewind after API call) uint8_t header[ZXC_FILE_HEADER_SIZE]; if (fseeko(f, 0, SEEK_SET) != 0 || fread(header, 1, ZXC_FILE_HEADER_SIZE, f) != ZXC_FILE_HEADER_SIZE) { fclose(f); fprintf(stderr, "Error: Cannot read file header\n"); return 1; } // Extract header fields const uint8_t format_version = header[4]; const size_t block_units = header[5] ? header[5] : 64; // Default 64 units = 256KB // Read footer for checksum info uint8_t footer[ZXC_FILE_FOOTER_SIZE]; if (fseeko(f, file_size - ZXC_FILE_FOOTER_SIZE, SEEK_SET) != 0 || fread(footer, 1, ZXC_FILE_FOOTER_SIZE, f) != ZXC_FILE_FOOTER_SIZE) { fclose(f); fprintf(stderr, "Error: Cannot read file footer\n"); return 1; } fclose(f); // Parse checksum (if non-zero, checksum was enabled) const uint32_t stored_checksum = footer[8] | ((uint32_t)footer[9] << 8) | ((uint32_t)footer[10] << 16) | ((uint32_t)footer[11] << 24); const char* checksum_method = (stored_checksum != 0) ? "RapidHash" : "-"; // Calculate ratio (uncompressed / compressed, e.g., 2.5 means 2.5x compression) const double ratio = (file_size > 0) ? ((double)uncompressed_size / (double)file_size) : 0.0; // Format sizes char comp_str[32], uncomp_str[32]; format_size_decimal((uint64_t)file_size, comp_str, sizeof(comp_str)); format_size_decimal((uint64_t)uncompressed_size, uncomp_str, sizeof(uncomp_str)); if (json_output) { // JSON mode printf( "{\n" " \"filename\": \"%s\",\n" " \"compressed_size_bytes\": %lld,\n" " \"uncompressed_size_bytes\": %lld,\n" " \"compression_ratio\": %.3f,\n" " \"format_version\": %u,\n" " \"block_size_kb\": %zu,\n" " \"checksum_method\": \"%s\",\n" " \"checksum_value\": \"0x%08X\"\n" "}\n", path, (long long)file_size, (long long)uncompressed_size, ratio, format_version, block_units * 4, (stored_checksum != 0) ? "RapidHash" : "none", stored_checksum); } else if (g_verbose) { // Verbose mode: detailed vertical layout printf( "\nFile: %s\n" "-----------------------\n" "Block Format: %u\n" "Block Units: %zu (x 4KB)\n" "Checksum Method: %s\n", path, format_version, block_units, (stored_checksum != 0) ? "RapidHash" : "None"); if (stored_checksum != 0) printf("Checksum Value: 0x%08X\n", stored_checksum); printf( "-----------------------\n" "Comp. Size: %s\n" "Uncomp. Size: %s\n" "Ratio: %.2f\n", comp_str, uncomp_str, ratio); } else { // Normal mode: table format printf("\n %12s %12s %5s %-10s %s\n", "Compressed", "Uncompressed", "Ratio", "Checksum", "Filename"); printf(" %12s %12s %5.2f %-10s %s\n", comp_str, uncomp_str, ratio, checksum_method, path); } return 0; } static int process_single_file(const char* in_path, const char* out_path_override, zxc_mode_t mode, int num_threads, int keep_input, int force, int to_stdout, int checksum_enabled, int level, size_t block_size, int json_output, int seekable) { FILE* f_in = stdin; FILE* f_out = stdout; char resolved_in_path[4096] = {0}; char out_path[4096] = {0}; char resolved_out_path[4096] = {0}; int use_stdin = 1, use_stdout = 0; int created_out_file = 0; int overall_ret = 0; if (in_path && strcmp(in_path, "-") != 0) { if (zxc_validate_input_path(in_path, resolved_in_path, sizeof(resolved_in_path)) != 0) { zxc_log("Error: Invalid input file '%s': %s\n", in_path, strerror(errno)); return 1; } f_in = fopen(resolved_in_path, "rb"); if (!f_in) { zxc_log("Error open input %s: %s\n", resolved_in_path, strerror(errno)); return 1; } use_stdin = 0; } else { use_stdin = 1; use_stdout = 1; // Default to stdout if reading from stdin in_path = NULL; } if (mode == MODE_INTEGRITY) { use_stdout = 0; f_out = NULL; } else if (to_stdout) { use_stdout = 1; } else if (!use_stdin) { // Auto-generate output filename if input is a file and no output specified if (out_path_override) { snprintf(out_path, sizeof(out_path), "%s", out_path_override); } else if (mode == MODE_COMPRESS) { snprintf(out_path, sizeof(out_path), "%s.zxc", in_path); } else { const size_t len = strlen(in_path); if (len > 4 && !strcmp(in_path + len - 4, ".zxc")) { const size_t base_len = len - 4; if (base_len >= sizeof(out_path)) { zxc_log("Error: Output path too long\n"); if (f_in) fclose(f_in); return 1; } memcpy(out_path, in_path, base_len); out_path[base_len] = '\0'; } else { zxc_log("Error: Cannot determine output filename: '%s' does not end with .zxc\n", in_path); if (f_in) fclose(f_in); return 1; } } use_stdout = 0; } // Safety check: prevent overwriting input file if (mode != MODE_INTEGRITY && !use_stdin && !use_stdout && strcmp(in_path ? in_path : "", out_path) == 0) { zxc_log("Error: Input and output filenames are identical for '%s'.\n", in_path); if (f_in) fclose(f_in); return 1; } // Open output file if not writing to stdout if (!use_stdout && mode != MODE_INTEGRITY) { if (zxc_validate_output_path(out_path, resolved_out_path, sizeof(resolved_out_path)) != 0) { zxc_log("Error: Invalid output path '%s': %s\n", out_path, strerror(errno)); if (f_in) fclose(f_in); return 1; } if (!force && access(resolved_out_path, F_OK) == 0) { zxc_log("Output exists. Use -f to overwrite '%s'.\n", resolved_out_path); fclose(f_in); return 1; } #ifdef _WIN32 f_out = fopen(resolved_out_path, "wb"); #else // Restrict permissions to 0644 const int fd = open(resolved_out_path, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd == -1) { zxc_log("Error creating output %s: %s\n", resolved_out_path, strerror(errno)); fclose(f_in); return 1; } f_out = fdopen(fd, "wb"); #endif if (!f_out) { zxc_log("Error open output %s: %s\n", resolved_out_path, strerror(errno)); if (f_in) fclose(f_in); #ifndef _WIN32 if (fd != -1) close(fd); #endif return 1; } created_out_file = 1; } // Prevent writing binary data to the terminal unless forced if (use_stdout && isatty(fileno(stdout)) && mode == MODE_COMPRESS && !force) { zxc_log( "Refusing to write compressed data to terminal.\n" "For help, type: zxc -h\n"); if (f_in) fclose(f_in); if (f_out) fclose(f_out); return 1; } // Set stdin/stdout to binary mode if using them #ifdef _WIN32 if (use_stdin) _setmode(_fileno(stdin), _O_BINARY); if (use_stdout) _setmode(_fileno(stdout), _O_BINARY); #else // On POSIX systems, there's no text/binary distinction, but we ensure // no buffering issues occur by using freopen if needed if (use_stdin) { if (!freopen(NULL, "rb", stdin)) zxc_log("Warning: Failed to reopen stdin in binary mode\n"); } if (use_stdout) { if (!freopen(NULL, "wb", stdout)) zxc_log("Warning: Failed to reopen stdout in binary mode\n"); } #endif // Determine if we should show progress bar and get file size // IMPORTANT: This must be done BEFORE setting large buffers with setvbuf // to avoid buffer inconsistency issues when reading the footer int show_progress = 0; uint64_t total_size = 0; if (!g_quiet && !use_stdout && !use_stdin && isatty(fileno(stderr))) { // Get file size based on mode if (mode == MODE_COMPRESS) { // Compression: get input file size const long long saved_pos = ftello(f_in); if (saved_pos >= 0) { if (fseeko(f_in, 0, SEEK_END) == 0) { const long long size = ftello(f_in); if (size > 0) total_size = (uint64_t)size; fseeko(f_in, saved_pos, SEEK_SET); } } } else { // Decompression: get decompressed size from footer (BEFORE starting decompression) const int64_t decomp_size = zxc_stream_get_decompressed_size(f_in); if (decomp_size > 0) total_size = (uint64_t)decomp_size; } // Only show progress for files > 1MB if (total_size > ZXC_IO_BUFFER_SIZE) show_progress = 1; } // Set large buffers for I/O performance (AFTER file size detection) char *b1 = malloc(ZXC_IO_BUFFER_SIZE), *b2 = malloc(ZXC_IO_BUFFER_SIZE); if (b1) setvbuf(f_in, b1, _IOFBF, ZXC_IO_BUFFER_SIZE); if (f_out && b2) setvbuf(f_out, b2, _IOFBF, ZXC_IO_BUFFER_SIZE); if (in_path && !g_quiet) { zxc_log_v("Processing %s... (Compression Level %d)\n", in_path, level); } if (g_verbose) zxc_log("Checksum: %s\n", checksum_enabled ? "enabled" : "disabled"); if (g_verbose && seekable) zxc_log("Seekable: enabled\n"); // Prepare progress context progress_ctx_t pctx = {.start_time = zxc_now(), .operation = (mode == MODE_COMPRESS) ? "Compressing" : "Decompressing", .total_size = total_size}; const double t0 = zxc_now(); int64_t bytes; if (mode == MODE_COMPRESS) { zxc_compress_opts_t copts = { .n_threads = num_threads, .level = level, .block_size = block_size, .checksum_enabled = checksum_enabled, .seekable = seekable, .progress_cb = show_progress ? cli_progress_callback : NULL, .user_data = &pctx, }; bytes = zxc_stream_compress(f_in, f_out, &copts); } else { zxc_decompress_opts_t dopts = { .n_threads = num_threads, .checksum_enabled = checksum_enabled, .progress_cb = show_progress ? cli_progress_callback : NULL, .user_data = &pctx, }; bytes = zxc_stream_decompress(f_in, f_out, &dopts); } const double dt = zxc_now() - t0; // Clear progress line on completion if (show_progress) { fprintf(stderr, "\r\033[K"); fflush(stderr); } if (!use_stdin) fclose(f_in); else setvbuf(stdin, NULL, _IONBF, 0); if (f_out && f_out != stdout) fclose(f_out); else if (use_stdout) { fflush(stdout); setvbuf(stdout, NULL, _IONBF, 0); } free(b1); free(b2); if (bytes >= 0) { if (mode == MODE_INTEGRITY) { // Test mode: show result if (json_output) { printf( "{\n" " \"filename\": \"%s\",\n" " \"status\": \"ok\",\n" " \"checksum_verified\": %s,\n" " \"time_seconds\": %.6f\n" "}\n", in_path ? in_path : "", checksum_enabled ? "true" : "false", dt); } else if (g_verbose) { printf( "%s: OK\n" " Checksum: %s\n" " Time: %.3fs\n", in_path ? in_path : "", checksum_enabled ? "verified (RapidHash)" : "not verified", dt); } else { printf("%s: OK\n", in_path ? in_path : ""); } } else { zxc_log_v("Processed %lld bytes in %.3fs\n", (long long)bytes, dt); } if (!use_stdin && !use_stdout && !keep_input && mode != MODE_INTEGRITY) unlink(resolved_in_path); } else { if (mode == MODE_INTEGRITY) { if (json_output) { printf( "{\n" " \"filename\": \"%s\",\n" " \"status\": \"failed\",\n" " \"error\": \"Integrity check failed (corrupted data or invalid checksum)\"\n" "}\n", in_path ? in_path : ""); } else { fprintf(stderr, "%s: FAILED\n", in_path ? in_path : ""); if (g_verbose) fprintf( stderr, " Reason: Integrity check failed (corrupted data or invalid checksum)\n"); } } else { zxc_log("Operation failed on %s.\n", in_path ? in_path : ""); if (created_out_file) unlink(resolved_out_path); } overall_ret = 1; } return overall_ret; } /** * @brief Main entry point. * Parses arguments and dispatches execution to Benchmark, Compress, or * Decompress modes. */ int main(int argc, char** argv) { zxc_mode_t mode = MODE_COMPRESS; int num_threads = 0; int keep_input = 0; int force = 0; int to_stdout = 0; int bench_seconds = 5; int checksum = -1; int level = 3; int json_output = 0; size_t block_size = 0; int seekable = 0; static const struct option long_options[] = {{"compress", no_argument, 0, 'z'}, {"decompress", no_argument, 0, 'd'}, {"list", no_argument, 0, 'l'}, {"test", no_argument, 0, 't'}, {"bench", optional_argument, 0, 'b'}, {"threads", required_argument, 0, 'T'}, {"keep", no_argument, 0, 'k'}, {"force", no_argument, 0, 'f'}, {"stdout", no_argument, 0, 'c'}, {"verbose", no_argument, 0, 'v'}, {"quiet", no_argument, 0, 'q'}, {"checksum", no_argument, 0, 'C'}, {"no-checksum", no_argument, 0, 'N'}, {"json", no_argument, 0, 'j'}, {"version", no_argument, 0, 'V'}, {"help", no_argument, 0, 'h'}, {"multiple", no_argument, 0, 'm'}, {"recursive", no_argument, 0, 'r'}, {"block-size", required_argument, 0, 'B'}, {"seekable", no_argument, 0, 'S'}, {0, 0, 0, 0}}; int opt; int multiple_mode = 0; int recursive_mode = 0; while ((opt = getopt_long(argc, argv, "12345b::B:cCdfhjklmrNqST:tvVz", long_options, NULL)) != -1) { switch (opt) { case 'z': mode = MODE_COMPRESS; break; case 'd': mode = MODE_DECOMPRESS; break; case 'l': mode = MODE_LIST; break; case 't': mode = MODE_INTEGRITY; break; case 'b': mode = MODE_BENCHMARK; const char* bench_arg = optarg; if (!bench_arg && optind < argc && argv[optind][0] >= '1' && argv[optind][0] <= '9') bench_arg = argv[optind++]; if (bench_arg) { bench_seconds = atoi(bench_arg); if (bench_seconds < 1 || bench_seconds > 3600) { fprintf(stderr, "Error: duration must be between 1 and 3600 seconds\n"); return 1; } } break; case '1': level = 1; break; case '2': level = 2; break; case '3': level = 3; break; case '4': level = 4; break; case '5': level = 5; break; case 'T': num_threads = atoi(optarg); if (num_threads < 0 || num_threads > ZXC_MAX_THREADS) { fprintf(stderr, "Error: num_threads must be between 0 and %d\n", ZXC_MAX_THREADS); return 1; } break; case 'k': keep_input = 1; break; case 'f': force = 1; break; case 'c': to_stdout = 1; break; case 'v': g_verbose = 1; break; case 'q': g_quiet = 1; break; case 'C': checksum = 1; break; case 'N': checksum = 0; break; case 'j': json_output = 1; break; case 'm': multiple_mode = 1; break; case 'S': seekable = 1; break; case 'r': recursive_mode = 1; multiple_mode = 1; // Recursive implies multiple mode for files processing break; case 'B': { char* end = NULL; const long long bs_val = strtoll(optarg, &end, 10); if (bs_val <= 0 || end == optarg) { fprintf(stderr, "Error: block-size must be a power of 2 between 4K and 2M\n" " Examples: -B 4K, -B 128K, -B 1M, -B 2M\n"); return 1; } long long multiplier = 1; if (end && (*end == 'k' || *end == 'K')) { multiplier = 1024; end++; if (*end == 'b' || *end == 'B') end++; // optional "B" in "KB" } else if (end && (*end == 'm' || *end == 'M')) { multiplier = 1024 * 1024; end++; if (*end == 'b' || *end == 'B') end++; // optional "B" in "MB" } const long long bs_bytes = bs_val * multiplier; if (!zxc_validate_block_size((size_t)bs_bytes)) { fprintf(stderr, "Error: block-size must be a power of 2 between 4K and 2M\n" " Examples: -B 4K, -B 128K, -B 1M, -B 2M\n"); return 1; } block_size = (size_t)bs_bytes; break; } case '?': print_help(argv[0]); return 1; case 'V': print_version(); return 0; case 'h': print_help(argv[0]); return 0; default: return 1; } } // Handle positional arguments for mode selection (e.g., "zxc z file") if (optind < argc && mode != MODE_BENCHMARK) { if (strcmp(argv[optind], "z") == 0) { mode = MODE_COMPRESS; optind++; } else if (strcmp(argv[optind], "d") == 0) { mode = MODE_DECOMPRESS; optind++; } else if (strcmp(argv[optind], "l") == 0 || strcmp(argv[optind], "list") == 0) { mode = MODE_LIST; optind++; } else if (strcmp(argv[optind], "t") == 0 || strcmp(argv[optind], "test") == 0) { mode = MODE_INTEGRITY; optind++; } else if (strcmp(argv[optind], "b") == 0) { mode = MODE_BENCHMARK; optind++; } } if (checksum == -1) { checksum = (mode == MODE_BENCHMARK) ? 0 : 1; } /* * Benchmark Mode * Loads the entire input file into RAM to measure raw algorithm throughput * without disk I/O bottlenecks. */ if (mode == MODE_BENCHMARK) { if (optind >= argc) { zxc_log("Benchmark requires input file.\n"); return 1; } const char* in_path = argv[optind]; int ret = 1; uint8_t* ram = NULL; uint8_t* c_dat = NULL; FILE* fm = NULL; char resolved_path[4096]; if (zxc_validate_input_path(in_path, resolved_path, sizeof(resolved_path)) != 0) { zxc_log("Error: Invalid input file '%s': %s\n", in_path, strerror(errno)); return 1; } if (num_threads < 0 || num_threads > ZXC_MAX_THREADS) { zxc_log("Error: num_threads must be between 0 and %d\n", ZXC_MAX_THREADS); return 1; } FILE* f_in = fopen(resolved_path, "rb"); if (!f_in) goto bench_cleanup; if (fseeko(f_in, 0, SEEK_END) != 0) goto bench_cleanup; const long long fsize = ftello(f_in); if (fsize <= 0) goto bench_cleanup; const size_t in_size = (size_t)fsize; if (fseeko(f_in, 0, SEEK_SET) != 0) goto bench_cleanup; ram = malloc(in_size); if (!ram) goto bench_cleanup; if (fread(ram, 1, in_size, f_in) != in_size) goto bench_cleanup; fclose(f_in); f_in = NULL; if (!json_output) printf( "Input: %s (%zu bytes)\n" "Running for %d seconds (threads: %d)...\n", in_path, in_size, bench_seconds, num_threads); #ifdef _WIN32 if (!json_output) printf("Note: Using tmpfile on Windows (slower than fmemopen).\n"); fm = tmpfile(); if (fm) { fwrite(ram, 1, in_size, fm); rewind(fm); } #else fm = fmemopen(ram, in_size, "rb"); #endif if (!fm) goto bench_cleanup; double best_compress = 1e30; int compress_iters = 0; const double compress_deadline = zxc_now() + (double)bench_seconds; const double compress_start = zxc_now(); while (zxc_now() < compress_deadline) { rewind(fm); const double t0 = zxc_now(); zxc_compress_opts_t bench_copts = {.n_threads = num_threads, .level = level, .block_size = block_size, .checksum_enabled = checksum}; zxc_stream_compress(fm, NULL, &bench_copts); const double dt = zxc_now() - t0; if (dt < best_compress) best_compress = dt; compress_iters++; if (!json_output && !g_quiet) fprintf(stderr, "\rCompressing... %d iters (%.1fs)", compress_iters, zxc_now() - compress_start); } if (!json_output && !g_quiet) fprintf(stderr, "\r\033[K"); fclose(fm); fm = NULL; const uint64_t max_c = zxc_compress_bound(in_size); c_dat = malloc((size_t)max_c); if (!c_dat) goto bench_cleanup; #ifdef _WIN32 FILE* fm_in = tmpfile(); FILE* fm_out = tmpfile(); if (!fm_in || !fm_out) { if (fm_in) fclose(fm_in); if (fm_out) fclose(fm_out); goto bench_cleanup; } fwrite(ram, 1, in_size, fm_in); rewind(fm_in); #else FILE* fm_in = fmemopen(ram, in_size, "rb"); FILE* fm_out = fmemopen(c_dat, max_c, "wb"); if (!fm_in || !fm_out) { if (fm_in) fclose(fm_in); if (fm_out) fclose(fm_out); goto bench_cleanup; } #endif zxc_compress_opts_t bench_copts2 = {.n_threads = num_threads, .level = level, .block_size = block_size, .checksum_enabled = checksum}; const int64_t c_sz = zxc_stream_compress(fm_in, fm_out, &bench_copts2); if (c_sz < 0) { fclose(fm_in); fclose(fm_out); fm_in = NULL; fm_out = NULL; goto bench_cleanup; } #ifdef _WIN32 rewind(fm_out); fread(c_dat, 1, (size_t)c_sz, fm_out); fclose(fm_in); fclose(fm_out); #else fclose(fm_in); fclose(fm_out); #endif #ifdef _WIN32 FILE* fc = tmpfile(); if (!fc) goto bench_cleanup; fwrite(c_dat, 1, (size_t)c_sz, fc); rewind(fc); #else FILE* fc = fmemopen(c_dat, (size_t)c_sz, "rb"); if (!fc) goto bench_cleanup; #endif double best_decompress = 1e30; int decompress_iters = 0; const double decompress_deadline = zxc_now() + (double)bench_seconds; const double decompress_start = zxc_now(); while (zxc_now() < decompress_deadline) { rewind(fc); const double t0 = zxc_now(); zxc_decompress_opts_t bench_dopts = {.n_threads = num_threads, .checksum_enabled = checksum}; zxc_stream_decompress(fc, NULL, &bench_dopts); const double dt = zxc_now() - t0; if (dt < best_decompress) best_decompress = dt; decompress_iters++; if (!json_output && !g_quiet) fprintf(stderr, "\rDecompressing... %d iters (%.1fs)", decompress_iters, zxc_now() - decompress_start); } if (!json_output && !g_quiet) fprintf(stderr, "\r\033[K"); fclose(fc); const double compress_speed_mbps = (double)in_size / (1000.0 * 1000.0) / best_compress; const double decompress_speed_mbps = (double)in_size / (1000.0 * 1000.0) / best_decompress; const double ratio = (c_sz > 0) ? ((double)in_size / c_sz) : 0.0; if (json_output) printf( "{\n" " \"input_file\": \"%s\",\n" " \"input_size_bytes\": %zu,\n" " \"compressed_size_bytes\": %lld,\n" " \"compression_ratio\": %.3f,\n" " \"duration_seconds\": %d,\n" " \"compress_iterations\": %d,\n" " \"decompress_iterations\": %d,\n" " \"threads\": %d,\n" " \"level\": %d,\n" " \"checksum_enabled\": %s,\n" " \"compress_speed_mbps\": %.3f,\n" " \"decompress_speed_mbps\": %.3f,\n" " \"compress_time_seconds\": %.6f,\n" " \"decompress_time_seconds\": %.6f\n" "}\n", in_path, in_size, (long long)c_sz, ratio, bench_seconds, compress_iters, decompress_iters, num_threads, level, checksum ? "true" : "false", compress_speed_mbps, decompress_speed_mbps, best_compress, best_decompress); else printf( "Compressed: %lld bytes (ratio %.3f)\n" "Compress : %.3f MB/s (%d iters)\n" "Decompress: %.3f MB/s (%d iters)\n", (long long)c_sz, ratio, compress_speed_mbps, compress_iters, decompress_speed_mbps, decompress_iters); ret = 0; bench_cleanup: if (fm) fclose(fm); if (f_in) fclose(f_in); free(ram); free(c_dat); return ret; } /* * List Mode * Displays archive information (compressed size, uncompressed size, ratio). */ if (mode == MODE_LIST) { if (optind >= argc) { zxc_log("List mode requires input file.\n"); return 1; } int ret = 0; const int num_files = argc - optind; if (json_output && num_files > 1) printf("[\n"); for (int i = optind; i < argc; i++) { ret |= zxc_list_archive(argv[i], json_output); if (json_output && num_files > 1 && i < argc - 1) { printf(",\n"); } } if (json_output && num_files > 1) { printf("]\n"); } return ret; } if (multiple_mode && to_stdout) { zxc_log("Error: cannot write to stdout when using multiple files mode (-m).\n"); return 1; } /* * File Processing Mode * Loops over files and determines input/output paths. */ int overall_ret = 0; const int start_optind = optind; // If no files passed but we aren't using stdin, or mode expects files: if (optind >= argc && mode == MODE_INTEGRITY) { zxc_log("Test mode requires at least one input file.\n"); return 1; } if (multiple_mode && optind >= argc) { zxc_log("Multiple files mode requires at least one input file.\n"); return 1; } // Default to processing at least once (for stdin) if no files are passed and not in a mode that // strictly needs files const int num_files_to_process = (optind < argc) ? (argc - optind) : 1; for (int file_idx = 0; file_idx < num_files_to_process; file_idx++) { const char* current_arg = (optind < argc) ? argv[start_optind + file_idx] : NULL; if (recursive_mode && current_arg && strcmp(current_arg, "-") != 0 && zxc_is_directory(current_arg)) { overall_ret |= process_directory(current_arg, mode, num_threads, keep_input, force, to_stdout, checksum, level, block_size, json_output, seekable); } else { const char* explicit_out_path = (!multiple_mode && optind + 1 < argc && current_arg && strcmp(current_arg, "-") != 0 && !to_stdout) ? argv[start_optind + 1] : NULL; overall_ret |= process_single_file(current_arg, explicit_out_path, mode, num_threads, keep_input, force, to_stdout, checksum, level, block_size, json_output, seekable); } if (!multiple_mode) { break; // Standard mode only does the first argument as input } } return overall_ret; } zxc-0.10.0/src/lib/000077500000000000000000000000001517010557200137345ustar00rootroot00000000000000zxc-0.10.0/src/lib/vendors/000077500000000000000000000000001517010557200154145ustar00rootroot00000000000000zxc-0.10.0/src/lib/vendors/rapidhash.h000066400000000000000000000514151517010557200175360ustar00rootroot00000000000000/* * rapidhash V3 - Very fast, high quality, platform-independent hashing algorithm. * * Based on 'wyhash', by Wang Yi * * Copyright (C) 2025 Nicolas De Carli * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * You can contact the author at: * - rapidhash source repository: https://github.com/Nicoshev/rapidhash */ #pragma once /* * Includes. */ #include #include #if defined(_MSC_VER) # include # if defined(_M_X64) && !defined(_M_ARM64EC) # pragma intrinsic(_umul128) # endif #endif /* * C/C++ macros. */ #ifdef _MSC_VER # define RAPIDHASH_ALWAYS_INLINE __forceinline #elif defined(__GNUC__) # define RAPIDHASH_ALWAYS_INLINE inline __attribute__((__always_inline__)) #else # define RAPIDHASH_ALWAYS_INLINE inline #endif #ifdef __cplusplus # define RAPIDHASH_NOEXCEPT noexcept # define RAPIDHASH_CONSTEXPR constexpr # ifndef RAPIDHASH_INLINE # define RAPIDHASH_INLINE RAPIDHASH_ALWAYS_INLINE # endif # if __cplusplus >= 201402L && !defined(_MSC_VER) # define RAPIDHASH_INLINE_CONSTEXPR RAPIDHASH_ALWAYS_INLINE constexpr # else # define RAPIDHASH_INLINE_CONSTEXPR RAPIDHASH_ALWAYS_INLINE # endif #else # define RAPIDHASH_NOEXCEPT # define RAPIDHASH_CONSTEXPR static const # ifndef RAPIDHASH_INLINE # define RAPIDHASH_INLINE static RAPIDHASH_ALWAYS_INLINE # endif # define RAPIDHASH_INLINE_CONSTEXPR RAPIDHASH_INLINE #endif /* * Unrolled macro. * Improves large input speed, but increases code size and worsens small input speed. * * RAPIDHASH_COMPACT: Normal behavior. * RAPIDHASH_UNROLLED: * */ #ifndef RAPIDHASH_UNROLLED # define RAPIDHASH_COMPACT #elif defined(RAPIDHASH_COMPACT) # error "cannot define RAPIDHASH_COMPACT and RAPIDHASH_UNROLLED simultaneously." #endif /* * Protection macro, alters behaviour of rapid_mum multiplication function. * * RAPIDHASH_FAST: Normal behavior, max speed. * RAPIDHASH_PROTECTED: Extra protection against entropy loss. */ #ifndef RAPIDHASH_PROTECTED # define RAPIDHASH_FAST #elif defined(RAPIDHASH_FAST) # error "cannot define RAPIDHASH_PROTECTED and RAPIDHASH_FAST simultaneously." #endif /* * Likely and unlikely macros. */ #if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__) # define _likely_(x) __builtin_expect(x,1) # define _unlikely_(x) __builtin_expect(x,0) #else # define _likely_(x) (x) # define _unlikely_(x) (x) #endif /* * Endianness macros. */ #ifndef RAPIDHASH_LITTLE_ENDIAN # if defined(_WIN32) || defined(__LITTLE_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) # define RAPIDHASH_LITTLE_ENDIAN # elif defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) # define RAPIDHASH_BIG_ENDIAN # else # warning "could not determine endianness! Falling back to little endian." # define RAPIDHASH_LITTLE_ENDIAN # endif #endif /* * Default secret parameters. */ RAPIDHASH_CONSTEXPR uint64_t rapid_secret[8] = { 0x2d358dccaa6c78a5ull, 0x8bb84b93962eacc9ull, 0x4b33a62ed433d4a3ull, 0x4d5a2da51de1aa47ull, 0xa0761d6478bd642full, 0xe7037ed1a0b428dbull, 0x90ed1765281c388cull, 0xaaaaaaaaaaaaaaaaull}; /* * 64*64 -> 128bit multiply function. * * @param A Address of 64-bit number. * @param B Address of 64-bit number. * * Calculates 128-bit C = *A * *B. * * When RAPIDHASH_FAST is defined: * Overwrites A contents with C's low 64 bits. * Overwrites B contents with C's high 64 bits. * * When RAPIDHASH_PROTECTED is defined: * Xors and overwrites A contents with C's low 64 bits. * Xors and overwrites B contents with C's high 64 bits. */ RAPIDHASH_INLINE_CONSTEXPR void rapid_mum(uint64_t *A, uint64_t *B) RAPIDHASH_NOEXCEPT { #if defined(__SIZEOF_INT128__) __uint128_t r=*A; r*=*B; #ifdef RAPIDHASH_PROTECTED *A^=(uint64_t)r; *B^=(uint64_t)(r>>64); #else *A=(uint64_t)r; *B=(uint64_t)(r>>64); #endif #elif defined(_MSC_VER) && (defined(_WIN64) || defined(_M_HYBRID_CHPE_ARM64)) #if defined(_M_X64) #ifdef RAPIDHASH_PROTECTED uint64_t a, b; a=_umul128(*A,*B,&b); *A^=a; *B^=b; #else *A=_umul128(*A,*B,B); #endif #else #ifdef RAPIDHASH_PROTECTED uint64_t a, b; b = __umulh(*A, *B); a = *A * *B; *A^=a; *B^=b; #else uint64_t c = __umulh(*A, *B); *A = *A * *B; *B = c; #endif #endif #else uint64_t ha=*A>>32, hb=*B>>32, la=(uint32_t)*A, lb=(uint32_t)*B; uint64_t rh=ha*hb, rm0=ha*lb, rm1=hb*la, rl=la*lb, t=rl+(rm0<<32), c=t>32)+(rm1>>32)+c; #ifdef RAPIDHASH_PROTECTED *A^=lo; *B^=hi; #else *A=lo; *B=hi; #endif #endif } /* * Multiply and xor mix function. * * @param A 64-bit number. * @param B 64-bit number. * * Calculates 128-bit C = A * B. * Returns 64-bit xor between high and low 64 bits of C. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapid_mix(uint64_t A, uint64_t B) RAPIDHASH_NOEXCEPT { rapid_mum(&A,&B); return A^B; } /* * Read functions. */ #ifdef RAPIDHASH_LITTLE_ENDIAN RAPIDHASH_INLINE uint64_t rapid_read64(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint64_t v; memcpy(&v, p, sizeof(uint64_t)); return v;} RAPIDHASH_INLINE uint64_t rapid_read32(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint32_t v; memcpy(&v, p, sizeof(uint32_t)); return v;} #elif defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__) RAPIDHASH_INLINE uint64_t rapid_read64(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint64_t v; memcpy(&v, p, sizeof(uint64_t)); return __builtin_bswap64(v);} RAPIDHASH_INLINE uint64_t rapid_read32(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint32_t v; memcpy(&v, p, sizeof(uint32_t)); return __builtin_bswap32(v);} #elif defined(_MSC_VER) RAPIDHASH_INLINE uint64_t rapid_read64(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint64_t v; memcpy(&v, p, sizeof(uint64_t)); return _byteswap_uint64(v);} RAPIDHASH_INLINE uint64_t rapid_read32(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint32_t v; memcpy(&v, p, sizeof(uint32_t)); return _byteswap_ulong(v);} #else RAPIDHASH_INLINE uint64_t rapid_read64(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint64_t v; memcpy(&v, p, 8); return (((v >> 56) & 0xff)| ((v >> 40) & 0xff00)| ((v >> 24) & 0xff0000)| ((v >> 8) & 0xff000000)| ((v << 8) & 0xff00000000)| ((v << 24) & 0xff0000000000)| ((v << 40) & 0xff000000000000)| ((v << 56) & 0xff00000000000000)); } RAPIDHASH_INLINE uint64_t rapid_read32(const uint8_t *p) RAPIDHASH_NOEXCEPT { uint32_t v; memcpy(&v, p, 4); return (((v >> 24) & 0xff)| ((v >> 8) & 0xff00)| ((v << 8) & 0xff0000)| ((v << 24) & 0xff000000)); } #endif /* * rapidhash main function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * @param seed 64-bit seed used to alter the hash result predictably. * @param secret Triplet of 64-bit secrets used to alter hash result predictably. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhash_internal(const void *key, size_t len, uint64_t seed, const uint64_t* secret) RAPIDHASH_NOEXCEPT { const uint8_t *p=(const uint8_t *)key; seed ^= rapid_mix(seed ^ secret[2], secret[1]); uint64_t a=0, b=0; size_t i = len; if (_likely_(len <= 16)) { if (len >= 4) { seed ^= len; if (len >= 8) { const uint8_t* plast = p + len - 8; a = rapid_read64(p); b = rapid_read64(plast); } else { const uint8_t* plast = p + len - 4; a = rapid_read32(p); b = rapid_read32(plast); } } else if (len > 0) { a = (((uint64_t)p[0])<<45)|p[len-1]; b = p[len>>1]; } else a = b = 0; } else { if (len > 112) { uint64_t see1 = seed, see2 = seed; uint64_t see3 = seed, see4 = seed; uint64_t see5 = seed, see6 = seed; #ifdef RAPIDHASH_COMPACT do { seed = rapid_mix(rapid_read64(p) ^ secret[0], rapid_read64(p + 8) ^ seed); see1 = rapid_mix(rapid_read64(p + 16) ^ secret[1], rapid_read64(p + 24) ^ see1); see2 = rapid_mix(rapid_read64(p + 32) ^ secret[2], rapid_read64(p + 40) ^ see2); see3 = rapid_mix(rapid_read64(p + 48) ^ secret[3], rapid_read64(p + 56) ^ see3); see4 = rapid_mix(rapid_read64(p + 64) ^ secret[4], rapid_read64(p + 72) ^ see4); see5 = rapid_mix(rapid_read64(p + 80) ^ secret[5], rapid_read64(p + 88) ^ see5); see6 = rapid_mix(rapid_read64(p + 96) ^ secret[6], rapid_read64(p + 104) ^ see6); p += 112; i -= 112; } while(i > 112); #else while (i > 224) { seed = rapid_mix(rapid_read64(p) ^ secret[0], rapid_read64(p + 8) ^ seed); see1 = rapid_mix(rapid_read64(p + 16) ^ secret[1], rapid_read64(p + 24) ^ see1); see2 = rapid_mix(rapid_read64(p + 32) ^ secret[2], rapid_read64(p + 40) ^ see2); see3 = rapid_mix(rapid_read64(p + 48) ^ secret[3], rapid_read64(p + 56) ^ see3); see4 = rapid_mix(rapid_read64(p + 64) ^ secret[4], rapid_read64(p + 72) ^ see4); see5 = rapid_mix(rapid_read64(p + 80) ^ secret[5], rapid_read64(p + 88) ^ see5); see6 = rapid_mix(rapid_read64(p + 96) ^ secret[6], rapid_read64(p + 104) ^ see6); seed = rapid_mix(rapid_read64(p + 112) ^ secret[0], rapid_read64(p + 120) ^ seed); see1 = rapid_mix(rapid_read64(p + 128) ^ secret[1], rapid_read64(p + 136) ^ see1); see2 = rapid_mix(rapid_read64(p + 144) ^ secret[2], rapid_read64(p + 152) ^ see2); see3 = rapid_mix(rapid_read64(p + 160) ^ secret[3], rapid_read64(p + 168) ^ see3); see4 = rapid_mix(rapid_read64(p + 176) ^ secret[4], rapid_read64(p + 184) ^ see4); see5 = rapid_mix(rapid_read64(p + 192) ^ secret[5], rapid_read64(p + 200) ^ see5); see6 = rapid_mix(rapid_read64(p + 208) ^ secret[6], rapid_read64(p + 216) ^ see6); p += 224; i -= 224; } if (i > 112) { seed = rapid_mix(rapid_read64(p) ^ secret[0], rapid_read64(p + 8) ^ seed); see1 = rapid_mix(rapid_read64(p + 16) ^ secret[1], rapid_read64(p + 24) ^ see1); see2 = rapid_mix(rapid_read64(p + 32) ^ secret[2], rapid_read64(p + 40) ^ see2); see3 = rapid_mix(rapid_read64(p + 48) ^ secret[3], rapid_read64(p + 56) ^ see3); see4 = rapid_mix(rapid_read64(p + 64) ^ secret[4], rapid_read64(p + 72) ^ see4); see5 = rapid_mix(rapid_read64(p + 80) ^ secret[5], rapid_read64(p + 88) ^ see5); see6 = rapid_mix(rapid_read64(p + 96) ^ secret[6], rapid_read64(p + 104) ^ see6); p += 112; i -= 112; } #endif seed ^= see1; see2 ^= see3; see4 ^= see5; seed ^= see6; see2 ^= see4; seed ^= see2; } if (i > 16) { seed = rapid_mix(rapid_read64(p) ^ secret[2], rapid_read64(p + 8) ^ seed); if (i > 32) { seed = rapid_mix(rapid_read64(p + 16) ^ secret[2], rapid_read64(p + 24) ^ seed); if (i > 48) { seed = rapid_mix(rapid_read64(p + 32) ^ secret[1], rapid_read64(p + 40) ^ seed); if (i > 64) { seed = rapid_mix(rapid_read64(p + 48) ^ secret[1], rapid_read64(p + 56) ^ seed); if (i > 80) { seed = rapid_mix(rapid_read64(p + 64) ^ secret[2], rapid_read64(p + 72) ^ seed); if (i > 96) { seed = rapid_mix(rapid_read64(p + 80) ^ secret[1], rapid_read64(p + 88) ^ seed); } } } } } } a=rapid_read64(p+i-16) ^ i; b=rapid_read64(p+i-8); } a ^= secret[1]; b ^= seed; rapid_mum(&a, &b); return rapid_mix(a ^ secret[7], b ^ secret[1] ^ i); } /* * rapidhashMicro main function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * @param seed 64-bit seed used to alter the hash result predictably. * @param secret Triplet of 64-bit secrets used to alter hash result predictably. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhashMicro_internal(const void *key, size_t len, uint64_t seed, const uint64_t* secret) RAPIDHASH_NOEXCEPT { const uint8_t *p=(const uint8_t *)key; seed ^= rapid_mix(seed ^ secret[2], secret[1]); uint64_t a=0, b=0; size_t i = len; if (_likely_(len <= 16)) { if (len >= 4) { seed ^= len; if (len >= 8) { const uint8_t* plast = p + len - 8; a = rapid_read64(p); b = rapid_read64(plast); } else { const uint8_t* plast = p + len - 4; a = rapid_read32(p); b = rapid_read32(plast); } } else if (len > 0) { a = (((uint64_t)p[0])<<45)|p[len-1]; b = p[len>>1]; } else a = b = 0; } else { if (i > 80) { uint64_t see1 = seed, see2 = seed; uint64_t see3 = seed, see4 = seed; do { seed = rapid_mix(rapid_read64(p) ^ secret[0], rapid_read64(p + 8) ^ seed); see1 = rapid_mix(rapid_read64(p + 16) ^ secret[1], rapid_read64(p + 24) ^ see1); see2 = rapid_mix(rapid_read64(p + 32) ^ secret[2], rapid_read64(p + 40) ^ see2); see3 = rapid_mix(rapid_read64(p + 48) ^ secret[3], rapid_read64(p + 56) ^ see3); see4 = rapid_mix(rapid_read64(p + 64) ^ secret[4], rapid_read64(p + 72) ^ see4); p += 80; i -= 80; } while(i > 80); seed ^= see1; see2 ^= see3; seed ^= see4; seed ^= see2; } if (i > 16) { seed = rapid_mix(rapid_read64(p) ^ secret[2], rapid_read64(p + 8) ^ seed); if (i > 32) { seed = rapid_mix(rapid_read64(p + 16) ^ secret[2], rapid_read64(p + 24) ^ seed); if (i > 48) { seed = rapid_mix(rapid_read64(p + 32) ^ secret[1], rapid_read64(p + 40) ^ seed); if (i > 64) { seed = rapid_mix(rapid_read64(p + 48) ^ secret[1], rapid_read64(p + 56) ^ seed); } } } } a=rapid_read64(p+i-16) ^ i; b=rapid_read64(p+i-8); } a ^= secret[1]; b ^= seed; rapid_mum(&a, &b); return rapid_mix(a ^ secret[7], b ^ secret[1] ^ i); } /* * rapidhashNano main function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * @param seed 64-bit seed used to alter the hash result predictably. * @param secret Triplet of 64-bit secrets used to alter hash result predictably. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhashNano_internal(const void *key, size_t len, uint64_t seed, const uint64_t* secret) RAPIDHASH_NOEXCEPT { const uint8_t *p=(const uint8_t *)key; seed ^= rapid_mix(seed ^ secret[2], secret[1]); uint64_t a=0, b=0; size_t i = len; if (_likely_(len <= 16)) { if (len >= 4) { seed ^= len; if (len >= 8) { const uint8_t* plast = p + len - 8; a = rapid_read64(p); b = rapid_read64(plast); } else { const uint8_t* plast = p + len - 4; a = rapid_read32(p); b = rapid_read32(plast); } } else if (len > 0) { a = (((uint64_t)p[0])<<45)|p[len-1]; b = p[len>>1]; } else a = b = 0; } else { if (i > 48) { uint64_t see1 = seed, see2 = seed; do { seed = rapid_mix(rapid_read64(p) ^ secret[0], rapid_read64(p + 8) ^ seed); see1 = rapid_mix(rapid_read64(p + 16) ^ secret[1], rapid_read64(p + 24) ^ see1); see2 = rapid_mix(rapid_read64(p + 32) ^ secret[2], rapid_read64(p + 40) ^ see2); p += 48; i -= 48; } while(i > 48); seed ^= see1; seed ^= see2; } if (i > 16) { seed = rapid_mix(rapid_read64(p) ^ secret[2], rapid_read64(p + 8) ^ seed); if (i > 32) { seed = rapid_mix(rapid_read64(p + 16) ^ secret[2], rapid_read64(p + 24) ^ seed); } } a=rapid_read64(p+i-16) ^ i; b=rapid_read64(p+i-8); } a ^= secret[1]; b ^= seed; rapid_mum(&a, &b); return rapid_mix(a ^ secret[7], b ^ secret[1] ^ i); } /* * rapidhash seeded hash function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * @param seed 64-bit seed used to alter the hash result predictably. * * Calls rapidhash_internal using provided parameters and default secrets. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhash_withSeed(const void *key, size_t len, uint64_t seed) RAPIDHASH_NOEXCEPT { return rapidhash_internal(key, len, seed, rapid_secret); } /* * rapidhash general purpose hash function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * * Calls rapidhash_withSeed using provided parameters and the default seed. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhash(const void *key, size_t len) RAPIDHASH_NOEXCEPT { return rapidhash_withSeed(key, len, 0); } /* * rapidhashMicro seeded hash function. * * Designed for HPC and server applications, where cache misses make a noticeable performance detriment. * Clang-18+ compiles it to ~140 instructions without stack usage, both on x86-64 and aarch64. * Faster for sizes up to 512 bytes, just 15%-20% slower for inputs above 1kb. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * @param seed 64-bit seed used to alter the hash result predictably. * * Calls rapidhash_internal using provided parameters and default secrets. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhashMicro_withSeed(const void *key, size_t len, uint64_t seed) RAPIDHASH_NOEXCEPT { return rapidhashMicro_internal(key, len, seed, rapid_secret); } /* * rapidhashMicro hash function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * * Calls rapidhash_withSeed using provided parameters and the default seed. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhashMicro(const void *key, size_t len) RAPIDHASH_NOEXCEPT { return rapidhashMicro_withSeed(key, len, 0); } /* * rapidhashNano seeded hash function. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * @param seed 64-bit seed used to alter the hash result predictably. * * Calls rapidhash_internal using provided parameters and default secrets. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhashNano_withSeed(const void *key, size_t len, uint64_t seed) RAPIDHASH_NOEXCEPT { return rapidhashNano_internal(key, len, seed, rapid_secret); } /* * rapidhashNano hash function. * * Designed for Mobile and embedded applications, where keeping a small code size is a top priority. * Clang-18+ compiles it to less than 100 instructions without stack usage, both on x86-64 and aarch64. * The fastest for sizes up to 48 bytes, but may be considerably slower for larger inputs. * * @param key Buffer to be hashed. * @param len @key length, in bytes. * * Calls rapidhash_withSeed using provided parameters and the default seed. * * Returns a 64-bit hash. */ RAPIDHASH_INLINE_CONSTEXPR uint64_t rapidhashNano(const void *key, size_t len) RAPIDHASH_NOEXCEPT { return rapidhashNano_withSeed(key, len, 0); } zxc-0.10.0/src/lib/zxc_common.c000066400000000000000000000632071517010557200162640ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /* * @file zxc_common.c * @brief Shared library utilities: context management, header I/O, bitpacking, * compress-bound calculation, and error-code name lookup. * * This translation unit contains the functions shared by both the buffer and * streaming APIs. It is linked into every build of libzxc. */ #include "../../include/zxc_buffer.h" #include "../../include/zxc_error.h" #include "../../include/zxc_sans_io.h" #include "zxc_internal.h" /* * ============================================================================ * CONTEXT MANAGEMENT * ============================================================================ */ /* * @brief Allocates memory aligned to the specified boundary. * * Uses `_aligned_malloc` on Windows and `posix_memalign` elsewhere. * * @param[in] size Number of bytes to allocate. * @param[in] alignment Required alignment (must be a power of two). * @return Pointer to the allocated block, or @c NULL on failure. */ void* zxc_aligned_malloc(const size_t size, const size_t alignment) { #if defined(_WIN32) return _aligned_malloc(size, alignment); #else void* ptr = NULL; if (posix_memalign(&ptr, alignment, size) != 0) return NULL; return ptr; #endif } /* * @brief Frees memory previously allocated by zxc_aligned_malloc(). * * @param[in] ptr Pointer returned by zxc_aligned_malloc() (may be @c NULL). */ void zxc_aligned_free(void* ptr) { #if defined(_WIN32) _aligned_free(ptr); #else free(ptr); #endif } /* * @brief Initialises a compression context, allocating all internal buffers. * * A single cache-line-aligned allocation is carved into hash table, chain * table, sequence buffers, token buffers, offset buffers, extra-length * buffers, and a literal buffer. * * @param[out] ctx Context to initialise (zeroed on entry). * @param[in] chunk_size Maximum uncompressed chunk size (bytes). * @param[in] mode 0 = decompression (skip buffer alloc), 1 = compression. * @param[in] level Compression level (stored in ctx). * @param[in] checksum_enabled Non-zero to enable checksum generation/verification. * @return @ref ZXC_OK on success, or @ref ZXC_ERROR_MEMORY on allocation failure. */ int zxc_cctx_init(zxc_cctx_t* RESTRICT ctx, const size_t chunk_size, const int mode, const int level, const int checksum_enabled) { ZXC_MEMSET(ctx, 0, sizeof(zxc_cctx_t)); ctx->checksum_enabled = checksum_enabled; /* Compute block-size derived parameters. */ ctx->chunk_size = chunk_size; const uint32_t offset_bits = zxc_log2_u32((uint32_t)chunk_size); ctx->offset_bits = offset_bits; ctx->offset_mask = (uint32_t)((1ULL << offset_bits) - 1); ctx->max_epoch = (uint32_t)(1ULL << (32 - offset_bits)); if (mode == 0) return ZXC_OK; const size_t max_seq = chunk_size / sizeof(uint32_t) + 256; const size_t sz_hash_pos = ZXC_LZ_HASH_SIZE * sizeof(uint32_t); const size_t sz_hash_tags = ZXC_LZ_HASH_SIZE * sizeof(uint8_t); const size_t sz_chain = chunk_size * sizeof(uint16_t); const size_t sz_sequences = max_seq * sizeof(uint32_t); const size_t sz_tokens = max_seq * sizeof(uint8_t); const size_t sz_offsets = max_seq * sizeof(uint16_t); const size_t sz_extras = max_seq * 2 * ZXC_VBYTE_ALLOC_LEN; // Max 3 bytes per LL/ML VByte (21 bits, sufficient for <= 2MB block) const size_t sz_lit = chunk_size + ZXC_PAD_SIZE; // Calculate sizes with alignment padding (64 bytes for cache line alignment) size_t total_size = 0; const size_t off_hash_pos = total_size; total_size += (sz_hash_pos + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_hash_tags = total_size; total_size += (sz_hash_tags + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_chain = total_size; total_size += (sz_chain + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_sequences = total_size; total_size += (sz_sequences + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_tokens = total_size; total_size += (sz_tokens + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_offsets = total_size; total_size += (sz_offsets + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_extras = total_size; total_size += (sz_extras + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t off_lit = total_size; total_size += (sz_lit + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; uint8_t* const mem = (uint8_t*)zxc_aligned_malloc(total_size, ZXC_CACHE_LINE_SIZE); if (UNLIKELY(!mem)) return ZXC_ERROR_MEMORY; ctx->memory_block = mem; ctx->hash_table = (uint32_t*)(mem + off_hash_pos); ctx->hash_tags = (uint8_t*)(mem + off_hash_tags); ctx->chain_table = (uint16_t*)(mem + off_chain); ctx->buf_sequences = (uint32_t*)(mem + off_sequences); ctx->buf_tokens = (uint8_t*)(mem + off_tokens); ctx->buf_offsets = (uint16_t*)(mem + off_offsets); ctx->buf_extras = (uint8_t*)(mem + off_extras); ctx->literals = (uint8_t*)(mem + off_lit); ctx->compression_level = level; ctx->epoch = 1; ZXC_MEMSET(ctx->hash_table, 0, sz_hash_pos); ZXC_MEMSET(ctx->hash_tags, 0, sz_hash_tags); return ZXC_OK; } /* * @brief Releases all resources owned by a compression context. * * After this call every pointer inside @p ctx is @c NULL and the context * may be safely re-initialised with zxc_cctx_init(). * * @param[in,out] ctx Context to tear down. */ void zxc_cctx_free(zxc_cctx_t* ctx) { if (ctx->memory_block) { zxc_aligned_free(ctx->memory_block); ctx->memory_block = NULL; } if (ctx->lit_buffer) { free(ctx->lit_buffer); ctx->lit_buffer = NULL; } if (ctx->work_buf) { free(ctx->work_buf); ctx->work_buf = NULL; } ctx->hash_table = NULL; ctx->hash_tags = NULL; ctx->chain_table = NULL; ctx->buf_sequences = NULL; ctx->buf_tokens = NULL; ctx->buf_offsets = NULL; ctx->buf_extras = NULL; ctx->literals = NULL; ctx->epoch = 0; ctx->lit_buffer_cap = 0; ctx->work_buf_cap = 0; } /* * ============================================================================ * HEADER I/O * ============================================================================ */ /* * @brief Serialises a ZXC file header into @p dst. * * Layout (16 bytes): Magic (4) | Version (1) | Chunk (1) | Flags (1) | * Reserved (7) | CRC-16 (2). * * @param[out] dst Destination buffer (>= @ref ZXC_FILE_HEADER_SIZE bytes). * @param[in] dst_capacity Capacity of @p dst. * @param[in] has_checksum Non-zero to set the checksum flag. * @return Number of bytes written (@ref ZXC_FILE_HEADER_SIZE) on success, * or a negative @ref zxc_error_t code. */ int zxc_write_file_header(uint8_t* RESTRICT dst, const size_t dst_capacity, const size_t chunk_size, const int has_checksum) { if (UNLIKELY(dst_capacity < ZXC_FILE_HEADER_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; zxc_store_le32(dst, ZXC_MAGIC_WORD); dst[4] = ZXC_FILE_FORMAT_VERSION; // Block size stored as log2 exponent (e.g. 18 = 256 KB) dst[5] = (uint8_t)zxc_log2_u32((uint32_t)chunk_size); // Flags are at offset 6 dst[6] = has_checksum ? (ZXC_FILE_FLAG_HAS_CHECKSUM | ZXC_CHECKSUM_RAPIDHASH) : 0; // Bytes 7-13: Reserved (must be 0, 7 bytes) ZXC_MEMSET(dst + 7, 0, 7); // Bytes 14-15: CRC (16-bit) zxc_store_le16(dst + 14, 0); // Zero out before hashing const uint16_t crc = zxc_hash16(dst); zxc_store_le16(dst + 14, crc); return ZXC_FILE_HEADER_SIZE; } /* * @brief Parses and validates a ZXC file header from @p src. * * Checks the magic word, format version, and CRC-16. * * @param[in] src Source buffer (>= @ref ZXC_FILE_HEADER_SIZE bytes). * @param[in] src_size Size of @p src. * @param[out] out_block_size Receives the decoded block size (may be @c NULL). * @param[out] out_has_checksum Receives 1 if checksums are present, 0 otherwise * (may be @c NULL). * @return @ref ZXC_OK on success, or a negative @ref zxc_error_t code. */ int zxc_read_file_header(const uint8_t* RESTRICT src, const size_t src_size, size_t* RESTRICT out_block_size, int* RESTRICT out_has_checksum) { if (UNLIKELY(src_size < ZXC_FILE_HEADER_SIZE)) return ZXC_ERROR_SRC_TOO_SMALL; if (UNLIKELY(zxc_le32(src) != ZXC_MAGIC_WORD)) return ZXC_ERROR_BAD_MAGIC; if (UNLIKELY(src[4] != ZXC_FILE_FORMAT_VERSION)) return ZXC_ERROR_BAD_VERSION; uint8_t temp[ZXC_FILE_HEADER_SIZE]; ZXC_MEMCPY(temp, src, ZXC_FILE_HEADER_SIZE); // Zero out CRC bytes (14-15) before hash check temp[14] = 0; temp[15] = 0; if (UNLIKELY(zxc_le16(src + 14) != zxc_hash16(temp))) return ZXC_ERROR_BAD_HEADER; if (out_block_size) { const uint8_t code = src[5]; size_t block_size; if (LIKELY(code >= ZXC_BLOCK_SIZE_MIN_LOG2 && code <= ZXC_BLOCK_SIZE_MAX_LOG2)) { // Exponent encoding: block_size = 2^code (4 KB - 2 MB) block_size = (size_t)1U << code; } else if (code == 64) { // Legacy: hardcoded 256 KB default block_size = 256 * 1024; } else { return ZXC_ERROR_BAD_BLOCK_SIZE; } *out_block_size = block_size; } // Flags are at offset 6 if (out_has_checksum) *out_has_checksum = (src[6] & ZXC_FILE_FLAG_HAS_CHECKSUM) ? 1 : 0; return ZXC_OK; } /* * @brief Serialises a block header (8 bytes) into @p dst. * * @param[out] dst Destination buffer (>= @ref ZXC_BLOCK_HEADER_SIZE bytes). * @param[in] dst_capacity Capacity of @p dst. * @param[in] bh Populated block header descriptor. * @return Number of bytes written (@ref ZXC_BLOCK_HEADER_SIZE) on success, * or a negative @ref zxc_error_t code. */ int zxc_write_block_header(uint8_t* RESTRICT dst, const size_t dst_capacity, const zxc_block_header_t* RESTRICT bh) { if (UNLIKELY(dst_capacity < ZXC_BLOCK_HEADER_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; dst[0] = bh->block_type; dst[1] = 0; // Flags not used currently dst[2] = 0; // Reserved zxc_store_le32(dst + 3, bh->comp_size); dst[7] = 0; // Zero before hashing dst[7] = zxc_hash8(dst); // Checksum at the end return ZXC_BLOCK_HEADER_SIZE; } /* * @brief Parses and validates a block header from @p src. * * Validates the 8-bit CRC embedded in the header. * * @param[in] src Source buffer (>= @ref ZXC_BLOCK_HEADER_SIZE bytes). * @param[in] src_size Size of @p src. * @param[out] bh Receives the decoded block header fields. * @return @ref ZXC_OK on success, or a negative @ref zxc_error_t code. */ int zxc_read_block_header(const uint8_t* RESTRICT src, const size_t src_size, zxc_block_header_t* RESTRICT bh) { if (UNLIKELY(src_size < ZXC_BLOCK_HEADER_SIZE)) return ZXC_ERROR_SRC_TOO_SMALL; uint8_t temp[ZXC_BLOCK_HEADER_SIZE]; ZXC_MEMCPY(temp, src, ZXC_BLOCK_HEADER_SIZE); temp[7] = 0; // Zero out checksum byte before hashing if (UNLIKELY(src[7] != zxc_hash8(temp))) return ZXC_ERROR_BAD_HEADER; bh->block_type = src[0]; bh->block_flags = 0; // Flags not used currently bh->reserved = src[2]; bh->comp_size = zxc_le32(src + 3); bh->header_crc = src[7]; return ZXC_OK; } /* * @brief Writes the 12-byte file footer (source size + global checksum). * * @param[out] dst Destination buffer (>= @ref ZXC_FILE_FOOTER_SIZE bytes). * @param[in] dst_capacity Capacity of @p dst. * @param[in] src_size Original uncompressed size in bytes. * @param[in] global_hash Accumulated global checksum value. * @param[in] checksum_enabled Non-zero to write the checksum; zero to zero-fill. * @return Number of bytes written (@ref ZXC_FILE_FOOTER_SIZE) on success, * or a negative @ref zxc_error_t code. */ int zxc_write_file_footer(uint8_t* RESTRICT dst, const size_t dst_capacity, const uint64_t src_size, const uint32_t global_hash, const int checksum_enabled) { if (UNLIKELY(dst_capacity < ZXC_FILE_FOOTER_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; zxc_store_le64(dst, src_size); if (checksum_enabled) { zxc_store_le32(dst + sizeof(uint64_t), global_hash); } else { ZXC_MEMSET(dst + sizeof(uint64_t), 0, sizeof(uint32_t)); } return ZXC_FILE_FOOTER_SIZE; } /* * @brief Serialises a NUM block header (16 bytes). * * @param[out] dst Destination buffer (>= @ref ZXC_NUM_HEADER_BINARY_SIZE bytes). * @param[in] rem Remaining capacity of @p dst. * @param[in] nh Populated NUM header descriptor. * @return Number of bytes written on success, or a negative @ref zxc_error_t code. */ int zxc_write_num_header(uint8_t* RESTRICT dst, const size_t rem, const zxc_num_header_t* RESTRICT nh) { if (UNLIKELY(rem < ZXC_NUM_HEADER_BINARY_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; zxc_store_le64(dst, nh->n_values); zxc_store_le16(dst + 8, nh->frame_size); zxc_store_le16(dst + 10, 0); zxc_store_le32(dst + 12, 0); return ZXC_NUM_HEADER_BINARY_SIZE; } /* * @brief Parses a NUM block header from @p src. * * @param[in] src Source buffer (>= @ref ZXC_NUM_HEADER_BINARY_SIZE bytes). * @param[in] src_size Size of @p src. * @param[out] nh Receives the decoded NUM header fields. * @return @ref ZXC_OK on success, or a negative @ref zxc_error_t code. */ int zxc_read_num_header(const uint8_t* RESTRICT src, const size_t src_size, zxc_num_header_t* RESTRICT nh) { if (UNLIKELY(src_size < ZXC_NUM_HEADER_BINARY_SIZE)) return ZXC_ERROR_SRC_TOO_SMALL; nh->n_values = zxc_le64(src); nh->frame_size = zxc_le16(src + 8); return ZXC_OK; } /* * @brief Serialises a GLO block header followed by its section descriptors. * * @param[out] dst Destination buffer. * @param[in] rem Remaining capacity of @p dst. * @param[in] gh Populated GLO header descriptor. * @param[in] desc Array of @ref ZXC_GLO_SECTIONS section descriptors. * @return Total bytes written on success, or a negative @ref zxc_error_t code. */ int zxc_write_glo_header_and_desc(uint8_t* RESTRICT dst, const size_t rem, const zxc_gnr_header_t* RESTRICT gh, const zxc_section_desc_t desc[ZXC_GLO_SECTIONS]) { const size_t needed = ZXC_GLO_HEADER_BINARY_SIZE + ZXC_GLO_SECTIONS * ZXC_SECTION_DESC_BINARY_SIZE; if (UNLIKELY(rem < needed)) return ZXC_ERROR_DST_TOO_SMALL; zxc_store_le32(dst, gh->n_sequences); zxc_store_le32(dst + 4, gh->n_literals); dst[8] = gh->enc_lit; dst[9] = gh->enc_litlen; dst[10] = gh->enc_mlen; dst[11] = gh->enc_off; zxc_store_le32(dst + 12, 0); uint8_t* p = dst + ZXC_GLO_HEADER_BINARY_SIZE; for (int i = 0; i < ZXC_GLO_SECTIONS; i++) { zxc_store_le64(p, desc[i].sizes); p += ZXC_SECTION_DESC_BINARY_SIZE; } return (int)needed; } /* * @brief Parses a GLO block header and its section descriptors from @p src. * * @param[in] src Source buffer. * @param[in] len Size of @p src. * @param[out] gh Receives the decoded GLO header. * @param[out] desc Receives @ref ZXC_GLO_SECTIONS decoded section descriptors. * @return @ref ZXC_OK on success, or a negative @ref zxc_error_t code. */ int zxc_read_glo_header_and_desc(const uint8_t* RESTRICT src, const size_t len, zxc_gnr_header_t* RESTRICT gh, zxc_section_desc_t desc[ZXC_GLO_SECTIONS]) { const size_t needed = ZXC_GLO_HEADER_BINARY_SIZE + ZXC_GLO_SECTIONS * ZXC_SECTION_DESC_BINARY_SIZE; if (UNLIKELY(len < needed)) return ZXC_ERROR_SRC_TOO_SMALL; gh->n_sequences = zxc_le32(src); gh->n_literals = zxc_le32(src + 4); gh->enc_lit = src[8]; gh->enc_litlen = src[9]; gh->enc_mlen = src[10]; gh->enc_off = src[11]; const uint8_t* p = src + ZXC_GLO_HEADER_BINARY_SIZE; for (int i = 0; i < ZXC_GLO_SECTIONS; i++) { desc[i].sizes = zxc_le64(p); p += ZXC_SECTION_DESC_BINARY_SIZE; } return ZXC_OK; } /* * @brief Serialises a GHI block header followed by its section descriptors. * * @param[out] dst Destination buffer. * @param[in] rem Remaining capacity of @p dst. * @param[in] gh Populated GHI header descriptor. * @param[in] desc Array of @ref ZXC_GHI_SECTIONS section descriptors. * @return Total bytes written on success, or a negative @ref zxc_error_t code. */ int zxc_write_ghi_header_and_desc(uint8_t* RESTRICT dst, const size_t rem, const zxc_gnr_header_t* RESTRICT gh, const zxc_section_desc_t desc[ZXC_GHI_SECTIONS]) { const size_t needed = ZXC_GHI_HEADER_BINARY_SIZE + ZXC_GHI_SECTIONS * ZXC_SECTION_DESC_BINARY_SIZE; if (UNLIKELY(rem < needed)) return ZXC_ERROR_DST_TOO_SMALL; zxc_store_le32(dst, gh->n_sequences); zxc_store_le32(dst + 4, gh->n_literals); dst[8] = gh->enc_lit; dst[9] = gh->enc_litlen; dst[10] = gh->enc_mlen; dst[11] = gh->enc_off; zxc_store_le32(dst + 12, 0); uint8_t* p = dst + ZXC_GHI_HEADER_BINARY_SIZE; for (int i = 0; i < ZXC_GHI_SECTIONS; i++) { zxc_store_le64(p, desc[i].sizes); p += ZXC_SECTION_DESC_BINARY_SIZE; } return (int)needed; } /* * @brief Parses a GHI block header and its section descriptors from @p src. * * @param[in] src Source buffer. * @param[in] len Size of @p src. * @param[out] gh Receives the decoded GHI header. * @param[out] desc Receives @ref ZXC_GHI_SECTIONS decoded section descriptors. * @return @ref ZXC_OK on success, or a negative @ref zxc_error_t code. */ int zxc_read_ghi_header_and_desc(const uint8_t* RESTRICT src, const size_t len, zxc_gnr_header_t* RESTRICT gh, zxc_section_desc_t desc[ZXC_GHI_SECTIONS]) { const size_t needed = ZXC_GHI_HEADER_BINARY_SIZE + ZXC_GHI_SECTIONS * ZXC_SECTION_DESC_BINARY_SIZE; if (UNLIKELY(len < needed)) return ZXC_ERROR_SRC_TOO_SMALL; gh->n_sequences = zxc_le32(src); gh->n_literals = zxc_le32(src + 4); gh->enc_lit = src[8]; gh->enc_litlen = src[9]; gh->enc_mlen = src[10]; gh->enc_off = src[11]; const uint8_t* p = src + ZXC_GHI_HEADER_BINARY_SIZE; for (int i = 0; i < ZXC_GHI_SECTIONS; i++) { desc[i].sizes = zxc_le64(p); p += ZXC_SECTION_DESC_BINARY_SIZE; } return ZXC_OK; } /* * ============================================================================ * BITPACKING UTILITIES * ============================================================================ */ /* * @brief Bit-packs an array of 32-bit values into a compact byte stream. * * Each value is masked to @p bits width and packed contiguously. * * @param[in] src Source array of 32-bit integers. * @param[in] count Number of values to pack. * @param[out] dst Destination byte buffer. * @param[in] dst_cap Capacity of @p dst. * @param[in] bits Number of bits per value (0-32). * @return Number of bytes written on success, or a negative @ref zxc_error_t code. */ int zxc_bitpack_stream_32(const uint32_t* RESTRICT src, const size_t count, uint8_t* RESTRICT dst, const size_t dst_cap, const uint8_t bits) { const size_t out_bytes = ((count * bits) + CHAR_BIT - 1) / CHAR_BIT; // +4 bytes: packing may write past out_bytes when the last value straddles a byte boundary. const size_t safe_bytes = out_bytes + sizeof(uint32_t); if (UNLIKELY(dst_cap < safe_bytes)) return ZXC_ERROR_DST_TOO_SMALL; size_t bit_pos = 0; ZXC_MEMSET(dst, 0, safe_bytes); // Create a mask for the input bits to prevent overflow // If bits is 32, the shift (1ULL << 32) is undefined behavior on 32-bit types, // but here we use uint64_t. (1ULL << 32) is fine on 64-bit. // However, if bits=64 (unlikely for a 32-bit packer), it would be an issue. // For 0 < bits <= 32: const uint64_t val_mask = (bits == sizeof(uint32_t) * CHAR_BIT) ? UINT32_MAX : ((1ULL << bits) - 1); for (size_t i = 0; i < count; i++) { // Mask the input value to ensure we don't write garbage const uint64_t v = ((uint64_t)src[i] & val_mask) << (bit_pos % CHAR_BIT); const size_t byte_idx = bit_pos / CHAR_BIT; dst[byte_idx] |= (uint8_t)v; if (bits + (bit_pos % CHAR_BIT) > 1 * CHAR_BIT) dst[byte_idx + 1] |= (uint8_t)(v >> (1 * CHAR_BIT)); if (bits + (bit_pos % CHAR_BIT) > 2 * CHAR_BIT) dst[byte_idx + 2] |= (uint8_t)(v >> (2 * CHAR_BIT)); if (bits + (bit_pos % CHAR_BIT) > 3 * CHAR_BIT) dst[byte_idx + 3] |= (uint8_t)(v >> (3 * CHAR_BIT)); if (bits + (bit_pos % CHAR_BIT) > 4 * CHAR_BIT) dst[byte_idx + 4] |= (uint8_t)(v >> (4 * CHAR_BIT)); bit_pos += bits; } return (int)out_bytes; } /* * ============================================================================ * COMPRESS BOUND CALCULATION * ============================================================================ */ /* * @brief Returns the maximum compressed size for a given input size. * * The result accounts for the file header, per-block headers, block * checksums, worst-case expansion, EOF block, seekable overhead (SEK * block), and the file footer. * * The block count is derived from @ref ZXC_BLOCK_SIZE_MIN (4 KB) to * guarantee the bound holds for all valid block sizes and seekable mode. * * @param[in] input_size Uncompressed input size in bytes. * @return Upper bound on compressed size, or 0 if @p input_size would overflow. */ uint64_t zxc_compress_bound(const size_t input_size) { // Guard UINT64_MAX / SIZE_MAX would overflow. if (UNLIKELY(input_size > (SIZE_MAX - (SIZE_MAX >> 8)))) return 0; uint64_t n = ((uint64_t)input_size + ZXC_BLOCK_SIZE_MIN - 1) / ZXC_BLOCK_SIZE_MIN; if (n == 0) n = 1; return ZXC_FILE_HEADER_SIZE + (n * (ZXC_BLOCK_HEADER_SIZE + ZXC_BLOCK_CHECKSUM_SIZE + 64)) + (uint64_t)input_size + ZXC_BLOCK_HEADER_SIZE + /* EOF block */ ZXC_BLOCK_HEADER_SIZE + /* SEK block header (seekable) */ (n * ZXC_SEEK_ENTRY_SIZE) + /* SEK entries: 4 bytes per block */ ZXC_FILE_FOOTER_SIZE; } /* * @brief Returns the maximum compressed size for a single block (no file framing). * * @param[in] input_size Uncompressed block size in bytes. * @return Upper bound on compressed block size, or 0 on overflow. */ uint64_t zxc_compress_block_bound(const size_t input_size) { if (UNLIKELY(input_size > (SIZE_MAX - (SIZE_MAX >> 8)))) return 0; // Block header + worst-case expansion (64B overhead) + checksum return (uint64_t)ZXC_BLOCK_HEADER_SIZE + (uint64_t)input_size + 64 + ZXC_BLOCK_CHECKSUM_SIZE; } /* * ============================================================================ * ERROR CODE UTILITIES * ============================================================================ */ /* * @brief Returns a human-readable string for the given error code. * * @param[in] code An error code from @ref zxc_error_t (or @ref ZXC_OK). * @return A static string such as @c "ZXC_OK" or @c "ZXC_ERROR_MEMORY". * Returns @c "ZXC_UNKNOWN_ERROR" for unrecognised codes. */ const char* zxc_error_name(const int code) { switch ((zxc_error_t)code) { case ZXC_OK: return "ZXC_OK"; case ZXC_ERROR_MEMORY: return "ZXC_ERROR_MEMORY"; case ZXC_ERROR_DST_TOO_SMALL: return "ZXC_ERROR_DST_TOO_SMALL"; case ZXC_ERROR_SRC_TOO_SMALL: return "ZXC_ERROR_SRC_TOO_SMALL"; case ZXC_ERROR_BAD_MAGIC: return "ZXC_ERROR_BAD_MAGIC"; case ZXC_ERROR_BAD_VERSION: return "ZXC_ERROR_BAD_VERSION"; case ZXC_ERROR_BAD_HEADER: return "ZXC_ERROR_BAD_HEADER"; case ZXC_ERROR_BAD_CHECKSUM: return "ZXC_ERROR_BAD_CHECKSUM"; case ZXC_ERROR_CORRUPT_DATA: return "ZXC_ERROR_CORRUPT_DATA"; case ZXC_ERROR_BAD_OFFSET: return "ZXC_ERROR_BAD_OFFSET"; case ZXC_ERROR_OVERFLOW: return "ZXC_ERROR_OVERFLOW"; case ZXC_ERROR_IO: return "ZXC_ERROR_IO"; case ZXC_ERROR_NULL_INPUT: return "ZXC_ERROR_NULL_INPUT"; case ZXC_ERROR_BAD_BLOCK_TYPE: return "ZXC_ERROR_BAD_BLOCK_TYPE"; case ZXC_ERROR_BAD_BLOCK_SIZE: return "ZXC_ERROR_BAD_BLOCK_SIZE"; default: return "ZXC_UNKNOWN_ERROR"; } } /* * ============================================================================ * LIBRARY INFORMATION * ============================================================================ */ /* * @brief Returns the minimum supported compression level. * * Returns the value of ZXC_LEVEL_FASTEST (currently 1). * This allows integrators to discover the level range at runtime without relying on * compile-time macros alone. */ int zxc_min_level(void) { return ZXC_LEVEL_FASTEST; } /* * @brief Returns the maximum supported compression level. * * Returns the value of ZXC_LEVEL_COMPACT (currently 5). */ int zxc_max_level(void) { return ZXC_LEVEL_COMPACT; } /* * @brief Returns the default compression level. * * Returns the value of ZXC_LEVEL_DEFAULT (currently 3). */ int zxc_default_level(void) { return ZXC_LEVEL_DEFAULT; } /* * @brief Returns the human-readable library version string. * * The returned pointer is a compile-time constant and must not be freed. * Example: "0.9.1". */ const char* zxc_version_string(void) { return ZXC_LIB_VERSION_STR; } zxc-0.10.0/src/lib/zxc_compress.c000066400000000000000000001725701517010557200166330ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_compress.c * @brief Block-level compression: LZ77 parsing, NUM / GLO / GHI / RAW encoding, * and the chunk-wrapper entry point. * * Compiled multiple times with different @c ZXC_FUNCTION_SUFFIX values to * produce AVX2, AVX-512, NEON, and scalar variants dispatched at runtime * by @ref zxc_dispatch.c. */ #include "../../include/zxc_error.h" #include "../../include/zxc_sans_io.h" #include "zxc_internal.h" /* * Function Multi-Versioning Support * If ZXC_FUNCTION_SUFFIX is defined (e.g. _avx2), rename the public entry point. */ #ifdef ZXC_FUNCTION_SUFFIX #define ZXC_CAT_IMPL(x, y) x##y #define ZXC_CAT(x, y) ZXC_CAT_IMPL(x, y) #define zxc_compress_chunk_wrapper ZXC_CAT(zxc_compress_chunk_wrapper, ZXC_FUNCTION_SUFFIX) #endif /** * @brief Computes a hash value for either a 4-byte or 5-byte sequence. * * @param[in] val The 64-bit integer sequence (e.g., 8 bytes read from input stream). * @param[in] use_hash5 Non-zero to use the 5-byte xorshift64* hash (Marsaglia/Vigna), zero for * 4-byte Marsaglia hash. * @return uint32_t A hash value suitable for indexing the match table. */ static ZXC_ALWAYS_INLINE uint32_t zxc_hash_func(const uint64_t val, const int use_hash5) { if (use_hash5) { const uint64_t v5 = val & 0xFFFFFFFFFFULL; return (uint32_t)((v5 * ZXC_LZ_HASH_PRIME2) >> (64 - ZXC_LZ_HASH_BITS)); } else { const uint64_t v4 = val ^ (val >> 15); return ((uint32_t)v4 * ZXC_LZ_HASH_PRIME1) >> (32 - ZXC_LZ_HASH_BITS); } } #if defined(ZXC_USE_AVX2) /** * @brief Reduces a 256-bit integer vector to a single scalar by finding the maximum unsigned 32-bit * integer element. * * This function performs a horizontal reduction across the 8 packed 32-bit unsigned integers * in the source vector to determine the maximum value. * * @param[in] v The 256-bit vector containing 8 unsigned 32-bit integers. * @return The maximum unsigned 32-bit integer found in the vector. */ // codeql[cpp/unused-static-function] : Used conditionally when ZXC_USE_AVX2 is defined static ZXC_ALWAYS_INLINE uint32_t zxc_mm256_reduce_max_epu32(__m256i v) { __m128i vlow = _mm256_castsi256_si128(v); // Extract the lower 128 bits __m128i vhigh = _mm256_extracti128_si256(v, 1); // Extract the upper 128 bits vlow = _mm_max_epu32(vlow, vhigh); // Element-wise max of lower and upper halves __m128i vshuf = _mm_shuffle_epi32(vlow, _MM_SHUFFLE(1, 0, 3, 2)); // Shuffle to swap pairs vlow = _mm_max_epu32(vlow, vshuf); // Max of original and swapped vshuf = _mm_shuffle_epi32(vlow, _MM_SHUFFLE(2, 3, 0, 1)); // Shuffle to bring remaining candidates vlow = _mm_max_epu32(vlow, vshuf); // Final max comparison return (uint32_t)_mm_cvtsi128_si32(vlow); // Extract the scalar result } #endif /** * @brief Writes a Prefix Varint encoded value to a buffer. * * This function encodes a 32-bit unsigned integer using Prefix Varint encoding * and writes it to the destination buffer. Unary prefix bits in the first * byte determine the total length (1-5 bytes), allowing for branchless or * predictable decoding. * * Format: * - 0xxxxxxx (1 byte) * - 10xxxxxx ... (2 bytes) * - 110xxxxx ... (3 bytes) * ... * * @param[out] dst Pointer to the destination buffer where the encoded value will be written. * @param[in] val The 32-bit unsigned integer value to encode. * @return The number of bytes written to the destination buffer. */ static ZXC_ALWAYS_INLINE size_t zxc_write_varint(uint8_t* RESTRICT dst, const uint32_t val) { // 1 byte: 0xxxxxxx (7 bits) = 2^7 = 128 if (LIKELY(val < (1 << 7))) { dst[0] = (uint8_t)val; return 1; } // 2 bytes: 10xxxxxx xxxxxxxx (14 bits) = 2^14 = 16384 if (LIKELY(val < (1 << 14))) { dst[0] = (uint8_t)(0x80 | (val & 0x3F)); dst[1] = (uint8_t)(val >> 6); return 2; } // 3 bytes: 110xxxxx xxxxxxxx xxxxxxxx (21 bits) = 2^21 = 2097152 // Max varint value is bounded by ZXC_BLOCK_SIZE_MAX (2MB = 2^21). // ZXC_VBYTE_ALLOC_LEN == 3 guarantees this is the last reachable path. dst[0] = (uint8_t)(0xC0 | (val & 0x1F)); dst[1] = (uint8_t)(val >> 5); dst[2] = (uint8_t)(val >> 13); return ZXC_VBYTE_ALLOC_LEN; } /** * @brief Structure representing a match found during compression. * * This structure holds information about a matching sequence found * in the input data during the compression process. * * @param ref Pointer to the reference data where the match was found. * @param len Length of the matching sequence in bytes. * @param backtrack Distance to backtrack from the current position to find the match. */ typedef struct { const uint8_t* ref; uint32_t len; uint32_t backtrack; } zxc_match_t; /** * @brief Finds the best matching sequence for LZ77 compression * * Uses a split hash table layout: * - hash_table[h] : uint32_t position + epoch (128 KB for 15-bit hash) * - hash_tags[h] : uint8_t tag for fast rejection (32 KB, L1-resident) * * @param[in] src Pointer to the start of the source buffer. * @param[in] ip Current input position pointer. * @param[in] iend Pointer to the end of the input buffer. * @param[in] mflimit Pointer to the match finding limit. * @param[in] anchor Pointer to the current anchor position. * @param[in,out] hash_table Pointer to the position table for match finding. * @param[in,out] hash_tags Pointer to the tag table for fast rejection. * @param[in,out] chain_table Pointer to the chain table for collision handling. * @param[in] epoch_mark Current epoch marker for hash table invalidation. * @param[in] p LZ77 parameters controlling search depth, lazy matching, and stepping. * @return zxc_match_t Structure containing the best match information * (reference pointer, length of the match, and backtrack distance). */ static ZXC_ALWAYS_INLINE zxc_match_t zxc_lz77_find_best_match( const uint8_t* src, const uint8_t* ip, const uint8_t* iend, const uint8_t* mflimit, const uint8_t* anchor, uint32_t* RESTRICT hash_table, uint8_t* RESTRICT hash_tags, uint16_t* RESTRICT chain_table, const uint32_t epoch_mark, const uint32_t offset_mask, const int level, const zxc_lz77_params_t p) { const int use_hash5 = (level >= 3); // Track the best match found so far. // ref is the pointer to the start of the match in the history buffer, // len is the match length, and backtrack is the distance from ip to ref. // Start with a sentinel length just below the minimum so any valid match will replace it. zxc_match_t best = (zxc_match_t){NULL, ZXC_LZ_MIN_MATCH_LEN - 1, 0}; // Load the 8-byte sequence at the current position. uint64_t cur_val8 = zxc_le64(ip); uint32_t cur_val = (uint32_t)cur_val8; uint32_t h = zxc_hash_func(cur_val8, use_hash5); // 8-bit tag: XOR fold of first 4 bytes for fast rejection const uint8_t cur_tag = (uint8_t)(cur_val ^ (cur_val >> 16)); // Current position in the input buffer expressed as a 32-bit index. const uint32_t cur_pos = (uint32_t)(ip - src); // Split table reads: tag table + position table const uint8_t stored_tag = hash_tags[h]; const uint32_t raw_head = hash_table[h]; // If the epoch in raw_head matches the current epoch_mark, extract the // stored position; otherwise treat this bucket as empty (index 0). uint32_t match_idx = ((raw_head & ~offset_mask) == epoch_mark) ? (raw_head & offset_mask) : 0; // Decide whether to skip the head entry of the hash chain. const int skip_head = (match_idx != 0) & (stored_tag != cur_tag); // If we should skip the head and level is low (<= 2), we drop the match entirely. const uint32_t drop_mask = (uint32_t)((skip_head & (level <= 2)) - 1); match_idx &= drop_mask; // Split table writes hash_table[h] = epoch_mark | cur_pos; hash_tags[h] = cur_tag; // Branchless chain table update const uint32_t dist = cur_pos - match_idx; const uint32_t valid_mask = -((int32_t)((match_idx != 0) & (dist < ZXC_LZ_WINDOW_SIZE))); chain_table[cur_pos] = (uint16_t)(dist & valid_mask); if (match_idx == 0) return best; int attempts = p.search_depth; // Optimization: If head tag doesn't match, advance immediately without loading the first // mismatch. if (skip_head) { const uint16_t delta = chain_table[match_idx]; const uint32_t next_idx = match_idx - delta; match_idx = (delta != 0) ? next_idx : 0; attempts--; } while (match_idx > 0 && attempts-- >= 0) { if (UNLIKELY(cur_pos - match_idx > ZXC_LZ_MAX_DIST)) break; const uint8_t* ref = src + match_idx; const uint32_t ref_val = zxc_le32(ref); const int tag_match = (ref_val == cur_val); // Simplified check: only tag match and next-byte match required const int should_compare = tag_match && (ref[best.len] == ip[best.len]); if (should_compare) { uint32_t mlen = sizeof(uint32_t); // We already know the first 4 bytes match // Fast path: Scalar 64-bit comparison for short matches (=< 64 bytes) // Most matches are short, so this avoids SIMD overhead for common cases const uint8_t* limit_8 = iend - sizeof(uint64_t); const uint8_t* scalar_limit = ip + mlen + 64; if (scalar_limit > limit_8) scalar_limit = limit_8; while (ip + mlen < scalar_limit) { uint64_t diff = zxc_le64(ip + mlen) ^ zxc_le64(ref + mlen); if (diff == 0) mlen += sizeof(uint64_t); else { mlen += (zxc_ctz64(diff) >> 3); goto _match_len_done; } } // Long match path: Use SIMD for matches exceeding 64 bytes #if defined(ZXC_USE_AVX512) const uint8_t* limit_64 = iend - 64; while (ip + mlen < limit_64) { const __m512i v_src = _mm512_loadu_si512((const void*)(ip + mlen)); const __m512i v_ref = _mm512_loadu_si512((const void*)(ref + mlen)); const __mmask64 mask = _mm512_cmpeq_epi8_mask(v_src, v_ref); if (mask == 0xFFFFFFFFFFFFFFFF) mlen += 64; else { mlen += (uint32_t)zxc_ctz64(~mask); goto _match_len_done; } } #elif defined(ZXC_USE_AVX2) const uint8_t* limit_32 = iend - 32; while (ip + mlen < limit_32) { const __m256i v_src = _mm256_loadu_si256((const __m256i*)(ip + mlen)); const __m256i v_ref = _mm256_loadu_si256((const __m256i*)(ref + mlen)); const __m256i v_cmp = _mm256_cmpeq_epi8(v_src, v_ref); const uint32_t mask = (uint32_t)_mm256_movemask_epi8(v_cmp); if (mask == 0xFFFFFFFF) mlen += 32; else { mlen += zxc_ctz32(~mask); goto _match_len_done; } } #elif defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) const uint8_t* limit_16 = iend - 16; while (ip + mlen < limit_16) { const uint8x16_t v_src = vld1q_u8(ip + mlen); const uint8x16_t v_ref = vld1q_u8(ref + mlen); const uint8x16_t v_cmp = vceqq_u8(v_src, v_ref); #if defined(ZXC_USE_NEON64) if (vminvq_u8(v_cmp) == 0xFF) mlen += 16; else { uint8x16_t v_diff = vmvnq_u8(v_cmp); uint64_t lo = vgetq_lane_u64(vreinterpretq_u64_u8(v_diff), 0); if (lo != 0) mlen += (zxc_ctz64(lo) >> 3); else mlen += 8 + (zxc_ctz64(vgetq_lane_u64(vreinterpretq_u64_u8(v_diff), 1)) >> 3); goto _match_len_done; } #else uint8x8_t p1 = vpmin_u8(vget_low_u8(v_cmp), vget_high_u8(v_cmp)); uint8x8_t p2 = vpmin_u8(p1, p1); uint8x8_t p3 = vpmin_u8(p2, p2); uint8x8_t p4 = vpmin_u8(p3, p3); uint8_t min_val = vget_lane_u8(p4, 0); if (min_val == 0xFF) mlen += 16; else { uint8x16_t v_diff = vmvnq_u8(v_cmp); uint64_t lo = (uint64_t)vgetq_lane_u32(vreinterpretq_u32_u8(v_diff), 0) | ((uint64_t)vgetq_lane_u32(vreinterpretq_u32_u8(v_diff), 1) << 32); if (lo != 0) mlen += (zxc_ctz64(lo) >> 3); else mlen += 8 + (zxc_ctz64((uint64_t)vgetq_lane_u32(vreinterpretq_u32_u8(v_diff), 2) | ((uint64_t)vgetq_lane_u32(vreinterpretq_u32_u8(v_diff), 3) << 32)) >> 3); goto _match_len_done; } #endif } #endif while (ip + mlen < limit_8) { const uint64_t diff = zxc_le64(ip + mlen) ^ zxc_le64(ref + mlen); if (diff == 0) mlen += sizeof(uint64_t); else { mlen += (zxc_ctz64(diff) >> 3); goto _match_len_done; } } while (ip + mlen < iend && ref[mlen] == ip[mlen]) mlen++; _match_len_done:; const int better = (mlen > best.len); best.len = better ? mlen : best.len; best.ref = better ? ref : best.ref; if (UNLIKELY(best.len >= (uint32_t)p.sufficient_len || ip + best.len >= iend)) break; } const uint16_t delta = chain_table[match_idx]; const uint32_t next_idx = match_idx - delta; ZXC_PREFETCH_READ(src + next_idx); match_idx = (delta != 0) ? next_idx : 0; } if (best.ref) { // Backtrack to extend match backwards const uint8_t* b_ip = ip; const uint8_t* b_ref = best.ref; while (b_ip > anchor && b_ref > src && b_ip[-1] == b_ref[-1]) { b_ip--; b_ref--; best.len++; best.backtrack++; } best.ref = b_ref; } if (p.use_lazy && best.ref && best.len < (uint32_t)p.lazy_len_threshold && ip + 1 < mflimit) { // --- Lazy evaluation at ip+1 --- const uint64_t next_val8 = zxc_le64(ip + 1); const uint32_t next_val = (uint32_t)next_val8; const uint32_t h2 = zxc_hash_func(next_val8, use_hash5); const uint8_t next_stored_tag = hash_tags[h2]; const uint32_t next_head = hash_table[h2]; uint32_t next_idx = (next_head & ~offset_mask) == epoch_mark ? (next_head & offset_mask) : 0; const uint8_t next_tag = (uint8_t)(next_val ^ (next_val >> 16)); const int skip_lazy_head = (next_idx > 0 && next_stored_tag != next_tag); uint32_t max_lazy2 = 0; int lazy_att = p.lazy_attempts; int is_lazy_first = 1; while (next_idx > 0 && lazy_att-- > 0) { if (UNLIKELY((uint32_t)(ip + 1 - src) - next_idx > ZXC_LZ_MAX_DIST)) break; const uint8_t* ref2 = src + next_idx; if ((!is_lazy_first || !skip_lazy_head) && zxc_le32(ref2) == next_val) { uint32_t l2 = sizeof(uint32_t); const uint8_t* limit = iend - sizeof(uint64_t); while (ip + 1 + l2 < limit) { const uint64_t v1 = zxc_le64(ip + 1 + l2); const uint64_t v2 = zxc_le64(ref2 + l2); if (v1 != v2) { l2 += (uint32_t)(zxc_ctz64(v1 ^ v2) >> 3); goto lazy2_done; } l2 += sizeof(uint64_t); } while (ip + 1 + l2 < iend && ref2[l2] == ip[1 + l2]) l2++; lazy2_done: max_lazy2 = l2 > max_lazy2 ? l2 : max_lazy2; } const uint16_t delta = chain_table[next_idx]; if (UNLIKELY(delta == 0)) break; next_idx -= delta; is_lazy_first = 0; } // --- Lazy evaluation at ip+2 (computed in parallel, no dependency on lazy 1) --- uint32_t max_lazy3 = 0; if (level >= 4 && ip + 2 < mflimit) { const uint64_t val3_8 = zxc_le64(ip + 2); const uint32_t val3 = (uint32_t)val3_8; const uint32_t h3 = zxc_hash_func(val3_8, use_hash5); const uint8_t tag3 = hash_tags[h3]; const uint32_t head3 = hash_table[h3]; uint32_t idx3 = (head3 & ~offset_mask) == epoch_mark ? (head3 & offset_mask) : 0; const uint8_t cur_tag3 = (uint8_t)(val3 ^ (val3 >> 16)); const int skip_head3 = (idx3 > 0 && tag3 != cur_tag3); int is_first3 = 1; lazy_att = p.lazy_attempts; while (idx3 > 0 && lazy_att-- > 0) { if (UNLIKELY((uint32_t)(ip + 2 - src) - idx3 > ZXC_LZ_MAX_DIST)) break; const uint8_t* ref3 = src + idx3; if ((!is_first3 || !skip_head3) && zxc_le32(ref3) == val3) { uint32_t l3 = sizeof(uint32_t); const uint8_t* limit = iend - sizeof(uint64_t); while (ip + 2 + l3 < limit) { const uint64_t v1 = zxc_le64(ip + 2 + l3); const uint64_t v2 = zxc_le64(ref3 + l3); if (v1 != v2) { l3 += (uint32_t)(zxc_ctz64(v1 ^ v2) >> 3); goto lazy3_done; } l3 += sizeof(uint64_t); } while (ip + 2 + l3 < iend && ref3[l3] == ip[2 + l3]) l3++; lazy3_done: max_lazy3 = l3 > max_lazy3 ? l3 : max_lazy3; } const uint16_t delta = chain_table[idx3]; if (UNLIKELY(delta == 0)) break; idx3 -= delta; is_first3 = 0; } } // Single decision: invalidate if either lazy position found a better match if (max_lazy2 > best.len + 1 || max_lazy3 > best.len + 2) best.ref = NULL; } return best; } /** * @brief Encodes a block of numerical data using delta encoding and * bit-packing. * * This function compresses a source buffer of 32-bit integers. It processes the * data in frames defined by `ZXC_NUM_FRAME_SIZE`. * * **Algorithm Steps:** * 1. **Delta Encoding:** Calculates `delta = value[i] - value[i-1]`. This * reduces the magnitude of numbers if the data is sequential or correlated. * - **SIMD Optimization:** Uses AVX2 (`_mm256_sub_epi32`) to compute deltas * for 8 integers at once. * 2. **ZigZag Encoding:** Maps signed deltas to unsigned integers (`(n << 1) ^ * (n >> 31)`). This ensures small negative numbers become small positive * numbers (e.g., -1 -> 1, 1 -> 2). * 3. **Bit Width Calculation:** Finds the maximum value in the frame to * determine the minimum number of bits (`b`) needed to represent all deltas. * 4. **Bit Packing:** Packs the ZigZag-encoded deltas into a compact bitstream * using `b` bits per value. * * @param[in] src Pointer to the source buffer containing raw 32-bit integer data. * @param[in] src_sz Size of the source buffer in bytes. Must be a multiple of 4 * and non-zero. * @param[out] dst Pointer to the destination buffer where compressed data will be * written. * @param[in] dst_cap Capacity of the destination buffer in bytes. * @param[out] out_sz Pointer to a variable where the total size of the compressed * output will be stored. * * @return ZXC_OK on success, or a negative zxc_error_t code (e.g., ZXC_ERROR_DST_TOO_SMALL) if an * error occurs (e.g., invalid input size, destination buffer too small). */ static int zxc_encode_block_num(const zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, size_t dst_cap, size_t* RESTRICT out_sz) { if (UNLIKELY(src_sz % sizeof(uint32_t) != 0 || src_sz == 0 || dst_cap < ZXC_BLOCK_HEADER_SIZE + ZXC_NUM_HEADER_BINARY_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; const size_t count = src_sz / sizeof(uint32_t); zxc_block_header_t bh = {.block_type = ZXC_BLOCK_NUM}; uint8_t* p_curr = dst + ZXC_BLOCK_HEADER_SIZE; size_t rem = dst_cap - ZXC_BLOCK_HEADER_SIZE; const zxc_num_header_t nh = {.n_values = count, .frame_size = ZXC_NUM_FRAME_SIZE}; const int hs = zxc_write_num_header(p_curr, rem, &nh); if (UNLIKELY(hs < 0)) return hs; p_curr += hs; rem -= hs; uint32_t deltas[ZXC_NUM_FRAME_SIZE]; const uint8_t* in_ptr = src; uint32_t prev = 0; for (size_t i = 0; i < count; i += ZXC_NUM_FRAME_SIZE) { const size_t frames = (count - i < ZXC_NUM_FRAME_SIZE) ? (count - i) : ZXC_NUM_FRAME_SIZE; uint32_t max_d = 0; const uint32_t base = prev; size_t j = 0; #if defined(ZXC_USE_AVX512) if (frames >= 16) { __m512i v_max_accum = _mm512_setzero_si512(); // Initialize max accumulator to 0 for (; j < (frames & ~15); j += 16) { if (UNLIKELY(i == 0 && j == 0)) goto _scalar; // Load 16 consecutive integers const __m512i vc = _mm512_loadu_si512((const void*)(in_ptr + j * 4)); // Load 16 integers offset by -1 to get previous values const __m512i vp = _mm512_loadu_si512((const void*)(in_ptr + j * 4 - 4)); const __m512i diff = _mm512_sub_epi32(vc, vp); // Compute deltas: curr - prev // ZigZag encode: (diff << 1) ^ (diff >> 31) const __m512i zigzag = _mm512_xor_si512(_mm512_slli_epi32(diff, 1), _mm512_srai_epi32(diff, 31)); _mm512_storeu_si512((void*)&deltas[j], zigzag); // Store results v_max_accum = _mm512_max_epu32(v_max_accum, zigzag); // Update max value seen so far } max_d = _mm512_reduce_max_epu32(v_max_accum); // Horizontal max reduction if (j > 0) prev = zxc_le32(in_ptr + (j - 1) * 4); } #elif defined(ZXC_USE_AVX2) if (frames >= 8) { __m256i v_max_accum = _mm256_setzero_si256(); // Initialize max accumulator to 0 for (; j < (frames & ~7); j += 8) { if (UNLIKELY(i == 0 && j == 0)) goto _scalar; // Load 8 consecutive integers const __m256i vc = _mm256_loadu_si256((const __m256i*)(in_ptr + j * 4)); // Load 8 integers offset by -1 const __m256i vp = _mm256_loadu_si256((const __m256i*)(in_ptr + j * 4 - 4)); const __m256i diff = _mm256_sub_epi32(vc, vp); // Compute deltas // ZigZag encode: (diff << 1) ^ (diff >> 31) const __m256i zigzag = _mm256_xor_si256(_mm256_slli_epi32(diff, 1), _mm256_srai_epi32(diff, 31)); _mm256_storeu_si256((__m256i*)&deltas[j], zigzag); // Store results v_max_accum = _mm256_max_epu32(v_max_accum, zigzag); // Update max accumulator } max_d = zxc_mm256_reduce_max_epu32(v_max_accum); // Horizontal max reduction if (j > 0) { prev = zxc_le32(in_ptr + (j - 1) * 4); } } #elif defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) // NEON processes 128-bit vectors (4 uint32 integers) if (frames >= 4) { uint32x4_t v_max_accum = vdupq_n_u32(0); // Initialize vector with zeros for (; j < (frames & ~3); j += 4) { if (UNLIKELY(i == 0 && j == 0)) goto _scalar; // Load 4 32-bit integers const uint32x4_t vc = vld1q_u32((const uint32_t*)(in_ptr + j * 4)); const uint32x4_t vp = vld1q_u32((const uint32_t*)(in_ptr + j * 4 - 4)); const uint32x4_t diff = vsubq_u32(vc, vp); // Calc deltas // ZigZag encode: (diff << 1) ^ (diff >> 31) const uint32x4_t z1 = vshlq_n_u32(diff, 1); // Arithmetic shift right to duplicate sign bit const uint32x4_t z2 = vreinterpretq_u32_s32(vshrq_n_s32(vreinterpretq_s32_u32(diff), 31)); const uint32x4_t zigzag = veorq_u32(z1, z2); vst1q_u32(&deltas[j], zigzag); // Store results v_max_accum = vmaxq_u32(v_max_accum, zigzag); // Update max accumulator } #if defined(ZXC_USE_NEON64) max_d = vmaxvq_u32(v_max_accum); // Reduce vector to single max value (AArch64) #else // NEON 32-bit (ARMv7) fallback for horizontal max using standard shifts // Reduce 4 elements -> 2 uint32x4_t v_swap = vextq_u32(v_max_accum, v_max_accum, 2); // Swap low/high 64-bit halves uint32x4_t v_max2 = vmaxq_u32(v_max_accum, v_swap); // Reduce 2 -> 1 v_swap = vextq_u32(v_max2, v_max2, 1); // Shift by 32 bits uint32x4_t v_max1 = vmaxq_u32(v_max2, v_swap); max_d = vgetq_lane_u32(v_max1, 0); #endif if (j > 0) prev = zxc_le32(in_ptr + (j - 1) * sizeof(uint32_t)); } #endif #if defined(ZXC_USE_AVX2) || defined(ZXC_USE_AVX512) || defined(ZXC_USE_NEON64) || \ defined(ZXC_USE_NEON32) _scalar: #endif for (; j < frames; j++) { const uint32_t v = zxc_le32(in_ptr + j * sizeof(uint32_t)); const uint32_t diff = zxc_zigzag_encode((int32_t)(v - prev)); deltas[j] = diff; if (diff > max_d) max_d = diff; prev = v; } in_ptr += frames * sizeof(uint32_t); const uint8_t bits = zxc_highbit32(max_d); const size_t packed = ((frames * bits) + CHAR_BIT - 1) / CHAR_BIT; if (UNLIKELY(rem < ZXC_NUM_CHUNK_HEADER_SIZE + packed + sizeof(uint32_t))) return ZXC_ERROR_DST_TOO_SMALL; zxc_store_le16(p_curr, (uint16_t)frames); zxc_store_le16(p_curr + 2, bits); zxc_store_le64(p_curr + 4, (uint64_t)base); zxc_store_le32(p_curr + 12, (uint32_t)packed); p_curr += ZXC_NUM_CHUNK_HEADER_SIZE; rem -= ZXC_NUM_CHUNK_HEADER_SIZE; const int pb = zxc_bitpack_stream_32(deltas, frames, p_curr, rem, bits); if (UNLIKELY(pb < 0)) return pb; p_curr += pb; rem -= pb; } bh.comp_size = (uint32_t)(p_curr - (dst + ZXC_BLOCK_HEADER_SIZE)); const int hw = zxc_write_block_header(dst, dst_cap, &bh); if (UNLIKELY(hw < 0)) return hw; // Checksum will be appended by the wrapper *out_sz = ZXC_BLOCK_HEADER_SIZE + bh.comp_size; return ZXC_OK; } /** * @brief Encodes a data block using the General (GLO) compression format. * * This function implements the core LZ77 compression logic. It dynamically * adjusts compression parameters (search depth, lazy matching strategy, and * step skipping) based on the compression level configured in the context. * * **LZ77 Implementation Details:** * 1. **Hash Chain:** Uses a hash table (`ctx->hash_table`) to find potential * match positions. Collisions are handled via a `chain_table`, allowing us to * search deeper into the history for a better match. * 2. **Lazy Matching:** If a match is found, we check the *next* byte to see if * it produces a longer match. If so, we output a literal and take the better * match. This is enabled for levels >= 3. * 3. **Step Skipping:** For lower levels (1-3), we skip bytes when updating the * hash table to increase speed (`step > 1`). For levels 4+, we process every * byte to maximize compression ratio. * 4. **SIMD Match Finding:** Uses AVX2/AVX512/NEON to compare 32/64 bytes at a * time during match length calculation, significantly speeding up long match * verification. * 5. **RLE Detection:** Analyzes literals to see if Run-Length Encoding would * be beneficial (saving > 10% space). * * The encoding process consists of: * 1. **LZ77 Parsing**: The function iterates through the source data, * maintaining a hash chain to find repeated patterns (matches). It supports * "Lazy Matching" for higher compression levels to optimize match selection. * 2. **Sequence Storage**: Matches are converted into sequences consisting of * literal lengths, match lengths, and offsets. * 3. **Bitpacking & Serialization**: The sequences are analyzed to determine * optimal bit-widths. The function then writes the block header, encodes * literals (using Raw or RLE encoding), and bit-packs the sequence streams into * the destination buffer. * * @param[in,out] ctx Pointer to the compression context containing hash tables * and configuration. * @param[in] src Pointer to the input source data. * @param[in] src_sz Size of the input data in bytes. * @param[out] dst Pointer to the destination buffer where compressed data will * be written. * @param[in] dst_cap Maximum capacity of the destination buffer. * @param[out] out_sz [Out] Pointer to a variable that will receive the total size * of the compressed output. * * @return ZXC_OK on success, or a negative zxc_error_t code (e.g., ZXC_ERROR_DST_TOO_SMALL) if an * error occurs (e.g., buffer overflow). */ static int zxc_encode_block_glo(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, size_t dst_cap, size_t* RESTRICT out_sz) { const int level = ctx->compression_level; const zxc_lz77_params_t lzp = zxc_get_lz77_params(level); ctx->epoch++; if (UNLIKELY(ctx->epoch >= ctx->max_epoch)) { ZXC_MEMSET(ctx->hash_table, 0, ZXC_LZ_HASH_SIZE * sizeof(uint32_t)); ZXC_MEMSET(ctx->hash_tags, 0, ZXC_LZ_HASH_SIZE * sizeof(uint8_t)); ctx->epoch = 1; } const uint32_t offset_bits = ctx->offset_bits; const uint32_t offset_mask = ctx->offset_mask; const uint32_t epoch_mark = ctx->epoch << offset_bits; const uint8_t *ip = src, *iend = src + src_sz, *anchor = ip, *mflimit = iend - 12; uint32_t* const hash_table = ctx->hash_table; uint8_t* const hash_tags = ctx->hash_tags; uint16_t* const chain_table = ctx->chain_table; uint8_t* const literals = ctx->literals; uint8_t* const buf_tokens = ctx->buf_tokens; uint16_t* const buf_offsets = ctx->buf_offsets; uint8_t* const buf_extras = ctx->buf_extras; uint32_t seq_c = 0; size_t lit_c = 0; size_t extras_sz = 0; uint16_t max_offset = 0; // Track max offset for 1-byte/2-byte mode decision while (LIKELY(ip < mflimit)) { const size_t dist = (size_t)(ip - anchor); size_t step = lzp.step_base + (dist >> lzp.step_shift); if (UNLIKELY(ip + step >= mflimit)) step = 1; const zxc_match_t m = zxc_lz77_find_best_match(src, ip, iend, mflimit, anchor, hash_table, hash_tags, chain_table, epoch_mark, offset_mask, level, lzp); if (m.ref) { ip -= m.backtrack; const uint32_t ll = (uint32_t)(ip - anchor); const uint32_t ml = (uint32_t)(m.len - ZXC_LZ_MIN_MATCH_LEN); const uint32_t off = (uint32_t)(ip - m.ref); if (ll > 0) { if (LIKELY(anchor + ZXC_PAD_SIZE <= iend)) { zxc_copy32(literals + lit_c, anchor); if (UNLIKELY(ll > ZXC_PAD_SIZE)) { ZXC_MEMCPY(literals + lit_c + ZXC_PAD_SIZE, anchor + ZXC_PAD_SIZE, ll - ZXC_PAD_SIZE); } } else { ZXC_MEMCPY(literals + lit_c, anchor, ll); } lit_c += ll; } const uint8_t ll_code = (ll >= ZXC_TOKEN_LL_MASK) ? ZXC_TOKEN_LL_MASK : (uint8_t)ll; const uint8_t ml_code = (ml >= ZXC_TOKEN_ML_MASK) ? ZXC_TOKEN_ML_MASK : (uint8_t)ml; buf_tokens[seq_c] = (ll_code << ZXC_TOKEN_LIT_BITS) | ml_code; buf_offsets[seq_c] = (uint16_t)(off - ZXC_LZ_OFFSET_BIAS); if ((off - ZXC_LZ_OFFSET_BIAS) > max_offset) max_offset = (uint16_t)(off - ZXC_LZ_OFFSET_BIAS); if (ll >= ZXC_TOKEN_LL_MASK) extras_sz += zxc_write_varint(buf_extras + extras_sz, ll - ZXC_TOKEN_LL_MASK); if (ml >= ZXC_TOKEN_ML_MASK) extras_sz += zxc_write_varint(buf_extras + extras_sz, ml - ZXC_TOKEN_ML_MASK); seq_c++; if (m.len > 2 && level > 4) { const uint8_t* match_end = ip + m.len; if (match_end < iend - 7) { const uint32_t pos_u = (uint32_t)((match_end - 2) - src); const uint64_t val_u8 = zxc_le64(match_end - 2); const uint32_t val_u = (uint32_t)val_u8; const uint32_t h_u = zxc_hash_func(val_u8, 1); // Only for level > 4, uses hash5 const uint32_t prev_head = hash_table[h_u]; const uint32_t prev_idx = (prev_head & ~offset_mask) == epoch_mark ? (prev_head & offset_mask) : 0; hash_table[h_u] = epoch_mark | pos_u; hash_tags[h_u] = (uint8_t)(val_u ^ (val_u >> 16)); chain_table[pos_u] = (prev_idx > 0 && (pos_u - prev_idx) < ZXC_LZ_WINDOW_SIZE) ? (uint16_t)(pos_u - prev_idx) : 0; } } ip += m.len; anchor = ip; } else { ip += step; } } const size_t last_lits = iend - anchor; if (last_lits > 0) { ZXC_MEMCPY(literals + lit_c, anchor, last_lits); lit_c += last_lits; } // --- RLE ANALYSIS --- size_t rle_size = 0; int enc_lit = ZXC_SECTION_ENCODING_RAW; if (lit_c > 0) { const uint8_t* p = literals; const uint8_t* const p_end = literals + lit_c; const uint8_t* const p_end_4 = p_end - 3; // Safe limit for 4-byte lookahead while (LIKELY(p < p_end)) { const uint8_t b = *p; const uint8_t* run_start = p++; // Fast run counting with early SIMD exit #if defined(ZXC_USE_AVX512) const __m512i vb = _mm512_set1_epi8((char)b); while (p <= p_end - 64) { const __m512i v = _mm512_loadu_si512((const void*)p); const __mmask64 mask = _mm512_cmpeq_epi8_mask(v, vb); if (mask != 0xFFFFFFFFFFFFFFFFULL) { p += (size_t)zxc_ctz64(~mask); goto _run_done; } p += 64; } #elif defined(ZXC_USE_AVX2) const __m256i vb = _mm256_set1_epi8((char)b); while (p <= p_end - 32) { const __m256i v = _mm256_loadu_si256((const __m256i*)p); const uint32_t mask = (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, vb)); if (mask != 0xFFFFFFFF) { p += zxc_ctz32(~mask); goto _run_done; } p += 32; } #elif defined(ZXC_USE_NEON64) const uint8x16_t vb = vdupq_n_u8(b); while (p <= p_end - 16) { const uint8x16_t v = vld1q_u8(p); const uint8x16_t eq = vceqq_u8(v, vb); if (vminvq_u8(eq) == 0xFF) { p += 16; } else { const uint8x16_t not_eq = vmvnq_u8(eq); const uint64_t lo = vgetq_lane_u64(vreinterpretq_u64_u8(not_eq), 0); if (lo != 0) { p += (zxc_ctz64(lo) >> 3); } else { const uint64_t hi = vgetq_lane_u64(vreinterpretq_u64_u8(not_eq), 1); p += 8 + (zxc_ctz64(hi) >> 3); } goto _run_done; } } #elif defined(ZXC_USE_NEON32) uint8x16_t vb = vdupq_n_u8(b); while (p <= p_end - 16) { uint8x16_t v = vld1q_u8(p); uint8x16_t eq = vceqq_u8(v, vb); uint8x16_t not_eq = vmvnq_u8(eq); // 32-bit ARM NEON doesn't always support vgetq_lane_u64 / vreinterpretq_u64_u8 so // we treat the 128-bit vector as 4 x 32-bit lanes */ const uint32x4_t neq32 = vreinterpretq_u32_u8(not_eq); const uint32_t l0 = vgetq_lane_u32(neq32, 0); const uint32_t l1 = vgetq_lane_u32(neq32, 1); const uint64_t lo = ((uint64_t)l1 << 32) | l0; if (lo != 0) { p += (size_t)(zxc_ctz64(lo) >> 3); goto _run_done; } const uint32_t h0 = vgetq_lane_u32(neq32, 2); const uint32_t h1 = vgetq_lane_u32(neq32, 3); const uint64_t hi = ((uint64_t)h1 << 32) | h0; if (hi != 0) { p += 8 + (zxc_ctz64(hi) >> 3); goto _run_done; } p += 16; } #endif while (p < p_end && *p == b) p++; #if defined(ZXC_USE_AVX512) || defined(ZXC_USE_AVX2) || defined(ZXC_USE_NEON64) || \ defined(ZXC_USE_NEON32) _run_done:; #endif const size_t run = (size_t)(p - run_start); if (run >= 4) { // RLE run: 2 bytes per 131 values, then remainder // Branchless: full_chunks * 2 + remainder handling const size_t full_chunks = run / 131; const size_t rem = run - full_chunks * 131; // Avoid modulo rle_size += full_chunks * 2; // Remainder: if >= 4 -> 2 bytes (RLE), else 1 + rem (literal) if (rem >= 4) rle_size += 2; else if (rem > 0) rle_size += 1 + rem; } else { // Literal run: scan ahead with fast SIMD lookahead const uint8_t* lit_start = run_start; #if defined(ZXC_USE_AVX512) while (p <= p_end_4 - 64) { const __m512i v0 = _mm512_loadu_si512((const void*)p); const __m512i v1 = _mm512_loadu_si512((const void*)(p + 1)); const __m512i v2 = _mm512_loadu_si512((const void*)(p + 2)); const __m512i v3 = _mm512_loadu_si512((const void*)(p + 3)); const __mmask64 mask = _mm512_cmpeq_epi8_mask(v0, v1) & _mm512_cmpeq_epi8_mask(v1, v2) & _mm512_cmpeq_epi8_mask(v2, v3); if (mask != 0) { p += (size_t)zxc_ctz64(mask); goto _lit_done; } p += 64; } #elif defined(ZXC_USE_AVX2) while (p <= p_end_4 - 32) { __m256i v0 = _mm256_loadu_si256((const __m256i*)p); __m256i v1 = _mm256_loadu_si256((const __m256i*)(p + 1)); __m256i v2 = _mm256_loadu_si256((const __m256i*)(p + 2)); __m256i v3 = _mm256_loadu_si256((const __m256i*)(p + 3)); __m256i vend = _mm256_and_si256( _mm256_cmpeq_epi8(v0, v1), _mm256_and_si256(_mm256_cmpeq_epi8(v1, v2), _mm256_cmpeq_epi8(v2, v3))); uint32_t mask = (uint32_t)_mm256_movemask_epi8(vend); if (mask != 0) { p += zxc_ctz32(mask); goto _lit_done; } p += 32; } #elif defined(ZXC_USE_NEON64) while (p <= p_end_4 - 16) { uint8x16_t v0 = vld1q_u8(p); uint8x16_t v1 = vld1q_u8(p + 1); uint8x16_t v2 = vld1q_u8(p + 2); uint8x16_t v3 = vld1q_u8(p + 3); uint8x16_t eq = vandq_u8(vceqq_u8(v0, v1), vandq_u8(vceqq_u8(v1, v2), vceqq_u8(v2, v3))); if (vmaxvq_u8(eq) == 0) { p += 16; } else { uint64_t lo = vgetq_lane_u64(vreinterpretq_u64_u8(eq), 0); if (lo != 0) { p += (zxc_ctz64(lo) >> 3); } else { uint64_t hi = vgetq_lane_u64(vreinterpretq_u64_u8(eq), 1); p += 8 + (zxc_ctz64(hi) >> 3); } goto _lit_done; } } #elif defined(ZXC_USE_NEON32) while (p <= p_end_4 - 16) { uint8x16_t v0 = vld1q_u8(p); uint8x16_t v1 = vld1q_u8(p + 1); uint8x16_t v2 = vld1q_u8(p + 2); uint8x16_t v3 = vld1q_u8(p + 3); uint8x16_t eq = vandq_u8(vceqq_u8(v0, v1), vandq_u8(vceqq_u8(v1, v2), vceqq_u8(v2, v3))); uint32x4_t eq32 = vreinterpretq_u32_u8(eq); uint32_t l0 = vgetq_lane_u32(eq32, 0); uint32_t l1 = vgetq_lane_u32(eq32, 1); uint64_t lo = ((uint64_t)l1 << 32) | l0; if (lo != 0) { p += (zxc_ctz64(lo) >> 3); goto _lit_done; } uint32_t h0 = vgetq_lane_u32(eq32, 2); uint32_t h1 = vgetq_lane_u32(eq32, 3); uint64_t hi = ((uint64_t)h1 << 32) | h0; if (hi != 0) { p += 8 + (zxc_ctz64(hi) >> 3); goto _lit_done; } p += 16; } #endif while (p < p_end_4) { // Check for RLE opportunity (4 identical bytes) if (UNLIKELY(p[0] == p[1] && p[1] == p[2] && p[2] == p[3])) break; p++; } // Handle remaining bytes near end while (p < p_end) { if (UNLIKELY(p + 3 < p_end && p[0] == p[1] && p[1] == p[2] && p[2] == p[3])) break; p++; } #if defined(ZXC_USE_AVX512) || defined(ZXC_USE_AVX2) || defined(ZXC_USE_NEON64) || \ defined(ZXC_USE_NEON32) _lit_done:; #endif const size_t lit_run = (size_t)(p - lit_start); // 1 header per 128 bytes + all data bytes // lit_run + ceil(lit_run / 128) rle_size += lit_run + ((lit_run + 127) >> 7); } } // Threshold: ~3% savings using integer math (97% ~= 1 - 1/32) if (rle_size < lit_c - (lit_c >> 5)) enc_lit = ZXC_SECTION_ENCODING_RLE; } zxc_block_header_t bh = {.block_type = ZXC_BLOCK_GLO}; uint8_t* const p = dst + ZXC_BLOCK_HEADER_SIZE; size_t rem = dst_cap - ZXC_BLOCK_HEADER_SIZE; // Decide offset encoding mode: 1-byte if all offsets <= 255 const int use_8bit_off = (max_offset <= 255) ? 1 : 0; const size_t off_stream_size = use_8bit_off ? seq_c : (seq_c * 2); const zxc_gnr_header_t gh = {.n_sequences = seq_c, .n_literals = (uint32_t)lit_c, .enc_lit = enc_lit, .enc_litlen = 0, .enc_mlen = 0, .enc_off = (uint8_t)use_8bit_off}; zxc_section_desc_t desc[ZXC_GLO_SECTIONS] = {0}; desc[0].sizes = (uint64_t)(enc_lit == ZXC_SECTION_ENCODING_RLE ? rle_size : lit_c) | ((uint64_t)lit_c << 32); desc[1].sizes = (uint64_t)seq_c | ((uint64_t)seq_c << 32); desc[2].sizes = (uint64_t)off_stream_size | ((uint64_t)off_stream_size << 32); desc[3].sizes = (uint64_t)extras_sz | ((uint64_t)extras_sz << 32); const int ghs = zxc_write_glo_header_and_desc(p, rem, &gh, desc); if (UNLIKELY(ghs < 0)) return ghs; uint8_t* p_curr = p + ghs; rem -= ghs; // Extract stream sizes once const size_t sz_lit = (size_t)(desc[0].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_tok = (size_t)(desc[1].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_off = (size_t)(desc[2].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_ext = (size_t)(desc[3].sizes & ZXC_SECTION_SIZE_MASK); if (UNLIKELY(rem < sz_lit)) return ZXC_ERROR_DST_TOO_SMALL; if (enc_lit == ZXC_SECTION_ENCODING_RLE) { // Write RLE - optimized single-pass encoding const uint8_t* lit_ptr = literals; const uint8_t* const lit_end = literals + lit_c; while (lit_ptr < lit_end) { uint8_t b = *lit_ptr; const uint8_t* run_start = lit_ptr++; // Count run length while (lit_ptr < lit_end && *lit_ptr == b) lit_ptr++; size_t run = (size_t)(lit_ptr - run_start); if (run >= 4) { // RLE runs: emit 2-byte tokens (header + value) while (run >= 4) { size_t chunk = (run > 131) ? 131 : run; *p_curr++ = (uint8_t)(ZXC_LIT_RLE_FLAG | (chunk - 4)); *p_curr++ = b; run -= chunk; } // Leftover < 4 bytes: emit as literal if (run > 0) { *p_curr++ = (uint8_t)(run - 1); ZXC_MEMCPY(p_curr, lit_ptr - run, run); p_curr += run; } } else { // Literal run: scan ahead to find next RLE opportunity const uint8_t* lit_run_start = run_start; while (lit_ptr < lit_end) { // Quick check: need 4 identical bytes to break if (UNLIKELY(lit_ptr + 3 < lit_end && lit_ptr[0] == lit_ptr[1] && lit_ptr[1] == lit_ptr[2] && lit_ptr[2] == lit_ptr[3])) { break; } lit_ptr++; } size_t lit_run = (size_t)(lit_ptr - lit_run_start); const uint8_t* src_ptr = lit_run_start; // Emit literal chunks (max 128 bytes each) while (lit_run > 0) { size_t chunk = (lit_run > 128) ? 128 : lit_run; *p_curr++ = (uint8_t)(chunk - 1); ZXC_MEMCPY(p_curr, src_ptr, chunk); p_curr += chunk; src_ptr += chunk; lit_run -= chunk; } } } } else { ZXC_MEMCPY(p_curr, literals, lit_c); p_curr += lit_c; } rem -= sz_lit; if (UNLIKELY(rem < sz_tok)) return ZXC_ERROR_DST_TOO_SMALL; ZXC_MEMCPY(p_curr, buf_tokens, seq_c); p_curr += seq_c; rem -= sz_tok; if (UNLIKELY(rem < sz_off)) return ZXC_ERROR_DST_TOO_SMALL; if (use_8bit_off) { // Write 1-byte offsets - unroll for better throughput uint32_t i = 0; for (; i + 8 <= seq_c; i += 8) { p_curr[0] = (uint8_t)buf_offsets[i + 0]; p_curr[1] = (uint8_t)buf_offsets[i + 1]; p_curr[2] = (uint8_t)buf_offsets[i + 2]; p_curr[3] = (uint8_t)buf_offsets[i + 3]; p_curr[4] = (uint8_t)buf_offsets[i + 4]; p_curr[5] = (uint8_t)buf_offsets[i + 5]; p_curr[6] = (uint8_t)buf_offsets[i + 6]; p_curr[7] = (uint8_t)buf_offsets[i + 7]; p_curr += 8; } for (; i < seq_c; i++) { *p_curr++ = (uint8_t)buf_offsets[i]; } } else { // Write 2-byte offsets in little-endian order #ifdef ZXC_BIG_ENDIAN for (uint32_t i = 0; i < seq_c; i++) { zxc_store_le16(p_curr, buf_offsets[i]); p_curr += sizeof(uint16_t); } #else ZXC_MEMCPY(p_curr, buf_offsets, seq_c * sizeof(uint16_t)); p_curr += seq_c * sizeof(uint16_t); #endif } rem -= sz_off; if (UNLIKELY(rem < sz_ext)) return ZXC_ERROR_DST_TOO_SMALL; ZXC_MEMCPY(p_curr, buf_extras, extras_sz); p_curr += extras_sz; bh.comp_size = (uint32_t)(p_curr - (dst + ZXC_BLOCK_HEADER_SIZE)); const int hw = zxc_write_block_header(dst, dst_cap, &bh); if (UNLIKELY(hw < 0)) return hw; // Checksum will be appended by the wrapper *out_sz = ZXC_BLOCK_HEADER_SIZE + bh.comp_size; return ZXC_OK; } /** * @brief Encodes a data block using the General High Velocity (GHI) compression format. * * 1. Compression Strategy * It uses an LZ77-based algorithm with a sliding window (64KB) and a hash table/chain table * mechanism. * * 2. Token Format (Fixed-Width) * Unlike the standard GLO block which uses 1-byte tokens (4-bit literal length / 4-bit match * length), GHI uses 4-byte (32-bit) sequence records for better performance on long runs: * Literal Length (LL): 8 bits (stores 0-254; 255 indicates overflow). * Match Length (ML): 8 bits (stores 0-254; 255 indicates overflow). * Offset: 16 bits (supports the full 64KB window). * This format minimizes the number of expensive VByte reads during decompression for common * sequences where lengths are between 16 and 255. * * @param[in,out] ctx Pointer to the compression context containing hash tables * and configuration. * @param[in] src Pointer to the input source data. * @param[in] src_sz Size of the input data in bytes. * @param[out] dst Pointer to the destination buffer where compressed data will * be written. * @param[in] dst_cap Maximum capacity of the destination buffer. * @param[out] out_sz Pointer to a variable that will receive the total size * of the compressed output. * * @return ZXC_OK on success, or a negative zxc_error_t code (e.g., ZXC_ERROR_DST_TOO_SMALL) if an * error occurs (e.g., buffer overflow). */ static int zxc_encode_block_ghi(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap, size_t* RESTRICT const out_sz) { const int level = ctx->compression_level; const zxc_lz77_params_t lzp = zxc_get_lz77_params(level); ctx->epoch++; if (UNLIKELY(ctx->epoch >= ctx->max_epoch)) { ZXC_MEMSET(ctx->hash_table, 0, ZXC_LZ_HASH_SIZE * sizeof(uint32_t)); ZXC_MEMSET(ctx->hash_tags, 0, ZXC_LZ_HASH_SIZE * sizeof(uint8_t)); ctx->epoch = 1; } const uint32_t offset_bits = ctx->offset_bits; const uint32_t offset_mask = ctx->offset_mask; const uint32_t epoch_mark = ctx->epoch << offset_bits; const uint8_t *ip = src, *iend = src + src_sz, *anchor = ip, *mflimit = iend - 12; uint32_t* const hash_table = ctx->hash_table; uint8_t* const hash_tags = ctx->hash_tags; uint8_t* const buf_extras = ctx->buf_extras; uint16_t* const chain_table = ctx->chain_table; uint8_t* const literals = ctx->literals; uint32_t* const buf_sequences = ctx->buf_sequences; uint32_t seq_c = 0; size_t extras_c = 0; size_t lit_c = 0; uint16_t max_offset = 0; while (LIKELY(ip < mflimit)) { size_t dist = (size_t)(ip - anchor); size_t step = lzp.step_base + (dist >> lzp.step_shift); if (UNLIKELY(ip + step >= mflimit)) step = 1; ZXC_PREFETCH_READ(ip + step * 4 + ZXC_CACHE_LINE_SIZE); const zxc_match_t m = zxc_lz77_find_best_match(src, ip, iend, mflimit, anchor, hash_table, hash_tags, chain_table, epoch_mark, offset_mask, level, lzp); if (m.ref) { ip -= m.backtrack; const uint32_t ll = (uint32_t)(ip - anchor); const uint32_t ml = (uint32_t)(m.len - ZXC_LZ_MIN_MATCH_LEN); const uint32_t off = (uint32_t)(ip - m.ref); if (ll > 0) { if (LIKELY(anchor + ZXC_PAD_SIZE <= iend)) { zxc_copy32(literals + lit_c, anchor); if (UNLIKELY(ll > ZXC_PAD_SIZE)) { ZXC_MEMCPY(literals + lit_c + ZXC_PAD_SIZE, anchor + ZXC_PAD_SIZE, ll - ZXC_PAD_SIZE); } } else { ZXC_MEMCPY(literals + lit_c, anchor, ll); } lit_c += ll; } const uint32_t ll_write = (ll >= ZXC_SEQ_LL_MASK) ? 255U : ll; const uint32_t ml_write = (ml >= ZXC_SEQ_ML_MASK) ? 255U : ml; const uint32_t seq_val = (ll_write << (ZXC_SEQ_ML_BITS + ZXC_SEQ_OFF_BITS)) | (ml_write << ZXC_SEQ_OFF_BITS) | ((off - ZXC_LZ_OFFSET_BIAS) & ZXC_SEQ_OFF_MASK); if ((off - ZXC_LZ_OFFSET_BIAS) > max_offset) max_offset = (uint16_t)(off - ZXC_LZ_OFFSET_BIAS); buf_sequences[seq_c] = seq_val; seq_c++; if (ll >= ZXC_SEQ_LL_MASK) extras_c += zxc_write_varint(buf_extras + extras_c, ll - ZXC_SEQ_LL_MASK); if (ml >= ZXC_SEQ_ML_MASK) extras_c += zxc_write_varint(buf_extras + extras_c, ml - ZXC_SEQ_ML_MASK); ip += m.len; anchor = ip; } else { ip += step; } } const size_t last_lits = iend - anchor; if (last_lits > 0) { ZXC_MEMCPY(literals + lit_c, anchor, last_lits); lit_c += last_lits; } zxc_block_header_t bh = {.block_type = ZXC_BLOCK_GHI}; uint8_t* const p = dst + ZXC_BLOCK_HEADER_SIZE; size_t rem = dst_cap - ZXC_BLOCK_HEADER_SIZE; // Decide offset encoding mode const zxc_gnr_header_t gh = {.n_sequences = seq_c, .n_literals = (uint32_t)lit_c, .enc_lit = ZXC_SECTION_ENCODING_RAW, .enc_litlen = 0, .enc_mlen = 0, .enc_off = (uint8_t)(max_offset <= 255) ? 1 : 0}; zxc_section_desc_t desc[ZXC_GHI_SECTIONS] = {0}; desc[0].sizes = (uint64_t)lit_c | ((uint64_t)lit_c << 32); size_t sz_seqs = seq_c * sizeof(uint32_t); desc[1].sizes = (uint64_t)sz_seqs | ((uint64_t)sz_seqs << 32); desc[2].sizes = (uint64_t)extras_c | ((uint64_t)extras_c << 32); const int ghs = zxc_write_ghi_header_and_desc(p, rem, &gh, desc); if (UNLIKELY(ghs < 0)) return ghs; uint8_t* p_curr = p + ghs; rem -= ghs; // Extract stream sizes once const size_t sz_lit = (size_t)(desc[0].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_seq = (size_t)(desc[1].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_ext = (size_t)(desc[2].sizes & ZXC_SECTION_SIZE_MASK); if (UNLIKELY(rem < sz_lit + sz_seq + sz_ext)) return ZXC_ERROR_DST_TOO_SMALL; ZXC_MEMCPY(p_curr, literals, lit_c); p_curr += lit_c; rem -= sz_lit; if (UNLIKELY(rem < sz_seq)) return ZXC_ERROR_DST_TOO_SMALL; // Write sequences in little-endian order #ifdef ZXC_BIG_ENDIAN for (uint32_t i = 0; i < seq_c; i++) { zxc_store_le32(p_curr, buf_sequences[i]); p_curr += sizeof(uint32_t); } #else ZXC_MEMCPY(p_curr, buf_sequences, sz_seq); p_curr += sz_seq; #endif // --- WRITE EXTRAS --- ZXC_MEMCPY(p_curr, buf_extras, sz_ext); p_curr += sz_ext; bh.comp_size = (uint32_t)(p_curr - (dst + ZXC_BLOCK_HEADER_SIZE)); const int hw = zxc_write_block_header(dst, dst_cap, &bh); if (UNLIKELY(hw < 0)) return hw; // Checksum will be appended by the wrapper *out_sz = ZXC_BLOCK_HEADER_SIZE + bh.comp_size; return ZXC_OK; } /** * @brief Encodes a raw data block (uncompressed). * * This function prepares and writes a "RAW" type block into the destination * buffer. It handles the block header, copying of source data, and optionally * the calculation and storage of a checksum. * * @param[in] src Pointer to the source data to encode. * @param[in] src_sz Size of the source data in bytes. * @param[out] dst Pointer to the destination buffer. * @param[in] dst_cap Maximum capacity of the destination buffer. * @param[out] out_sz Pointer to a variable receiving the total written size * (header * + data + checksum). * @param[in] chk Boolean flag: if non-zero, a checksum is calculated and added. * * @return ZXC_OK on success, or a negative zxc_error_t code (e.g., ZXC_ERROR_DST_TOO_SMALL) if the * destination buffer capacity is insufficient. */ static int zxc_encode_block_raw(const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT const dst, const size_t dst_cap, size_t* RESTRICT const out_sz) { if (UNLIKELY(dst_cap < ZXC_BLOCK_HEADER_SIZE + src_sz)) return ZXC_ERROR_DST_TOO_SMALL; // Compute block RAW zxc_block_header_t bh; bh.block_type = ZXC_BLOCK_RAW; bh.block_flags = 0; // Checksum flag moved to file header bh.reserved = 0; bh.comp_size = (uint32_t)src_sz; const int hw = zxc_write_block_header(dst, dst_cap, &bh); if (UNLIKELY(hw < 0)) return hw; ZXC_MEMCPY(dst + ZXC_BLOCK_HEADER_SIZE, src, src_sz); // Checksum will be appended by the wrapper *out_sz = ZXC_BLOCK_HEADER_SIZE + src_sz; return ZXC_OK; } /** * @brief Checks if the given byte array represents a numeric value. * * This function examines the provided buffer to determine if it contains * only numeric characters (e.g., ASCII digits '0'-'9'). * * Improved heuristic: * 1. Must be aligned to 4 bytes. * 2. Samples the first 128 integers (more accurate). * 3. Calculates bit width of deltas (fewer bits = better for NUM). * 4. Estimates compression ratio: if NUM would save >20% vs raw, use it. * * @param[in] src Pointer to the input byte array to be checked. * @param[in] size The number of bytes in the input array. * @return int Returns 1 if the array is numeric, 0 otherwise. */ static int zxc_probe_is_numeric(const uint8_t* src, const size_t size) { if (UNLIKELY(size % sizeof(uint32_t) != 0 || size < (4 * sizeof(uint32_t)))) return 0; const size_t total_vals = size / sizeof(uint32_t); const size_t sample_len = 16; // Sample 2 contiguous regions: start and middle of the block. // Each region computes its own deltas independently. const size_t offsets[2] = {0, (total_vals / 2) & ~(size_t)3}; // Align to uint32_t boundary const size_t n_regions = (total_vals > sample_len * 2) ? 2 : 1; uint32_t max_zigzag = 0; uint32_t small_count = 0; // Deltas < 256 (8 bits) uint32_t medium_count = 0; // Deltas < 65536 (16 bits) size_t total_sampled = 0; for (size_t r = 0; r < n_regions; r++) { const uint8_t* p = src + offsets[r] * sizeof(uint32_t); const size_t region_count = ((total_vals - offsets[r]) < sample_len) ? (total_vals - offsets[r]) : sample_len; uint32_t prev = zxc_le32(p); p += sizeof(uint32_t); for (size_t i = 1; i < region_count; i++) { const uint32_t curr = zxc_le32(p); const int32_t diff = (int32_t)(curr - prev); const uint32_t zigzag = zxc_zigzag_encode(diff); max_zigzag = zigzag > max_zigzag ? zigzag : max_zigzag; small_count += (uint32_t)(zigzag < 256); medium_count += (uint32_t)(zigzag >= 256) & (uint32_t)(zigzag < 65536); prev = curr; p += sizeof(uint32_t); } total_sampled += region_count - 1; } const uint32_t bits_needed = zxc_highbit32(max_zigzag); // Estimate compression ratio: // NUM uses ~bits_needed per value, Raw uses 32 bits per value // Worth it if bits_needed <= 20 (saves >37.5%) if (bits_needed <= 16) return 1; if (bits_needed <= 20 && (small_count + medium_count) >= (total_sampled * 85) / 100) return 1; // Fallback: if 90% of deltas are small, still use NUM if ((small_count + medium_count) >= (total_sampled * 90) / 100) return 1; return 0; } // cppcheck-suppress unusedFunction int zxc_compress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT chunk, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) { size_t w = 0; int res = ZXC_OK; int try_num = zxc_probe_is_numeric(chunk, src_sz); if (UNLIKELY(try_num)) { res = zxc_encode_block_num(ctx, chunk, src_sz, dst, dst_cap, &w); if (res != ZXC_OK || w > (src_sz - (src_sz >> 2))) // w > 75% of src_sz try_num = 0; // NUM didn't compress well, try GLO/GHI instead } if (LIKELY(!try_num)) { if (ctx->compression_level <= 2) res = zxc_encode_block_ghi(ctx, chunk, src_sz, dst, dst_cap, &w); else res = zxc_encode_block_glo(ctx, chunk, src_sz, dst, dst_cap, &w); } // Check expansion. W contains Header + Payload. if (UNLIKELY(res != ZXC_OK || w >= src_sz)) { res = zxc_encode_block_raw(chunk, src_sz, dst, dst_cap, &w); if (UNLIKELY(res != ZXC_OK)) return res; } if (ctx->checksum_enabled) { // Calculate checksum on the compressed payload (w currently excludes checksum) // Header is at dst, data starts at dst + ZXC_BLOCK_HEADER_SIZE if (UNLIKELY(w < ZXC_BLOCK_HEADER_SIZE || w + ZXC_BLOCK_CHECKSUM_SIZE > dst_cap)) return ZXC_ERROR_OVERFLOW; uint32_t payload_sz = (uint32_t)(w - ZXC_BLOCK_HEADER_SIZE); uint32_t crc = zxc_checksum(dst + ZXC_BLOCK_HEADER_SIZE, payload_sz, ZXC_CHECKSUM_RAPIDHASH); zxc_store_le32(dst + w, crc); w += ZXC_BLOCK_CHECKSUM_SIZE; } return (int)w; } zxc-0.10.0/src/lib/zxc_decompress.c000066400000000000000000002120411517010557200171300ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_decompress.c * @brief Block-level decompression: NUM / GLO / GHI / RAW decoding with * SIMD-accelerated prefix-sum, bit-unpacking, and overlapping copies. * * Like @ref zxc_compress.c, this file is compiled multiple times with * @c ZXC_FUNCTION_SUFFIX to produce per-ISA variants. */ #include "../../include/zxc_error.h" #include "../../include/zxc_sans_io.h" #include "zxc_internal.h" /* * Function Multi-Versioning Support * If ZXC_FUNCTION_SUFFIX is defined (e.g. _avx2), rename the public entry point. */ #ifdef ZXC_FUNCTION_SUFFIX #define ZXC_CAT_IMPL(x, y) x##y #define ZXC_CAT(x, y) ZXC_CAT_IMPL(x, y) #define zxc_decompress_chunk_wrapper ZXC_CAT(zxc_decompress_chunk_wrapper, ZXC_FUNCTION_SUFFIX) #endif /** * @brief Consumes a specified number of bits from the bit reader buffer without * performing safety checks. * * This function advances the bit reader's state by `n` bits. It is marked as * always inline for performance critical paths. * * @warning This is a "fast" variant, meaning it assumes the buffer has enough * bits available. The caller is responsible for ensuring that at least `n` bits * are present in the accumulator or buffer before calling this function to * avoid undefined behavior or reading past valid memory. * * @param[in,out] br Pointer to the bit reader instance. * @param[in] n The number of bits to consume (must be <= 32, typically <= 24 * depending on implementation). * @return The value of the consumed bits as a 32-bit unsigned integer. */ static ZXC_ALWAYS_INLINE uint32_t zxc_br_consume_fast(zxc_bit_reader_t* RESTRICT br, const uint8_t n) { #if !defined(ZXC_DISABLE_SIMD) && defined(__BMI2__) && (defined(__x86_64__) || defined(_M_X64)) // BMI2 Optimization: _bzhi_u64(x, n) copies the lower n bits of x to dst and // clears the rest. It is equivalent to x & ((1ULL << n) - 1) but executes in // a single cycle without dependency chains. const uint32_t val = (uint32_t)_bzhi_u64(br->accum, n); #else const uint32_t val = (uint32_t)(br->accum & ((1ULL << n) - 1)); #endif br->accum >>= n; br->bits -= n; return val; } /** * @brief Reads a Prefix Varint encoded integer from a stream. * * This function decodes a 32-bit unsigned integer encoded in Prefix Varint format * from the provided byte stream. Unary prefix bits in the first byte determine * the total length (1-5 bytes). * * Format: * - 1 byte (0xxxxxxx): 7-bit payload (val < 2^7 = 128) * - 2 bytes (10xxxxxx): 14-bit payload (val < 2^14 = 16384) * - 3 bytes (110xxxxx): 21-bit payload (val < 2^21 = 2097152) * - 4 bytes (1110xxxx): 28-bit payload (val < 2^28 = 268435456) * - 5 bytes (11110xxx): 32-bit payload (full uint32_t range) * * @param[in,out] ptr Pointer to a pointer to the current position in the stream. * @param[in] end Pointer to the end of the readable stream (for bounds checking). * @return The decoded 32-bit integer, or 0 if reading would overflow bounds (safe default). */ static ZXC_ALWAYS_INLINE uint32_t zxc_read_varint(const uint8_t** ptr, const uint8_t* end) { const uint8_t* p = *ptr; // Bounds check: need at least 1 byte if (UNLIKELY(p >= end)) return 0; const uint32_t b0 = p[0]; // 1 Byte: 0xxxxxxx (7 bits) -> val < 128 (2^7) if (LIKELY(b0 < 0x80)) { *ptr = p + 1; return b0; } // 2 Bytes: 10xxxxxx xxxxxxxx (14 bits) -> val < 16384 (2^14) if (LIKELY(b0 < 0xC0)) { if (UNLIKELY(p + 1 >= end)) { *ptr = end; return 0; } *ptr = p + 2; return (b0 & 0x3F) | ((uint32_t)p[1] << 6); } // 3 Bytes: 110xxxxx xxxxxxxx xxxxxxxx (21 bits) -> val < 2097152 (2^21) if (LIKELY(b0 < 0xE0)) { if (UNLIKELY(p + 2 >= end)) { *ptr = end; return 0; } *ptr = p + 3; return (b0 & 0x1F) | ((uint32_t)p[1] << 5) | ((uint32_t)p[2] << 13); } // 4 Bytes: 1110xxxx ... (28 bits) -> val < 268435456 (2^28) if (UNLIKELY(b0 < 0xF0)) { if (UNLIKELY(p + 3 >= end)) { *ptr = end; return 0; } *ptr = p + 4; return (b0 & 0x0F) | ((uint32_t)p[1] << 4) | ((uint32_t)p[2] << 12) | ((uint32_t)p[3] << 20); } // 5 Bytes: 11110xxx ... (32 bits) -> val < 4294967296 (2^32) if (UNLIKELY(p + 4 >= end)) { *ptr = end; return 0; } *ptr = p + 5; return (b0 & 0x07) | ((uint32_t)p[1] << 3) | ((uint32_t)p[2] << 11) | ((uint32_t)p[3] << 19) | ((uint32_t)p[4] << 27); } /** * @brief Shuffle masks for overlapping copies with small offsets (0-15). * * Shared between ARM NEON and x86 SSSE3. Each row defines how to replicate * source bytes to fill 16 bytes when offset < 16. */ #if defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) || defined(ZXC_USE_AVX2) || \ defined(ZXC_USE_AVX512) /** * @brief Precomputed masks for handling overlapping data during decompression. * * This 16x16 lookup table contains 128-bit aligned masks used to efficiently * mask off or combine bytes when processing overlapping copy operations or * boundary conditions in the ZXC decompression algorithm. * * The alignment to 16 bytes ensures compatibility with SIMD instructions * (like SSE/AVX) for optimized memory operations. */ static const ZXC_ALIGN(16) uint8_t zxc_overlap_masks[16][16] = { {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // off=0 (unused) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // off=1 (RLE handled separately) {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}, // off=2 {0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0}, // off=3 {0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3}, // off=4 {0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0}, // off=5 {0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3}, // off=6 {0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1}, // off=7 {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7}, // off=8 {0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3, 4, 5, 6}, // off=9 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5}, // off=10 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 1, 2, 3, 4}, // off=11 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3}, // off=12 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 1, 2}, // off=13 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 1}, // off=14 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0} // off=15 }; #endif /** * @brief Copies 16 bytes from an overlapping source to the destination. * * This function is designed to handle memory copies where the source and * destination regions might overlap, specifically copying 16 bytes from * `dst - off` to `dst`. It is typically used in decompression routines * (like LZ77) where repeating a previous sequence is required. * * Handles NEON64, NEON32, SSSE3/AVX2 and generic scalar fallback. * * @param[out] dst Pointer to the destination buffer where bytes will be written. * @param[in] off The offset backwards from the destination pointer to read from. * (i.e., source address is `dst - off`). */ // codeql[cpp/unused-static-function] : False positive, used in DECODE_SEQ_SAFE/FAST macros static ZXC_ALWAYS_INLINE void zxc_copy_overlap16(uint8_t* dst, uint32_t off) { // off is always >= ZXC_LZ_OFFSET_BIAS by design (offset bias encoding: stored + // ZXC_LZ_OFFSET_BIAS) #if defined(ZXC_USE_NEON64) uint8x16_t mask = vld1q_u8(zxc_overlap_masks[off]); uint8x16_t src_data = vld1q_u8(dst - off); vst1q_u8(dst, vqtbl1q_u8(src_data, mask)); #elif defined(ZXC_USE_NEON32) uint8x8x2_t src_tbl; src_tbl.val[0] = vld1_u8(dst - off); src_tbl.val[1] = vld1_u8(dst - off + 8); uint8x8_t mask_lo = vld1_u8(zxc_overlap_masks[off]); uint8x8_t mask_hi = vld1_u8(zxc_overlap_masks[off] + 8); vst1_u8(dst, vtbl2_u8(src_tbl, mask_lo)); vst1_u8(dst + 8, vtbl2_u8(src_tbl, mask_hi)); #elif defined(ZXC_USE_AVX2) || defined(ZXC_USE_AVX512) __m128i mask = _mm_load_si128((const __m128i*)zxc_overlap_masks[off]); __m128i src_data = _mm_loadu_si128((const __m128i*)(dst - off)); _mm_storeu_si128((__m128i*)dst, _mm_shuffle_epi8(src_data, mask)); #else const uint8_t* src = dst - off; for (size_t i = 0; i < 16; i++) { dst[i] = src[i % off]; } #endif } #if defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) /** * @brief Computes the prefix sum of a 128-bit vector of 32-bit unsigned * integers using NEON intrinsics. * * This function calculates the running total of the elements in the input * vector `v`. If the input vector is `[a, b, c, d]`, the result will be `[a, * a+b, a+b+c, a+b+c+d]`. This operation is typically used for calculating * cumulative distributions or offsets in parallel. * * @param[in] v The input vector containing four 32-bit unsigned integers. * @return A uint32x4_t vector containing the prefix sums. */ static ZXC_ALWAYS_INLINE uint32x4_t zxc_neon_prefix_sum_u32(uint32x4_t v) { const uint32x4_t zero = vdupq_n_u32(0); // Create a vector of zeros // Rotate right by 1 element (shift 4 bytes) const uint32x4_t s1 = vreinterpretq_u32_u8(vextq_u8(vreinterpretq_u8_u32(zero), vreinterpretq_u8_u32(v), 12)); v = vaddq_u32(v, s1); // Add shifted version: [a, b, c, d] + [0, a, b, c] -> // [a, a+b, b+c, c+d] // Rotate right by 2 elements (shift 8 bytes) const uint32x4_t s2 = vreinterpretq_u32_u8(vextq_u8(vreinterpretq_u8_u32(zero), vreinterpretq_u8_u32(v), 8)); v = vaddq_u32(v, s2); // Add shifted version to complete prefix sum return v; } #endif #if defined(ZXC_USE_AVX2) /** * @brief Computes the prefix sum of 32-bit integers within a 256-bit vector. * * This function calculates the cumulative sum of the eight 32-bit integers * contained in the input vector `v`. * * Operation logic (conceptually): * out[0] = v[0] * out[1] = v[0] + v[1] * ... * out[7] = v[0] + v[1] + ... + v[7] * * @param[in] v The input 256-bit vector containing eight 32-bit integers. * @return A 256-bit vector containing the prefix sums of the input elements. */ // codeql[cpp/unused-static-function] : Used conditionally when ZXC_USE_AVX2 is defined static ZXC_ALWAYS_INLINE __m256i zxc_mm256_prefix_sum_epi32(__m256i v) { v = _mm256_add_epi32(v, _mm256_slli_si256(v, 4)); // Add value shifted by 1 element v = _mm256_add_epi32(v, _mm256_slli_si256(v, 8)); // Add value shifted by 2 elements // Use permute/shuffle to bridge the 128-bit lane gap __m256i v_bridge = _mm256_permute2x128_si256(v, v, 0x00); // Duplicate lower 128 to upper v_bridge = _mm256_shuffle_epi32(v_bridge, 0xFF); // Broadcast last element of lower 128 v_bridge = _mm256_blend_epi32(_mm256_setzero_si256(), v_bridge, 0xF0); // Only apply to upper lane return _mm256_add_epi32(v, v_bridge); // Add bridge value to upper lane } #endif #if defined(ZXC_USE_AVX512) /** * @brief Computes the prefix sum of 32-bit integers within a 512-bit vector. * * This function calculates the running sum of the 16 packed 32-bit integers * in the input vector `v`. * * For an input vector v = [x0, x1, x2, ... x15], the result will be: * [x0, x0+x1, x0+x1+x2, ... , sum(x0..x15)]. * * @note This function is forced inline for performance reasons. * * @param[in] v The input 512-bit vector containing sixteen 32-bit integers. * @return A 512-bit vector containing the prefix sums of the input elements. */ static ZXC_ALWAYS_INLINE __m512i zxc_mm512_prefix_sum_epi32(__m512i v) { __m512i t = _mm512_bslli_epi128(v, 4); // Shift left by 4 bytes (1 int) v = _mm512_add_epi32(v, t); // Add shifted value t = _mm512_bslli_epi128(v, 8); // Shift left by 8 bytes (2 ints) v = _mm512_add_epi32(v, t); // Add shifted value // Propagate sums across 128-bit lanes (sequential dependency) __m512i v_l0 = _mm512_shuffle_i32x4(v, v, 0x00); // Broadcast lane 0 v_l0 = _mm512_shuffle_epi32(v_l0, 0xFF); // Broadcast last element of lane 0 v = _mm512_mask_add_epi32(v, 0x00F0, v, v_l0); // Add to lane 1 only __m512i v_l1 = _mm512_shuffle_i32x4(v, v, 0x55); // Broadcast lane 1 v_l1 = _mm512_shuffle_epi32(v_l1, 0xFF); // Broadcast last element of lane 1 v = _mm512_mask_add_epi32(v, 0x0F00, v, v_l1); // Add to lane 2 only __m512i v_l2 = _mm512_shuffle_i32x4(v, v, 0xAA); // Broadcast lane 2 v_l2 = _mm512_shuffle_epi32(v_l2, 0xFF); // Broadcast last element of lane 2 v = _mm512_mask_add_epi32(v, 0xF000, v, v_l2); // Add to lane 3 only return v; } #endif /** * @brief Decodes a block of numerical data compressed with the ZXC format. * * This function reads a compressed numerical block from the source buffer, * parses the header to determine the number of values and encoding parameters, * and then decompresses the data into the destination buffer. * * **Algorithm Details:** * 1. **Header Parsing:** Reads the `zxc_num_header_t` to get the count of * values. * 2. **Bit Unpacking:** For each chunk of values, it initializes a bit reader. * - **Unrolling:** The main loop is unrolled 4x to minimize branch overhead * and maximize instruction throughput. * 3. **ZigZag Decoding:** Converts the unsigned unpacked value back to a signed * delta using `(n >> 1) ^ -(n & 1)`. * 4. **Delta Reconstruction:** Adds the signed delta to a `running_val` * accumulator to recover the original integer sequence. * * @param[in] src Pointer to the source buffer containing compressed data. * @param[in] src_size Size of the source buffer in bytes. * @param[out] dst Pointer to the destination buffer where decompressed data will be * written. * @param[in] dst_capacity Maximum capacity of the destination buffer in bytes. * * @return The number of bytes written to the destination buffer on success, * or a negative zxc_error_t code if an error occurs (e.g., buffer overflow, invalid header, * or malformed compressed stream). */ static int zxc_decode_block_num(const uint8_t* RESTRICT src, const size_t src_size, uint8_t* RESTRICT dst, const size_t dst_capacity) { zxc_num_header_t nh; if (UNLIKELY(zxc_read_num_header(src, src_size, &nh) != ZXC_OK)) return ZXC_ERROR_BAD_HEADER; size_t offset = ZXC_NUM_HEADER_BINARY_SIZE; uint8_t* d_ptr = dst; const uint8_t* const d_end = dst + dst_capacity; uint64_t vals_remaining = nh.n_values; uint32_t running_val = 0; ZXC_ALIGN(ZXC_CACHE_LINE_SIZE) uint32_t deltas[ZXC_NUM_DEC_BATCH]; while (vals_remaining > 0) { if (UNLIKELY(offset + ZXC_NUM_CHUNK_HEADER_SIZE > src_size)) return ZXC_ERROR_SRC_TOO_SMALL; const uint16_t nvals = zxc_le16(src + offset); const uint16_t bits = zxc_le16(src + offset + 2); const uint32_t psize = zxc_le32(src + offset + 12); // padding + nvals + bits offset += ZXC_NUM_CHUNK_HEADER_SIZE; if (UNLIKELY(nvals > vals_remaining || src_size < offset + psize || (size_t)(d_end - d_ptr) < (size_t)nvals * sizeof(uint32_t) || bits > (sizeof(uint32_t) * CHAR_BIT))) return ZXC_ERROR_CORRUPT_DATA; zxc_bit_reader_t br; zxc_br_init(&br, src + offset, psize); size_t i = 0; for (; i + ZXC_NUM_DEC_BATCH <= nvals; i += ZXC_NUM_DEC_BATCH) { for (int k = 0; k < ZXC_NUM_DEC_BATCH; k += 4) { zxc_br_ensure(&br, bits); deltas[k + 0] = zxc_zigzag_decode(zxc_br_consume_fast(&br, (uint8_t)bits)); zxc_br_ensure(&br, bits); deltas[k + 1] = zxc_zigzag_decode(zxc_br_consume_fast(&br, (uint8_t)bits)); zxc_br_ensure(&br, bits); deltas[k + 2] = zxc_zigzag_decode(zxc_br_consume_fast(&br, (uint8_t)bits)); zxc_br_ensure(&br, bits); deltas[k + 3] = zxc_zigzag_decode(zxc_br_consume_fast(&br, (uint8_t)bits)); } uint32_t* batch_dst = (uint32_t*)d_ptr; #if defined(ZXC_USE_AVX512) __m512i v_run = _mm512_set1_epi32(running_val); // Broadcast initial running total for (int k = 0; k < ZXC_NUM_DEC_BATCH; k += 16) { __m512i v_deltas = _mm512_load_si512((void*)&deltas[k]); // Load 16 deltas __m512i v_sum = zxc_mm512_prefix_sum_epi32(v_deltas); // Compute local prefix sums v_sum = _mm512_add_epi32(v_sum, v_run); // Add base running total _mm512_storeu_si512((void*)&batch_dst[k], v_sum); // Store decoded values // Broadcast 15th element of v_sum to v_run directly within ZMM registers // 1. Align upper 128-bit lane down to all lanes __m512i v_last128 = _mm512_shuffle_i32x4(v_sum, v_sum, 0xFF); // 2. Broadcast the 3rd element of those lanes v_run = _mm512_shuffle_epi32(v_last128, 0xFF); } // Extract final running_val back to GPR for scalar fallback running_val = (uint32_t)_mm_cvtsi128_si32(_mm512_castsi512_si128(v_run)); #elif defined(ZXC_USE_AVX2) __m256i v_run = _mm256_set1_epi32(running_val); // Broadcast initial running total for (int k = 0; k < ZXC_NUM_DEC_BATCH; k += 8) { __m256i v_deltas = _mm256_load_si256((const __m256i*)&deltas[k]); // Load 8 deltas __m256i v_sum = zxc_mm256_prefix_sum_epi32(v_deltas); // Compute local prefix sums v_sum = _mm256_add_epi32(v_sum, v_run); // Add base _mm256_storeu_si256((__m256i*)&batch_dst[k], v_sum); // Store decoded values // Compute v_run directly from vector register without memory readback // Duplicate upper 128-bits into both lanes __m256i last_val = _mm256_permute2x128_si256(v_sum, v_sum, 0x11); // Broadcast 4th element to all elements v_run = _mm256_shuffle_epi32(last_val, 0xFF); } // Extract final running_val back to GPR for scalar fallback running_val = (uint32_t)_mm_cvtsi128_si32(_mm256_castsi256_si128(v_run)); #elif defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) uint32x4_t v_run = vdupq_n_u32(running_val); // Broadcast running total for (int k = 0; k < ZXC_NUM_DEC_BATCH; k += 4) { uint32x4_t v_deltas = vld1q_u32(&deltas[k]); // Load 4 deltas uint32x4_t v_sum = zxc_neon_prefix_sum_u32(v_deltas); // Compute local prefix sums v_sum = vaddq_u32(v_sum, v_run); // Add base vst1q_u32(&batch_dst[k], v_sum); // Store decoded values #if defined(ZXC_USE_NEON64) v_run = vdupq_laneq_u32(v_sum, 3); // Update vector directly (no GPR transit) #else running_val = vgetq_lane_u32(v_sum, 3); // Extract last element v_run = vdupq_n_u32(running_val); // Update vector for next iter #endif } #if defined(ZXC_USE_NEON64) running_val = vgetq_lane_u32(v_run, 0); // Extract once at the end of the batch #endif #else for (int k = 0; k < ZXC_NUM_DEC_BATCH; k++) { running_val += deltas[k]; #ifdef ZXC_BIG_ENDIAN zxc_store_le32(&batch_dst[k], running_val); #else batch_dst[k] = running_val; #endif } #endif d_ptr += ZXC_NUM_DEC_BATCH * sizeof(uint32_t); } for (; i < nvals; i++) { zxc_br_ensure(&br, bits); const uint32_t delta = zxc_zigzag_decode(zxc_br_consume_fast(&br, (uint8_t)bits)); running_val += delta; zxc_store_le32(d_ptr, running_val); d_ptr += sizeof(uint32_t); } offset += psize; vals_remaining -= nvals; } return (int)(d_ptr - dst); } /** * @brief Decodes a General Low (GLO) format compressed block. * * This function handles the decoding of a compressed block formatted with the * internal GLO structure. The decompressed size is derived from Section * Descriptors within the compressed payload. * * @param[in,out] ctx Pointer to the compression context (`zxc_cctx_t`) containing * @param[in] src Pointer to the source buffer containing compressed data. * @param[in] src_size Size of the source buffer in bytes. * @param[out] dst Pointer to the destination buffer for decompressed data. * @param[in] dst_capacity Maximum capacity of the destination buffer. * * @return The number of bytes written to the destination buffer on success, or * a negative zxc_error_t code on failure (e.g., invalid header, buffer overflow, or corrupted * data). */ static int zxc_decode_block_glo(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_size, uint8_t* RESTRICT dst, const size_t dst_capacity) { zxc_gnr_header_t gh; zxc_section_desc_t desc[ZXC_GLO_SECTIONS]; if (UNLIKELY(zxc_read_glo_header_and_desc(src, src_size, &gh, desc) != ZXC_OK)) return ZXC_ERROR_BAD_HEADER; const uint8_t* p_data = src + ZXC_GLO_HEADER_BINARY_SIZE + ZXC_GLO_SECTIONS * ZXC_SECTION_DESC_BINARY_SIZE; const uint8_t* p_curr = p_data; // --- Literal Stream Setup --- const uint8_t* l_ptr; const uint8_t* l_end; uint8_t* rle_buf = NULL; size_t lit_stream_size = (size_t)(desc[0].sizes & ZXC_SECTION_SIZE_MASK); if (gh.enc_lit == ZXC_SECTION_ENCODING_RLE) { const size_t required_size = (size_t)(desc[0].sizes >> 32); if (required_size > 0) { if (UNLIKELY(required_size > dst_capacity)) return ZXC_ERROR_DST_TOO_SMALL; if (ctx->lit_buffer_cap < required_size + ZXC_PAD_SIZE) { uint8_t* new_buf = (uint8_t*)realloc(ctx->lit_buffer, required_size + ZXC_PAD_SIZE); if (UNLIKELY(!new_buf)) { free(ctx->lit_buffer); ctx->lit_buffer = NULL; ctx->lit_buffer_cap = 0; return ZXC_ERROR_MEMORY; } ctx->lit_buffer = new_buf; ctx->lit_buffer_cap = required_size + ZXC_PAD_SIZE; } rle_buf = ctx->lit_buffer; if (UNLIKELY(!rle_buf || lit_stream_size > (size_t)(src + src_size - p_curr))) return ZXC_ERROR_CORRUPT_DATA; const uint8_t* r_ptr = p_curr; const uint8_t* r_end = r_ptr + lit_stream_size; uint8_t* w_ptr = rle_buf; const uint8_t* const w_end = rle_buf + required_size; while (r_ptr < r_end && w_ptr < w_end) { uint8_t token = *r_ptr++; if (LIKELY(!(token & ZXC_LIT_RLE_FLAG))) { // Raw copy (most common path): use ZXC_PAD_SIZE-byte wild copies // token is 7-bit (0-127), so len is 1-128 bytes const uint32_t len = (uint32_t)token + 1; if (UNLIKELY(w_ptr + len > w_end || r_ptr + len > r_end)) return ZXC_ERROR_CORRUPT_DATA; // Destination has ZXC_PAD_SIZE bytes of safe overrun space. // Source may not - check before wild copy. // Fast path: source has ZXC_PAD_SIZE-byte read headroom (most common) if (LIKELY(r_ptr + ZXC_PAD_SIZE <= r_end)) { // Single 32-byte copy covers len <= ZXC_PAD_SIZE (most tokens) zxc_copy32(w_ptr, r_ptr); if (UNLIKELY(len > ZXC_PAD_SIZE)) { // Unroll: max len=128, so max 4 copies total // Use unconditional stores with overlap - faster than branches if (len <= 2 * ZXC_PAD_SIZE) { zxc_copy32(w_ptr + len - ZXC_PAD_SIZE, r_ptr + len - ZXC_PAD_SIZE); } else if (len <= 3 * ZXC_PAD_SIZE) { zxc_copy32(w_ptr + ZXC_PAD_SIZE, r_ptr + ZXC_PAD_SIZE); zxc_copy32(w_ptr + len - ZXC_PAD_SIZE, r_ptr + len - ZXC_PAD_SIZE); } else { zxc_copy32(w_ptr + ZXC_PAD_SIZE, r_ptr + ZXC_PAD_SIZE); zxc_copy32(w_ptr + 2 * ZXC_PAD_SIZE, r_ptr + 2 * ZXC_PAD_SIZE); zxc_copy32(w_ptr + len - ZXC_PAD_SIZE, r_ptr + len - ZXC_PAD_SIZE); } } } else { // Near end of source: safe copy (rare cold path) ZXC_MEMCPY(w_ptr, r_ptr, len); } w_ptr += len; r_ptr += len; } else { // RLE run: fill with single byte const uint32_t len = (token & ZXC_LIT_LEN_MASK) + 4; if (UNLIKELY(w_ptr + len > w_end || r_ptr >= r_end)) return ZXC_ERROR_CORRUPT_DATA; ZXC_MEMSET(w_ptr, *r_ptr++, len); w_ptr += len; } } if (UNLIKELY(w_ptr != w_end)) return ZXC_ERROR_CORRUPT_DATA; l_ptr = rle_buf; l_end = rle_buf + required_size; } else { l_ptr = p_curr; l_end = p_curr; } } else { l_ptr = p_curr; l_end = p_curr + lit_stream_size; } p_curr += lit_stream_size; // --- Stream Pointers & Validation --- const size_t sz_tokens = (size_t)(desc[1].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_offsets = (size_t)(desc[2].sizes & ZXC_SECTION_SIZE_MASK); const size_t sz_extras = (size_t)(desc[3].sizes & ZXC_SECTION_SIZE_MASK); // Validate stream sizes match sequence count (early rejection of malformed data) const size_t expected_off_size = (gh.enc_off == 1) ? (size_t)gh.n_sequences : (size_t)gh.n_sequences * 2; const uint8_t* t_ptr = p_curr; const uint8_t* o_ptr = t_ptr + sz_tokens; const uint8_t* e_ptr = o_ptr + sz_offsets; const uint8_t* const e_end = e_ptr + sz_extras; // For vbyte overflow detection // Validate streams don't overflow source buffer + // Validate stream sizes match sequence count (early rejection of malformed data) if (UNLIKELY((e_end != src + src_size) || sz_tokens < gh.n_sequences || sz_offsets < expected_off_size)) return ZXC_ERROR_CORRUPT_DATA; uint8_t* d_ptr = dst; const uint8_t* const d_end = dst + dst_capacity; // Destination safe margin for 4x loop: max output without varint extension. // ll_max = 14, ml_max = 14 + 5 = 19, per-seq = 33, 4x = 132. // Add ZXC_PAD_SIZE (32) for the wild zxc_copy32 overshoot + 4 safety = 168. const uint8_t* const d_end_safe = d_end - (132 + ZXC_PAD_SIZE + 4); // Literal stream safe threshold for 4x-unrolled loops. // Without varint extension, max ll per sequence = ZXC_TOKEN_LL_MASK - 1 = 14. // For 4 sequences: 4 * 14 = 56. With this margin, l_ptr checks are only needed // on the cold varint path, keeping the hot path free of l_ptr overhead. const size_t glo_sz_lit = (size_t)(l_end - l_ptr); const size_t glo_margin_4x = 4 * (ZXC_TOKEN_LL_MASK - 1); // 56 const size_t glo_margin_1x = ZXC_TOKEN_LL_MASK - 1; // 14 const uint8_t* const l_end_safe_4x = (glo_sz_lit > glo_margin_4x) ? l_end - glo_margin_4x : l_ptr; const uint8_t* const l_end_safe_1x = (glo_sz_lit > glo_margin_1x) ? l_end - glo_margin_1x : l_ptr; uint32_t n_seq = gh.n_sequences; // Track bytes written for offset validation // For 1-byte offsets (enc_off==1): validate until 256 bytes written (max 8-bit offset) // For 2-byte offsets (enc_off==0): validate until 65536 bytes written (max 16-bit offset) // After threshold, all offsets are guaranteed valid (can't exceed written bytes) size_t written = 0; // --- Factored sub-macros for literal copy + match copy --- // Used by DECODE_SEQ_SAFE and DECODE_SEQ_FAST to avoid duplication. // Copy literals from l_ptr to d_ptr using 32-byte wild copies #define DECODE_COPY_LITERALS(ll) \ do { \ const uint8_t* src_lit = l_ptr; \ uint8_t* dst_lit = d_ptr; \ zxc_copy32(dst_lit, src_lit); \ if (UNLIKELY(ll > ZXC_PAD_SIZE)) { \ dst_lit += ZXC_PAD_SIZE; \ src_lit += ZXC_PAD_SIZE; \ size_t rem = ll - ZXC_PAD_SIZE; \ while (rem > ZXC_PAD_SIZE) { \ zxc_copy32(dst_lit, src_lit); \ dst_lit += ZXC_PAD_SIZE; \ src_lit += ZXC_PAD_SIZE; \ rem -= ZXC_PAD_SIZE; \ } \ zxc_copy32(dst_lit, src_lit); \ } \ l_ptr += ll; \ d_ptr += ll; \ } while (0) // Copy match from d_ptr - off to d_ptr, handling overlap cases #define DECODE_COPY_MATCH(ml, off) \ do { \ const uint8_t* match_src = d_ptr - off; \ if (LIKELY(off >= ZXC_PAD_SIZE)) { \ zxc_copy32(d_ptr, match_src); \ if (UNLIKELY(ml > ZXC_PAD_SIZE)) { \ uint8_t* out = d_ptr + ZXC_PAD_SIZE; \ const uint8_t* ref = match_src + ZXC_PAD_SIZE; \ size_t rem = ml - ZXC_PAD_SIZE; \ while (rem > ZXC_PAD_SIZE) { \ zxc_copy32(out, ref); \ out += ZXC_PAD_SIZE; \ ref += ZXC_PAD_SIZE; \ rem -= ZXC_PAD_SIZE; \ } \ zxc_copy32(out, ref); \ } \ d_ptr += ml; \ } else if (off >= (ZXC_PAD_SIZE / 2)) { \ zxc_copy16(d_ptr, match_src); \ if (UNLIKELY(ml > (ZXC_PAD_SIZE / 2))) { \ uint8_t* out = d_ptr + (ZXC_PAD_SIZE / 2); \ const uint8_t* ref = match_src + (ZXC_PAD_SIZE / 2); \ size_t rem = ml - (ZXC_PAD_SIZE / 2); \ while (rem > (ZXC_PAD_SIZE / 2)) { \ zxc_copy16(out, ref); \ out += (ZXC_PAD_SIZE / 2); \ ref += (ZXC_PAD_SIZE / 2); \ rem -= (ZXC_PAD_SIZE / 2); \ } \ zxc_copy16(out, ref); \ } \ d_ptr += ml; \ } else if (off == 1) { \ ZXC_MEMSET(d_ptr, match_src[0], ml); \ d_ptr += ml; \ } else { \ size_t copied = 0; \ while (copied < ml) { \ zxc_copy_overlap16(d_ptr + copied, off); \ copied += (ZXC_PAD_SIZE / 2); \ } \ d_ptr += ml; \ } \ } while (0) // SAFE version: validates offset against written bytes #define DECODE_SEQ_SAFE(ll, ml, off) \ do { \ DECODE_COPY_LITERALS(ll); \ written += ll; \ if (UNLIKELY(off > written)) return ZXC_ERROR_BAD_OFFSET; \ DECODE_COPY_MATCH(ml, off); \ written += ml; \ } while (0) // FAST version: no offset validation (for use after written >= 256 or 65536) #define DECODE_SEQ_FAST(ll, ml, off) \ do { \ DECODE_COPY_LITERALS(ll); \ DECODE_COPY_MATCH(ml, off); \ } while (0) // --- SAFE Loop: offset validation until threshold (4x unroll) --- // For 1-byte offsets: bounds check until 256 bytes written // For 2-byte offsets: bounds check until 65536 bytes written const size_t bounds_threshold = (gh.enc_off == 1) ? (1U << 8) : (1U << 16); while (n_seq >= 4 && d_ptr < d_end_safe && l_ptr < l_end_safe_4x && written < bounds_threshold) { uint32_t tokens = zxc_le32(t_ptr); t_ptr += 4; uint32_t off1 = ZXC_LZ_OFFSET_BIAS, off2 = ZXC_LZ_OFFSET_BIAS, off3 = ZXC_LZ_OFFSET_BIAS, off4 = ZXC_LZ_OFFSET_BIAS; if (gh.enc_off == 1) { // Read 4 x 1-byte offsets uint32_t offsets = zxc_le32(o_ptr); o_ptr += 4; off1 += offsets & 0xFF; off2 += (offsets >> 8) & 0xFF; off3 += (offsets >> 16) & 0xFF; off4 += (offsets >> 24) & 0xFF; } else { // Read 4 x 2-byte offsets uint64_t offsets = zxc_le64(o_ptr); o_ptr += 8; off1 += (uint32_t)(offsets & 0xFFFF); off2 += (uint32_t)((offsets >> 16) & 0xFFFF); off3 += (uint32_t)((offsets >> 32) & 0xFFFF); off4 += (uint32_t)((offsets >> 48) & 0xFFFF); } uint32_t ll1 = (tokens & 0x0F0) >> 4; uint32_t ml1 = (tokens & 0x00F); if (UNLIKELY(ll1 == ZXC_TOKEN_LL_MASK)) { ll1 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll1 > l_end || d_ptr + ll1 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml1 == ZXC_TOKEN_ML_MASK)) { ml1 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll1 + ml1 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml1 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_SAFE(ll1, ml1, off1); uint32_t ll2 = (tokens & 0x0F000) >> 12; uint32_t ml2 = (tokens & 0x00F00) >> 8; if (UNLIKELY(ll2 == ZXC_TOKEN_LL_MASK)) { ll2 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll2 > l_end || d_ptr + ll2 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml2 == ZXC_TOKEN_ML_MASK)) { ml2 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll2 + ml2 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml2 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_SAFE(ll2, ml2, off2); uint32_t ll3 = (tokens & 0x0F00000) >> 20; uint32_t ml3 = (tokens & 0x00F0000) >> 16; if (UNLIKELY(ll3 == ZXC_TOKEN_LL_MASK)) { ll3 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll3 > l_end || d_ptr + ll3 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml3 == ZXC_TOKEN_ML_MASK)) { ml3 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll3 + ml3 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml3 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_SAFE(ll3, ml3, off3); uint32_t ll4 = (tokens >> 28); uint32_t ml4 = (tokens >> 24) & 0x0F; if (UNLIKELY(ll4 == ZXC_TOKEN_LL_MASK)) { ll4 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll4 > l_end || d_ptr + ll4 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml4 == ZXC_TOKEN_ML_MASK)) { ml4 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll4 + ml4 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml4 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_SAFE(ll4, ml4, off4); n_seq -= 4; } // --- FAST Loop: After threshold, no offset validation needed (4x unroll) --- while (n_seq >= 4 && d_ptr < d_end_safe && l_ptr < l_end_safe_4x) { uint32_t tokens = zxc_le32(t_ptr); t_ptr += 4; uint32_t off1 = ZXC_LZ_OFFSET_BIAS, off2 = ZXC_LZ_OFFSET_BIAS, off3 = ZXC_LZ_OFFSET_BIAS, off4 = ZXC_LZ_OFFSET_BIAS; if (gh.enc_off == 1) { // Read 4 x 1-byte offsets uint32_t offsets = zxc_le32(o_ptr); o_ptr += 4; off1 += offsets & 0xFF; off2 += (offsets >> 8) & 0xFF; off3 += (offsets >> 16) & 0xFF; off4 += (offsets >> 24) & 0xFF; } else { // Read 4 x 2-byte offsets uint64_t offsets = zxc_le64(o_ptr); o_ptr += 8; off1 += (uint32_t)(offsets & 0xFFFF); off2 += (uint32_t)((offsets >> 16) & 0xFFFF); off3 += (uint32_t)((offsets >> 32) & 0xFFFF); off4 += (uint32_t)((offsets >> 48) & 0xFFFF); } uint32_t ll1 = (tokens & 0x0F0) >> 4; uint32_t ml1 = (tokens & 0x00F); if (UNLIKELY(ll1 == ZXC_TOKEN_LL_MASK)) { ll1 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll1 > l_end || d_ptr + ll1 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml1 == ZXC_TOKEN_ML_MASK)) { ml1 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll1 + ml1 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml1 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_FAST(ll1, ml1, off1); uint32_t ll2 = (tokens & 0x0F000) >> 12; uint32_t ml2 = (tokens & 0x00F00) >> 8; if (UNLIKELY(ll2 == ZXC_TOKEN_LL_MASK)) { ll2 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll2 > l_end || d_ptr + ll2 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml2 == ZXC_TOKEN_ML_MASK)) { ml2 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll2 + ml2 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml2 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_FAST(ll2, ml2, off2); uint32_t ll3 = (tokens & 0x0F00000) >> 20; uint32_t ml3 = (tokens & 0x00F0000) >> 16; if (UNLIKELY(ll3 == ZXC_TOKEN_LL_MASK)) { ll3 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll3 > l_end || d_ptr + ll3 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml3 == ZXC_TOKEN_ML_MASK)) { ml3 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll3 + ml3 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml3 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_FAST(ll3, ml3, off3); uint32_t ll4 = (tokens >> 28); uint32_t ml4 = (tokens >> 24) & 0x0F; if (UNLIKELY(ll4 == ZXC_TOKEN_LL_MASK)) { ll4 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll4 > l_end || d_ptr + ll4 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } if (UNLIKELY(ml4 == ZXC_TOKEN_ML_MASK)) { ml4 += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(d_ptr + ll4 + ml4 + ZXC_LZ_MIN_MATCH_LEN + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } ml4 += ZXC_LZ_MIN_MATCH_LEN; DECODE_SEQ_FAST(ll4, ml4, off4); n_seq -= 4; } #undef DECODE_SEQ_SAFE #undef DECODE_SEQ_FAST #undef DECODE_COPY_LITERALS #undef DECODE_COPY_MATCH // Validate vbyte reads didn't overflow if (UNLIKELY(e_ptr > e_end)) return ZXC_ERROR_CORRUPT_DATA; // --- Remaining 1 sequence (Fast Path) --- while (n_seq > 0 && d_ptr < d_end_safe && l_ptr < l_end_safe_1x) { // Save pointers before reading (in case we need to fall back to Safe Path) const uint8_t* t_save = t_ptr; const uint8_t* o_save = o_ptr; const uint8_t* e_save = e_ptr; uint8_t token = *t_ptr++; uint32_t ll = token >> ZXC_TOKEN_LIT_BITS; uint32_t ml = token & ZXC_TOKEN_ML_MASK; uint32_t offset = ZXC_LZ_OFFSET_BIAS; if (gh.enc_off == 1) { offset += *o_ptr++; // 1-byte offset (biased) } else { offset += zxc_le16(o_ptr); // 2-byte offset (biased) o_ptr += 2; } if (UNLIKELY(ll == ZXC_TOKEN_LL_MASK)) { ll += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(l_ptr + ll > l_end)) { t_ptr = t_save; o_ptr = o_save; e_ptr = e_save; break; } } if (UNLIKELY(ml == ZXC_TOKEN_ML_MASK)) ml += zxc_read_varint(&e_ptr, e_end); ml += ZXC_LZ_MIN_MATCH_LEN; // Check bounds before wild copies - if too close to end, fall back to Safe Path if (UNLIKELY(d_ptr + ll + ml + ZXC_PAD_SIZE > d_end)) { // Restore pointers and let Safe Path handle this sequence t_ptr = t_save; o_ptr = o_save; e_ptr = e_save; break; } { const uint8_t* src_lit = l_ptr; uint8_t* dst_lit = d_ptr; zxc_copy32(dst_lit, src_lit); if (UNLIKELY(ll > ZXC_PAD_SIZE)) { dst_lit += ZXC_PAD_SIZE; src_lit += ZXC_PAD_SIZE; size_t rem = ll - ZXC_PAD_SIZE; while (rem > ZXC_PAD_SIZE) { zxc_copy32(dst_lit, src_lit); dst_lit += ZXC_PAD_SIZE; src_lit += ZXC_PAD_SIZE; rem -= ZXC_PAD_SIZE; } zxc_copy32(dst_lit, src_lit); } l_ptr += ll; d_ptr += ll; written += ll; } { // Skip check if written >= bounds_threshold (256 for 8-bit, 65536 for 16-bit) if (UNLIKELY(written < bounds_threshold && offset > written)) return ZXC_ERROR_BAD_OFFSET; const uint8_t* match_src = d_ptr - offset; if (LIKELY(offset >= ZXC_PAD_SIZE)) { zxc_copy32(d_ptr, match_src); if (UNLIKELY(ml > ZXC_PAD_SIZE)) { uint8_t* out = d_ptr + ZXC_PAD_SIZE; const uint8_t* ref = match_src + ZXC_PAD_SIZE; size_t rem = ml - ZXC_PAD_SIZE; while (rem > ZXC_PAD_SIZE) { zxc_copy32(out, ref); out += ZXC_PAD_SIZE; ref += ZXC_PAD_SIZE; rem -= ZXC_PAD_SIZE; } zxc_copy32(out, ref); } d_ptr += ml; written += ml; } else if (offset == 1) { ZXC_MEMSET(d_ptr, match_src[0], ml); d_ptr += ml; written += ml; } else { for (size_t i = 0; i < ml; i++) d_ptr[i] = match_src[i]; d_ptr += ml; written += ml; } } n_seq--; } // --- Safe Path for Remaining Sequences --- while (n_seq > 0) { uint8_t token = *t_ptr++; uint32_t ll = token >> ZXC_TOKEN_LIT_BITS; uint32_t ml = token & ZXC_TOKEN_ML_MASK; uint32_t offset = ZXC_LZ_OFFSET_BIAS; if (gh.enc_off == 1) { offset += *o_ptr++; // 1-byte offset (biased) } else { offset += zxc_le16(o_ptr); // 2-byte offset (biased) o_ptr += 2; } if (UNLIKELY(ll == ZXC_TOKEN_LL_MASK)) ll += zxc_read_varint(&e_ptr, e_end); if (UNLIKELY(ml == ZXC_TOKEN_ML_MASK)) ml += zxc_read_varint(&e_ptr, e_end); ml += ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(d_ptr + ll > d_end || l_ptr + ll > l_end)) return ZXC_ERROR_OVERFLOW; ZXC_MEMCPY(d_ptr, l_ptr, ll); l_ptr += ll; d_ptr += ll; const uint8_t* match_src = d_ptr - offset; if (UNLIKELY(match_src < dst || d_ptr + ml > d_end)) return ZXC_ERROR_BAD_OFFSET; if (offset < ml) { for (size_t i = 0; i < ml; i++) d_ptr[i] = match_src[i]; } else { ZXC_MEMCPY(d_ptr, match_src, ml); } d_ptr += ml; n_seq--; } // --- Trailing Literals --- // Copy remaining literals from source stream (literal exhaustion) if (UNLIKELY(l_ptr > l_end)) return ZXC_ERROR_CORRUPT_DATA; const size_t remaining_literals = (size_t)(l_end - l_ptr); if (remaining_literals > 0) { if (UNLIKELY(d_ptr + remaining_literals > d_end)) return ZXC_ERROR_OVERFLOW; ZXC_MEMCPY(d_ptr, l_ptr, remaining_literals); d_ptr += remaining_literals; } return (int)(d_ptr - dst); } /** * @brief Decodes a GHI (General High) format compressed block. * * This function handles the decoding of a compressed block formatted with the * internal GHI structure. The decompressed size is derived from Section * Descriptors within the compressed payload. * * @param[in] ctx Pointer to the decompression context (unused in current implementation). * @param[in] src Pointer to the source buffer containing compressed data. * @param[in] src_size Size of the source buffer in bytes. * @param[out] dst Pointer to the destination buffer for decompressed data. * @param[in] dst_capacity Capacity of the destination buffer in bytes. * @return int Returns the number of bytes written on success, or a negative zxc_error_t code on * failure. */ static int zxc_decode_block_ghi(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_size, uint8_t* RESTRICT dst, const size_t dst_capacity) { (void)ctx; zxc_gnr_header_t gh; zxc_section_desc_t desc[ZXC_GHI_SECTIONS]; if (UNLIKELY(zxc_read_ghi_header_and_desc(src, src_size, &gh, desc) != ZXC_OK)) return ZXC_ERROR_BAD_HEADER; const uint8_t* p_curr = src + ZXC_GHI_HEADER_BINARY_SIZE + ZXC_GHI_SECTIONS * ZXC_SECTION_DESC_BINARY_SIZE; // --- Stream Pointers & Validation --- const size_t sz_lit = (uint32_t)desc[0].sizes; const size_t sz_seqs = (uint32_t)desc[1].sizes; const size_t sz_exts = (uint32_t)desc[2].sizes; const uint8_t* l_ptr = p_curr; const uint8_t* l_end = l_ptr + sz_lit; p_curr += sz_lit; const uint8_t* seq_ptr = p_curr; const uint8_t* extras_ptr = p_curr + sz_seqs; const uint8_t* const extras_end = extras_ptr + sz_exts; // Validate streams don't overflow source buffer + // Validate sequence stream size matches sequence count if (UNLIKELY((extras_end != src + src_size) || (sz_seqs < (size_t)gh.n_sequences * 4))) return ZXC_ERROR_CORRUPT_DATA; uint8_t* d_ptr = dst; const uint8_t* const d_end = dst + dst_capacity; const uint8_t* const d_end_safe = d_end - (ZXC_PAD_SIZE * 4); // 128 // Safety margin for 4x unrolled loop: 4 * (ZXC_SEQ_LL_MASK LL + // ZXC_SEQ_ML_MASK+ZXC_LZ_MIN_MATCH_LEN ML) + ZXC_PAD_SIZE Pad = 4 x (255 + 255 + 5) + 32 = 2092 const uint8_t* const d_end_fast = d_end - (ZXC_PAD_SIZE * 66); // 2112 // Literal stream safe thresholds for GHI loops. // Without varint extension, max ll per sequence = ZXC_SEQ_LL_MASK - 1 = 254. // For 4 sequences: 4 * 254 = 1016. With this margin, l_ptr checks are only needed // on the cold varint path, keeping the hot path free of l_ptr overhead. const size_t ghi_margin_4x = 4 * (ZXC_SEQ_LL_MASK - 1); // 1016 const size_t ghi_margin_1x = ZXC_SEQ_LL_MASK - 1; // 254 const uint8_t* const l_end_safe_4x = (sz_lit > ghi_margin_4x) ? l_end - ghi_margin_4x : l_ptr; const uint8_t* const l_end_safe_1x = (sz_lit > ghi_margin_1x) ? l_end - ghi_margin_1x : l_ptr; uint32_t n_seq = gh.n_sequences; // Track bytes written for offset validation // For 1-byte offsets (enc_off==1): validate until 256 bytes written (max 8-bit offset) // For 2-byte offsets (enc_off==0): validate until 65536 bytes written (max 16-bit offset) // After threshold, all offsets are guaranteed valid (can't exceed written bytes) size_t written = 0; // --- Factored sub-macros for literal copy + match copy --- // Used by DECODE_SEQ_SAFE and DECODE_SEQ_FAST to avoid duplication. #define DECODE_COPY_LITERALS(ll) \ do { \ const uint8_t* src_lit = l_ptr; \ uint8_t* dst_lit = d_ptr; \ zxc_copy32(dst_lit, src_lit); \ if (UNLIKELY(ll > ZXC_PAD_SIZE)) { \ dst_lit += ZXC_PAD_SIZE; \ src_lit += ZXC_PAD_SIZE; \ size_t rem = ll - ZXC_PAD_SIZE; \ while (rem > ZXC_PAD_SIZE) { \ zxc_copy32(dst_lit, src_lit); \ dst_lit += ZXC_PAD_SIZE; \ src_lit += ZXC_PAD_SIZE; \ rem -= ZXC_PAD_SIZE; \ } \ zxc_copy32(dst_lit, src_lit); \ } \ l_ptr += ll; \ d_ptr += ll; \ } while (0) #define DECODE_COPY_MATCH(ml, off) \ do { \ const uint8_t* match_src = d_ptr - off; \ if (LIKELY(off >= ZXC_PAD_SIZE)) { \ zxc_copy32(d_ptr, match_src); \ if (UNLIKELY(ml > ZXC_PAD_SIZE)) { \ uint8_t* out = d_ptr + ZXC_PAD_SIZE; \ const uint8_t* ref = match_src + ZXC_PAD_SIZE; \ size_t rem = ml - ZXC_PAD_SIZE; \ while (rem > ZXC_PAD_SIZE) { \ zxc_copy32(out, ref); \ out += ZXC_PAD_SIZE; \ ref += ZXC_PAD_SIZE; \ rem -= ZXC_PAD_SIZE; \ } \ zxc_copy32(out, ref); \ } \ d_ptr += ml; \ } else if (off >= (ZXC_PAD_SIZE / 2)) { \ zxc_copy16(d_ptr, match_src); \ if (UNLIKELY(ml > (ZXC_PAD_SIZE / 2))) { \ uint8_t* out = d_ptr + (ZXC_PAD_SIZE / 2); \ const uint8_t* ref = match_src + (ZXC_PAD_SIZE / 2); \ size_t rem = ml - (ZXC_PAD_SIZE / 2); \ while (rem > (ZXC_PAD_SIZE / 2)) { \ zxc_copy16(out, ref); \ out += (ZXC_PAD_SIZE / 2); \ ref += (ZXC_PAD_SIZE / 2); \ rem -= (ZXC_PAD_SIZE / 2); \ } \ zxc_copy16(out, ref); \ } \ d_ptr += ml; \ } else if (off == 1) { \ ZXC_MEMSET(d_ptr, match_src[0], ml); \ d_ptr += ml; \ } else { \ size_t copied = 0; \ while (copied < ml) { \ zxc_copy_overlap16(d_ptr + copied, off); \ copied += (ZXC_PAD_SIZE / 2); \ } \ d_ptr += ml; \ } \ } while (0) #define DECODE_SEQ_SAFE(ll, ml, off) \ do { \ DECODE_COPY_LITERALS(ll); \ written += ll; \ if (UNLIKELY(off > written)) return ZXC_ERROR_BAD_OFFSET; \ DECODE_COPY_MATCH(ml, off); \ written += ml; \ } while (0) #define DECODE_SEQ_FAST(ll, ml, off) \ do { \ DECODE_COPY_LITERALS(ll); \ DECODE_COPY_MATCH(ml, off); \ } while (0) // --- SAFE Loop: offset validation until threshold (4x unroll) --- // Since offset is 16-bit, threshold is 65536. // For 1-byte offsets (enc_off==1): validate until 256 bytes written // For 2-byte offsets (enc_off==0): validate until 65536 bytes written const size_t bounds_threshold = (gh.enc_off == 1) ? (1U << 8) : (1U << 16); while (n_seq >= 4 && d_ptr < d_end_safe && l_ptr < l_end_safe_4x && written < bounds_threshold) { uint32_t s1 = zxc_le32(seq_ptr); uint32_t s2 = zxc_le32(seq_ptr + 4); uint32_t s3 = zxc_le32(seq_ptr + 8); uint32_t s4 = zxc_le32(seq_ptr + 12); seq_ptr += 16; uint32_t ll1 = (uint32_t)(s1 >> 24); if (UNLIKELY(ll1 == ZXC_SEQ_LL_MASK)) { ll1 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll1 > l_end || d_ptr + ll1 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m1b = (uint32_t)((s1 >> 16) & 0xFF); uint32_t ml1 = m1b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m1b == ZXC_SEQ_ML_MASK)) { ml1 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll1 + ml1 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off1 = (uint32_t)(s1 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_SAFE(ll1, ml1, off1); uint32_t ll2 = (uint32_t)(s2 >> 24); if (UNLIKELY(ll2 == ZXC_SEQ_LL_MASK)) { ll2 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll2 > l_end || d_ptr + ll2 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m2b = (uint32_t)((s2 >> 16) & 0xFF); uint32_t ml2 = m2b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m2b == ZXC_SEQ_ML_MASK)) { ml2 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll2 + ml2 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off2 = (uint32_t)(s2 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_SAFE(ll2, ml2, off2); uint32_t ll3 = (uint32_t)(s3 >> 24); if (UNLIKELY(ll3 == ZXC_SEQ_LL_MASK)) { ll3 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll3 > l_end || d_ptr + ll3 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m3b = (uint32_t)((s3 >> 16) & 0xFF); uint32_t ml3 = m3b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m3b == ZXC_SEQ_ML_MASK)) { ml3 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll3 + ml3 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off3 = (uint32_t)(s3 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_SAFE(ll3, ml3, off3); uint32_t ll4 = (uint32_t)(s4 >> 24); if (UNLIKELY(ll4 == ZXC_SEQ_LL_MASK)) { ll4 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll4 > l_end || d_ptr + ll4 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m4b = (uint32_t)((s4 >> 16) & 0xFF); uint32_t ml4 = m4b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m4b == ZXC_SEQ_ML_MASK)) { ml4 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll4 + ml4 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off4 = (uint32_t)(s4 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_SAFE(ll4, ml4, off4); n_seq -= 4; } // --- SAFE Loop tail: remaining sequences with offset validation (1x) --- while (n_seq > 0 && d_ptr < d_end_safe && written < bounds_threshold) { uint32_t seq = zxc_le32(seq_ptr); seq_ptr += 4; uint32_t ll = (uint32_t)(seq >> 24); if (UNLIKELY(ll == ZXC_SEQ_LL_MASK)) ll += zxc_read_varint(&extras_ptr, extras_end); uint32_t m_bits = (uint32_t)((seq >> 16) & 0xFF); uint32_t ml = m_bits + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m_bits == ZXC_SEQ_ML_MASK)) ml += zxc_read_varint(&extras_ptr, extras_end); uint32_t offset = (uint32_t)(seq & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; // Strict bounds check: sequence must fit, AND wild copies must not overshoot // Check both destination (d_ptr) and source literal stream (l_ptr) if (UNLIKELY(d_ptr + ll + ml + ZXC_PAD_SIZE > d_end || l_ptr + ll + ZXC_PAD_SIZE > l_end)) { // Fallback to exact copy (slow but safe) if (UNLIKELY(d_ptr + ll > d_end || l_ptr + ll > l_end)) return ZXC_ERROR_OVERFLOW; ZXC_MEMCPY(d_ptr, l_ptr, ll); l_ptr += ll; d_ptr += ll; written += ll; if (UNLIKELY(offset > written || d_ptr + ml > d_end)) return ZXC_ERROR_BAD_OFFSET; const uint8_t* match_src = d_ptr - offset; if (offset < ml) { for (size_t i = 0; i < ml; i++) d_ptr[i] = match_src[i]; } else { ZXC_MEMCPY(d_ptr, match_src, ml); } d_ptr += ml; written += ml; } else { // Safe to process with wild copies DECODE_SEQ_SAFE(ll, ml, offset); } n_seq--; } // --- FAST Loop: After threshold, check large margin to avoid individual bounds checks --- while (n_seq >= 4 && d_ptr < d_end_fast && l_ptr < l_end_safe_4x) { uint32_t s1 = zxc_le32(seq_ptr); uint32_t s2 = zxc_le32(seq_ptr + 4); uint32_t s3 = zxc_le32(seq_ptr + 8); uint32_t s4 = zxc_le32(seq_ptr + 12); seq_ptr += 16; // Prefetch ahead in literal and extras streams to hide memory latency ZXC_PREFETCH_READ(l_ptr + ZXC_CACHE_LINE_SIZE); uint32_t ll1 = (uint32_t)(s1 >> 24); if (UNLIKELY(ll1 == ZXC_SEQ_LL_MASK)) { ll1 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll1 > l_end || d_ptr + ll1 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m1b = (uint32_t)((s1 >> 16) & 0xFF); uint32_t ml1 = m1b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m1b == ZXC_SEQ_ML_MASK)) { ml1 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll1 + ml1 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off1 = (uint32_t)(s1 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_FAST(ll1, ml1, off1); uint32_t ll2 = (uint32_t)(s2 >> 24); if (UNLIKELY(ll2 == ZXC_SEQ_LL_MASK)) { ll2 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll2 > l_end || d_ptr + ll2 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m2b = (uint32_t)((s2 >> 16) & 0xFF); uint32_t ml2 = m2b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m2b == ZXC_SEQ_ML_MASK)) { ml2 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll2 + ml2 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off2 = (uint32_t)(s2 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_FAST(ll2, ml2, off2); uint32_t ll3 = (uint32_t)(s3 >> 24); if (UNLIKELY(ll3 == ZXC_SEQ_LL_MASK)) { ll3 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll3 > l_end || d_ptr + ll3 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m3b = (uint32_t)((s3 >> 16) & 0xFF); uint32_t ml3 = m3b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m3b == ZXC_SEQ_ML_MASK)) { ml3 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll3 + ml3 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off3 = (uint32_t)(s3 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_FAST(ll3, ml3, off3); uint32_t ll4 = (uint32_t)(s4 >> 24); if (UNLIKELY(ll4 == ZXC_SEQ_LL_MASK)) { ll4 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll4 > l_end || d_ptr + ll4 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t m4b = (uint32_t)((s4 >> 16) & 0xFF); uint32_t ml4 = m4b + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m4b == ZXC_SEQ_ML_MASK)) { ml4 += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(d_ptr + ll4 + ml4 + ZXC_PAD_SIZE > d_end)) return ZXC_ERROR_OVERFLOW; } uint32_t off4 = (uint32_t)(s4 & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; DECODE_SEQ_FAST(ll4, ml4, off4); n_seq -= 4; } #undef DECODE_SEQ_SAFE #undef DECODE_SEQ_FAST #undef DECODE_COPY_LITERALS #undef DECODE_COPY_MATCH // --- Remaining 1 sequence (Fast Path) --- while (n_seq > 0 && d_ptr < d_end_safe && l_ptr < l_end_safe_1x) { // Save state for fallback const uint8_t* seq_save = seq_ptr; const uint8_t* ext_save = extras_ptr; const uint32_t seq = zxc_le32(seq_ptr); seq_ptr += 4; uint32_t ll = (uint32_t)(seq >> 24); if (UNLIKELY(ll == ZXC_SEQ_LL_MASK)) { ll += zxc_read_varint(&extras_ptr, extras_end); if (UNLIKELY(l_ptr + ll > l_end)) { seq_ptr = seq_save; extras_ptr = ext_save; break; } } uint32_t m_bits = (uint32_t)((seq >> 16) & 0xFF); uint32_t ml = m_bits + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m_bits == ZXC_SEQ_ML_MASK)) ml += zxc_read_varint(&extras_ptr, extras_end); // Strict bounds checks (including wild copy overrun safety) if (UNLIKELY(d_ptr + ll + ml + ZXC_PAD_SIZE > d_end)) { // Restore state and break to Safe Path seq_ptr = seq_save; extras_ptr = ext_save; break; } uint32_t offset = (uint32_t)(seq & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; { const uint8_t* src_lit = l_ptr; uint8_t* dst_lit = d_ptr; zxc_copy32(dst_lit, src_lit); if (UNLIKELY(ll > ZXC_PAD_SIZE)) { dst_lit += ZXC_PAD_SIZE; src_lit += ZXC_PAD_SIZE; size_t rem = ll - ZXC_PAD_SIZE; while (rem > ZXC_PAD_SIZE) { zxc_copy32(dst_lit, src_lit); dst_lit += ZXC_PAD_SIZE; src_lit += ZXC_PAD_SIZE; rem -= ZXC_PAD_SIZE; } zxc_copy32(dst_lit, src_lit); } l_ptr += ll; d_ptr += ll; written += ll; } { // Skip check if written >= bounds_threshold (256 for 8-bit, 65536 for 16-bit) if (UNLIKELY(written < bounds_threshold && offset > written)) return ZXC_ERROR_BAD_OFFSET; const uint8_t* match_src = d_ptr - offset; if (LIKELY(offset >= ZXC_PAD_SIZE)) { zxc_copy32(d_ptr, match_src); if (UNLIKELY(ml > ZXC_PAD_SIZE)) { uint8_t* out = d_ptr + ZXC_PAD_SIZE; const uint8_t* ref = match_src + ZXC_PAD_SIZE; size_t rem = ml - ZXC_PAD_SIZE; while (rem > ZXC_PAD_SIZE) { zxc_copy32(out, ref); out += ZXC_PAD_SIZE; ref += ZXC_PAD_SIZE; rem -= ZXC_PAD_SIZE; } zxc_copy32(out, ref); } d_ptr += ml; written += ml; } else if (offset == 1) { ZXC_MEMSET(d_ptr, match_src[0], ml); d_ptr += ml; written += ml; } else { for (size_t i = 0; i < ml; i++) d_ptr[i] = match_src[i]; d_ptr += ml; written += ml; } } n_seq--; } // --- Safe Path for Remaining Sequences --- while (n_seq > 0) { uint32_t seq = zxc_le32(seq_ptr); seq_ptr += 4; uint32_t ll = (uint32_t)(seq >> 24); if (UNLIKELY(ll == ZXC_SEQ_LL_MASK)) ll += zxc_read_varint(&extras_ptr, extras_end); uint32_t m_bits = (uint32_t)((seq >> 16) & 0xFF); uint32_t ml = m_bits + ZXC_LZ_MIN_MATCH_LEN; if (UNLIKELY(m_bits == ZXC_SEQ_ML_MASK)) ml += zxc_read_varint(&extras_ptr, extras_end); uint32_t offset = (uint32_t)(seq & 0xFFFF) + ZXC_LZ_OFFSET_BIAS; if (UNLIKELY(d_ptr + ll > d_end || l_ptr + ll > l_end)) return ZXC_ERROR_OVERFLOW; ZXC_MEMCPY(d_ptr, l_ptr, ll); l_ptr += ll; d_ptr += ll; const uint8_t* match_src = d_ptr - offset; if (UNLIKELY(match_src < dst || d_ptr + ml > d_end)) return ZXC_ERROR_BAD_OFFSET; if (offset < ml) { for (size_t i = 0; i < ml; i++) d_ptr[i] = match_src[i]; } else { ZXC_MEMCPY(d_ptr, match_src, ml); } d_ptr += ml; n_seq--; } // --- Trailing Literals --- // Copy remaining literals from source stream (literal exhaustion) if (UNLIKELY(l_ptr > l_end)) return ZXC_ERROR_CORRUPT_DATA; const size_t remaining_literals = (size_t)(l_end - l_ptr); if (remaining_literals > 0) { if (UNLIKELY(d_ptr + remaining_literals > d_end)) return ZXC_ERROR_OVERFLOW; ZXC_MEMCPY(d_ptr, l_ptr, remaining_literals); d_ptr += remaining_literals; } return (int)(d_ptr - dst); } // cppcheck-suppress unusedFunction int zxc_decompress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) { if (UNLIKELY(src_sz < ZXC_BLOCK_HEADER_SIZE)) return ZXC_ERROR_SRC_TOO_SMALL; const uint8_t type = src[0]; const uint32_t comp_sz = zxc_le32(src + 3); const int has_crc = ctx->checksum_enabled; // Check bounds: Header + Body + Checksum(if any) const size_t expected_sz = (size_t)ZXC_BLOCK_HEADER_SIZE + comp_sz + (has_crc ? ZXC_BLOCK_CHECKSUM_SIZE : 0); if (UNLIKELY(src_sz < expected_sz)) return ZXC_ERROR_SRC_TOO_SMALL; const uint8_t* data = src + ZXC_BLOCK_HEADER_SIZE; if (has_crc) { const uint32_t stored = zxc_le32(data + comp_sz); const uint32_t calc = zxc_checksum(data, comp_sz, ZXC_CHECKSUM_RAPIDHASH); if (UNLIKELY(stored != calc)) return ZXC_ERROR_BAD_CHECKSUM; } int decoded_sz = ZXC_ERROR_BAD_BLOCK_TYPE; switch (type) { case ZXC_BLOCK_GLO: decoded_sz = zxc_decode_block_glo(ctx, data, comp_sz, dst, dst_cap); break; case ZXC_BLOCK_GHI: decoded_sz = zxc_decode_block_ghi(ctx, data, comp_sz, dst, dst_cap); break; case ZXC_BLOCK_RAW: // For RAW blocks, comp_sz == raw_sz (uncompressed data stored as-is) if (UNLIKELY(comp_sz > dst_cap)) return ZXC_ERROR_DST_TOO_SMALL; ZXC_MEMCPY(dst, data, comp_sz); decoded_sz = (int)comp_sz; break; case ZXC_BLOCK_NUM: decoded_sz = zxc_decode_block_num(data, comp_sz, dst, dst_cap); break; case ZXC_BLOCK_EOF: // EOF should be handled by the dispatcher, not here return ZXC_ERROR_CORRUPT_DATA; default: return ZXC_ERROR_BAD_BLOCK_TYPE; } return decoded_sz; } zxc-0.10.0/src/lib/zxc_dispatch.c000066400000000000000000001140501517010557200165640ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_dispatch.c * @brief Runtime CPU feature detection and SIMD dispatch layer. * * Detects AVX2/AVX512/NEON at runtime and routes compress/decompress calls * to the best available implementation via lazy-initialised function pointers. * Also contains the public one-shot buffer API (@ref zxc_compress, * @ref zxc_decompress, @ref zxc_get_decompressed_size). */ #include "../../include/zxc_error.h" #include "../../include/zxc_seekable.h" #include "zxc_internal.h" /* * ZXC_DISABLE_SIMD => force ZXC_ONLY_DEFAULT so the dispatcher never selects * an AVX2/AVX512/NEON variant. */ #if defined(ZXC_DISABLE_SIMD) && !defined(ZXC_ONLY_DEFAULT) #define ZXC_ONLY_DEFAULT #endif #if defined(_MSC_VER) #include #endif #if defined(__linux__) && (defined(__arm__) || defined(_M_ARM)) #include #include #endif /* * ============================================================================ * PROTOTYPES FOR MULTI-VERSIONED VARIANTS * ============================================================================ * These are compiled in separate translation units with different flags. */ // Decompression Prototypes int zxc_decompress_chunk_wrapper_default(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); #ifndef ZXC_ONLY_DEFAULT #if defined(__x86_64__) || defined(_M_X64) int zxc_decompress_chunk_wrapper_avx2(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); int zxc_decompress_chunk_wrapper_avx512(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); #elif defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) int zxc_decompress_chunk_wrapper_neon(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); #endif #endif // Compression Prototypes int zxc_compress_chunk_wrapper_default(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); #if defined(__x86_64__) || defined(_M_X64) int zxc_compress_chunk_wrapper_avx2(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); int zxc_compress_chunk_wrapper_avx512(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); #elif defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) int zxc_compress_chunk_wrapper_neon(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); #endif /* * ============================================================================ * CPU DETECTION LOGIC * ============================================================================ */ /** * @enum zxc_cpu_feature_t * @brief Detected CPU SIMD capability level. */ typedef enum { ZXC_CPU_GENERIC = 0, /**< @brief Scalar-only fallback. */ ZXC_CPU_AVX2 = 1, /**< @brief x86-64 AVX2 available. */ ZXC_CPU_AVX512 = 2, /**< @brief x86-64 AVX-512F+BW available. */ ZXC_CPU_NEON = 3 /**< @brief ARM NEON available. */ } zxc_cpu_feature_t; /** * @brief Probes the running CPU for SIMD support. * * Uses CPUID on x86-64 (MSVC and GCC/Clang paths), `getauxval` on * 32-bit ARM Linux, and compile-time constants on AArch64. * * @return The highest @ref zxc_cpu_feature_t level supported. */ static zxc_cpu_feature_t zxc_detect_cpu_features(void) { #ifdef ZXC_ONLY_DEFAULT return ZXC_CPU_GENERIC; #else zxc_cpu_feature_t features = ZXC_CPU_GENERIC; #if defined(__x86_64__) || defined(_M_X64) #if defined(_MSC_VER) // MSVC detection using __cpuid // Function ID 1: EAX=1. ECX: Bit 28=AVX. // Function ID 7: EAX=7, ECX=0. EBX: Bit 5=AVX2, Bit 16=AVX512F, Bit 30=AVX512BW. int regs[4]; int avx = 0; int avx2 = 0; int avx512 = 0; __cpuid(regs, 1); if (regs[2] & (1 << 28)) avx = 1; if (avx) { __cpuidex(regs, 7, 0); if (regs[1] & (1 << 5)) avx2 = 1; if ((regs[1] & (1 << 16)) && (regs[1] & (1 << 30))) avx512 = 1; } if (avx512) { features = ZXC_CPU_AVX512; } else if (avx2) { features = ZXC_CPU_AVX2; } #else // GCC/Clang built-in detection __builtin_cpu_init(); if (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512bw")) { features = ZXC_CPU_AVX512; } else if (__builtin_cpu_supports("avx2")) { features = ZXC_CPU_AVX2; } #endif #elif defined(__aarch64__) || defined(_M_ARM64) // ARM64 usually guarantees NEON features = ZXC_CPU_NEON; #elif defined(__arm__) || defined(_M_ARM) // ARM32 Runtime detection for Linux #if defined(__linux__) const unsigned long hwcaps = getauxval(AT_HWCAP); if (hwcaps & HWCAP_NEON) { features = ZXC_CPU_NEON; } #else // Fallback for non-Linux: rely on compiler flags. // If compiled with -mfpu=neon, we assume target supports it. // Otherwise, safe default is GENERIC. #if defined(__ARM_NEON) features = ZXC_CPU_NEON; #endif #endif #endif return features; #endif } /* * ============================================================================ * DISPATCHERS * ============================================================================ * We use a function pointer initialized on first use (lazy initialization). */ /** @brief Function pointer type for the chunk decompressor. */ typedef int (*zxc_decompress_func_t)(zxc_cctx_t* RESTRICT, const uint8_t* RESTRICT, const size_t, uint8_t* RESTRICT, const size_t); /** @brief Function pointer type for the chunk compressor. */ typedef int (*zxc_compress_func_t)(zxc_cctx_t* RESTRICT, const uint8_t* RESTRICT, const size_t, uint8_t* RESTRICT, const size_t); /** @brief Lazily-resolved pointer to the best decompression variant. */ static ZXC_ATOMIC zxc_decompress_func_t zxc_decompress_ptr = (zxc_decompress_func_t)0; /** @brief Lazily-resolved pointer to the best compression variant. */ static ZXC_ATOMIC zxc_compress_func_t zxc_compress_ptr = (zxc_compress_func_t)0; /** * @brief First-call initialiser for the decompression dispatcher. * * Detects CPU features, selects the best implementation, stores the * pointer atomically, then tail-calls into it. */ static int zxc_decompress_dispatch_init(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) { const zxc_cpu_feature_t cpu = zxc_detect_cpu_features(); zxc_decompress_func_t zxc_decompress_ptr_local = NULL; #ifndef ZXC_ONLY_DEFAULT #if defined(__x86_64__) || defined(_M_X64) if (cpu == ZXC_CPU_AVX512) zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_avx512; else if (cpu == ZXC_CPU_AVX2) zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_avx2; else zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default; #elif defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) // cppcheck-suppress knownConditionTrueFalse if (cpu == ZXC_CPU_NEON) zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_neon; else zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default; #else (void)cpu; zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default; #endif #else (void)cpu; zxc_decompress_ptr_local = zxc_decompress_chunk_wrapper_default; #endif #if ZXC_USE_C11_ATOMICS atomic_store_explicit(&zxc_decompress_ptr, zxc_decompress_ptr_local, memory_order_release); #else zxc_decompress_ptr = zxc_decompress_ptr_local; #endif return zxc_decompress_ptr_local(ctx, src, src_sz, dst, dst_cap); } /** * @brief First-call initialiser for the compression dispatcher. * * Detects CPU features, selects the best implementation, stores the * pointer atomically, then tail-calls into it. */ static int zxc_compress_dispatch_init(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) { const zxc_cpu_feature_t cpu = zxc_detect_cpu_features(); zxc_compress_func_t zxc_compress_ptr_local = NULL; #ifndef ZXC_ONLY_DEFAULT #if defined(__x86_64__) || defined(_M_X64) if (cpu == ZXC_CPU_AVX512) zxc_compress_ptr_local = zxc_compress_chunk_wrapper_avx512; else if (cpu == ZXC_CPU_AVX2) zxc_compress_ptr_local = zxc_compress_chunk_wrapper_avx2; else zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default; #elif defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) // cppcheck-suppress knownConditionTrueFalse if (cpu == ZXC_CPU_NEON) zxc_compress_ptr_local = zxc_compress_chunk_wrapper_neon; else zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default; #else (void)cpu; zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default; #endif #else (void)cpu; zxc_compress_ptr_local = zxc_compress_chunk_wrapper_default; #endif #if ZXC_USE_C11_ATOMICS atomic_store_explicit(&zxc_compress_ptr, zxc_compress_ptr_local, memory_order_release); #else zxc_compress_ptr = zxc_compress_ptr_local; #endif return zxc_compress_ptr_local(ctx, src, src_sz, dst, dst_cap); } /** * @brief Public decompression dispatcher (calls lazily-resolved implementation). * * @param[in,out] ctx Decompression context. * @param[in] src Compressed input chunk (header + payload + optional checksum). * @param[in] src_sz Size of @p src in bytes. * @param[out] dst Destination buffer for decompressed data. * @param[in] dst_cap Capacity of @p dst. * @return Decompressed size in bytes, or a negative @ref zxc_error_t code. */ int zxc_decompress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) { #if ZXC_USE_C11_ATOMICS const zxc_decompress_func_t func = atomic_load_explicit(&zxc_decompress_ptr, memory_order_acquire); #else const zxc_decompress_func_t func = zxc_decompress_ptr; #endif if (UNLIKELY(!func)) return zxc_decompress_dispatch_init(ctx, src, src_sz, dst, dst_cap); return func(ctx, src, src_sz, dst, dst_cap); } /** * @brief Public compression dispatcher (calls lazily-resolved implementation). * * @param[in,out] ctx Compression context. * @param[in] src Uncompressed input chunk. * @param[in] src_sz Size of @p src in bytes. * @param[out] dst Destination buffer for compressed data. * @param[in] dst_cap Capacity of @p dst. * @return Compressed size in bytes, or a negative @ref zxc_error_t code. */ int zxc_compress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap) { #if ZXC_USE_C11_ATOMICS const zxc_compress_func_t func = atomic_load_explicit(&zxc_compress_ptr, memory_order_acquire); #else const zxc_compress_func_t func = zxc_compress_ptr; #endif if (UNLIKELY(!func)) return zxc_compress_dispatch_init(ctx, src, src_sz, dst, dst_cap); return func(ctx, src, src_sz, dst, dst_cap); } /* * ============================================================================ * PUBLIC UTILITY API * ============================================================================ * These wrapper functions provide a simplified interface by managing context * allocation and looping over blocks. They call the dispatched wrappers above. */ /** * @brief Compresses an entire buffer in one call. * * Manages context allocation internally, loops over blocks, writes the * file header / EOF block / footer, and accumulates the global checksum. * * @param[in] src Uncompressed input data. * @param[in] src_size Size of @p src in bytes. * @param[out] dst Destination buffer (use zxc_compress_bound() to size). * @param[in] dst_capacity Capacity of @p dst. * @param[in] level Compression level (1-5). * @param[in] checksum_enabled Non-zero to enable per-block and global checksums. * @return Total compressed size in bytes, or a negative @ref zxc_error_t code. */ // cppcheck-suppress unusedFunction int64_t zxc_compress(const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, const size_t dst_capacity, const zxc_compress_opts_t* opts) { if (UNLIKELY(!src || !dst || src_size == 0 || dst_capacity == 0)) return ZXC_ERROR_NULL_INPUT; const int checksum_enabled = opts ? opts->checksum_enabled : 0; const int seekable = opts ? opts->seekable : 0; const int level = (opts && opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT; const size_t block_size = (opts && opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT; if (UNLIKELY(!zxc_validate_block_size(block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; const uint8_t* ip = (const uint8_t*)src; uint8_t* op = (uint8_t*)dst; const uint8_t* op_start = op; const uint8_t* op_end = op + dst_capacity; uint32_t global_hash = 0; zxc_cctx_t ctx; // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&ctx, block_size, 1, level, checksum_enabled) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP const int h_val = zxc_write_file_header(op, (size_t)(op_end - op), block_size, checksum_enabled); // LCOV_EXCL_START if (UNLIKELY(h_val < 0)) { zxc_cctx_free(&ctx); return h_val; } // LCOV_EXCL_STOP op += h_val; /* Seekable: dynamic array for per-block compressed sizes */ uint32_t* seek_comp = NULL; uint32_t seek_count = 0; uint32_t seek_cap = 0; if (seekable) { const size_t block_count = src_size / block_size; if (UNLIKELY(block_count > (size_t)UINT32_MAX - 2)) { zxc_cctx_free(&ctx); return ZXC_ERROR_BAD_BLOCK_SIZE; } seek_cap = (uint32_t)(block_count + 2); seek_comp = (uint32_t*)malloc(seek_cap * sizeof(uint32_t)); // LCOV_EXCL_START if (UNLIKELY(!seek_comp)) { zxc_cctx_free(&ctx); return ZXC_ERROR_MEMORY; } // LCOV_EXCL_STOP } size_t pos = 0; while (pos < src_size) { const size_t chunk_len = (src_size - pos > block_size) ? block_size : (src_size - pos); const size_t rem_cap = (size_t)(op_end - op); const int res = zxc_compress_chunk_wrapper(&ctx, ip + pos, chunk_len, op, rem_cap); if (UNLIKELY(res < 0)) { free(seek_comp); zxc_cctx_free(&ctx); return res; } if (checksum_enabled) { // Update Global Hash (Rotation + XOR) // Block checksum is at the end of the written block data if (LIKELY(res >= ZXC_GLOBAL_CHECKSUM_SIZE)) { const uint32_t block_hash = zxc_le32(op + res - ZXC_GLOBAL_CHECKSUM_SIZE); global_hash = zxc_hash_combine_rotate(global_hash, block_hash); } } /* Seekable: record compressed block size */ if (seekable) { // LCOV_EXCL_START if (UNLIKELY(seek_count >= seek_cap)) { seek_cap = seek_cap * 2; uint32_t* nc = (uint32_t*)realloc(seek_comp, seek_cap * sizeof(uint32_t)); if (UNLIKELY(!nc)) { free(seek_comp); zxc_cctx_free(&ctx); return ZXC_ERROR_MEMORY; } seek_comp = nc; } // LCOV_EXCL_STOP seek_comp[seek_count] = (uint32_t)res; seek_count++; } op += res; pos += chunk_len; } zxc_cctx_free(&ctx); // Write EOF Block const size_t rem_cap = (size_t)(op_end - op); const zxc_block_header_t eof_bh = { .block_type = ZXC_BLOCK_EOF, .block_flags = 0, .reserved = 0, .comp_size = 0}; const int eof_val = zxc_write_block_header(op, rem_cap, &eof_bh); // LCOV_EXCL_START if (UNLIKELY(eof_val < 0)) { free(seek_comp); return eof_val; } // LCOV_EXCL_STOP op += eof_val; /* Seekable: write seek table between EOF block and footer */ if (seekable && seek_count > 0) { const size_t st_cap = (size_t)(op_end - op); const int64_t st_val = zxc_write_seek_table(op, st_cap, seek_comp, seek_count); free(seek_comp); if (UNLIKELY(st_val < 0)) return (int64_t)st_val; // LCOV_EXCL_LINE op += st_val; } else { free(seek_comp); } if (UNLIKELY((size_t)(op_end - op) < ZXC_FILE_FOOTER_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; // LCOV_EXCL_LINE // Write 12-byte Footer: [Source Size (8)] + [Global Hash (4)] const int footer_val = zxc_write_file_footer(op, (size_t)(op_end - op), src_size, global_hash, checksum_enabled); if (UNLIKELY(footer_val < 0)) return footer_val; // LCOV_EXCL_LINE op += footer_val; return (int64_t)(op - op_start); } /** * @brief Decompresses an entire buffer in one call. * * Validates the file header and footer, loops over compressed blocks, * and verifies the global checksum when enabled. * * @param[in] src Compressed input data. * @param[in] src_size Size of @p src in bytes. * @param[out] dst Destination buffer for decompressed data. * @param[in] dst_capacity Capacity of @p dst. * @param[in] checksum_enabled Non-zero to verify per-block and global checksums. * @return Total decompressed size in bytes, or a negative @ref zxc_error_t code. */ // cppcheck-suppress unusedFunction int64_t zxc_decompress(const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, const size_t dst_capacity, const zxc_decompress_opts_t* opts) { if (UNLIKELY(!src || !dst || src_size < ZXC_FILE_HEADER_SIZE)) return ZXC_ERROR_NULL_INPUT; const int checksum_enabled = opts ? opts->checksum_enabled : 0; const uint8_t* ip = (const uint8_t*)src; const uint8_t* ip_end = ip + src_size; uint8_t* op = (uint8_t*)dst; const uint8_t* op_start = op; const uint8_t* op_end = op + dst_capacity; size_t runtime_chunk_size = 0; zxc_cctx_t ctx; int file_has_checksums = 0; // File header verification and context initialization if (UNLIKELY(zxc_read_file_header(ip, src_size, &runtime_chunk_size, &file_has_checksums) != ZXC_OK || zxc_cctx_init(&ctx, runtime_chunk_size, 0, 0, file_has_checksums && checksum_enabled) != ZXC_OK)) { return ZXC_ERROR_BAD_HEADER; } ip += ZXC_FILE_HEADER_SIZE; // GLO/GHI wild copies (zxc_copy32) overshoot by up to ZXC_PAD_SIZE bytes. // Decode into a padded scratch buffer, then memcpy the exact result out. const size_t work_sz = runtime_chunk_size + ZXC_PAD_SIZE; if (ctx.work_buf_cap < work_sz) { free(ctx.work_buf); ctx.work_buf = (uint8_t*)malloc(work_sz); // LCOV_EXCL_START if (UNLIKELY(!ctx.work_buf)) { zxc_cctx_free(&ctx); return ZXC_ERROR_MEMORY; } // LCOV_EXCL_STOP ctx.work_buf_cap = work_sz; } // Block decompression loop uint32_t global_hash = 0; while (ip < ip_end) { const size_t rem_src = (size_t)(ip_end - ip); zxc_block_header_t bh; // Read the block header to determine the compressed size if (UNLIKELY(zxc_read_block_header(ip, rem_src, &bh) != ZXC_OK)) { zxc_cctx_free(&ctx); return ZXC_ERROR_BAD_HEADER; } // Handle EOF block separately (not a real chunk to decompress) if (UNLIKELY(bh.block_type == ZXC_BLOCK_EOF)) { // Footer is always the last ZXC_FILE_FOOTER_SIZE bytes of the source, // even when a seek table is inserted between EOF block and footer. // LCOV_EXCL_START if (UNLIKELY(src_size < ZXC_FILE_FOOTER_SIZE)) { zxc_cctx_free(&ctx); return ZXC_ERROR_SRC_TOO_SMALL; } // LCOV_EXCL_STOP const uint8_t* const footer = (const uint8_t*)src + src_size - ZXC_FILE_FOOTER_SIZE; // Validate source size matches what we decompressed const uint64_t stored_size = zxc_le64(footer); if (UNLIKELY(stored_size != (uint64_t)(op - op_start))) { zxc_cctx_free(&ctx); return ZXC_ERROR_CORRUPT_DATA; } // Validate global checksum if enabled and file has checksums if (checksum_enabled && file_has_checksums) { const uint32_t stored_hash = zxc_le32(footer + sizeof(uint64_t)); if (UNLIKELY(stored_hash != global_hash)) { zxc_cctx_free(&ctx); return ZXC_ERROR_BAD_CHECKSUM; } } break; // EOF reached, exit loop } int res; const size_t rem_cap = (size_t)(op_end - op); if (LIKELY(rem_cap >= runtime_chunk_size + 2 * ZXC_PAD_SIZE)) { // Fast path: decode directly into dst. Cap dst_cap to chunk_size + PAD res = zxc_decompress_chunk_wrapper(&ctx, ip, rem_src, op, runtime_chunk_size + ZXC_PAD_SIZE); } else { // Safe path: decode into bounce buffer, then copy exact result. res = zxc_decompress_chunk_wrapper(&ctx, ip, rem_src, ctx.work_buf, runtime_chunk_size); if (LIKELY(res > 0)) { // LCOV_EXCL_START if (UNLIKELY((size_t)res > rem_cap)) { zxc_cctx_free(&ctx); return ZXC_ERROR_DST_TOO_SMALL; } // LCOV_EXCL_STOP ZXC_MEMCPY(op, ctx.work_buf, (size_t)res); } } if (UNLIKELY(res < 0)) { zxc_cctx_free(&ctx); return res; } // Update global hash from block checksum if (checksum_enabled && file_has_checksums) { const uint32_t block_hash = zxc_le32(ip + ZXC_BLOCK_HEADER_SIZE + bh.comp_size); global_hash = zxc_hash_combine_rotate(global_hash, block_hash); } ip += ZXC_BLOCK_HEADER_SIZE + bh.comp_size + (file_has_checksums ? ZXC_BLOCK_CHECKSUM_SIZE : 0); op += res; } zxc_cctx_free(&ctx); return (int64_t)(op - op_start); } /** * @brief Reads the decompressed size from a ZXC-compressed buffer. * * The size is stored in the file footer (last @ref ZXC_FILE_FOOTER_SIZE bytes). * * @param[in] src Compressed data. * @param[in] src_size Size of @p src in bytes. * @return Original uncompressed size, or 0 on error. */ uint64_t zxc_get_decompressed_size(const void* src, const size_t src_size) { if (UNLIKELY(src_size < ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE)) return 0; const uint8_t* const p = (const uint8_t*)src; if (UNLIKELY(zxc_le32(p) != ZXC_MAGIC_WORD)) return 0; const uint8_t* const footer = p + src_size - ZXC_FILE_FOOTER_SIZE; return zxc_le64(footer); } /* * ============================================================================ * REUSABLE CONTEXT API (Opaque) * ============================================================================ * * Provides heap-allocated, opaque contexts that integrators can reuse across * multiple compress / decompress calls, eliminating per-call malloc/free * overhead. */ /* --- Compression --------------------------------------------------------- */ struct zxc_cctx_s { zxc_cctx_t inner; /* existing internal context */ int initialized; /* 1 if inner has live allocations */ size_t last_block_size; /* block size used for last init */ /* Sticky options (remembered from create or last compress call). */ int stored_level; int stored_checksum; size_t stored_block_size; }; zxc_cctx* zxc_create_cctx(const zxc_compress_opts_t* opts) { zxc_cctx* const cctx = (zxc_cctx*)calloc(1, sizeof(zxc_cctx)); if (UNLIKELY(!cctx)) return NULL; // LCOV_EXCL_LINE /* Resolve and store sticky defaults. */ cctx->stored_level = (opts && opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT; cctx->stored_block_size = (opts && opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT; cctx->stored_checksum = opts ? opts->checksum_enabled : 0; if (opts) { // LCOV_EXCL_START if (UNLIKELY(!zxc_validate_block_size(cctx->stored_block_size) || zxc_cctx_init(&cctx->inner, cctx->stored_block_size, 1, cctx->stored_level, cctx->stored_checksum) != ZXC_OK)) { free(cctx); return NULL; } // LCOV_EXCL_STOP cctx->last_block_size = cctx->stored_block_size; cctx->initialized = 1; } return cctx; } void zxc_free_cctx(zxc_cctx* cctx) { if (!cctx) return; if (cctx->initialized) zxc_cctx_free(&cctx->inner); free(cctx); } int64_t zxc_compress_cctx(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, const size_t dst_capacity, const zxc_compress_opts_t* opts) { if (UNLIKELY(!cctx)) return ZXC_ERROR_NULL_INPUT; if (UNLIKELY(!src || !dst || src_size == 0 || dst_capacity == 0)) return ZXC_ERROR_NULL_INPUT; const int checksum_enabled = opts ? opts->checksum_enabled : cctx->stored_checksum; const int level = (opts && opts->level > 0) ? opts->level : cctx->stored_level; const size_t block_size = (opts && opts->block_size > 0) ? opts->block_size : cctx->stored_block_size; cctx->stored_level = level; cctx->stored_block_size = block_size; cctx->stored_checksum = checksum_enabled; if (UNLIKELY(!zxc_validate_block_size(block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; /* Re-init only when block_size changed (it drives buffer sizes). */ if (!cctx->initialized || cctx->last_block_size != block_size) { if (cctx->initialized) { zxc_cctx_free(&cctx->inner); cctx->initialized = 0; } // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&cctx->inner, block_size, 1, level, checksum_enabled) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP cctx->last_block_size = block_size; cctx->initialized = 1; } else { /* Same block_size: update level + checksum without realloc. */ cctx->inner.compression_level = level; cctx->inner.checksum_enabled = checksum_enabled; } zxc_cctx_t* const ctx = &cctx->inner; uint8_t* op = (uint8_t*)dst; const uint8_t* const op_start = op; const uint8_t* const op_end = op + dst_capacity; const uint8_t* const ip = (const uint8_t*)src; uint32_t global_hash = 0; const int h_val = zxc_write_file_header(op, (size_t)(op_end - op), block_size, checksum_enabled); if (UNLIKELY(h_val < 0)) return h_val; // LCOV_EXCL_LINE op += h_val; size_t pos = 0; while (pos < src_size) { const size_t chunk_len = (src_size - pos > block_size) ? block_size : (src_size - pos); const size_t rem_cap = (size_t)(op_end - op); const int res = zxc_compress_chunk_wrapper(ctx, ip + pos, chunk_len, op, rem_cap); if (UNLIKELY(res < 0)) return res; if (checksum_enabled) { if (LIKELY(res >= ZXC_GLOBAL_CHECKSUM_SIZE)) { const uint32_t block_hash = zxc_le32(op + res - ZXC_GLOBAL_CHECKSUM_SIZE); global_hash = zxc_hash_combine_rotate(global_hash, block_hash); } } op += res; pos += chunk_len; } /* EOF block */ const size_t rem_cap = (size_t)(op_end - op); const zxc_block_header_t eof_bh = { .block_type = ZXC_BLOCK_EOF, .block_flags = 0, .reserved = 0, .comp_size = 0}; const int eof_val = zxc_write_block_header(op, rem_cap, &eof_bh); if (UNLIKELY(eof_val < 0)) return eof_val; // LCOV_EXCL_LINE op += eof_val; if (UNLIKELY(rem_cap < (size_t)eof_val + ZXC_FILE_FOOTER_SIZE)) return ZXC_ERROR_DST_TOO_SMALL; // LCOV_EXCL_LINE const int footer_val = zxc_write_file_footer(op, (size_t)(op_end - op), src_size, global_hash, checksum_enabled); if (UNLIKELY(footer_val < 0)) return footer_val; // LCOV_EXCL_LINE op += footer_val; return (int64_t)(op - op_start); } /* --- Decompression ------------------------------------------------------- */ struct zxc_dctx_s { zxc_cctx_t inner; /* reuses the same internal context type */ size_t last_block_size; /* block size from last header parse */ int initialized; /* 1 if inner has live allocations */ }; zxc_dctx* zxc_create_dctx(void) { zxc_dctx* const dctx = (zxc_dctx*)calloc(1, sizeof(zxc_dctx)); return dctx; } void zxc_free_dctx(zxc_dctx* dctx) { if (!dctx) return; if (dctx->initialized) zxc_cctx_free(&dctx->inner); free(dctx); } int64_t zxc_decompress_dctx(zxc_dctx* dctx, const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, const size_t dst_capacity, const zxc_decompress_opts_t* opts) { if (UNLIKELY(!dctx)) return ZXC_ERROR_NULL_INPUT; if (UNLIKELY(!src || !dst || src_size < ZXC_FILE_HEADER_SIZE)) return ZXC_ERROR_NULL_INPUT; const int checksum_enabled = opts ? opts->checksum_enabled : 0; const uint8_t* ip = (const uint8_t*)src; const uint8_t* const ip_end = ip + src_size; uint8_t* op = (uint8_t*)dst; const uint8_t* const op_start = op; const uint8_t* const op_end = op + dst_capacity; size_t runtime_chunk_size = 0; int file_has_checksums = 0; if (UNLIKELY(zxc_read_file_header(ip, src_size, &runtime_chunk_size, &file_has_checksums) != ZXC_OK)) return ZXC_ERROR_BAD_HEADER; /* Re-init only when block size changed. */ if (!dctx->initialized || dctx->last_block_size != runtime_chunk_size) { if (dctx->initialized) { zxc_cctx_free(&dctx->inner); dctx->initialized = 0; } // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&dctx->inner, runtime_chunk_size, 0, 0, file_has_checksums && checksum_enabled) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP dctx->last_block_size = runtime_chunk_size; dctx->initialized = 1; } else { dctx->inner.checksum_enabled = file_has_checksums && checksum_enabled; } zxc_cctx_t* const ctx = &dctx->inner; ip += ZXC_FILE_HEADER_SIZE; /* Ensure scratch buffer is large enough. */ const size_t work_sz = runtime_chunk_size + ZXC_PAD_SIZE; if (ctx->work_buf_cap < work_sz) { free(ctx->work_buf); ctx->work_buf = (uint8_t*)malloc(work_sz); if (UNLIKELY(!ctx->work_buf)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE ctx->work_buf_cap = work_sz; } uint32_t global_hash = 0; while (ip < ip_end) { const size_t rem_src = (size_t)(ip_end - ip); zxc_block_header_t bh; if (UNLIKELY(zxc_read_block_header(ip, rem_src, &bh) != ZXC_OK)) return ZXC_ERROR_BAD_HEADER; if (UNLIKELY(bh.block_type == ZXC_BLOCK_EOF)) { if (UNLIKELY(rem_src < ZXC_BLOCK_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE)) return ZXC_ERROR_SRC_TOO_SMALL; const uint8_t* const footer = ip + ZXC_BLOCK_HEADER_SIZE; const uint64_t stored_size = zxc_le64(footer); if (UNLIKELY(stored_size != (uint64_t)(op - op_start))) return ZXC_ERROR_CORRUPT_DATA; if (checksum_enabled && file_has_checksums) { const uint32_t stored_hash = zxc_le32(footer + sizeof(uint64_t)); if (UNLIKELY(stored_hash != global_hash)) return ZXC_ERROR_BAD_CHECKSUM; } break; } const size_t rem_cap = (size_t)(op_end - op); int res; if (LIKELY(rem_cap >= runtime_chunk_size + ZXC_PAD_SIZE)) { // Fast path: decode directly into dst (enough padding for wild copies). res = zxc_decompress_chunk_wrapper(ctx, ip, rem_src, op, rem_cap); } else { // Safe path: decode into bounce buffer, then copy exact result. res = zxc_decompress_chunk_wrapper(ctx, ip, rem_src, ctx->work_buf, runtime_chunk_size); if (LIKELY(res > 0)) { if (UNLIKELY((size_t)res > rem_cap)) return ZXC_ERROR_DST_TOO_SMALL; // LCOV_EXCL_LINE ZXC_MEMCPY(op, ctx->work_buf, (size_t)res); } } if (UNLIKELY(res < 0)) return res; if (checksum_enabled && file_has_checksums) { const uint32_t block_hash = zxc_le32(ip + ZXC_BLOCK_HEADER_SIZE + bh.comp_size); global_hash = zxc_hash_combine_rotate(global_hash, block_hash); } ip += ZXC_BLOCK_HEADER_SIZE + bh.comp_size + (file_has_checksums ? ZXC_BLOCK_CHECKSUM_SIZE : 0); op += res; } return (int64_t)(op - op_start); } /* ========================================================================= */ /* Block-Level API (no file framing) */ /* ========================================================================= */ int64_t zxc_compress_block(zxc_cctx* cctx, const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, const size_t dst_capacity, const zxc_compress_opts_t* opts) { if (UNLIKELY(!cctx || !src || !dst || src_size == 0 || dst_capacity == 0)) return ZXC_ERROR_NULL_INPUT; const int checksum_enabled = opts ? opts->checksum_enabled : cctx->stored_checksum; const int level = (opts && opts->level > 0) ? opts->level : cctx->stored_level; /* For block API, block_size == src_size (the caller compresses one block at a time). */ const size_t block_size = (opts && opts->block_size > 0) ? opts->block_size : cctx->stored_block_size; const size_t effective_block_size = (block_size > 0) ? block_size : src_size; cctx->stored_level = level; cctx->stored_block_size = effective_block_size; cctx->stored_checksum = checksum_enabled; /* Re-init only when block_size changed. */ if (!cctx->initialized || cctx->last_block_size != effective_block_size) { if (cctx->initialized) { zxc_cctx_free(&cctx->inner); cctx->initialized = 0; } // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&cctx->inner, effective_block_size, 1, level, checksum_enabled) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP cctx->last_block_size = effective_block_size; cctx->initialized = 1; } else { cctx->inner.compression_level = level; cctx->inner.checksum_enabled = checksum_enabled; } const int res = zxc_compress_chunk_wrapper(&cctx->inner, (const uint8_t*)src, src_size, (uint8_t*)dst, dst_capacity); if (UNLIKELY(res < 0)) return res; return (int64_t)res; } int64_t zxc_decompress_block(zxc_dctx* dctx, const void* RESTRICT src, const size_t src_size, void* RESTRICT dst, const size_t dst_capacity, const zxc_decompress_opts_t* opts) { if (UNLIKELY(!dctx || !src || !dst || src_size < ZXC_BLOCK_HEADER_SIZE)) return ZXC_ERROR_NULL_INPUT; const int checksum_enabled = opts ? opts->checksum_enabled : 0; /* * Derive the block_size from dst_capacity (callers know the original size). * Re-init only when needed. */ const size_t block_size = dst_capacity; if (!dctx->initialized || dctx->last_block_size != block_size) { if (dctx->initialized) { zxc_cctx_free(&dctx->inner); dctx->initialized = 0; } // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&dctx->inner, block_size, 0, 0, checksum_enabled) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP dctx->last_block_size = block_size; dctx->initialized = 1; } else { dctx->inner.checksum_enabled = checksum_enabled; } zxc_cctx_t* const ctx = &dctx->inner; /* Ensure scratch buffer for safe-path wild copies. */ const size_t work_sz = block_size + ZXC_PAD_SIZE; if (ctx->work_buf_cap < work_sz) { free(ctx->work_buf); ctx->work_buf = (uint8_t*)malloc(work_sz); if (UNLIKELY(!ctx->work_buf)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE ctx->work_buf_cap = work_sz; } int res; if (LIKELY(dst_capacity >= block_size + ZXC_PAD_SIZE)) { res = zxc_decompress_chunk_wrapper(ctx, (const uint8_t*)src, src_size, (uint8_t*)dst, dst_capacity); } else { /* Bounce through work_buf when output can't absorb wild copies. */ res = zxc_decompress_chunk_wrapper(ctx, (const uint8_t*)src, src_size, ctx->work_buf, block_size); if (LIKELY(res > 0)) { if (UNLIKELY((size_t)res > dst_capacity)) return ZXC_ERROR_DST_TOO_SMALL; ZXC_MEMCPY(dst, ctx->work_buf, (size_t)res); } } if (UNLIKELY(res < 0)) return res; return (int64_t)res; } zxc-0.10.0/src/lib/zxc_driver.c000066400000000000000000001147541517010557200162730ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_driver.c * @brief Multi-threaded streaming compression / decompression engine. * * Implements a ring-buffer producer-worker-consumer architecture that * parallelises block processing over @c FILE* streams. Also provides * the public @ref zxc_stream_compress, @ref zxc_stream_decompress, and * extended variants with progress callbacks. */ #include #include #include #include "../../include/zxc_buffer.h" #include "../../include/zxc_error.h" #include "../../include/zxc_sans_io.h" #include "../../include/zxc_seekable.h" #include "../../include/zxc_stream.h" #include "zxc_internal.h" /* * ============================================================================ * WINDOWS THREADING EMULATION * ============================================================================ * Maps POSIX pthread calls to Windows Native API (CriticalSection, * ConditionVariable, Threads). Allows the same threading logic to compile on * Linux/macOS and Windows. */ #if defined(_WIN32) #include #include #include #include // Map POSIX file positioning functions to Windows equivalents #define fseeko _fseeki64 #define ftello _ftelli64 // Simple sysconf emulation to get core count static int zxc_get_num_procs(void) { SYSTEM_INFO sysinfo; GetSystemInfo(&sysinfo); return sysinfo.dwNumberOfProcessors; } typedef CRITICAL_SECTION pthread_mutex_t; typedef CONDITION_VARIABLE pthread_cond_t; typedef HANDLE pthread_t; #define pthread_mutex_init(m, a) InitializeCriticalSection(m) #define pthread_mutex_destroy(m) DeleteCriticalSection(m) #define pthread_mutex_lock(m) EnterCriticalSection(m) #define pthread_mutex_unlock(m) LeaveCriticalSection(m) #define pthread_cond_init(c, a) InitializeConditionVariable(c) #define pthread_cond_destroy(c) (void)(0) #define pthread_cond_wait(c, m) SleepConditionVariableCS(c, m, INFINITE) #define pthread_cond_signal(c) WakeConditionVariable(c) #define pthread_cond_broadcast(c) WakeAllConditionVariable(c) typedef struct { void* (*func)(void*); void* arg; } zxc_win_thread_arg_t; static unsigned __stdcall zxc_win_thread_entry(void* p) { zxc_win_thread_arg_t* a = (zxc_win_thread_arg_t*)p; void* (*f)(void*) = a->func; void* arg = a->arg; free(a); f(arg); return 0; } static int pthread_create(pthread_t* thread, const void* attr, void* (*start_routine)(void*), void* arg) { (void)attr; zxc_win_thread_arg_t* wrapper = malloc(sizeof(zxc_win_thread_arg_t)); if (UNLIKELY(!wrapper)) return ZXC_ERROR_MEMORY; wrapper->func = start_routine; wrapper->arg = arg; uintptr_t handle = _beginthreadex(NULL, 0, zxc_win_thread_entry, wrapper, 0, NULL); if (UNLIKELY(handle == 0)) { free(wrapper); return ZXC_ERROR_MEMORY; } *thread = (HANDLE)handle; return 0; } static int pthread_join(pthread_t thread, void** retval) { (void)retval; WaitForSingleObject(thread, INFINITE); CloseHandle(thread); return 0; } #define sysconf(x) zxc_get_num_procs() #define _SC_NPROCESSORS_ONLN 0 #else #include #include #endif /* * ============================================================================ * STREAMING ENGINE (Producer / Worker / Consumer) * ============================================================================ * Implements a Ring Buffer architecture to parallelize block processing. */ /** * @enum job_status_t * @brief Represents the lifecycle states of a processing job within the ring * buffer. * * @var JOB_STATUS_FREE * The job slot is empty and available to be filled with new data by the * writer. * @var JOB_STATUS_FILLED * The job slot has been populated with input data and is ready for * processing by a worker. * @var JOB_STATUS_PROCESSED * The worker has finished processing the data; the result is ready to be * consumed/written out. */ typedef enum { JOB_STATUS_FREE, JOB_STATUS_FILLED, JOB_STATUS_PROCESSED } job_status_t; /** * @struct zxc_stream_job_t * @brief Represents a single unit of work (a chunk of data) to be processed. * * This structure holds the input and output buffers for a specific chunk of * data, along with its processing status. It is padded to align with cache * lines to prevent false sharing in a multi-threaded environment. * * @var zxc_stream_job_t::in_buf * Pointer to the buffer containing raw input data. * @var zxc_stream_job_t::in_cap * The total allocated capacity of the input buffer. * @var zxc_stream_job_t::in_sz * The actual size of the valid data currently in the input buffer. * @var zxc_stream_job_t::out_buf * Pointer to the buffer where processed (compressed/decompressed) data is * stored. * @var zxc_stream_job_t::out_cap * The total allocated capacity of the output buffer. * @var zxc_stream_job_t::result_sz * The actual size of the valid data produced in the output buffer. * @var zxc_stream_job_t::job_id * A unique identifier for the job, often used for ordering or debugging. * @var zxc_stream_job_t::status * The current state of this job (Free, Filled, or Processed). * @var zxc_stream_job_t::pad * Padding bytes to ensure the structure size aligns with typical cache * lines (64 bytes), minimizing cache contention between threads accessing * adjacent jobs. */ typedef struct { uint8_t* in_buf; size_t in_cap, in_sz; uint8_t* out_buf; size_t out_cap, result_sz; int job_id; ZXC_ATOMIC job_status_t status; // Atomic for lock-free status updates char pad[ZXC_CACHE_LINE_SIZE]; // Prevent False Sharing } zxc_stream_job_t; /** * @typedef zxc_chunk_processor_t * @brief Function pointer type for processing a chunk of data. * * This type defines the signature for internal functions responsible for * processing (compressing or transforming) a specific chunk of input data. * * @param ctx Pointer to the compression context containing state and * configuration. * @param in Pointer to the input data buffer. * @param in_sz Size of the input data in bytes. * @param out Pointer to the output buffer where processed data will be * written. * @param out_cap Capacity of the output buffer in bytes. * * @return The number of bytes written to the output buffer on success, or a * negative error code on failure. */ typedef int (*zxc_chunk_processor_t)(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT in, const size_t in_sz, uint8_t* RESTRICT out, const size_t out_cap); /** * @struct zxc_stream_ctx_t * @brief The main context structure managing the streaming * compression/decompression state. * * This structure orchestrates the producer-consumer workflow. It manages the * ring buffer of jobs, the worker queue, synchronization primitives (mutexes * and condition variables), and configuration settings for the compression * algorithm. * * @var zxc_stream_ctx_t::jobs * Array of job structures acting as the ring buffer. * @var zxc_stream_ctx_t::ring_size * The total number of slots in the jobs array. * @var zxc_stream_ctx_t::worker_queue * A circular queue containing indices of jobs ready to be picked up by * worker threads. * @var zxc_stream_ctx_t::wq_head * Index of the head of the worker queue (where workers take jobs). * @var zxc_stream_ctx_t::wq_tail * Index of the tail of the worker queue (where the writer adds jobs). * @var zxc_stream_ctx_t::wq_count * Current number of items in the worker queue. * @var zxc_stream_ctx_t::lock * Mutex used to protect access to shared resources (queue indices, status * changes). * @var zxc_stream_ctx_t::cond_reader * Condition variable to signal the output thread (reader) that processed * data is available. * @var zxc_stream_ctx_t::cond_worker * Condition variable to signal worker threads that new work is available. * @var zxc_stream_ctx_t::cond_writer * Condition variable to signal the input thread (writer) that job slots * are free. * @var zxc_stream_ctx_t::shutdown_workers * Flag indicating that worker threads should terminate. * @var zxc_stream_ctx_t::compression_mode * Indicates the operation mode (e.g., compression or decompression). * @var zxc_stream_ctx_t::io_error * Atomic flag to signal if an I/O error occurred during processing. * @var zxc_stream_ctx_t::processor * Function pointer or object responsible for the actual chunk processing * logic. * @var zxc_stream_ctx_t::write_idx * The index of the next job slot to be written to by the main thread. * @var zxc_stream_ctx_t::compression_level * The configured level of compression (trading off speed vs. ratio). * @var zxc_stream_ctx_t::chunk_size * The size of each data chunk to be processed. * @var zxc_stream_ctx_t::checksum_enabled * Flag indicating whether checksum verification/generation is active. * @var zxc_stream_ctx_t::file_has_checksum * Flag indicating whether the input file includes checksums. * @var zxc_stream_ctx_t::progress_cb * Optional callback function for reporting progress during processing. * @var zxc_stream_ctx_t::progress_user_data * User data pointer to be passed to the progress callback function. * @var zxc_stream_ctx_t::total_input_bytes * Total size of the input data in bytes, used for progress tracking. */ typedef struct { zxc_stream_job_t* jobs; size_t ring_size; int* worker_queue; int wq_head, wq_tail, wq_count; pthread_mutex_t lock; pthread_cond_t cond_reader, cond_worker, cond_writer; int shutdown_workers; int compression_mode; ZXC_ATOMIC int io_error; zxc_chunk_processor_t processor; int write_idx; int compression_level; size_t chunk_size; int checksum_enabled; int file_has_checksum; zxc_progress_callback_t progress_cb; void* progress_user_data; uint64_t total_input_bytes; } zxc_stream_ctx_t; /** * @struct writer_args_t * @brief Structure containing arguments for the writer callback function. * * This structure is used to pass necessary context and state information * to the function responsible for writing compressed or decompressed data * to a file stream. * * @var writer_args_t::ctx * Pointer to the ZXC stream context, holding the state of the * compression/decompression stream. * * @var writer_args_t::f * Pointer to the output file stream where data will be written. * * @var writer_args_t::total_bytes * Accumulator for the total number of bytes written to the file so far. * * @var writer_args_t::global_hash * The global hash accumulated during processing. * * @var writer_args_t::bytes_processed * The number of bytes processed so far, used for progress reporting. * * @var writer_args_t::seek_comp * Array of compressed block sizes for seek table construction. * * @var writer_args_t::seek_count * Number of entries in the seek table. * * @var writer_args_t::seek_cap * Capacity of the seek table array. */ typedef struct { zxc_stream_ctx_t* ctx; FILE* f; int64_t total_bytes; uint32_t global_hash; uint64_t bytes_processed; // For progress callback uint32_t* seek_comp; uint32_t seek_count; uint32_t seek_cap; } writer_args_t; /** * @brief Worker thread function for parallel stream processing. * * This function serves as the entry point for worker threads in the ZXC * streaming compression/decompression context. It continuously retrieves jobs * from a shared work queue, processes them using a thread-local compression * context (`zxc_cctx_t`), and signals the writer thread upon completion. * * **Worker Lifecycle & Synchronization:** * 1. **Initialization:** Allocates a thread-local `zxc_cctx_t` to avoid lock * contention during compression/decompression. * 2. **Wait Loop:** Uses `pthread_cond_wait` on `cond_worker` to sleep until a * job is available in the `worker_queue`. * 3. **Job Retrieval:** Dequeues a job ID from the ring buffer. The * `worker_queue` acts as a load balancer. * 4. **Processing:** Calls `ctx->processor` (the compression/decompression * function) on the job's data. This is the CPU-intensive part and runs in * parallel. * 5. **Completion:** Updates `job->status` to `JOB_STATUS_PROCESSED`. * 6. **Signaling:** If the processed job is the *next* one expected by the * writer * (`jid == ctx->write_idx`), it signals `cond_writer`. This optimization * prevents unnecessary wake-ups of the writer thread for out-of-order * completions. * * @param[in] arg A pointer to the shared stream context (`zxc_stream_ctx_t`). * @return Always returns NULL. */ static void* zxc_stream_worker(void* arg) { zxc_stream_ctx_t* const ctx = (zxc_stream_ctx_t*)arg; zxc_cctx_t cctx; const int unified_chk = (ctx->compression_mode == 1) ? ctx->checksum_enabled : (ctx->file_has_checksum && ctx->checksum_enabled); if (zxc_cctx_init(&cctx, ctx->chunk_size, ctx->compression_mode, ctx->compression_level, unified_chk) != ZXC_OK) { // LCOV_EXCL_START zxc_cctx_free(&cctx); pthread_mutex_lock(&ctx->lock); ctx->io_error = 1; pthread_cond_broadcast(&ctx->cond_writer); pthread_cond_broadcast(&ctx->cond_reader); pthread_mutex_unlock(&ctx->lock); return NULL; // LCOV_EXCL_STOP } cctx.compression_level = ctx->compression_level; while (1) { zxc_stream_job_t* job = NULL; pthread_mutex_lock(&ctx->lock); while (ctx->wq_count == 0 && !ctx->shutdown_workers) { pthread_cond_wait(&ctx->cond_worker, &ctx->lock); } if (ctx->shutdown_workers && ctx->wq_count == 0) { pthread_mutex_unlock(&ctx->lock); break; } const int jid = ctx->worker_queue[ctx->wq_tail]; ctx->wq_tail = (ctx->wq_tail + 1) % ctx->ring_size; ctx->wq_count--; job = &ctx->jobs[jid]; pthread_mutex_unlock(&ctx->lock); const int res = ctx->processor(&cctx, job->in_buf, job->in_sz, job->out_buf, job->out_cap); pthread_mutex_lock(&ctx->lock); job->result_sz = UNLIKELY(res < 0) ? 0 : (size_t)res; job->status = JOB_STATUS_PROCESSED; if (UNLIKELY(res < 0)) { ctx->io_error = 1; pthread_cond_broadcast(&ctx->cond_writer); pthread_cond_broadcast(&ctx->cond_reader); } else if (jid == ctx->write_idx) { pthread_cond_signal(&ctx->cond_writer); } pthread_mutex_unlock(&ctx->lock); } zxc_cctx_free(&cctx); return NULL; } /** * @brief Asynchronous writer thread function. * * This function runs as a separate thread responsible for writing processed * data chunks to the output file. It operates on a ring buffer of jobs shared * with the reader and worker threads. * * **Ordering Enforcement:** * The writer MUST write blocks in the exact order they were read. Even if * worker threads finish jobs out of order (e.g., job 2 finishes before job 1), * the writer waits for `ctx->write_idx` (job 1) to be `JOB_STATUS_PROCESSED`. * * **Workflow:** * 1. **Wait:** Sleeps on `cond_writer` until the job at `ctx->write_idx` is * ready. * 2. **Write:** Writes the `out_buf` to the file. * 3. **Release:** Sets the job status to `JOB_STATUS_FREE` and signals * `cond_reader`, allowing the main thread to reuse this slot for new input. * 4. **Advance:** Increments `ctx->write_idx` to wait for the next sequential * block. * * @param[in] arg Pointer to a `writer_args_t` structure containing the stream * context, the output file handle, and a counter for total bytes written. * @return Always returns NULL. */ static void* zxc_async_writer(void* arg) { writer_args_t* const args = (writer_args_t*)arg; zxc_stream_ctx_t* const ctx = args->ctx; while (1) { zxc_stream_job_t* const job = &ctx->jobs[ctx->write_idx]; pthread_mutex_lock(&ctx->lock); while (job->status != JOB_STATUS_PROCESSED && !ctx->io_error) pthread_cond_wait(&ctx->cond_writer, &ctx->lock); if (job->result_sz == (size_t)-1) { pthread_mutex_unlock(&ctx->lock); break; } pthread_mutex_unlock(&ctx->lock); if (args->f && job->result_sz > 0) { if (fwrite(job->out_buf, 1, job->result_sz, args->f) != job->result_sz) { pthread_mutex_lock(&ctx->lock); ctx->io_error = 1; pthread_cond_signal(&ctx->cond_reader); pthread_mutex_unlock(&ctx->lock); } else if (ctx->checksum_enabled && ctx->compression_mode == 1) { // Update Global Hash (Rotation + XOR) if (LIKELY(job->result_sz >= ZXC_GLOBAL_CHECKSUM_SIZE)) { uint32_t block_hash = zxc_le32(job->out_buf + job->result_sz - ZXC_GLOBAL_CHECKSUM_SIZE); args->global_hash = zxc_hash_combine_rotate(args->global_hash, block_hash); } } } if (UNLIKELY(ctx->io_error)) { pthread_mutex_lock(&ctx->lock); job->status = JOB_STATUS_FREE; pthread_cond_signal(&ctx->cond_reader); pthread_mutex_unlock(&ctx->lock); break; } args->total_bytes += (int64_t)job->result_sz; /* Seekable: record compressed block size */ if (args->seek_comp && ctx->compression_mode == 1) { if (UNLIKELY(args->seek_count >= args->seek_cap)) { args->seek_cap = args->seek_cap * 2; uint32_t* nc = (uint32_t*)realloc(args->seek_comp, args->seek_cap * sizeof(uint32_t)); // LCOV_EXCL_START if (UNLIKELY(!nc)) { ctx->io_error = 1; pthread_mutex_lock(&ctx->lock); job->status = JOB_STATUS_FREE; pthread_cond_signal(&ctx->cond_reader); pthread_mutex_unlock(&ctx->lock); break; } // LCOV_EXCL_STOP args->seek_comp = nc; } args->seek_comp[args->seek_count++] = (uint32_t)job->result_sz; } // Update progress callback if (ctx->progress_cb) { // LCOV_EXCL_START args->bytes_processed += ctx->compression_mode == 1 ? job->in_sz : job->result_sz; ctx->progress_cb(args->bytes_processed, ctx->total_input_bytes, ctx->progress_user_data); // LCOV_EXCL_STOP } pthread_mutex_lock(&ctx->lock); job->status = JOB_STATUS_FREE; ctx->write_idx = (ctx->write_idx + 1) % ctx->ring_size; pthread_cond_signal(&ctx->cond_reader); pthread_mutex_unlock(&ctx->lock); } return NULL; } /** * @brief Orchestrates the multithreaded streaming compression or decompression * engine. * * This function initializes the stream context, allocates the necessary ring * buffer memory for jobs and I/O buffers, and spawns the worker threads and the * asynchronous writer thread. It acts as the main "producer" (reader) loop. * * **Architecture: Producer-Consumer with Ring Buffer** * - **Ring Buffer:** A fixed-size array of `zxc_stream_job_t` structures. * - **Producer (Main Thread):** Reads chunks from `f_in` and fills "Free" slots * in the ring buffer. It blocks if no slots are free (backpressure). * - **Workers:** Pick up "Filled" jobs from a queue, process them, and mark * them as "Processed". * - **Consumer (Writer Thread):** Waits for the *next sequential* job to be * "Processed", writes it to `f_out`, and marks the slot as "Free". * * **Double-Buffering & Zero-Copy:** * We allocate `alloc_in` and `alloc_out` buffers for each job. The reader reads * directly into `in_buf`, and the writer writes directly from `out_buf`, * minimizing memory copies. * * @param[in] f_in Pointer to the input file stream (source). * @param[out] f_out Pointer to the output file stream (destination). * @param[in] n_threads Number of worker threads to spawn. If set to 0 or less, the * function automatically detects the number of online processors. * @param[in] mode Operation mode: 1 for compression, 0 for decompression. * @param[in] level Compression level to be applied (relevant for compression * mode). * @param[in] checksum_enabled Flag indicating whether to enable checksum * generation/verification. * @param[in] func Function pointer to the chunk processor (compression or * decompression logic). * * @return The total number of bytes written to the output stream on success, or * -1 if an initialization or I/O error occurred. */ static int64_t zxc_stream_engine_run(FILE* f_in, FILE* f_out, const int n_threads, const int mode, const int level, const size_t block_size, const int checksum_enabled, const int seekable, zxc_chunk_processor_t func, zxc_progress_callback_t progress_cb, void* user_data) { zxc_stream_ctx_t ctx; ZXC_MEMSET(&ctx, 0, sizeof(ctx)); size_t runtime_chunk_sz = (block_size > 0) ? block_size : ZXC_BLOCK_SIZE_DEFAULT; int file_has_chk = 0; // Try to get input file size for progress tracking (compression mode only) // For decompression, the CLI precomputes the size and passes it via user_data uint64_t total_file_size = 0; if (mode == 1 && progress_cb) { // LCOV_EXCL_START const long long saved_pos = ftello(f_in); if (saved_pos >= 0) { if (fseeko(f_in, 0, SEEK_END) == 0) { const long long size = ftello(f_in); if (size > 0) total_file_size = (uint64_t)size; fseeko(f_in, saved_pos, SEEK_SET); } } // LCOV_EXCL_STOP } if (mode == 0) { // Decompression Mode: Read and validate file header uint8_t h[ZXC_FILE_HEADER_SIZE]; if (UNLIKELY(fread(h, 1, ZXC_FILE_HEADER_SIZE, f_in) != ZXC_FILE_HEADER_SIZE || zxc_read_file_header(h, ZXC_FILE_HEADER_SIZE, &runtime_chunk_sz, &file_has_chk) != ZXC_OK)) return ZXC_ERROR_BAD_HEADER; } int num_threads = (n_threads > 0) ? n_threads : (int)sysconf(_SC_NPROCESSORS_ONLN); if (num_threads > ZXC_MAX_THREADS) num_threads = ZXC_MAX_THREADS; // Reserve 1 thread for Writer/Reader overhead if possible const int num_workers = (num_threads > 1) ? num_threads - 1 : 1; ctx.compression_mode = mode; ctx.processor = func; ctx.io_error = 0; ctx.compression_level = level; ctx.ring_size = (size_t)num_workers * 4U; ctx.chunk_size = runtime_chunk_sz; ctx.checksum_enabled = checksum_enabled; ctx.file_has_checksum = mode == 1 ? checksum_enabled : file_has_chk; ctx.progress_cb = progress_cb; ctx.progress_user_data = user_data; ctx.total_input_bytes = total_file_size; uint32_t d_global_hash = 0; const uint64_t max_out = zxc_compress_bound(runtime_chunk_sz); const size_t raw_alloc_in = (size_t)(((mode) ? runtime_chunk_sz : max_out) + ZXC_PAD_SIZE); const size_t alloc_in = (raw_alloc_in + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t raw_alloc_out = (size_t)(((mode) ? max_out : runtime_chunk_sz) + ZXC_PAD_SIZE); const size_t alloc_out = (raw_alloc_out + ZXC_ALIGNMENT_MASK) & ~ZXC_ALIGNMENT_MASK; const size_t per_job_sz = sizeof(zxc_stream_job_t) + sizeof(int) + alloc_in + alloc_out; const size_t alloc_size = ctx.ring_size * per_job_sz; uint8_t* const mem_block = zxc_aligned_malloc(alloc_size, ZXC_CACHE_LINE_SIZE); if (UNLIKELY(!mem_block || per_job_sz > SIZE_MAX / ctx.ring_size)) { // LCOV_EXCL_START zxc_aligned_free(mem_block); return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP } uint8_t* ptr = mem_block; ctx.jobs = (zxc_stream_job_t*)ptr; ptr += ctx.ring_size * sizeof(zxc_stream_job_t); ctx.worker_queue = (int*)ptr; ptr += ctx.ring_size * sizeof(int); uint8_t* buf_in = ptr; ptr += ctx.ring_size * alloc_in; uint8_t* buf_out = ptr; ZXC_MEMSET(mem_block, 0, alloc_size); for (size_t i = 0; i < ctx.ring_size; i++) { ctx.jobs[i].job_id = (int)i; ctx.jobs[i].status = JOB_STATUS_FREE; ctx.jobs[i].in_buf = buf_in + (i * alloc_in); ctx.jobs[i].in_cap = alloc_in - ZXC_PAD_SIZE; ctx.jobs[i].in_sz = 0; ctx.jobs[i].out_buf = buf_out + (i * alloc_out); ctx.jobs[i].out_cap = alloc_out - ZXC_PAD_SIZE; ctx.jobs[i].result_sz = 0; } pthread_mutex_init(&ctx.lock, NULL); pthread_cond_init(&ctx.cond_reader, NULL); pthread_cond_init(&ctx.cond_worker, NULL); pthread_cond_init(&ctx.cond_writer, NULL); pthread_t* const workers = malloc((size_t)num_workers * sizeof(pthread_t)); if (UNLIKELY(!workers)) { // LCOV_EXCL_START zxc_aligned_free(mem_block); return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP } int started_workers = 0; for (int i = 0; i < num_workers; i++) { if (UNLIKELY(pthread_create(&workers[i], NULL, zxc_stream_worker, &ctx) != 0)) break; started_workers++; } if (UNLIKELY(started_workers == 0)) { // LCOV_EXCL_START pthread_cond_destroy(&ctx.cond_writer); pthread_cond_destroy(&ctx.cond_worker); pthread_cond_destroy(&ctx.cond_reader); pthread_mutex_destroy(&ctx.lock); free(workers); zxc_aligned_free(mem_block); return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP } writer_args_t w_args = {&ctx, f_out, 0, 0, 0, NULL, 0, 0}; /* Seekable: allocate initial block-size tracking array */ if (mode == 1 && seekable) { w_args.seek_cap = 64; w_args.seek_comp = (uint32_t*)malloc(w_args.seek_cap * sizeof(uint32_t)); // LCOV_EXCL_START if (UNLIKELY(!w_args.seek_comp)) { pthread_mutex_lock(&ctx.lock); ctx.shutdown_workers = 1; pthread_cond_broadcast(&ctx.cond_worker); pthread_mutex_unlock(&ctx.lock); for (int i = 0; i < started_workers; i++) pthread_join(workers[i], NULL); pthread_cond_destroy(&ctx.cond_writer); pthread_cond_destroy(&ctx.cond_worker); pthread_cond_destroy(&ctx.cond_reader); pthread_mutex_destroy(&ctx.lock); free(workers); zxc_aligned_free(mem_block); return ZXC_ERROR_MEMORY; } // LCOV_EXCL_STOP } if (mode == 1 && f_out) { uint8_t h[ZXC_FILE_HEADER_SIZE]; zxc_write_file_header(h, ZXC_FILE_HEADER_SIZE, runtime_chunk_sz, checksum_enabled); if (UNLIKELY(fwrite(h, 1, ZXC_FILE_HEADER_SIZE, f_out) != ZXC_FILE_HEADER_SIZE)) ctx.io_error = 1; w_args.total_bytes = ZXC_FILE_HEADER_SIZE; } pthread_t writer_th; if (UNLIKELY(pthread_create(&writer_th, NULL, zxc_async_writer, &w_args) != 0)) { // LCOV_EXCL_START pthread_mutex_lock(&ctx.lock); ctx.shutdown_workers = 1; pthread_cond_broadcast(&ctx.cond_worker); pthread_mutex_unlock(&ctx.lock); for (int i = 0; i < started_workers; i++) pthread_join(workers[i], NULL); pthread_cond_destroy(&ctx.cond_writer); pthread_cond_destroy(&ctx.cond_worker); pthread_cond_destroy(&ctx.cond_reader); pthread_mutex_destroy(&ctx.lock); free(workers); zxc_aligned_free(mem_block); return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP } int read_idx = 0; int read_eof = 0; uint64_t total_src_bytes = 0; // Reader Loop: Reads from file, prepares jobs, pushes to worker queue. while (!read_eof && !ctx.io_error) { zxc_stream_job_t* const job = &ctx.jobs[read_idx]; pthread_mutex_lock(&ctx.lock); while (job->status != JOB_STATUS_FREE && !ctx.io_error) pthread_cond_wait(&ctx.cond_reader, &ctx.lock); pthread_mutex_unlock(&ctx.lock); if (UNLIKELY(ctx.io_error)) break; size_t read_sz = 0; if (mode == 1) { read_sz = fread(job->in_buf, 1, runtime_chunk_sz, f_in); total_src_bytes += read_sz; if (UNLIKELY(read_sz == 0)) read_eof = 1; } else { uint8_t bh_buf[ZXC_BLOCK_HEADER_SIZE]; size_t h_read = fread(bh_buf, 1, ZXC_BLOCK_HEADER_SIZE, f_in); if (UNLIKELY(h_read < ZXC_BLOCK_HEADER_SIZE)) { read_eof = 1; } else { zxc_block_header_t bh; if (UNLIKELY(zxc_read_block_header(bh_buf, ZXC_BLOCK_HEADER_SIZE, &bh) != ZXC_OK)) { read_eof = 1; goto _job_prepared; } if (bh.block_type == ZXC_BLOCK_EOF) { if (UNLIKELY(bh.comp_size != 0)) { ctx.io_error = 1; goto _job_prepared; } read_eof = 1; read_sz = 0; goto _job_prepared; } const int has_crc = ctx.file_has_checksum; const size_t checksum_sz = (has_crc ? ZXC_BLOCK_CHECKSUM_SIZE : 0); const size_t body_total = bh.comp_size + checksum_sz; const size_t total_len = ZXC_BLOCK_HEADER_SIZE + body_total; if (UNLIKELY(total_len > job->in_cap)) { ctx.io_error = 1; break; } ZXC_MEMCPY(job->in_buf, bh_buf, ZXC_BLOCK_HEADER_SIZE); // Single fread for body + checksum (reduces syscalls) const size_t body_read = fread(job->in_buf + ZXC_BLOCK_HEADER_SIZE, 1, body_total, f_in); if (UNLIKELY(body_read != body_total)) { ctx.io_error = 1; break; } else if (has_crc) { // Update Global Hash for Decompression const uint32_t b_crc = zxc_le32(job->in_buf + ZXC_BLOCK_HEADER_SIZE + bh.comp_size); d_global_hash = zxc_hash_combine_rotate(d_global_hash, b_crc); } read_sz = ZXC_BLOCK_HEADER_SIZE + body_read; } } _job_prepared: if (UNLIKELY(read_eof && read_sz == 0)) break; job->in_sz = read_sz; pthread_mutex_lock(&ctx.lock); job->status = JOB_STATUS_FILLED; ctx.worker_queue[ctx.wq_head] = read_idx; ctx.wq_head = (ctx.wq_head + 1) % ctx.ring_size; ctx.wq_count++; read_idx = (read_idx + 1) % ctx.ring_size; pthread_cond_signal(&ctx.cond_worker); pthread_mutex_unlock(&ctx.lock); if (UNLIKELY(read_sz < runtime_chunk_sz && mode == 1)) read_eof = 1; } zxc_stream_job_t* const end_job = &ctx.jobs[read_idx]; pthread_mutex_lock(&ctx.lock); while (end_job->status != JOB_STATUS_FREE && !ctx.io_error) pthread_cond_wait(&ctx.cond_reader, &ctx.lock); end_job->result_sz = -1; end_job->status = JOB_STATUS_PROCESSED; pthread_cond_broadcast(&ctx.cond_writer); pthread_mutex_unlock(&ctx.lock); pthread_join(writer_th, NULL); pthread_mutex_lock(&ctx.lock); ctx.shutdown_workers = 1; pthread_cond_broadcast(&ctx.cond_worker); pthread_mutex_unlock(&ctx.lock); for (int i = 0; i < started_workers; i++) pthread_join(workers[i], NULL); pthread_cond_destroy(&ctx.cond_writer); pthread_cond_destroy(&ctx.cond_worker); pthread_cond_destroy(&ctx.cond_reader); pthread_mutex_destroy(&ctx.lock); // Write EOF Block + optional Seek Table + Footer if compression and no error if (mode == 1 && !ctx.io_error && w_args.total_bytes >= 0) { /* EOF block */ uint8_t eof_buf[ZXC_BLOCK_HEADER_SIZE]; const zxc_block_header_t eof_bh = { .block_type = ZXC_BLOCK_EOF, .block_flags = 0, .reserved = 0, .comp_size = 0}; zxc_write_block_header(eof_buf, ZXC_BLOCK_HEADER_SIZE, &eof_bh); if (UNLIKELY(f_out && fwrite(eof_buf, 1, ZXC_BLOCK_HEADER_SIZE, f_out) != ZXC_BLOCK_HEADER_SIZE)) ctx.io_error = 1; else w_args.total_bytes += ZXC_BLOCK_HEADER_SIZE; /* Seekable: write SEK block between EOF and footer */ if (!ctx.io_error && w_args.seek_comp && w_args.seek_count > 0) { const size_t st_size = zxc_seek_table_size(w_args.seek_count); uint8_t* const st_buf = (uint8_t*)malloc(st_size); if (st_buf) { const int64_t st_val = zxc_write_seek_table(st_buf, st_size, w_args.seek_comp, w_args.seek_count); if (st_val > 0 && f_out && fwrite(st_buf, 1, (size_t)st_val, f_out) == (size_t)st_val) w_args.total_bytes += st_val; free(st_buf); } } /* Footer */ uint8_t footer_buf[ZXC_FILE_FOOTER_SIZE]; zxc_write_file_footer(footer_buf, ZXC_FILE_FOOTER_SIZE, total_src_bytes, w_args.global_hash, checksum_enabled); if (UNLIKELY(f_out && fwrite(footer_buf, 1, ZXC_FILE_FOOTER_SIZE, f_out) != ZXC_FILE_FOOTER_SIZE)) ctx.io_error = 1; else w_args.total_bytes += ZXC_FILE_FOOTER_SIZE; } else if (mode == 0 && !ctx.io_error) { /* * After the EOF block, the stream may contain: * (a) [FOOTER 12B] - no seekable table * (b) [SEK header 8B] [payload] [FOOTER 12B] - seekable archive */ uint8_t peek_buf[ZXC_BLOCK_HEADER_SIZE]; uint8_t footer[ZXC_FILE_FOOTER_SIZE]; if (UNLIKELY(fread(peek_buf, 1, ZXC_BLOCK_HEADER_SIZE, f_in) != ZXC_BLOCK_HEADER_SIZE)) { ctx.io_error = 1; } else { zxc_block_header_t peek_bh; const int is_sek = (zxc_read_block_header(peek_buf, ZXC_BLOCK_HEADER_SIZE, &peek_bh) == ZXC_OK && peek_bh.block_type == ZXC_BLOCK_SEK); if (is_sek) { /* Drain the SEK payload (read + discard) */ size_t remaining = (size_t)peek_bh.comp_size; uint8_t discard[512]; while (remaining > 0 && !ctx.io_error) { const size_t chunk = remaining < sizeof(discard) ? remaining : sizeof(discard); if (UNLIKELY(fread(discard, 1, chunk, f_in) != chunk)) ctx.io_error = 1; remaining -= chunk; } /* Read full 12-byte footer */ if (!ctx.io_error && UNLIKELY(fread(footer, 1, ZXC_FILE_FOOTER_SIZE, f_in) != ZXC_FILE_FOOTER_SIZE)) ctx.io_error = 1; } else { /* peek_buf contains the first 8 bytes of the 12-byte footer. * Read the remaining 4 bytes and assemble. */ ZXC_MEMCPY(footer, peek_buf, ZXC_BLOCK_HEADER_SIZE); const size_t tail = ZXC_FILE_FOOTER_SIZE - ZXC_BLOCK_HEADER_SIZE; /* 4 */ if (UNLIKELY(fread(footer + ZXC_BLOCK_HEADER_SIZE, 1, tail, f_in) != tail)) ctx.io_error = 1; } } /* Verify Footer Content: Source Size and Global Checksum */ if (!ctx.io_error) { int valid = (zxc_le64(footer) == (uint64_t)w_args.total_bytes); if (valid && checksum_enabled && ctx.file_has_checksum) valid = (zxc_le32(footer + sizeof(uint64_t)) == d_global_hash); if (UNLIKELY(!valid)) ctx.io_error = 1; } } free(w_args.seek_comp); free(workers); zxc_aligned_free(mem_block); if (UNLIKELY(ctx.io_error)) return ZXC_ERROR_IO; return w_args.total_bytes; } int64_t zxc_stream_compress(FILE* f_in, FILE* f_out, const zxc_compress_opts_t* opts) { if (UNLIKELY(!f_in)) return ZXC_ERROR_NULL_INPUT; const int n_threads = opts ? opts->n_threads : 0; const int checksum_enabled = opts ? opts->checksum_enabled : 0; const int seekable = opts ? opts->seekable : 0; const int level = (opts && opts->level > 0) ? opts->level : ZXC_LEVEL_DEFAULT; const size_t block_size = (opts && opts->block_size > 0) ? opts->block_size : ZXC_BLOCK_SIZE_DEFAULT; zxc_progress_callback_t cb = opts ? opts->progress_cb : NULL; void* ud = opts ? opts->user_data : NULL; if (UNLIKELY(!zxc_validate_block_size(block_size))) return ZXC_ERROR_BAD_BLOCK_SIZE; return zxc_stream_engine_run(f_in, f_out, n_threads, 1, level, block_size, checksum_enabled, seekable, zxc_compress_chunk_wrapper, cb, ud); } int64_t zxc_stream_decompress(FILE* f_in, FILE* f_out, const zxc_decompress_opts_t* opts) { if (UNLIKELY(!f_in)) return ZXC_ERROR_NULL_INPUT; const int n_threads = opts ? opts->n_threads : 0; const int checksum_enabled = opts ? opts->checksum_enabled : 0; zxc_progress_callback_t cb = opts ? opts->progress_cb : NULL; void* ud = opts ? opts->user_data : NULL; return zxc_stream_engine_run(f_in, f_out, n_threads, 0, 0, 0, checksum_enabled, 0, (zxc_chunk_processor_t)zxc_decompress_chunk_wrapper, cb, ud); } int64_t zxc_stream_get_decompressed_size(FILE* f_in) { if (UNLIKELY(!f_in)) return ZXC_ERROR_NULL_INPUT; const long long saved_pos = ftello(f_in); if (UNLIKELY(saved_pos < 0)) return ZXC_ERROR_IO; // Get file size if (fseeko(f_in, 0, SEEK_END) != 0) return ZXC_ERROR_IO; const long long file_size = ftello(f_in); if (UNLIKELY(file_size < (long long)(ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE))) { fseeko(f_in, saved_pos, SEEK_SET); return ZXC_ERROR_SRC_TOO_SMALL; } uint8_t header[ZXC_FILE_HEADER_SIZE]; if (UNLIKELY(fseeko(f_in, 0, SEEK_SET) != 0 || fread(header, 1, ZXC_FILE_HEADER_SIZE, f_in) != ZXC_FILE_HEADER_SIZE)) { fseeko(f_in, saved_pos, SEEK_SET); return ZXC_ERROR_IO; } if (UNLIKELY(zxc_le32(header) != ZXC_MAGIC_WORD)) { fseeko(f_in, saved_pos, SEEK_SET); return ZXC_ERROR_BAD_MAGIC; } uint8_t footer[ZXC_FILE_FOOTER_SIZE]; if (UNLIKELY(fseeko(f_in, file_size - ZXC_FILE_FOOTER_SIZE, SEEK_SET) != 0 || fread(footer, 1, ZXC_FILE_FOOTER_SIZE, f_in) != ZXC_FILE_FOOTER_SIZE)) { fseeko(f_in, saved_pos, SEEK_SET); return ZXC_ERROR_IO; } fseeko(f_in, saved_pos, SEEK_SET); return (int64_t)zxc_le64(footer); } zxc-0.10.0/src/lib/zxc_internal.h000066400000000000000000001425301517010557200166120ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_internal.h * @brief Internal definitions, constants, SIMD helpers, and utility functions. * * This header is **not** part of the public API. It is shared across the * library's translation units and contains: * - Platform detection and SIMD intrinsic includes. * - Compiler-abstraction macros (LIKELY, PREFETCH, MEMCPY, ALIGN, ...). * - Endianness detection and byte-swap helpers. * - File-format constants (magic word, header sizes, block sizes, ...). * - Inline helpers for hashing, endian-safe loads/stores, bit manipulation, * ZigZag encoding, aligned allocation, and bitstream reading. * - Internal function prototypes for chunk-level compression/decompression. * * @warning Do not include this header from user code; use the public headers * zxc_buffer.h, zxc_stream.h, or zxc_sans_io.h instead. */ #ifndef ZXC_INTERNAL_H #define ZXC_INTERNAL_H #include #include #include #include #include #include #include #include "../../include/zxc_buffer.h" #include "../../include/zxc_constants.h" #include "../../include/zxc_sans_io.h" #include "rapidhash.h" #ifdef __cplusplus extern "C" { #endif /** * @defgroup internal Internal Helpers * @brief Platform abstractions, constants, and utility functions (private). * @{ */ /** * @name Atomic Qualifier * @brief Provides a portable atomic / volatile qualifier. * * If C11 atomics are available, @c ZXC_ATOMIC expands to @c _Atomic; * otherwise it falls back to @c volatile. * @{ */ #if !defined(__cplusplus) && defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && \ !defined(__STDC_NO_ATOMICS__) #include #define ZXC_ATOMIC _Atomic #define ZXC_USE_C11_ATOMICS 1 #else #define ZXC_ATOMIC volatile #define ZXC_USE_C11_ATOMICS 0 #endif /** @} */ /* end of Atomic Qualifier */ /** * @name SIMD Intrinsics & Compiler Macros * @brief Auto-detected SIMD feature macros for x86 (SSE/AVX) and ARM (NEON). * * Depending on the target architecture and compiler flags the following macros * may be defined: * - @c ZXC_USE_AVX512 - AVX-512F + AVX-512BW available. * - @c ZXC_USE_AVX2 - AVX2 available. * - @c ZXC_USE_NEON64 - AArch64 NEON available. * - @c ZXC_USE_NEON32 - ARMv7 NEON available. * * Define @c ZXC_DISABLE_SIMD to gate all hand-written SIMD paths (intrinsics, * inline assembly). Compiler auto-vectorisation is unaffected. * @{ */ #ifndef ZXC_DISABLE_SIMD #if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) #include #include #if defined(__AVX512F__) && defined(__AVX512BW__) #ifndef ZXC_USE_AVX512 #define ZXC_USE_AVX512 #endif #endif #if defined(__AVX2__) #ifndef ZXC_USE_AVX2 #define ZXC_USE_AVX2 #endif #endif #elif (defined(__ARM_NEON) || defined(__ARM_NEON__) || defined(_M_ARM64) || \ defined(ZXC_USE_NEON32) || defined(ZXC_USE_NEON64)) #if !defined(_MSC_VER) #include #endif #include #if defined(__aarch64__) || defined(_M_ARM64) #ifndef ZXC_USE_NEON64 #define ZXC_USE_NEON64 #endif #else #ifndef ZXC_USE_NEON32 #define ZXC_USE_NEON32 #endif #endif #endif #endif /* ZXC_DISABLE_SIMD */ /** @} */ /* end of SIMD Intrinsics */ /** * @name Compiler Abstractions * @brief Portable wrappers for branch hints, prefetch, memory ops, alignment, * and forced inlining. * @{ */ #if defined(__GNUC__) || defined(__clang__) /** @def LIKELY * @brief Branch prediction hint: expression is likely true. * @param x Expression to evaluate. */ #define LIKELY(x) (__builtin_expect(!!(x), 1)) /** @def UNLIKELY * @brief Branch prediction hint: expression is unlikely to be true. * @param x Expression to evaluate. */ #define UNLIKELY(x) (__builtin_expect(!!(x), 0)) /** @def RESTRICT * @brief Pointer aliasing hint (maps to __restrict__). */ #define RESTRICT __restrict__ /** @def ZXC_PREFETCH_READ * @brief Prefetch data for reading. * @param ptr Pointer to data to prefetch. */ #define ZXC_PREFETCH_READ(ptr) __builtin_prefetch((const void*)(ptr), 0, 3) /** @def ZXC_PREFETCH_WRITE * @brief Prefetch data for writing. * @param ptr Pointer to data to prefetch. */ #define ZXC_PREFETCH_WRITE(ptr) __builtin_prefetch((const void*)(ptr), 1, 3) /** @def ZXC_MEMCPY * @brief Optimized memory copy using compiler built-in. */ #define ZXC_MEMCPY(dst, src, n) __builtin_memcpy(dst, src, n) /** @def ZXC_MEMSET * @brief Optimized memory set using compiler built-in. */ #define ZXC_MEMSET(dst, val, n) __builtin_memset(dst, val, n) /** @def ZXC_ALIGN * @brief Specifies memory alignment for a variable or structure. * @param x Alignment boundary in bytes (must be a power of 2). */ #define ZXC_ALIGN(x) __attribute__((aligned(x))) /** @def ZXC_ALWAYS_INLINE * @brief Forces a function to be inlined at all optimization levels. */ #define ZXC_ALWAYS_INLINE inline __attribute__((always_inline)) #elif defined(_MSC_VER) #include #if defined(_M_IX86) || defined(_M_X64) || defined(_M_AMD64) #include #define ZXC_PREFETCH_READ(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0) #define ZXC_PREFETCH_WRITE(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0) #else #define ZXC_PREFETCH_READ(ptr) __prefetch((const void*)(ptr)) #define ZXC_PREFETCH_WRITE(ptr) __prefetch((const void*)(ptr)) #endif #define LIKELY(x) (x) #define UNLIKELY(x) (x) #define RESTRICT __restrict #pragma intrinsic(memcpy, memset) #define ZXC_MEMCPY(dst, src, n) memcpy(dst, src, n) #define ZXC_MEMSET(dst, val, n) memset(dst, val, n) /** @def ZXC_ALIGN * @brief Specifies memory alignment for a variable or structure (MSVC). * @param x Alignment boundary in bytes (must be a power of 2). */ #define ZXC_ALIGN(x) __declspec(align(x)) /** @def ZXC_ALWAYS_INLINE * @brief Forces a function to be inlined at all optimization levels (MSVC). */ #define ZXC_ALWAYS_INLINE __forceinline #pragma intrinsic(_BitScanReverse) #else #define LIKELY(x) (x) #define UNLIKELY(x) (x) #define RESTRICT #define ZXC_PREFETCH_READ(ptr) #define ZXC_PREFETCH_WRITE(ptr) #define ZXC_MEMCPY(dst, src, n) memcpy(dst, src, n) #define ZXC_MEMSET(dst, val, n) memset(dst, val, n) /** @def ZXC_ALWAYS_INLINE * @brief Forces a function to be inlined (fallback for non-GCC/Clang/MSVC compilers). */ #define ZXC_ALWAYS_INLINE inline #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L #include /** @def ZXC_ALIGN * @brief Specifies memory alignment using C11 _Alignas. * @param x Alignment boundary in bytes (must be a power of 2). */ #define ZXC_ALIGN(x) _Alignas(x) #else /** @def ZXC_ALIGN * @brief No-op alignment macro for compilers without alignment support. * @param x Ignored (alignment not supported). */ #define ZXC_ALIGN(x) #endif #endif /** @} */ /* end of Compiler Abstractions */ /** * @name Endianness Detection * @brief Compile-time detection of host byte order. * * Defines exactly one of @c ZXC_LITTLE_ENDIAN or @c ZXC_BIG_ENDIAN. * @{ */ #ifndef ZXC_LITTLE_ENDIAN #if defined(_WIN32) || defined(__LITTLE_ENDIAN__) || \ (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) #define ZXC_LITTLE_ENDIAN #elif defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) #define ZXC_BIG_ENDIAN #else #warning "Endianness not detected, defaulting to little-endian" #define ZXC_LITTLE_ENDIAN #endif #endif /** @} */ /* end of Endianness Detection */ /** * @name Byte-Swap Helpers * @brief 16/32/64-bit byte-swap macros (only defined under @c ZXC_BIG_ENDIAN). * @{ */ #ifdef ZXC_BIG_ENDIAN #if defined(__GNUC__) || defined(__clang__) #define ZXC_BSWAP16(x) __builtin_bswap16(x) #define ZXC_BSWAP32(x) __builtin_bswap32(x) #define ZXC_BSWAP64(x) __builtin_bswap64(x) #elif defined(_MSC_VER) #define ZXC_BSWAP16(x) _byteswap_ushort(x) #define ZXC_BSWAP32(x) _byteswap_ulong(x) #define ZXC_BSWAP64(x) _byteswap_uint64(x) #else #define ZXC_BSWAP16(x) ((uint16_t)(((x) >> 8) | ((x) << 8))) #define ZXC_BSWAP32(x) \ ((uint32_t)(((x) >> 24) | (((x) >> 8) & 0xFF00) | (((x) << 8) & 0xFF0000) | ((x) << 24))) #define ZXC_BSWAP64(x) \ ((uint64_t)(((uint64_t)ZXC_BSWAP32((uint32_t)(x)) << 32) | ZXC_BSWAP32((uint32_t)((x) >> 32)))) #endif #endif /** @} */ /* end of Byte-Swap Helpers */ /** * @name File Format Constants * @brief Magic words, header sizes, block sizes, and related constants. * @{ */ /** @brief Magic word identifying ZXC files (little-endian 0x9CB02EF5). */ #define ZXC_MAGIC_WORD 0x9CB02EF5U /** @brief Current on-disk file format version. */ #define ZXC_FILE_FORMAT_VERSION 5 /** @brief Size of stdio I/O buffers (1 MB). */ #define ZXC_IO_BUFFER_SIZE (1024 * 1024) /** @brief Maximum number of threads allowed for streaming operations. */ #define ZXC_MAX_THREADS 512 /** @brief Safety padding appended to buffers to tolerate overruns. */ #define ZXC_PAD_SIZE 32 /** @brief Assumed CPU cache line size for alignment. */ #define ZXC_CACHE_LINE_SIZE 64 /** @brief Bitmask for cache-line alignment checks. */ #define ZXC_ALIGNMENT_MASK (ZXC_CACHE_LINE_SIZE - 1) /** @brief Allocation-safe max vbyte length (sufficient for < 2 MB blocks). */ #define ZXC_VBYTE_ALLOC_LEN 3 /** @brief File header size: Magic(4)+Version(1)+Chunk(1)+Flags(1)+Reserved(7)+CRC(2). */ #define ZXC_FILE_HEADER_SIZE 16 /** @brief Bit flag in the Flags byte indicating checksum presence (bit 7). */ #define ZXC_FILE_FLAG_HAS_CHECKSUM 0x80U /** @brief Mask for the checksum algorithm id (bits 0-3). */ #define ZXC_FILE_CHECKSUM_ALGO_MASK 0x0FU /** @brief Block header size: Type(1)+Flags(1)+Reserved(1)+CRC(1)+CompSize(4). */ #define ZXC_BLOCK_HEADER_SIZE 8 /** @brief Size of the per-block checksum field in bytes. */ #define ZXC_BLOCK_CHECKSUM_SIZE 4 /** @brief Binary size of a NUM block sub-header. */ #define ZXC_NUM_HEADER_BINARY_SIZE 16 /** @brief Binary size of a GLO block sub-header. */ #define ZXC_GLO_HEADER_BINARY_SIZE 16 /** @brief Binary size of a GHI block sub-header. */ #define ZXC_GHI_HEADER_BINARY_SIZE 16 /** @brief Binary size of a NUM chunk sub-frame header (nvals + bits + base + psize). */ #define ZXC_NUM_CHUNK_HEADER_SIZE 16 /** @brief Number of numeric values to decode in a single SIMD batch (NUM block). */ #define ZXC_NUM_DEC_BATCH 32 /** @brief Maximum number of frames that can be processed in a single compression operation (NUM * block). */ #define ZXC_NUM_FRAME_SIZE 128 /** @brief Binary size of a section descriptor (comp_size + raw_size). */ #define ZXC_SECTION_DESC_BINARY_SIZE 8 /** @brief 32-bit mask for extracting sizes from a section descriptor. */ #define ZXC_SECTION_SIZE_MASK 0xFFFFFFFFU /** @brief Number of sections in a GLO block. */ #define ZXC_GLO_SECTIONS 4 /** @brief Number of sections in a GHI block. */ #define ZXC_GHI_SECTIONS 3 /** @brief Checksum algorithm id for RapidHash (default, sole implementation). */ #define ZXC_CHECKSUM_RAPIDHASH 0 /** @brief Size of the global checksum appended after EOF block (4 bytes). */ #define ZXC_GLOBAL_CHECKSUM_SIZE 4 /** @brief File footer size: original_size(8) + global_checksum(4). */ #define ZXC_FILE_FOOTER_SIZE 12 /** @name Seekable Format Constants * @brief Seek table block appended between EOF block and footer. * * The seek table is optional (opt-in at compression time) and allows * random-access decompression by recording per-block compressed and * decompressed sizes. It uses a standard ZXC block header with * @c block_type = @ref ZXC_BLOCK_SEK. * * Detection from the end of the file: the reader derives @c num_blocks * from the file footer (total decompressed size) and file header (block size). * It then seeks backward to validate the SEK block header. * @{ */ /** @brief Per-block entry size: comp_size(4) only. decomp_size is derived * from the file header's block_size (all blocks except the last are full). */ #define ZXC_SEEK_ENTRY_SIZE 4 /** @} */ /* end of Seekable Format Constants */ /** @name GLO Token Constants * @brief 4-bit literal length / 4-bit match length / 16-bit offset. * @{ */ /** @brief Bits for Literal Length in a GLO token. */ #define ZXC_TOKEN_LIT_BITS 4 /** @brief Bits for Match Length in a GLO token. */ #define ZXC_TOKEN_ML_BITS 4 /** @brief Mask to extract Literal Length from a GLO token. */ #define ZXC_TOKEN_LL_MASK ((1U << ZXC_TOKEN_LIT_BITS) - 1) /** @brief Mask to extract Match Length from a GLO token. */ #define ZXC_TOKEN_ML_MASK ((1U << ZXC_TOKEN_ML_BITS) - 1) /** @} */ /** @name GHI Sequence Constants * @brief 8-bit literal length / 8-bit match length / 16-bit offset. * @{ */ /** @brief Bits for Literal Length in a GHI sequence. */ #define ZXC_SEQ_LL_BITS 8 /** @brief Bits for Match Length in a GHI sequence. */ #define ZXC_SEQ_ML_BITS 8 /** @brief Bits for Offset in a GHI sequence. */ #define ZXC_SEQ_OFF_BITS 16 /** @brief Mask to extract Literal Length from a GHI sequence. */ #define ZXC_SEQ_LL_MASK ((1U << ZXC_SEQ_LL_BITS) - 1) /** @brief Mask to extract Match Length from a GHI sequence. */ #define ZXC_SEQ_ML_MASK ((1U << ZXC_SEQ_ML_BITS) - 1) /** @brief Mask to extract Offset from a GHI sequence. */ #define ZXC_SEQ_OFF_MASK ((1U << ZXC_SEQ_OFF_BITS) - 1) /** @} */ /** @name Literal Stream Encoding * @{ */ /** @brief Flag bit indicating an RLE run in the literal stream (0x80). */ #define ZXC_LIT_RLE_FLAG 0x80U /** @brief Mask to extract the run/literal length (lower 7 bits). */ #define ZXC_LIT_LEN_MASK (ZXC_LIT_RLE_FLAG - 1) /** @} */ /** @name LZ77 Constants * @brief Hash table geometry, sliding window, and match parameters. * * The hash table uses a split layout with 15-bit addressing (32 768 buckets): * - `hash_positions[]`: uint32_t, stores `(epoch << offset_bits) | position` (128 KB). * - `hash_tags[]`: uint8_t, stores an 8-bit tag for fast rejection (32 KB). * Total: 160 KB. The tag table fits in L1 cache, enabling a * "filter-first" access pattern that avoids cold loads into hash_positions * on the ~60-75% of lookups where the tag mismatches. * The 64 KB sliding window allows `chain_table` to use `uint16_t`. * @{ */ /** @brief Address bits for the LZ77 hash table (2^15 = 32 768 buckets). */ #define ZXC_LZ_HASH_BITS 15 /** @brief Marsaglia multiplicative hash constant for 4-byte hashing. */ #define ZXC_LZ_HASH_PRIME1 0x2D35182DU /** @brief Marsaglia/Vigna xorshift* multiplier for 5-byte hashing. */ #define ZXC_LZ_HASH_PRIME2 0x2545F4914F6CDD1DULL /** @brief Maximum number of entries in the hash table. */ #define ZXC_LZ_HASH_SIZE (1U << ZXC_LZ_HASH_BITS) /** @brief Sliding window size (64 KB). */ #define ZXC_LZ_WINDOW_SIZE (1U << 16) /** @brief Minimum match length for an LZ77 match. */ #define ZXC_LZ_MIN_MATCH_LEN 5 /** @brief Base bias added to encoded offsets (stored = actual - bias). */ #define ZXC_LZ_OFFSET_BIAS 1 /** @brief Maximum allowed offset distance. */ #define ZXC_LZ_MAX_DIST (ZXC_LZ_WINDOW_SIZE - 1) /** @} */ /** @name Hash Prime Constants * @brief Mixing primes used by internal hash functions. * @{ */ /** @brief Hash prime 1. */ #define ZXC_HASH_PRIME1 0x9E3779B97F4A7C15ULL /** @brief Hash prime 2. */ #define ZXC_HASH_PRIME2 0xD2D84A61D2D84A61ULL /** @} */ /** @name Block Size Helpers * @brief Runtime helpers for variable block sizes. * @{ */ /** * @brief Integer log-base-2 for a 32-bit value. * @param v Must be a power of two (undefined for zero). * @return Floor of log2(v). */ static ZXC_ALWAYS_INLINE uint32_t zxc_log2_u32(const uint32_t v) { #ifdef _MSC_VER unsigned long index; return (v == 0) ? 0 : (_BitScanReverse(&index, v) ? index : 0); #else return (v == 0) ? 0 : (uint32_t)(31 - __builtin_clz(v)); #endif } /** * @brief Validates a block size. * Must be a power of two in [ZXC_BLOCK_SIZE_MIN, ZXC_BLOCK_SIZE_MAX]. * @return 1 if valid, 0 otherwise. */ static ZXC_ALWAYS_INLINE int zxc_validate_block_size(const size_t bs) { return bs >= ZXC_BLOCK_SIZE_MIN && bs <= ZXC_BLOCK_SIZE_MAX && (bs & (bs - 1)) == 0; } /** @} */ /** @} */ /* end of File Format Constants */ /** * @struct zxc_lz77_params_t * @brief Search parameters for LZ77 compression levels. * * Each compression level maps to a specific set of parameters that control the * trade-off between compression speed and ratio. Higher search depths and lazy * matching improve ratio at the expense of throughput; larger step values * accelerate literal scanning but may miss short matches. */ typedef struct { /** Maximum number of candidates explored in the hash chain per position. * Higher values find better matches but increase CPU cost linearly. */ int search_depth; /** "Good enough" match length: once a match reaches this threshold the * chain walk stops immediately, avoiding wasted effort on an already * excellent match. */ int sufficient_len; /** Enable lazy matching. When set, after finding a match at position * @c ip the compressor probes @c ip+1 (and @c ip+2 for level >= 4) to * see if a longer match exists. If so, a literal is emitted and the * better match is taken instead. Improves ratio but costs extra work. */ int use_lazy; /** Maximum number of candidates explored during lazy evaluation (same * semantics as @ref search_depth but applied to the ip+1 / ip+2 probes). * Only meaningful when @ref use_lazy is non-zero. */ int lazy_attempts; /** Skip lazy evaluation when the current match length already reaches * this threshold: a match this long is unlikely to be beaten at the * next byte. Set to 0 when @ref use_lazy is disabled. */ int lazy_len_threshold; /** Base step size when advancing through unmatched literals. * 1 = test every byte (best ratio), 4 = skip aggressively (fastest). */ uint32_t step_base; /** Acceleration factor for step size: @c step = step_base + (distance >> step_shift). * A larger value keeps the step conservative (grows slowly with distance); * a smaller value ramps up quickly, skipping more in long literal runs. */ uint32_t step_shift; } zxc_lz77_params_t; /** * @brief Retrieves LZ77 compression parameters based on the specified compression level. * * This inline function returns the appropriate LZ77 parameters configuration * for the given compression level. * * @param[in] level The compression level to use for determining LZ77 parameters. * @return zxc_lz77_params_t The LZ77 parameters structure corresponding to the specified level. */ static ZXC_ALWAYS_INLINE zxc_lz77_params_t zxc_get_lz77_params(const int level) { if (level >= 5) return (zxc_lz77_params_t){64, 256, 1, 16, 128, 1, 8}; // search_depth, sufficient_len, use_lazy, lazy_attempts, lazy_len_threshold, step_base, // step_shift static const zxc_lz77_params_t table[5] = { {3, 16, 0, 0, 0, 4, 4}, // fallback {3, 16, 0, 0, 0, 4, 4}, // level 1 {3, 18, 0, 0, 0, 3, 6}, // level 2 {3, 16, 1, 4, 128, 1, 4}, // level 3 {3, 18, 1, 4, 128, 1, 5} // level 4 }; return table[level < 1 ? 1 : level]; } /** * @enum zxc_block_type_t * @brief Defines the different types of data blocks supported by the ZXC * format. * * This enumeration categorizes blocks based on the compression strategy * applied: * - `ZXC_BLOCK_RAW` (0): No compression. Used when data is incompressible (high * entropy) or when compression would expand the data size. * - `ZXC_BLOCK_GLO` (1): General-purpose compression (LZ77 + Bitpacking). This * is the default for most data (text, binaries, JSON, etc.). Includes 4 sections descriptors. * - `ZXC_BLOCK_NUM` (2): Specialized compression for arrays of 32-bit integers. * Uses Delta Encoding + ZigZag + Bitpacking. * - `ZXC_BLOCK_GHI` (3): General-purpose high-velocity mode using LZ77 with advanced * techniques (lazy matching, step skipping) for maximum ratio. Includes 3 sections descriptors. * - `ZXC_BLOCK_SEK` (254): Seek table block. Contains per-block compressed/decompressed sizes * for random-access decompression. Placed between EOF block and file footer. * - `ZXC_BLOCK_EOF` (255): End of file marker. */ typedef enum { ZXC_BLOCK_RAW = 0, ZXC_BLOCK_GLO = 1, ZXC_BLOCK_NUM = 2, ZXC_BLOCK_GHI = 3, ZXC_BLOCK_SEK = 254, ZXC_BLOCK_EOF = 255 } zxc_block_type_t; /** * @enum zxc_section_encoding_t * @brief Specifies the encoding methods used for internal data sections. * * These modes determine how specific components (like literals, match lengths, * or offsets) are stored within a block. * - `ZXC_SECTION_ENCODING_RAW`: Data is stored uncompressed. * - `ZXC_SECTION_ENCODING_RLE`: Run-Length Encoding. */ typedef enum { ZXC_SECTION_ENCODING_RAW = 0, ZXC_SECTION_ENCODING_RLE = 1 } zxc_section_encoding_t; /** * @struct zxc_gnr_header_t * @brief Header specific to General (LZ-based) compression blocks. * * This header follows the main block header when the block type is GLO/GHI. It * describes the layout of sequences and literals. * * @var zxc_gnr_header_t::n_sequences * The total count of LZ sequences in the block. * @var zxc_gnr_header_t::n_literals * The total count of literal bytes. * @var zxc_gnr_header_t::enc_lit * Encoding method used for the literal stream. * @var zxc_gnr_header_t::enc_litlen * Encoding method used for the literal lengths stream. * @var zxc_gnr_header_t::enc_mlen * Encoding method used for the match lengths stream. * @var zxc_gnr_header_t::enc_off * Encoding method used for the offset stream. */ typedef struct { uint32_t n_sequences; // Number of sequences uint32_t n_literals; // Number of literals uint8_t enc_lit; // Literal encoding uint8_t enc_litlen; // Literal lengths encoding uint8_t enc_mlen; // Match lengths encoding uint8_t enc_off; // Offset encoding (Unused in Token format, kept for alignment) } zxc_gnr_header_t; /** * @struct zxc_section_desc_t * @brief Describes the size attributes of a specific data section. * * Used to track the compressed and uncompressed sizes of sub-components * (e.g., a literal stream or offset stream) within a block. */ typedef struct { uint64_t sizes; /**< Packed sizes: compressed size (low 32 bits) | raw size (high 32 bits). */ } zxc_section_desc_t; /** * @struct zxc_num_header_t * @brief Header specific to Numeric compression blocks. * * This header follows the main block header when the block type is NUM. * * @var zxc_num_header_t::n_values * The total number of numeric values encoded in the block. * @var zxc_num_header_t::frame_size * The size of the frame used for processing. */ typedef struct { uint64_t n_values; uint16_t frame_size; } zxc_num_header_t; /** * @struct zxc_bit_reader_t * @brief Internal bit reader structure for ZXC compression/decompression. * * This structure maintains the state of the bit stream reading operation. * It buffers bits from the input byte stream into an accumulator to allow * reading variable-length bit sequences. */ typedef struct { const uint8_t* ptr; /**< Pointer to the current position in the input byte stream. */ const uint8_t* end; /**< Pointer to the end of the input byte stream. */ uint64_t accum; /**< Bit accumulator holding buffered bits (64-bit buffer). */ int bits; /**< Number of valid bits currently in the accumulator. */ } zxc_bit_reader_t; /** * ============================================================================ * MEMORY & ENDIANNESS HELPERS * ============================================================================ * Functions to handle unaligned memory access and Little Endian conversion. */ /** * @brief Reads a 16-bit unsigned integer from memory in little-endian format. * * This function interprets the bytes at the given memory address as a * little-endian 16-bit integer, regardless of the host system's endianness. * It is marked as always inline for performance critical paths. * * @param[in] p Pointer to the memory location to read from. * @return The 16-bit unsigned integer value read from memory. */ static ZXC_ALWAYS_INLINE uint16_t zxc_le16(const void* p) { uint16_t v; ZXC_MEMCPY(&v, p, sizeof(v)); #ifdef ZXC_BIG_ENDIAN return ZXC_BSWAP16(v); #else return v; #endif } /** * @brief Reads a 32-bit unsigned integer from memory in little-endian format. * * This function interprets the bytes at the given pointer address as a * little-endian 32-bit integer, regardless of the host system's endianness. * It is marked as always inline for performance critical paths. * * @param[in] p Pointer to the memory location to read from. * @return The 32-bit unsigned integer value read from memory. */ static ZXC_ALWAYS_INLINE uint32_t zxc_le32(const void* p) { uint32_t v; ZXC_MEMCPY(&v, p, sizeof(v)); #ifdef ZXC_BIG_ENDIAN return ZXC_BSWAP32(v); #else return v; #endif } /** * @brief Reads a 64-bit unsigned integer from memory in little-endian format. * * This function interprets the bytes at the given memory address as a * little-endian 64-bit integer, regardless of the host system's endianness. * It is marked as always inline for performance critical paths. * * @param[in] p Pointer to the memory location to read from. * @return The 64-bit unsigned integer value read from memory. */ static ZXC_ALWAYS_INLINE uint64_t zxc_le64(const void* p) { uint64_t v; ZXC_MEMCPY(&v, p, sizeof(v)); #ifdef ZXC_BIG_ENDIAN return ZXC_BSWAP64(v); #else return v; #endif } /** * @brief Stores a 16-bit integer in memory using little-endian byte order. * * This function copies the value of a 16-bit unsigned integer to the specified * memory location. It uses memcpy to avoid strict aliasing violations and * potential unaligned access issues. * * @note This function assumes the system is little-endian or that the compiler * optimizes the memcpy to a store instruction that handles endianness if necessary * (though the implementation shown is a direct copy). * * @param[out] p Pointer to the destination memory where the value will be stored. * Must point to a valid memory region of at least 2 bytes. * @param[in] v The 16-bit unsigned integer value to store. */ static ZXC_ALWAYS_INLINE void zxc_store_le16(void* p, const uint16_t v) { #ifdef ZXC_BIG_ENDIAN const uint16_t s = ZXC_BSWAP16(v); ZXC_MEMCPY(p, &s, sizeof(s)); #else ZXC_MEMCPY(p, &v, sizeof(v)); #endif } /** * @brief Stores a 32-bit unsigned integer in little-endian format at the specified memory location. * * This function writes the 32-bit value `v` to the memory pointed to by `p`. * It uses `ZXC_MEMCPY` to ensure safe memory access, avoiding potential alignment issues * that could occur with direct pointer casting on some architectures. * * @note This function is marked as `ZXC_ALWAYS_INLINE` to minimize function call overhead. * * @param[out] p Pointer to the destination memory where the value will be stored. * @param[in] v The 32-bit unsigned integer value to store. */ static ZXC_ALWAYS_INLINE void zxc_store_le32(void* p, const uint32_t v) { #ifdef ZXC_BIG_ENDIAN const uint32_t s = ZXC_BSWAP32(v); ZXC_MEMCPY(p, &s, sizeof(s)); #else ZXC_MEMCPY(p, &v, sizeof(v)); #endif } /** * @brief Stores a 64-bit unsigned integer in little-endian format at the specified memory location. * * This function copies the 64-bit value `v` to the memory pointed to by `p`. * It uses `ZXC_MEMCPY` to ensure safe memory access, avoiding potential alignment issues * that might occur with direct pointer dereferencing on some architectures. * * @note This function assumes the system is little-endian or that the compiler optimizes * the memcpy to a store instruction that handles endianness correctly if `ZXC_MEMCPY` * is defined appropriately. * * @param[out] p Pointer to the destination memory where the value will be stored. * @param[in] v The 64-bit unsigned integer value to store. */ static ZXC_ALWAYS_INLINE void zxc_store_le64(void* p, const uint64_t v) { #ifdef ZXC_BIG_ENDIAN const uint64_t s = ZXC_BSWAP64(v); ZXC_MEMCPY(p, &s, sizeof(s)); #else ZXC_MEMCPY(p, &v, sizeof(v)); #endif } /** * @brief Computes the 1-byte checksum for block headers. * * Implementation based on Marsaglia's Xorshift (PRNG) principles. * * @param[in] p Pointer to the input data to be hashed (8 bytes) * @return uint8_t The computed hash value. */ static ZXC_ALWAYS_INLINE uint8_t zxc_hash8(const uint8_t* p) { const uint64_t v = zxc_le64(p); uint64_t h = v ^ ZXC_HASH_PRIME1; h ^= h << 13; h ^= h >> 7; h ^= h << 17; return (uint8_t)((h >> 32) ^ h); } /** * @brief Computes the 2-byte checksum for file headers. * * This function generates a hash value by reading data from the given pointer. * The result is a 16-bit hash. * Implementation based on Marsaglia's Xorshift (PRNG) principles. * * @param[in] p Pointer to the input data to be hashed (16 bytes) * @return uint16_t The computed hash value. */ static ZXC_ALWAYS_INLINE uint16_t zxc_hash16(const uint8_t* p) { const uint64_t v1 = zxc_le64(p); const uint64_t v2 = zxc_le64(p + 8); uint64_t h = v1 ^ v2 ^ ZXC_HASH_PRIME2; h ^= h << 13; h ^= h >> 7; h ^= h << 17; const uint32_t res = (uint32_t)((h >> 32) ^ h); return (uint16_t)((res >> 16) ^ res); } /** * @brief Copies 16 bytes from the source memory location to the destination memory location. * * This function is forced to be inlined and uses SIMD intrinsics when available. * SSE2 on x86/x64, NEON on ARM, or memcpy as fallback. * * @param[out] dst Pointer to the destination memory block. * @param[in] src Pointer to the source memory block. */ static ZXC_ALWAYS_INLINE void zxc_copy16(void* dst, const void* src) { #if defined(ZXC_USE_AVX2) || defined(ZXC_USE_AVX512) // AVX2/AVX512: Single 128-bit unaligned load/store _mm_storeu_si128((__m128i*)dst, _mm_loadu_si128((const __m128i*)src)); #elif defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) vst1q_u8((uint8_t*)dst, vld1q_u8((const uint8_t*)src)); #else ZXC_MEMCPY(dst, src, 16); #endif } /** * @brief Copies 32 bytes from source to destination using SIMD when available. * * Uses AVX2 on x86, NEON on ARM64/ARM32, or two 16-byte copies as fallback. * * @param[out] dst Pointer to the destination memory block. * @param[in] src Pointer to the source memory block. */ static ZXC_ALWAYS_INLINE void zxc_copy32(void* dst, const void* src) { #if defined(ZXC_USE_AVX2) || defined(ZXC_USE_AVX512) // AVX2/AVX512: Single 256-bit (32 byte) unaligned load/store _mm256_storeu_si256((__m256i*)dst, _mm256_loadu_si256((const __m256i*)src)); #elif defined(ZXC_USE_NEON64) || defined(ZXC_USE_NEON32) // NEON: Two 128-bit (16 byte) unaligned load/stores vst1q_u8((uint8_t*)dst, vld1q_u8((const uint8_t*)src)); vst1q_u8((uint8_t*)dst + 16, vld1q_u8((const uint8_t*)src + 16)); #else ZXC_MEMCPY(dst, src, 32); #endif } /** * @brief Counts trailing zeros in a 32-bit unsigned integer. * * This function returns the number of contiguous zero bits starting from the * least significant bit (LSB). If the input is 0, it returns 32. * * It utilizes compiler-specific built-ins for GCC/Clang (`__builtin_ctz`) and * MSVC (`_BitScanForward`) for optimal performance. If no supported compiler * is detected, it falls back to a portable De Bruijn sequence implementation. * * @param[in] x The 32-bit unsigned integer to scan. * @return The number of trailing zeros (0-32). */ static ZXC_ALWAYS_INLINE int zxc_ctz32(const uint32_t x) { if (x == 0) return 32; #if defined(__GNUC__) || defined(__clang__) return __builtin_ctz(x); #elif defined(_MSC_VER) unsigned long r; _BitScanForward(&r, x); return (int)r; #else // Fallback De Bruijn (32 bits) static const int DeBruijn32[32] = {0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; return DeBruijn32[((uint32_t)((x & (0U - x)) * 0x077CB531U)) >> 27]; #endif } /** * @brief Counts the number of trailing zeros in a 64-bit unsigned integer. * * This function determines the number of zero bits following the least significant * one bit in the binary representation of `x`. * * @param[in] x The 64-bit unsigned integer to scan. * @return The number of trailing zeros. Returns 64 if `x` is 0. * * @note This implementation uses compiler built-ins for GCC/Clang (`__builtin_ctzll`) * and MSVC (`_BitScanForward64`) when available for optimal performance. * It falls back to a De Bruijn sequence multiplication method for other compilers. */ static ZXC_ALWAYS_INLINE int zxc_ctz64(const uint64_t x) { if (x == 0) return 64; #if defined(__GNUC__) || defined(__clang__) return __builtin_ctzll(x); #elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64)) unsigned long r; _BitScanForward64(&r, x); return (int)r; #elif defined(_MSC_VER) // Use two 32-bit scans to avoid fragile 64-bit De Bruijn multiplication. unsigned long r; const uint32_t lo = (uint32_t)x; if (_BitScanForward(&r, lo)) return (int)r; _BitScanForward(&r, (uint32_t)(x >> 32)); return 32 + (int)r; #else // Fallback De Bruijn for non-GCC/non-MSVC compilers static const int Debruijn64[64] = { 0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4, 62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5, 63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11, 46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6}; return Debruijn64[((x & (0ULL - x)) * 0x03F79D71B4CA8B09ULL) >> 58]; #endif } /** * @brief Calculates the index of the highest set bit (most significant bit) in a 32-bit integer. * * This function determines the position of the most significant bit that is set to 1. * * @param[in] n The 32-bit unsigned integer to analyze. * @return The 0-based index of the highest set bit. If n is 0, the behavior is undefined. */ static ZXC_ALWAYS_INLINE uint8_t zxc_highbit32(const uint32_t n) { #ifdef _MSC_VER unsigned long index; return (n == 0) ? 0 : (_BitScanReverse(&index, n) ? (uint8_t)(index + 1) : 0); #else return (n == 0) ? 0 : (32 - __builtin_clz(n)); #endif } /** * @brief Encodes a signed 32-bit integer using ZigZag encoding. * * ZigZag encoding maps signed integers to unsigned integers so that numbers with a small * absolute value (for instance, -1) have a small variant encoded value too. It does this * by "zig-zagging" back and forth through the positive and negative integers: * * 0 => 0 * -1 => 1 * 1 => 2 * -2 => 3 * 2 => 4 * * This is particularly useful for variable-length encoding (varint) of signed integers, * as standard varint encoding is inefficient for negative numbers (which are interpreted * as very large unsigned integers). * * @param[in] n The signed 32-bit integer to encode. * @return The ZigZag encoded unsigned 32-bit integer. */ static ZXC_ALWAYS_INLINE uint32_t zxc_zigzag_encode(const int32_t n) { return ((uint32_t)n << 1) ^ (uint32_t)(-(int32_t)((uint32_t)n >> 31)); } /** * @brief Decodes a 32-bit unsigned integer using ZigZag decoding. * * ZigZag encoding maps signed integers to unsigned integers so that numbers with a small * absolute value (for instance, -1) have a small variant encoded value too. It does this * by "zig-zagging" back and forth through the positive and negative integers: * 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. * * This function reverses that process, converting the unsigned representation back into * the original signed 32-bit integer. * * @param[in] n The unsigned 32-bit integer to decode. * @return The decoded signed 32-bit integer. */ static ZXC_ALWAYS_INLINE int32_t zxc_zigzag_decode(const uint32_t n) { return (int32_t)(n >> 1) ^ -(int32_t)(n & 1); } /** * @brief Allocates aligned memory in a cross-platform manner. * * This function provides a unified interface for allocating memory with a specific * alignment requirement. It wraps `_aligned_malloc` for Windows * environments and `posix_memalign` for POSIX-compliant systems. * * @param[in] size The size of the memory block to allocate, in bytes. * @param[in] alignment The alignment value, which must be a power of two and a multiple * of `sizeof(void *)`. * @return A pointer to the allocated memory block, or NULL if the allocation fails. * The returned pointer must be freed using the corresponding aligned free function. */ void* zxc_aligned_malloc(const size_t size, const size_t alignment); /** * @brief Frees memory previously allocated with an aligned allocation function. * * This function provides a cross-platform wrapper for freeing aligned memory. * On Windows, it calls `_aligned_free`. * On other platforms, it falls back to the standard `free` function. * * @param[in] ptr A pointer to the memory block to be freed. If ptr is NULL, no operation is * performed. */ void zxc_aligned_free(void* ptr); /* * ============================================================================ * COMPRESSION CONTEXT & STRUCTS * ============================================================================ */ /* * INTERNAL API * ------------ */ /** * @brief Calculates a 32-bit hash for a given input buffer. * @param[in] input Pointer to the data buffer. * @param[in] len Length of the data in bytes. * @param[in] hash_method Checksum algorithm identifier (e.g., ZXC_CHECKSUM_RAPIDHASH). * @return The calculated 32-bit hash value. */ static ZXC_ALWAYS_INLINE uint32_t zxc_checksum(const void* RESTRICT input, const size_t len, const uint8_t hash_method) { (void)hash_method; /* single algorithm for now; extend when adding more */ const uint64_t hash = rapidhash(input, len); return (uint32_t)(hash ^ (hash >> (sizeof(uint32_t) * CHAR_BIT))); } /** * @brief Combines a running hash with a new block hash using rotate-left and XOR. * * This function updates a global checksum by rotating the current hash left by 1 bit * (with wraparound) and XORing with the new block hash. This provides a simple but * effective rolling hash that depends on the order of blocks. * * Formula: result = ((hash << 1) | (hash >> 31)) ^ block_hash * * @param[in] hash The current running hash value. * @param[in] block_hash The hash of the new block to combine. * @return The updated combined hash value. */ static ZXC_ALWAYS_INLINE uint32_t zxc_hash_combine_rotate(const uint32_t hash, const uint32_t block_hash) { return ((hash << 1) | (hash >> 31)) ^ block_hash; } /** * @brief Loads up to 7 bytes from memory in little-endian order into a uint64_t. * * This is used for partial reads at stream boundaries where fewer than 8 bytes * remain. Unlike ZXC_MEMCPY into a uint64_t (which is endian-dependent), this * function always produces a value with byte 0 in the least-significant bits. * * @param[in] p Pointer to the source bytes. * @param[in] n Number of bytes to read (must be < 8). * @return The loaded value in native host order, with bytes arranged as if * read from a little-endian stream. */ static ZXC_ALWAYS_INLINE uint64_t zxc_le_partial(const uint8_t* p, size_t n) { #ifdef ZXC_BIG_ENDIAN uint64_t v = 0; for (size_t i = 0; i < n; i++) v |= (uint64_t)p[i] << (i * CHAR_BIT); return v; #else uint64_t v = 0; n = n > sizeof(v) ? sizeof(v) : n; ZXC_MEMCPY(&v, p, n); return v; #endif } /** * @brief Initializes a bit reader structure. * * Sets up the internal state of the bit reader to read from the specified * source buffer. * * @param[out] br Pointer to the bit reader structure to initialize. * @param[in] src Pointer to the source buffer containing the data to read. * @param[in] size The size of the source buffer in bytes. */ static ZXC_ALWAYS_INLINE void zxc_br_init(zxc_bit_reader_t* RESTRICT br, const uint8_t* RESTRICT src, const size_t size) { br->ptr = src; br->end = src + size; // Safety check: ensure we have at least 8 bytes to fill the accumulator if (UNLIKELY(size < sizeof(uint64_t))) { br->accum = zxc_le_partial(src, size); br->ptr += size; br->bits = (int)(size * CHAR_BIT); } else { br->accum = zxc_le64(br->ptr); br->ptr += sizeof(uint64_t); br->bits = sizeof(uint64_t) * CHAR_BIT; } } /** * @brief Ensures that the bit reader buffer contains at least the specified * number of bits. * * This function checks if the internal buffer of the bit reader has enough bits * available to satisfy a subsequent read operation of `needed` bits. If not, it * refills the buffer from the source. * * @param[in,out] br Pointer to the bit reader context. * @param[in] needed The number of bits required to be available in the buffer. */ static ZXC_ALWAYS_INLINE void zxc_br_ensure(zxc_bit_reader_t* RESTRICT br, const int needed) { if (UNLIKELY(br->bits < needed)) { const int safe_bits = (br->bits < 0) ? 0 : br->bits; br->bits = safe_bits; // Mask out garbage bits (retain only valid existing bits) #if !defined(ZXC_DISABLE_SIMD) && defined(__BMI2__) && (defined(__x86_64__) || defined(_M_X64)) br->accum = _bzhi_u64(br->accum, safe_bits); #else br->accum &= (safe_bits < 64) ? ((1ULL << safe_bits) - 1) : ~0ULL; #endif // Calculate how many bytes we can read // We want to fill up to the accumulation capability (64 bits for uint64_t) // Bytes needed = (capacity_bits - safe_bits) / 8 const int bytes_needed = ((int)(sizeof(uint64_t) * CHAR_BIT) - safe_bits) / CHAR_BIT; // Bounds check: zxc_le64 always reads 8 bytes, so we need at least 8 const size_t bytes_left = (size_t)(br->end - br->ptr); if (UNLIKELY(bytes_left < sizeof(uint64_t))) { // Partial read (slow path / end of stream) const size_t to_read = (bytes_left < (size_t)bytes_needed) ? bytes_left : (size_t)bytes_needed; const uint64_t raw = zxc_le_partial(br->ptr, to_read); br->accum |= (safe_bits < 64) ? (raw << safe_bits) : 0; br->ptr += to_read; br->bits = safe_bits + (int)to_read * CHAR_BIT; } else { // Fast path: full 8-byte read is safe const uint64_t raw = zxc_le64(br->ptr); br->accum |= (safe_bits < 64) ? (raw << safe_bits) : 0; br->ptr += bytes_needed; br->bits = safe_bits + bytes_needed * CHAR_BIT; } } } /** * @brief Bit-packs a stream of 32-bit integers into a destination buffer. * * Compresses an array of 32-bit integers by packing them using a specified * number of bits per integer. * * @param[in] src Pointer to the source array of 32-bit integers. * @param[in] count The number of integers to pack. * @param[out] dst Pointer to the destination buffer where packed data will be * written. * @param[in] dst_cap The capacity of the destination buffer in bytes. * @param[in] bits The number of bits to use for each integer during packing. * @return int The number of bytes written to the destination buffer, or a negative * error code on failure. */ int zxc_bitpack_stream_32(const uint32_t* RESTRICT src, const size_t count, uint8_t* RESTRICT dst, const size_t dst_cap, const uint8_t bits); /** * @brief Writes a numeric header structure to a destination buffer. * * Serializes the `zxc_num_header_t` structure into the output stream. * * @param[out] dst Pointer to the destination buffer. * @param[in] rem The remaining space in the destination buffer. * @param[in] nh Pointer to the numeric header structure to write. * @return int The number of bytes written, or a negative error code if the buffer * is too small. */ int zxc_write_num_header(uint8_t* RESTRICT dst, const size_t rem, const zxc_num_header_t* RESTRICT nh); /** * @brief Reads a numeric header structure from a source buffer. * * Deserializes data from the input stream into a `zxc_num_header_t` structure. * * @param[in] src Pointer to the source buffer. * @param[in] src_size The size of the source buffer available for reading. * @param[out] nh Pointer to the numeric header structure to populate. * @return int The number of bytes read from the source, or a negative error code on * failure. */ int zxc_read_num_header(const uint8_t* RESTRICT src, const size_t src_size, zxc_num_header_t* RESTRICT nh); /** * @brief Writes a generic header and section descriptors to a destination * buffer. * * Serializes the `zxc_gnr_header_t` and an array of 4 section descriptors. * * @param[out] dst Pointer to the destination buffer. * @param[in] rem The remaining space in the destination buffer. * @param[in] gh Pointer to the generic header structure to write. * @param[in] desc Array of 4 section descriptors to write. * @return int The number of bytes written, or a negative error code if the buffer * is too small. */ int zxc_write_glo_header_and_desc(uint8_t* RESTRICT dst, const size_t rem, const zxc_gnr_header_t* RESTRICT gh, const zxc_section_desc_t desc[ZXC_GLO_SECTIONS]); /** * @brief Reads a generic header and section descriptors from a source buffer. * * Deserializes data into a `zxc_gnr_header_t` and an array of 4 section * descriptors. * * @param[in] src Pointer to the source buffer. * @param[in] len The length of the source buffer available for reading. * @param[out] gh Pointer to the generic header structure to populate. * @param[out] desc Array of 4 section descriptors to populate. * * @return int Returns ZXC_OK on success, or a negative zxc_error_t code on failure. */ int zxc_read_glo_header_and_desc(const uint8_t* RESTRICT src, const size_t len, zxc_gnr_header_t* RESTRICT gh, zxc_section_desc_t desc[ZXC_GLO_SECTIONS]); /** * @brief Writes a record header and description to the destination buffer. * * @param dst Pointer to the destination buffer where the header and description will be written. * @param rem Remaining size available in the destination buffer. * @param gh Pointer to the GNR header structure containing header information. * @param desc Array of 3 section descriptors to be written along with the header. * * @return int Returns the number of bytes written on success, or a negative error code on failure. */ int zxc_write_ghi_header_and_desc(uint8_t* RESTRICT dst, const size_t rem, const zxc_gnr_header_t* RESTRICT gh, const zxc_section_desc_t desc[ZXC_GHI_SECTIONS]); /** * @brief Reads a record header and section descriptors from a buffer. * * This function parses the source buffer to extract a general header and * up to three section descriptors from a ZXC record. * * @param[in] src Pointer to the source buffer containing the record data. * @param[in] len Length of the source buffer in bytes. * @param[out] gh Pointer to a zxc_gnr_header_t structure to store the parsed header. * @param[out] desc Array of 3 zxc_section_desc_t structures to store the parsed section * descriptors. * * @return int Returns ZXC_OK on success, or a negative zxc_error_t code on failure. */ int zxc_read_ghi_header_and_desc(const uint8_t* RESTRICT src, const size_t len, zxc_gnr_header_t* RESTRICT gh, zxc_section_desc_t desc[ZXC_GHI_SECTIONS]); /** * @brief Internal wrapper function to decompress a single chunk of data. * * This function handles the decompression of a specific chunk from the source * buffer into the destination buffer using the provided compression context. It * serves as an abstraction layer over the core decompression logic. * * @param[in,out] ctx Pointer to the ZXC compression context structure containing * internal state and configuration. * @param[in] src Pointer to the source buffer containing compressed data. * @param[in] src_sz Size of the compressed data in the source buffer (in bytes). * @param[out] dst Pointer to the destination buffer where decompressed data will * be written. * @param[in] dst_cap Capacity of the destination buffer (maximum bytes that can be * written). * * @return int Returns ZXC_OK on success, or a negative zxc_error_t code on failure. * Specific error codes depend on the underlying ZXC * implementation. */ int zxc_decompress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT src, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); /** * @brief Wraps the internal chunk compression logic. * * This function acts as a wrapper to compress a single chunk of data using the * provided compression context. It handles the interaction with the underlying * compression algorithm for a specific block of memory. * * @param[in,out] ctx Pointer to the ZXC compression context containing configuration * and state. * @param[in] chunk Pointer to the source buffer containing the raw data to * compress. * @param[in] src_sz The size of the source chunk in bytes. * @param[out] dst Pointer to the destination buffer where compressed data will be * written. * @param[in] dst_cap The capacity of the destination buffer (maximum bytes to write). * * @return int The number of bytes written to the destination buffer on success, * or a negative error code on failure. */ int zxc_compress_chunk_wrapper(zxc_cctx_t* RESTRICT ctx, const uint8_t* RESTRICT chunk, const size_t src_sz, uint8_t* RESTRICT dst, const size_t dst_cap); /** @} */ /* end of internal */ #ifdef __cplusplus } #endif #endif // ZXC_INTERNAL_Hzxc-0.10.0/src/lib/zxc_seekable.c000066400000000000000000001005021517010557200165350ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file zxc_seekable.c * @brief Seekable archive reader (random-access decompression) and seek table writer. * * The seek table is a standard ZXC block (type = ZXC_BLOCK_SEK) appended * between the EOF block and the file footer. It records the compressed size * of every block (decompressed sizes are derived from the header's block_size), * enabling O(1) lookup + O(block_size) decompression for any byte range. * * On-disk layout of a SEK block: * * [Block Header (8B)] block_type=SEK, block_flags=0, comp_size=N*4 * [N x Entry (4B)] comp_size(u32 LE) per block * * Detection from end of file: * 1. Read file header (first 16 bytes) => block_size * 2. Read file footer (last 12 bytes) => total_decompressed_size * 3. Derive num_blocks = ceil(total_decomp / block_size) * 4. Compute seek block size, read backward to the block header * 5. Validate block_type == ZXC_BLOCK_SEK */ #include "../../include/zxc_seekable.h" #include "../../include/zxc_error.h" #include "../../include/zxc_sans_io.h" #include "zxc_internal.h" /* ========================================================================= */ /* Platform Threading & I/O Layer */ /* ========================================================================= */ // LCOV_EXCL_START - Windows platform layer, not reachable on POSIX CI #if defined(_WIN32) #include /* _get_osfhandle, _fileno */ #include /* _beginthreadex */ #include /* MSVC does not provide fseeko/ftello - map to 64-bit equivalents */ #if defined(_MSC_VER) && !defined(fseeko) #define fseeko _fseeki64 #define ftello _ftelli64 #endif /* Map POSIX threading primitives to Windows equivalents */ typedef HANDLE zxc_thread_t; typedef struct { void* (*func)(void*); void* arg; } zxc_seek_thread_arg_t; static unsigned __stdcall zxc_seek_thread_entry(void* p) { zxc_seek_thread_arg_t* a = (zxc_seek_thread_arg_t*)p; void* (*f)(void*) = a->func; void* arg = a->arg; free(a); f(arg); return 0; } static int zxc_seek_thread_create(zxc_thread_t* t, void* (*fn)(void*), void* arg) { zxc_seek_thread_arg_t* wrapper = malloc(sizeof(zxc_seek_thread_arg_t)); if (UNLIKELY(!wrapper)) return ZXC_ERROR_MEMORY; wrapper->func = fn; wrapper->arg = arg; uintptr_t handle = _beginthreadex(NULL, 0, zxc_seek_thread_entry, wrapper, 0, NULL); if (UNLIKELY(handle == 0)) { free(wrapper); return ZXC_ERROR_MEMORY; } *t = (HANDLE)handle; return 0; } static void zxc_seek_thread_join(zxc_thread_t t) { WaitForSingleObject(t, INFINITE); CloseHandle(t); } static int zxc_seek_get_num_procs(void) { SYSTEM_INFO si; GetSystemInfo(&si); return (int)si.dwNumberOfProcessors; } /** * @brief Thread-safe positional read (Windows). * * Uses ReadFile + Overlapped to read at a specific offset without moving the * file pointer, making it safe for concurrent access from multiple threads. */ static int zxc_seek_pread(HANDLE hFile, void* buf, size_t count, uint64_t offset) { OVERLAPPED ov; ZXC_MEMSET(&ov, 0, sizeof(ov)); ov.Offset = (DWORD)(offset & 0xFFFFFFFF); ov.OffsetHigh = (DWORD)(offset >> 32); DWORD bytes_read = 0; if (!ReadFile(hFile, buf, (DWORD)count, &bytes_read, &ov)) return ZXC_ERROR_IO; return (bytes_read == (DWORD)count) ? (int)count : ZXC_ERROR_IO; } // LCOV_EXCL_STOP #else /* POSIX */ #include #include typedef pthread_t zxc_thread_t; static int zxc_seek_thread_create(zxc_thread_t* t, void* (*fn)(void*), void* arg) { return pthread_create(t, NULL, fn, arg) == 0 ? 0 : ZXC_ERROR_MEMORY; } static void zxc_seek_thread_join(zxc_thread_t t) { pthread_join(t, NULL); } static int zxc_seek_get_num_procs(void) { const long n = sysconf(_SC_NPROCESSORS_ONLN); return (n > 0) ? (int)n : 1; } /** * @brief Thread-safe positional read (POSIX). * * Uses pread() which reads at a given offset without modifying the file * descriptor's current position, making it inherently thread-safe. */ static int zxc_seek_pread(int fd, void* buf, size_t count, uint64_t offset) { const ssize_t r = pread(fd, buf, count, (off_t)offset); return (r == (ssize_t)count) ? (int)count : ZXC_ERROR_IO; } #endif /* _WIN32 */ /* ========================================================================= */ /* Seek Table Writer */ /* ========================================================================= */ size_t zxc_seek_table_size(const uint32_t num_blocks) { return ZXC_BLOCK_HEADER_SIZE + (size_t)num_blocks * ZXC_SEEK_ENTRY_SIZE; } int64_t zxc_write_seek_table(uint8_t* dst, const size_t dst_capacity, const uint32_t* comp_sizes, const uint32_t num_blocks) { if (UNLIKELY(num_blocks > UINT32_MAX / ZXC_SEEK_ENTRY_SIZE)) return ZXC_ERROR_OVERFLOW; const size_t total = zxc_seek_table_size(num_blocks); if (UNLIKELY(dst_capacity < total)) return ZXC_ERROR_DST_TOO_SMALL; if (UNLIKELY(!dst || !comp_sizes)) return ZXC_ERROR_NULL_INPUT; const uint32_t payload_size = num_blocks * ZXC_SEEK_ENTRY_SIZE; /* Write standard ZXC block header */ const zxc_block_header_t bh = { .block_type = ZXC_BLOCK_SEK, .block_flags = 0, .reserved = 0, .comp_size = payload_size}; const int hdr_res = zxc_write_block_header(dst, dst_capacity, &bh); if (UNLIKELY(hdr_res < 0)) return hdr_res; uint8_t* p = dst + hdr_res; /* Write entries: comp_size(4) only */ for (uint32_t i = 0; i < num_blocks; i++) { zxc_store_le32(p, comp_sizes[i]); p += sizeof(uint32_t); } return (int64_t)(p - dst); } /* ========================================================================= */ /* Seekable Reader (Opaque Handle) */ /* ========================================================================= */ struct zxc_seekable_s { /* Source - exactly one is non-NULL */ const uint8_t* src; uint64_t src_size; FILE* file; /* Native file descriptor for thread-safe pread() I/O */ #if defined(_WIN32) HANDLE native_handle; /* from GetOSFileHandle or _get_osfhandle */ #else int fd; /* from fileno() */ #endif /* Parsed seek table */ uint32_t num_blocks; uint32_t* comp_sizes; /* array[num_blocks] */ uint64_t* comp_offsets; /* prefix-sum: byte offset in compressed file per block */ uint64_t total_decomp; /* total decompressed size (from footer) */ /* File header info - block_size is always a power of 2 in [4KB, 2MB], * fits in 21 bits. */ uint32_t block_size; int file_has_checksums; /* Reusable decompression context (single-threaded path only) */ zxc_cctx_t dctx; int dctx_initialized; }; /** * @brief Parses the seek table from raw bytes at the end of the archive. * * Detection (backward from end): * 1. Read file header => block_size * 2. Read file footer => total_decomp_size * 3. Derive num_blocks = ceil(total_decomp_size / block_size) * 4. Compute expected seek block position, validate block_type == SEK * 5. Read comp_sizes; derive decomp_sizes from block_size */ static zxc_seekable* zxc_seekable_parse(const uint8_t* data, const size_t data_size) { /* Minimum: file_header(16) + eof_block(8) + seek_block_header(8) * + file_footer(12) = 44 */ const size_t MIN_SEEKABLE_SIZE = ZXC_FILE_HEADER_SIZE + ZXC_BLOCK_HEADER_SIZE + ZXC_BLOCK_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE; if (UNLIKELY(data_size < MIN_SEEKABLE_SIZE)) return NULL; /* Step 1: validate file header => block_size */ size_t block_size_sz = 0; int file_has_chk = 0; if (UNLIKELY(zxc_read_file_header(data, data_size, &block_size_sz, &file_has_chk) != ZXC_OK)) return NULL; const uint32_t block_size = (uint32_t)block_size_sz; if (UNLIKELY(block_size == 0)) return NULL; // LCOV_EXCL_LINE /* Step 2: read total decompressed size from the file footer */ const uint8_t* const footer_ptr = data + data_size - ZXC_FILE_FOOTER_SIZE; const uint64_t total_decomp = zxc_le64(footer_ptr); /* A value of 0 means empty file - no seek table */ if (UNLIKELY(total_decomp == 0)) return NULL; /* Step 3: derive num_blocks = ceil(total_decomp / block_size) */ const uint64_t num_blocks_64 = (total_decomp + block_size - 1) / block_size; if (UNLIKELY(num_blocks_64 > UINT32_MAX)) return NULL; const uint32_t num_blocks = (uint32_t)num_blocks_64; /* Step 4: compute seek block position and validate. */ const uint64_t entries_total_64 = num_blocks_64 * ZXC_SEEK_ENTRY_SIZE; if (UNLIKELY(entries_total_64 > SIZE_MAX - ZXC_BLOCK_HEADER_SIZE)) return NULL; const size_t entries_total = (size_t)entries_total_64; const size_t seek_block_total = ZXC_BLOCK_HEADER_SIZE + entries_total; if (UNLIKELY(seek_block_total + ZXC_FILE_FOOTER_SIZE > data_size)) return NULL; const uint8_t* const seek_block_start = data + data_size - ZXC_FILE_FOOTER_SIZE - seek_block_total; if (UNLIKELY(seek_block_start < data)) return NULL; /* Read and validate SEK block header */ zxc_block_header_t bh; if (UNLIKELY(zxc_read_block_header(seek_block_start, seek_block_total, &bh) != ZXC_OK)) return NULL; if (UNLIKELY(bh.block_type != ZXC_BLOCK_SEK)) return NULL; if (UNLIKELY(bh.comp_size != (uint32_t)entries_total)) return NULL; /* Step 5: allocate handle and parse entries */ zxc_seekable* const s = (zxc_seekable*)calloc(1, sizeof(zxc_seekable)); // LCOV_EXCL_START if (UNLIKELY(!s)) return NULL; // LCOV_EXCL_STOP s->num_blocks = num_blocks; s->block_size = block_size; s->file_has_checksums = file_has_chk; s->src = data; s->src_size = (uint64_t)data_size; /* Allocate arrays */ s->comp_sizes = (uint32_t*)calloc(num_blocks, sizeof(uint32_t)); s->comp_offsets = (uint64_t*)calloc((size_t)num_blocks + 1, sizeof(uint64_t)); // LCOV_EXCL_START if (UNLIKELY(!s->comp_sizes || !s->comp_offsets)) { zxc_seekable_free(s); return NULL; } // LCOV_EXCL_STOP s->total_decomp = total_decomp; /* Parse comp_sizes and build compressed prefix sums. * Validate each comp_size against data_size to prevent prefix-sum overflow * and out-of-bounds reads during decompression. */ const uint8_t* ep = seek_block_start + ZXC_BLOCK_HEADER_SIZE; uint64_t comp_acc = ZXC_FILE_HEADER_SIZE; /* blocks start after file header */ for (uint32_t i = 0; i < num_blocks; i++) { s->comp_sizes[i] = zxc_le32(ep); ep += sizeof(uint32_t); /* Reject entries below minimum (block header) or larger than the file */ if (UNLIKELY(s->comp_sizes[i] < ZXC_BLOCK_HEADER_SIZE || s->comp_sizes[i] > (uint64_t)data_size)) { zxc_seekable_free(s); return NULL; } s->comp_offsets[i] = comp_acc; comp_acc += s->comp_sizes[i]; /* Reject if cumulative offset exceeds file size (inconsistent table) */ if (UNLIKELY(comp_acc > (uint64_t)data_size)) { zxc_seekable_free(s); return NULL; } } s->comp_offsets[num_blocks] = comp_acc; /* Verify prefix-sum lands exactly at the EOF block position. * Expected layout: [header 16][data blocks][EOF 8][SEK block][footer 12] * So comp_acc (end of data blocks) + EOF(8) == seek_block_start. */ const uint64_t expected_eof_offset = (uint64_t)(seek_block_start - data) - ZXC_BLOCK_HEADER_SIZE; if (UNLIKELY(comp_acc != expected_eof_offset)) { zxc_seekable_free(s); return NULL; } /* Validate that an actual EOF block header exists at the computed offset */ if (UNLIKELY(comp_acc + ZXC_BLOCK_HEADER_SIZE > data_size)) { zxc_seekable_free(s); return NULL; } zxc_block_header_t eof_bh; if (UNLIKELY(zxc_read_block_header(data + comp_acc, ZXC_BLOCK_HEADER_SIZE, &eof_bh) != ZXC_OK || eof_bh.block_type != ZXC_BLOCK_EOF)) { zxc_seekable_free(s); return NULL; } return s; } zxc_seekable* zxc_seekable_open(const void* src, const size_t src_size) { if (UNLIKELY(!src || src_size == 0)) return NULL; return zxc_seekable_parse((const uint8_t*)src, src_size); } zxc_seekable* zxc_seekable_open_file(FILE* f) { if (UNLIKELY(!f)) return NULL; const long long saved_pos = ftello(f); if (UNLIKELY(saved_pos < 0)) return NULL; // LCOV_EXCL_LINE // LCOV_EXCL_START if (UNLIKELY(fseeko(f, 0, SEEK_END) != 0)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } // LCOV_EXCL_STOP const long long file_size = ftello(f); // LCOV_EXCL_START if (UNLIKELY(file_size <= 0)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } // LCOV_EXCL_STOP /* For simplicity and correctness: read the file into memory. * The seek table parsing needs the file header. */ if ((size_t)file_size <= 64 * 1024 * 1024) { /* File <= 64 MB: read it all into memory */ uint8_t* const full = (uint8_t*)malloc((size_t)file_size); // LCOV_EXCL_START if (UNLIKELY(!full)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } if (UNLIKELY(fseeko(f, 0, SEEK_SET) != 0 || fread(full, 1, (size_t)file_size, f) != (size_t)file_size)) { free(full); fseeko(f, saved_pos, SEEK_SET); return NULL; } // LCOV_EXCL_STOP fseeko(f, saved_pos, SEEK_SET); zxc_seekable* const s = zxc_seekable_parse(full, (size_t)file_size); if (s) { s->src = NULL; s->src_size = (uint64_t)file_size; s->file = f; #if defined(_WIN32) s->native_handle = (HANDLE)(intptr_t)_get_osfhandle(_fileno(f)); #else s->fd = fileno(f); #endif } free(full); return s; } // LCOV_EXCL_START - large file path (>64MB), not reachable in unit tests /* Large file: read header + footer separately */ uint8_t header[ZXC_FILE_HEADER_SIZE]; if (UNLIKELY(fseeko(f, 0, SEEK_SET) != 0 || fread(header, 1, ZXC_FILE_HEADER_SIZE, f) != ZXC_FILE_HEADER_SIZE)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } size_t bs_sz = 0; int fhc = 0; if (UNLIKELY(zxc_read_file_header(header, ZXC_FILE_HEADER_SIZE, &bs_sz, &fhc) != ZXC_OK)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } const uint32_t bs = (uint32_t)bs_sz; /* Read footer (12 bytes) to get total_decomp_size */ uint8_t footer_buf[ZXC_FILE_FOOTER_SIZE]; if (UNLIKELY(fseeko(f, file_size - (long long)ZXC_FILE_FOOTER_SIZE, SEEK_SET) != 0 || fread(footer_buf, 1, ZXC_FILE_FOOTER_SIZE, f) != ZXC_FILE_FOOTER_SIZE)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } const uint64_t total_decomp = zxc_le64(footer_buf); if (UNLIKELY(total_decomp == 0)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } /* Derive num_blocks = ceil(total_decomp / block_size). * Guard against uint32_t overflow. */ const uint64_t num_blocks_64 = (total_decomp + bs - 1) / bs; if (UNLIKELY(num_blocks_64 > UINT32_MAX)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } const uint32_t num_blocks = (uint32_t)num_blocks_64; /* Guard against size_t multiplication overflow. */ const uint64_t entries_total_64 = (uint64_t)num_blocks * ZXC_SEEK_ENTRY_SIZE; if (UNLIKELY(entries_total_64 > SIZE_MAX - ZXC_BLOCK_HEADER_SIZE)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } /* Read the full seek block */ const size_t seek_block_total = ZXC_BLOCK_HEADER_SIZE + (size_t)entries_total_64; uint8_t* const seek_buf = (uint8_t*)malloc(seek_block_total); if (UNLIKELY(!seek_buf)) { fseeko(f, saved_pos, SEEK_SET); return NULL; } const long long seek_offset = file_size - (long long)ZXC_FILE_FOOTER_SIZE - (long long)seek_block_total; if (UNLIKELY(seek_offset < 0 || fseeko(f, seek_offset, SEEK_SET) != 0 || fread(seek_buf, 1, seek_block_total, f) != seek_block_total)) { free(seek_buf); fseeko(f, saved_pos, SEEK_SET); return NULL; } fseeko(f, saved_pos, SEEK_SET); /* Validate block header */ zxc_block_header_t bh; if (UNLIKELY(zxc_read_block_header(seek_buf, seek_block_total, &bh) != ZXC_OK) || bh.block_type != ZXC_BLOCK_SEK || bh.comp_size != (uint32_t)entries_total_64) { free(seek_buf); return NULL; } /* Build seekable handle */ zxc_seekable* const s = (zxc_seekable*)calloc(1, sizeof(zxc_seekable)); if (UNLIKELY(!s)) { free(seek_buf); return NULL; } s->file = f; s->src = NULL; #if defined(_WIN32) s->native_handle = (HANDLE)(intptr_t)_get_osfhandle(_fileno(f)); #else s->fd = fileno(f); #endif s->src_size = (uint64_t)file_size; s->num_blocks = num_blocks; s->block_size = bs; s->file_has_checksums = fhc; s->comp_sizes = (uint32_t*)calloc(num_blocks, sizeof(uint32_t)); s->comp_offsets = (uint64_t*)calloc((size_t)num_blocks + 1, sizeof(uint64_t)); if (UNLIKELY(!s->comp_sizes || !s->comp_offsets)) { free(seek_buf); zxc_seekable_free(s); return NULL; } s->total_decomp = total_decomp; const uint8_t* ep = seek_buf + ZXC_BLOCK_HEADER_SIZE; uint64_t comp_acc = ZXC_FILE_HEADER_SIZE; for (uint32_t i = 0; i < num_blocks; i++) { s->comp_sizes[i] = zxc_le32(ep); ep += sizeof(uint32_t); /* Reject entries larger than the entire file */ if (UNLIKELY(s->comp_sizes[i] > (uint64_t)file_size)) { free(seek_buf); zxc_seekable_free(s); return NULL; } s->comp_offsets[i] = comp_acc; comp_acc += s->comp_sizes[i]; } s->comp_offsets[num_blocks] = comp_acc; free(seek_buf); return s; // LCOV_EXCL_STOP } uint32_t zxc_seekable_get_num_blocks(const zxc_seekable* s) { return s ? s->num_blocks : 0; } uint64_t zxc_seekable_get_decompressed_size(const zxc_seekable* s) { return s ? s->total_decomp : 0; } uint32_t zxc_seekable_get_block_comp_size(const zxc_seekable* s, const uint32_t block_idx) { if (UNLIKELY(!s || block_idx >= s->num_blocks)) return 0; return s->comp_sizes[block_idx]; } uint32_t zxc_seekable_get_block_decomp_size(const zxc_seekable* s, const uint32_t block_idx) { if (UNLIKELY(!s || block_idx >= s->num_blocks)) return 0; const uint64_t start = (uint64_t)block_idx * (uint64_t)s->block_size; const uint64_t remaining = s->total_decomp - start; return (remaining >= (uint64_t)s->block_size) ? s->block_size : (uint32_t)remaining; } /* ========================================================================= */ /* Random-Access Decompression */ /* ========================================================================= */ /** @brief O(1) block lookup: block_index = offset / block_size. */ static uint32_t zxc_seek_find_block(const uint32_t block_size, const uint64_t offset) { return (uint32_t)(offset / (uint64_t)block_size); } /** @brief O(1) decompressed offset for block @p idx. */ static uint64_t zxc_seek_decomp_offset(const uint32_t block_size, const uint32_t idx) { return (uint64_t)idx * (uint64_t)block_size; } /** @brief O(1) decompressed size for block @p idx. */ static uint32_t zxc_seek_decomp_size(const uint32_t block_size, const uint64_t total_decomp, const uint32_t idx) { const uint64_t start = (uint64_t)idx * (uint64_t)block_size; const uint64_t remaining = total_decomp - start; return (remaining >= (uint64_t)block_size) ? block_size : (uint32_t)remaining; } /** * @brief Reads a compressed block from buffer or file. */ static int zxc_seek_read_block(const zxc_seekable* s, const uint32_t block_idx, uint8_t* buf, const size_t buf_cap) { const uint64_t off = s->comp_offsets[block_idx]; const uint32_t csz = s->comp_sizes[block_idx]; if (UNLIKELY(csz > buf_cap)) return ZXC_ERROR_DST_TOO_SMALL; if (s->src) { /* Buffer mode */ if (UNLIKELY(off + csz > s->src_size)) return ZXC_ERROR_SRC_TOO_SMALL; ZXC_MEMCPY(buf, s->src + off, csz); } else if (s->file) { /* File mode */ // LCOV_EXCL_START if (UNLIKELY(fseeko(s->file, (long long)off, SEEK_SET) != 0 || fread(buf, 1, csz, s->file) != csz)) return ZXC_ERROR_IO; // LCOV_EXCL_STOP } else { return ZXC_ERROR_NULL_INPUT; // LCOV_EXCL_LINE } return (int)csz; } int64_t zxc_seekable_decompress_range(zxc_seekable* s, void* dst, const size_t dst_capacity, const uint64_t offset, const size_t len) { if (UNLIKELY(!s || !dst)) return ZXC_ERROR_NULL_INPUT; if (UNLIKELY(len == 0)) return 0; if (UNLIKELY(dst_capacity < len)) return ZXC_ERROR_DST_TOO_SMALL; if (UNLIKELY(offset + len > s->total_decomp)) return ZXC_ERROR_SRC_TOO_SMALL; /* Initialize decompression context on first use */ if (!s->dctx_initialized) { // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&s->dctx, (size_t)s->block_size, 0, 0, 0) != ZXC_OK)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_STOP s->dctx_initialized = 1; } /* Ensure work buffer is large enough */ const size_t work_sz = (size_t)s->block_size + ZXC_PAD_SIZE; if (s->dctx.work_buf_cap < work_sz) { free(s->dctx.work_buf); s->dctx.work_buf = (uint8_t*)malloc(work_sz); if (UNLIKELY(!s->dctx.work_buf)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE s->dctx.work_buf_cap = work_sz; } /* Find block range - O(1) division */ const uint32_t blk_start = zxc_seek_find_block(s->block_size, offset); const uint32_t blk_end = zxc_seek_find_block(s->block_size, offset + len - 1); uint8_t* out = (uint8_t*)dst; size_t remaining = len; /* Allocate read buffer for compressed blocks */ size_t max_comp = 0; for (uint32_t bi = blk_start; bi <= blk_end; bi++) { if (s->comp_sizes[bi] > max_comp) max_comp = s->comp_sizes[bi]; } uint8_t* const read_buf = (uint8_t*)malloc(max_comp + ZXC_PAD_SIZE); if (UNLIKELY(!read_buf)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE for (uint32_t bi = blk_start; bi <= blk_end; bi++) { /* Read compressed block data */ const int read_res = zxc_seek_read_block(s, bi, read_buf, max_comp + ZXC_PAD_SIZE); if (UNLIKELY(read_res < 0)) { free(read_buf); return read_res; } /* Decompress the block */ const int dec_res = zxc_decompress_chunk_wrapper(&s->dctx, read_buf, (size_t)read_res, s->dctx.work_buf, work_sz); if (UNLIKELY(dec_res < 0)) { free(read_buf); return dec_res; } /* Calculate which portion of this block's decompressed data we need */ const uint64_t blk_decomp_start = zxc_seek_decomp_offset(s->block_size, bi); const size_t skip = (offset > blk_decomp_start) ? (size_t)(offset - blk_decomp_start) : 0; if (UNLIKELY((size_t)dec_res < skip)) { free(read_buf); return ZXC_ERROR_CORRUPT_DATA; } const size_t avail = (size_t)dec_res - skip; const size_t copy = (avail < remaining) ? avail : remaining; ZXC_MEMCPY(out, s->dctx.work_buf + skip, copy); out += copy; remaining -= copy; } free(read_buf); return (int64_t)len; } /* ========================================================================= */ /* Multi-Threaded Random-Access Decompression (Fork-Join) */ /* ========================================================================= */ /** * @brief Per-block job descriptor for multi-threaded decompression. * * Each worker thread receives a pointer to one of these, performs the read + * decompress + memcpy sequence, and writes the result code into @c result. * The main thread inspects @c result after join. */ typedef struct { const zxc_seekable* s; /* shared handle (read-only) */ uint32_t block_idx; /* block to decompress */ uint8_t* dst; /* output pointer within caller's buffer */ size_t skip; /* bytes to skip at start of decompressed block */ size_t copy_len; /* bytes to copy into dst */ int result; /* 0 = OK, < 0 = error */ } zxc_seek_mt_job_t; /** * @brief Thread-safe block read using pread (for file mode) or memcpy (buffer mode). */ static int zxc_seek_read_block_mt(const zxc_seekable* s, const uint32_t block_idx, uint8_t* buf, const size_t buf_cap) { const uint64_t off = s->comp_offsets[block_idx]; const uint32_t csz = s->comp_sizes[block_idx]; if (UNLIKELY(csz > buf_cap)) return ZXC_ERROR_DST_TOO_SMALL; if (s->src) { /* Buffer mode - memcpy is inherently thread-safe on const data */ if (UNLIKELY(off + csz > s->src_size)) return ZXC_ERROR_SRC_TOO_SMALL; ZXC_MEMCPY(buf, s->src + off, csz); } else if (s->file) { /* File mode - use pread for concurrent, lock-free reads */ #if defined(_WIN32) const int r = zxc_seek_pread(s->native_handle, buf, csz, off); #else const int r = zxc_seek_pread(s->fd, buf, csz, off); #endif if (UNLIKELY(r < 0)) return r; } else { return ZXC_ERROR_NULL_INPUT; // LCOV_EXCL_LINE } return (int)csz; } /** * @brief Worker thread entry point for multi-threaded seekable decompression. * * Each worker: * 1. Allocates a thread-local decompression context. * 2. Reads the compressed block via pread (thread-safe). * 3. Decompresses into a local work buffer. * 4. Copies the requested sub-range into the caller's output buffer. */ static void* zxc_seek_mt_worker(void* arg) { zxc_seek_mt_job_t* const job = (zxc_seek_mt_job_t*)arg; const zxc_seekable* const s = job->s; const uint32_t bi = job->block_idx; /* Thread-local decompression context (mode=0 for decompress-only) */ zxc_cctx_t dctx; // LCOV_EXCL_START if (UNLIKELY(zxc_cctx_init(&dctx, (size_t)s->block_size, 0, 0, 0) != ZXC_OK)) { job->result = ZXC_ERROR_MEMORY; return NULL; } // LCOV_EXCL_STOP /* Allocate work buffer for decompressed output */ const size_t work_sz = (size_t)s->block_size + ZXC_PAD_SIZE; dctx.work_buf = (uint8_t*)malloc(work_sz); // LCOV_EXCL_START if (UNLIKELY(!dctx.work_buf)) { zxc_cctx_free(&dctx); job->result = ZXC_ERROR_MEMORY; return NULL; } // LCOV_EXCL_STOP dctx.work_buf_cap = work_sz; /* Read compressed block */ const uint32_t csz = s->comp_sizes[bi]; uint8_t* const read_buf = (uint8_t*)malloc(csz + ZXC_PAD_SIZE); // LCOV_EXCL_START if (UNLIKELY(!read_buf)) { zxc_cctx_free(&dctx); job->result = ZXC_ERROR_MEMORY; return NULL; } // LCOV_EXCL_STOP const int read_res = zxc_seek_read_block_mt(s, bi, read_buf, csz + ZXC_PAD_SIZE); // LCOV_EXCL_START if (UNLIKELY(read_res < 0)) { free(read_buf); zxc_cctx_free(&dctx); job->result = read_res; return NULL; } // LCOV_EXCL_STOP /* Decompress */ const int dec_res = zxc_decompress_chunk_wrapper(&dctx, read_buf, (size_t)read_res, dctx.work_buf, work_sz); free(read_buf); // LCOV_EXCL_START if (UNLIKELY(dec_res < 0)) { zxc_cctx_free(&dctx); job->result = dec_res; return NULL; } if (UNLIKELY((size_t)dec_res < job->skip + job->copy_len)) { zxc_cctx_free(&dctx); job->result = ZXC_ERROR_CORRUPT_DATA; return NULL; } // LCOV_EXCL_STOP /* Copy the requested portion directly into the caller's output buffer */ ZXC_MEMCPY(job->dst, dctx.work_buf + job->skip, job->copy_len); zxc_cctx_free(&dctx); job->result = 0; return NULL; } int64_t zxc_seekable_decompress_range_mt(zxc_seekable* s, void* dst, const size_t dst_capacity, const uint64_t offset, const size_t len, int n_threads) { if (UNLIKELY(!s || !dst)) return ZXC_ERROR_NULL_INPUT; if (UNLIKELY(len == 0)) return 0; if (UNLIKELY(dst_capacity < len)) return ZXC_ERROR_DST_TOO_SMALL; if (UNLIKELY(offset + len > s->total_decomp)) return ZXC_ERROR_SRC_TOO_SMALL; /* Find block range - O(1) division */ const uint32_t blk_start = zxc_seek_find_block(s->block_size, offset); const uint32_t blk_end = zxc_seek_find_block(s->block_size, offset + len - 1); const uint32_t num_jobs = blk_end - blk_start + 1; /* Auto-detect thread count (0 = use all available cores) */ if (n_threads == 0) n_threads = zxc_seek_get_num_procs(); /* Fallback to single-threaded path for trivial cases */ if (n_threads <= 1 || num_jobs <= 1) { return zxc_seekable_decompress_range(s, dst, dst_capacity, offset, len); } /* Cap threads to number of blocks and max limit */ if ((uint32_t)n_threads > num_jobs) n_threads = (int)num_jobs; if (n_threads > ZXC_MAX_THREADS) n_threads = ZXC_MAX_THREADS; /* Allocate job descriptors */ zxc_seek_mt_job_t* const jobs = (zxc_seek_mt_job_t*)calloc(num_jobs, sizeof(zxc_seek_mt_job_t)); if (UNLIKELY(!jobs)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE /* Plan jobs: compute skip, copy_len, and dst pointer for each block */ uint8_t* out = (uint8_t*)dst; size_t remaining = len; for (uint32_t i = 0; i < num_jobs; i++) { const uint32_t bi = blk_start + i; const uint64_t blk_decomp_start = zxc_seek_decomp_offset(s->block_size, bi); const size_t skip = (offset > blk_decomp_start) ? (size_t)(offset - blk_decomp_start) : 0; const size_t blk_decomp_sz = zxc_seek_decomp_size(s->block_size, s->total_decomp, bi); if (UNLIKELY(blk_decomp_sz < skip)) { free(jobs); return ZXC_ERROR_CORRUPT_DATA; } const size_t avail = blk_decomp_sz - skip; const size_t copy = (avail < remaining) ? avail : remaining; jobs[i].s = s; jobs[i].block_idx = bi; jobs[i].dst = out; jobs[i].skip = skip; jobs[i].copy_len = copy; jobs[i].result = 0; out += copy; remaining -= copy; } /* Launch worker threads (fork phase) */ zxc_thread_t* const threads = (zxc_thread_t*)malloc((size_t)n_threads * sizeof(zxc_thread_t)); // LCOV_EXCL_START if (UNLIKELY(!threads)) { free(jobs); return ZXC_ERROR_MEMORY; } // LCOV_EXCL_STOP /* * Distribute jobs across threads round-robin style. * If num_jobs > n_threads, some threads handle multiple blocks sequentially. * We process jobs in waves: spawn n_threads at a time, join, repeat. */ int error = 0; uint32_t job_idx = 0; while (job_idx < num_jobs && !error) { const int wave_size = ((int)(num_jobs - job_idx) < n_threads) ? (int)(num_jobs - job_idx) : n_threads; int launched = 0; for (int t = 0; t < wave_size; t++) { // LCOV_EXCL_START if (zxc_seek_thread_create(&threads[t], zxc_seek_mt_worker, &jobs[job_idx + t]) != 0) { /* Failed to create thread - mark remaining jobs as errors */ for (uint32_t j = job_idx + (uint32_t)t; j < num_jobs; j++) jobs[j].result = ZXC_ERROR_MEMORY; error = 1; break; } // LCOV_EXCL_STOP launched++; } /* Join phase */ for (int t = 0; t < launched; t++) { zxc_seek_thread_join(threads[t]); if (jobs[job_idx + t].result < 0) error = 1; } job_idx += (uint32_t)launched; } free(threads); /* Check for errors */ int64_t result = (int64_t)len; if (error) { for (uint32_t i = 0; i < num_jobs; i++) { if (jobs[i].result < 0) { result = (int64_t)jobs[i].result; break; } } } free(jobs); return result; } void zxc_seekable_free(zxc_seekable* s) { if (!s) return; if (s->dctx_initialized) zxc_cctx_free(&s->dctx); free(s->comp_sizes); free(s->comp_offsets); free(s); } zxc-0.10.0/tests/000077500000000000000000000000001517010557200135415ustar00rootroot00000000000000zxc-0.10.0/tests/fuzz_decompress.c000066400000000000000000000012401517010557200171240ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ #include #include #include #include "../include/zxc_buffer.h" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { static uint8_t out_buf[1048576]; /* 1 MB max for fuzzer */ size_t out_capacity = sizeof(out_buf); uint64_t expected_size = zxc_get_decompressed_size(data, size); if (expected_size > 0 && expected_size <= out_capacity) out_capacity = (size_t)expected_size; zxc_decompress(data, size, out_buf, out_capacity, 0); return 0; }zxc-0.10.0/tests/fuzz_roundtrip.c000066400000000000000000000025311517010557200170120ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ #include #include #include #include #include #include "../include/zxc_buffer.h" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { static void* comp_buf = NULL; static size_t comp_cap = 0; static void* decomp_buf = NULL; static size_t decomp_cap = 0; const size_t bound = zxc_compress_bound(size); if (bound > comp_cap) { void* new_buf = realloc(comp_buf, bound); if (!new_buf) return 0; comp_buf = new_buf; comp_cap = bound; } const int level = size > 0 ? (data[0] % 5) + 1 : 1; zxc_compress_opts_t copts = {.level = level}; const int64_t csize = zxc_compress(data, size, comp_buf, bound, &copts); if (csize < 0) return 0; if (size == 0) return 0; if (size > decomp_cap) { void* new_buf = realloc(decomp_buf, size); if (!new_buf) return 0; decomp_buf = new_buf; decomp_cap = size; } const int64_t dsize = zxc_decompress(comp_buf, (size_t)csize, decomp_buf, size, NULL); if (dsize >= 0) { assert((size_t)dsize == size); assert(memcmp(data, decomp_buf, size) == 0); } return 0; }zxc-0.10.0/tests/test.c000066400000000000000000004065441517010557200147010ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ #include #include #include #include #include #include #ifdef _MSC_VER #include #include #endif // Creates a temporary file with restricted permissions (0600). // Returns a FILE* opened for writing, or NULL on failure. static FILE* create_restricted_file(const char* path) { #ifdef _MSC_VER int fd = -1; _sopen_s(&fd, path, _O_CREAT | _O_WRONLY | _O_TRUNC, _SH_DENYNO, _S_IREAD | _S_IWRITE); return fd >= 0 ? _fdopen(fd, "w") : NULL; #else const int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR); return fd >= 0 ? fdopen(fd, "w") : NULL; #endif } #include "../include/zxc_buffer.h" #include "../include/zxc_error.h" #include "../include/zxc_sans_io.h" #include "../include/zxc_seekable.h" #include "../include/zxc_stream.h" #include "../src/lib/zxc_internal.h" // --- Helpers --- // Generates a buffer of random data (To force RAW) void gen_random_data(uint8_t* const buf, const size_t size) { for (size_t i = 0; i < size; i++) buf[i] = rand() & 0xFF; } // Generates repetitive data (To force GLO/GHI/LZ) void gen_lz_data(uint8_t* const buf, const size_t size) { const char* const pattern = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " "mollit anim id est laborum."; const size_t pat_len = strlen(pattern); for (size_t i = 0; i < size; i++) buf[i] = pattern[i % pat_len]; } // Generates a regular numeric sequence (To force NUM) void gen_num_data(uint8_t* const buf, const size_t size) { // Fill with 32-bit integers uint32_t* const ptr = (uint32_t*)buf; const size_t count = size / 4; uint32_t val = 0; for (size_t i = 0; i < count; i++) { // Arithmetic sequence: 0, 100, 200... // Deltas are constant (100), perfect for NUM ptr[i] = val; val += 100; } } // Generates numeric sequence with 0 deltas (all identical) void gen_num_data_zero(uint8_t* const buf, const size_t size) { uint32_t* const ptr = (uint32_t*)buf; const size_t count = size / 4; for (size_t i = 0; i < count; i++) { ptr[i] = 42; } } // Generates numeric data with alternating small deltas (+1, -1) void gen_num_data_small(uint8_t* const buf, const size_t size) { uint32_t* const ptr = (uint32_t*)buf; const size_t count = size / 4; uint32_t val = 1000; for (size_t i = 0; i < count; i++) { ptr[i] = val; val += (i % 2 == 0) ? 1 : -1; } } // Generates numeric data with very large deltas to maximize bit width void gen_num_data_large(uint8_t* const buf, const size_t size) { uint32_t* const ptr = (uint32_t*)buf; const size_t count = size / 4; for (size_t i = 0; i < count; i++) { // Alternate between 0 and 0xFFFFFFFF (delta is huge) ptr[i] = (i % 2 == 0) ? 0 : 0xFFFFFFFF; } } void gen_binary_data(uint8_t* const buf, const size_t size) { // Pattern with problematic bytes that could be corrupted in text mode: // 0x0A (LF), 0x0D (CR), 0x00 (NULL), 0x1A (EOF/CTRL-Z), 0xFF const uint8_t pattern[] = { 0x5A, 0x58, 0x43, 0x00, // "ZXC" + NULL 0x0A, 0x0D, 0x0A, 0x00, // LF, CR, LF, NULL 0xFF, 0xFE, 0x0A, 0x0D, // High bytes + LF/CR 0x1A, 0x00, 0x0A, 0x0D, // EOF marker + NULL + LF/CR 0x00, 0x00, 0x0A, 0x0A, // Multiple NULLs and LFs }; const size_t pat_len = sizeof(pattern); for (size_t i = 0; i < size; i++) { buf[i] = pattern[i % pat_len]; } } // Generates data with small offsets (<=255 bytes) to force 1-byte offset encoding // This creates short repeating patterns with matches very close to each other void gen_small_offset_data(uint8_t* const buf, const size_t size) { // Create short repeating patterns with very short distances. // Uses a 5-byte period (not aligned to uint32_t) to avoid being // classified as NUM data by zxc_probe_is_numeric(). // LZ will match at offset=5 (< 255), exercising 8-bit offset encoding. const uint8_t pattern[] = "ABCDE"; for (size_t i = 0; i < size; i++) { buf[i] = pattern[i % 5]; } } // Generates data with large offsets (>255 bytes) to force 2-byte offset encoding // This creates patterns where matches are far apart void gen_large_offset_data(uint8_t* const buf, const size_t size) { // First 300 bytes: unique random data (no matches possible) for (size_t i = 0; i < 300 && i < size; i++) { buf[i] = (uint8_t)((i * 7 + 13) % 256); } // Then: repeat patterns from the beginning (offset > 255) for (size_t i = 300; i < size; i++) { buf[i] = buf[i - 300]; // Offset of 300 bytes (requires 2-byte encoding) } } // Generic Round-Trip test function (Compress -> Decompress -> Compare) int test_round_trip(const char* test_name, const uint8_t* input, size_t size, int level, int checksum_enabled) { printf("=== TEST: %s (Sz: %zu, Lvl: %d, CRC: %s) ===\n", test_name, size, level, checksum_enabled ? "Enabled" : "Disabled"); FILE* const f_in = tmpfile(); FILE* const f_comp = tmpfile(); FILE* const f_decomp = tmpfile(); if (!f_in || !f_comp || !f_decomp) { perror("tmpfile"); if (f_in) fclose(f_in); if (f_comp) fclose(f_comp); if (f_decomp) fclose(f_decomp); return 0; } fwrite(input, 1, size, f_in); fseek(f_in, 0, SEEK_SET); zxc_compress_opts_t _sco1 = { .n_threads = 1, .level = level, .checksum_enabled = checksum_enabled}; if (zxc_stream_compress(f_in, f_comp, &_sco1) < 0) { printf("Compression Failed!\n"); fclose(f_in); fclose(f_comp); fclose(f_decomp); return 0; } long comp_size = ftell(f_comp); printf("Compressed Size: %ld (Ratio: %.2f)\n", comp_size, (double)size / (comp_size > 0 ? comp_size : 1)); fseek(f_comp, 0, SEEK_SET); zxc_decompress_opts_t _sdo2 = {.n_threads = 1, .checksum_enabled = checksum_enabled}; if (zxc_stream_decompress(f_comp, f_decomp, &_sdo2) < 0) { printf("Decompression Failed!\n"); fclose(f_in); fclose(f_comp); fclose(f_decomp); return 0; } long decomp_size = ftell(f_decomp); if (decomp_size != (long)size) { printf("Size Mismatch! Expected %zu, got %ld\n", size, decomp_size); fclose(f_in); fclose(f_comp); fclose(f_decomp); return 0; } fseek(f_decomp, 0, SEEK_SET); uint8_t* out_buf = malloc(size > 0 ? size : 1); if (fread(out_buf, 1, size, f_decomp) != size) { printf("Read validation failed (incomplete read)!\n"); free(out_buf); fclose(f_in); fclose(f_comp); fclose(f_decomp); return 0; } if (size > 0 && memcmp(input, out_buf, size) != 0) { printf("Data Mismatch (Content Corruption)!\n"); free(out_buf); fclose(f_in); fclose(f_comp); fclose(f_decomp); return 0; } printf("PASS\n\n"); free(out_buf); fclose(f_in); fclose(f_comp); fclose(f_decomp); return 1; } // Checks that the stream decompression can accept NULL output (Integrity Check Mode) int test_null_output_decompression() { printf("=== TEST: Unit - NULL Output Decompression (Integrity Check) ===\n"); size_t size = 64 * 1024; uint8_t* input = malloc(size); if (!input) return 0; gen_lz_data(input, size); FILE* f_in = tmpfile(); FILE* f_comp = tmpfile(); if (!f_in || !f_comp) { if (f_in) fclose(f_in); if (f_comp) fclose(f_comp); free(input); return 0; } fwrite(input, 1, size, f_in); fseek(f_in, 0, SEEK_SET); // Compress with checksum zxc_compress_opts_t _sco3 = {.n_threads = 1, .level = 3, .checksum_enabled = 1}; if (zxc_stream_compress(f_in, f_comp, &_sco3) < 0) { printf("Compression Failed!\n"); fclose(f_in); fclose(f_comp); free(input); return 0; } fseek(f_comp, 0, SEEK_SET); // Decompress with NULL output // This should return the decompressed size but write nothing zxc_decompress_opts_t _sdo4 = {.n_threads = 1, .checksum_enabled = 1}; int64_t d_sz = zxc_stream_decompress(f_comp, NULL, &_sdo4); if (d_sz != (int64_t)size) { printf("Failed: Expected size %zu, got %lld\n", size, (long long)d_sz); fclose(f_in); fclose(f_comp); free(input); return 0; } printf("PASS\n\n"); fclose(f_in); fclose(f_comp); free(input); return 1; } // Checks that the utility function calculates a sufficient size int test_max_compressed_size_logic() { printf("=== TEST: Unit - zxc_compress_bound ===\n"); // Case 1: 0 bytes (must at least contain the header) size_t sz0 = (size_t)zxc_compress_bound(0); if (sz0 == 0) { printf("Failed: Size for 0 bytes should not be 0 (headers required)\n"); return 0; } // Case 2: Small input size_t input_val = 100; size_t sz100 = (size_t)zxc_compress_bound(input_val); if (sz100 < input_val) { printf("Failed: Output buffer size (%zu) too small for input (%zu)\n", sz100, input_val); return 0; } // Case 3: Consistency (size should not decrease arbitrarily) if (zxc_compress_bound(2000) < zxc_compress_bound(1000)) { printf("Failed: Max size function is not monotonic\n"); return 0; } printf("PASS\n\n"); return 1; } // Checks API robustness against invalid arguments int test_invalid_arguments() { printf("=== TEST: Unit - Invalid Arguments ===\n"); FILE* f = tmpfile(); if (!f) return 0; FILE* f_valid = tmpfile(); if (!f_valid) { fclose(f); return 0; } // Prepare a valid compressed stream for decompression tests zxc_compress_opts_t _sco5 = {.n_threads = 1, .level = 1, .checksum_enabled = 0}; zxc_stream_compress(f, f_valid, &_sco5); rewind(f_valid); // 1. Input NULL -> Must fail zxc_compress_opts_t _sco6 = {.n_threads = 1, .level = 5, .checksum_enabled = 0}; if (zxc_stream_compress(NULL, f, &_sco6) >= 0) { printf("Failed: Should return < 0 when Input is NULL\n"); fclose(f); return 0; } // 2. Output NULL -> Must SUCCEED (Benchmark / Dry-Run Mode) zxc_compress_opts_t _sco7 = {.n_threads = 1, .level = 5, .checksum_enabled = 0}; if (zxc_stream_compress(f, NULL, &_sco7) < 0) { printf("Failed: Should allow NULL Output (Benchmark mode support)\n"); fclose(f); return 0; } // 3. Decompression Input NULL -> Must fail zxc_decompress_opts_t _sdo8 = {.n_threads = 1, .checksum_enabled = 0}; if (zxc_stream_decompress(NULL, f, &_sdo8) >= 0) { printf("Failed: Decompress should return < 0 when Input is NULL\n"); fclose(f); return 0; } // 3b. Decompression Output NULL -> Must SUCCEED (Benchmark mode) zxc_decompress_opts_t _sdo9 = {.n_threads = 1, .checksum_enabled = 0}; if (zxc_stream_decompress(f_valid, NULL, &_sdo9) < 0) { printf("Failed: Decompress should allow NULL Output (Benchmark mode support)\n"); fclose(f_valid); return 0; } // 4. zxc_compress NULL checks zxc_compress_opts_t _co10 = {.level = 3, .checksum_enabled = 0}; if (zxc_compress(NULL, 100, (void*)1, 100, &_co10) >= 0) { printf("Failed: zxc_compress should return < 0 when src is NULL\n"); fclose(f); return 0; } zxc_compress_opts_t _co11 = {.level = 3, .checksum_enabled = 0}; if (zxc_compress((void*)1, 100, NULL, 100, &_co11) >= 0) { printf("Failed: zxc_compress should return < 0 when dst is NULL\n"); fclose(f); return 0; } // 5. zxc_decompress NULL checks zxc_decompress_opts_t _do12 = {.checksum_enabled = 0}; if (zxc_decompress(NULL, 100, (void*)1, 100, &_do12) >= 0) { printf("Failed: zxc_decompress should return < 0 when src is NULL\n"); fclose(f); return 0; } zxc_decompress_opts_t _do13 = {.checksum_enabled = 0}; if (zxc_decompress((void*)1, 100, NULL, 100, &_do13) >= 0) { printf("Failed: zxc_decompress should return < 0 when dst is NULL\n"); fclose(f); return 0; } // 6. zxc_compress_bound overflow check if (zxc_compress_bound(SIZE_MAX) != 0) { printf("Failed: zxc_compress_bound should return 0 on overflow\n"); fclose(f); return 0; } printf("PASS\n\n"); fclose(f); return 1; } // Checks behavior with truncated compressed input int test_truncated_input() { printf("=== TEST: Unit - Truncated Input (Stream) ===\n"); const size_t SRC_SIZE = 1024; uint8_t src[1024]; gen_lz_data(src, SRC_SIZE); size_t cap = (size_t)zxc_compress_bound(SRC_SIZE); uint8_t* compressed = malloc(cap); uint8_t* decomp_buf = malloc(SRC_SIZE); if (!compressed || !decomp_buf) { free(compressed); free(decomp_buf); return 0; } zxc_compress_opts_t _co14 = {.level = 3, .checksum_enabled = 1}; int64_t comp_sz = zxc_compress(src, SRC_SIZE, compressed, cap, &_co14); if (comp_sz <= 0) { printf("Prepare failed\n"); free(compressed); free(decomp_buf); return 0; } // Try decompressing with progressively cropped size // 1. Cut off the Footer (last ZXC_FILE_FOOTER_SIZE bytes) if (comp_sz > ZXC_FILE_FOOTER_SIZE) { zxc_decompress_opts_t _do15 = {.checksum_enabled = 1}; if (zxc_decompress(compressed, (size_t)(comp_sz - ZXC_FILE_FOOTER_SIZE), decomp_buf, SRC_SIZE, &_do15) >= 0) { printf("Failed: Should fail when footer is missing\n"); free(compressed); free(decomp_buf); return 0; } } // 2. Cut off half the file zxc_decompress_opts_t _do16 = {.checksum_enabled = 1}; if (zxc_decompress(compressed, (size_t)(comp_sz / 2), decomp_buf, SRC_SIZE, &_do16) >= 0) { printf("Failed: Should fail when stream is truncated by half\n"); free(compressed); free(decomp_buf); return 0; } // 3. Cut off just 1 byte zxc_decompress_opts_t _do17 = {.checksum_enabled = 1}; if (zxc_decompress(compressed, (size_t)(comp_sz - 1), decomp_buf, SRC_SIZE, &_do17) >= 0) { printf("Failed: Should fail when stream is truncated by 1 byte\n"); free(compressed); free(decomp_buf); return 0; } printf("PASS\n\n"); free(compressed); free(decomp_buf); return 1; } // Checks behavior if writing fails int test_io_failures() { printf("=== TEST: Unit - I/O Failures ===\n"); FILE* f_in = tmpfile(); if (!f_in) return 0; // Create a dummy file to simulate failure // Open it in "rb" (read-only) and pass it as "wb" output file. // fwrite should return 0 and trigger the error. const char* bad_filename = "zxc_test_readonly.tmp"; FILE* f_dummy = create_restricted_file(bad_filename); if (f_dummy) fclose(f_dummy); FILE* f_out = fopen(bad_filename, "rb"); if (!f_out) { perror("fopen readonly"); fclose(f_in); return 0; } // Write some data to input fputs("test data to compress", f_in); fseek(f_in, 0, SEEK_SET); // This should fail cleanly (return < 0) because writing to f_out is impossible zxc_compress_opts_t _sco18 = {.n_threads = 1, .level = 5, .checksum_enabled = 0}; if (zxc_stream_compress(f_in, f_out, &_sco18) >= 0) { printf("Failed: Should detect write error on read-only stream\n"); fclose(f_in); fclose(f_out); remove(bad_filename); return 0; } printf("PASS\n\n"); fclose(f_in); fclose(f_out); remove(bad_filename); return 1; } // Checks thread selector behavior int test_thread_params() { printf("=== TEST: Unit - Thread Parameters ===\n"); FILE* f_in = tmpfile(); FILE* f_out = tmpfile(); if (!f_in || !f_out) { if (f_in) fclose(f_in); if (f_out) fclose(f_out); return 0; } // Test with 0 (Auto) and negative value - must not crash zxc_compress_opts_t _sco19 = {.n_threads = 0, .level = 5, .checksum_enabled = 0}; zxc_stream_compress(f_in, f_out, &_sco19); fseek(f_in, 0, SEEK_SET); fseek(f_out, 0, SEEK_SET); zxc_compress_opts_t _sco20 = {.n_threads = -5, .level = 5, .checksum_enabled = 0}; zxc_stream_compress(f_in, f_out, &_sco20); printf("PASS (No crash observed)\n\n"); fclose(f_in); fclose(f_out); return 1; } // Multi-threaded round-trip test for TSan coverage int test_multithread_roundtrip() { printf("=== TEST: Multi-Thread Round-Trip (TSan Coverage) ===\n"); const size_t SIZE = 4 * 1024 * 1024; // 4MB to ensure multiple chunks const int ITERATIONS = 3; // Multiple runs increase race detection int result = 0; uint8_t* input = malloc(SIZE); uint8_t* output = malloc(SIZE); if (!input || !output) goto cleanup; gen_lz_data(input, SIZE); for (int iter = 0; iter < ITERATIONS; iter++) { FILE* f_in = tmpfile(); FILE* f_comp = tmpfile(); FILE* f_decomp = tmpfile(); if (!f_in || !f_comp || !f_decomp) { if (f_in) fclose(f_in); if (f_comp) fclose(f_comp); if (f_decomp) fclose(f_decomp); goto cleanup; } fwrite(input, 1, SIZE, f_in); fseek(f_in, 0, SEEK_SET); // Vary thread count: 2, 4, 8 int num_threads = 2 << iter; zxc_compress_opts_t _sco21 = {.n_threads = num_threads, .level = 3, .checksum_enabled = 1}; if (zxc_stream_compress(f_in, f_comp, &_sco21) < 0) { printf("Compression failed (threads=%d)!\n", num_threads); fclose(f_in); fclose(f_comp); fclose(f_decomp); goto cleanup; } fseek(f_comp, 0, SEEK_SET); zxc_decompress_opts_t _sdo22 = {.n_threads = num_threads, .checksum_enabled = 1}; if (zxc_stream_decompress(f_comp, f_decomp, &_sdo22) < 0) { printf("Decompression failed (threads=%d)!\n", num_threads); fclose(f_in); fclose(f_comp); fclose(f_decomp); goto cleanup; } long decomp_size = ftell(f_decomp); fseek(f_decomp, 0, SEEK_SET); if (decomp_size != (long)SIZE || fread(output, 1, SIZE, f_decomp) != SIZE || memcmp(input, output, SIZE) != 0) { printf("Verification failed (threads=%d)!\n", num_threads); fclose(f_in); fclose(f_comp); fclose(f_decomp); goto cleanup; } fclose(f_in); fclose(f_comp); fclose(f_decomp); printf(" Iteration %d: PASS (%d threads)\n", iter + 1, num_threads); } printf("PASS (3 iterations, 2/4/8 threads)\n\n"); result = 1; cleanup: free(input); free(output); return result; } // Checks the buffer-based API (zxc_compress / zxc_decompress) int test_buffer_api() { printf("=== TEST: Unit - Buffer API (zxc_compress/zxc_decompress) ===\n"); size_t src_size = 128 * 1024; uint8_t* src = malloc(src_size); gen_lz_data(src, src_size); // 1. Calculate max compressed size size_t max_dst_size = (size_t)zxc_compress_bound(src_size); uint8_t* compressed = malloc(max_dst_size); int checksum_enabled = 1; // 2. Compress zxc_compress_opts_t _co23 = {.level = 3, .checksum_enabled = checksum_enabled}; int64_t compressed_size = zxc_compress(src, src_size, compressed, max_dst_size, &_co23); if (compressed_size <= 0) { printf("Failed: zxc_compress returned %lld\n", (long long)compressed_size); free(src); free(compressed); return 0; } printf("Compressed %zu bytes to %lld bytes\n", src_size, (long long)compressed_size); // 3. Decompress uint8_t* decompressed = malloc(src_size); zxc_decompress_opts_t _do24 = {.checksum_enabled = checksum_enabled}; int64_t decompressed_size = zxc_decompress(compressed, (size_t)compressed_size, decompressed, src_size, &_do24); if (decompressed_size != (int64_t)src_size) { printf("Failed: zxc_decompress returned %lld, expected %zu\n", (long long)decompressed_size, src_size); free(src); free(compressed); free(decompressed); return 0; } // 4. Verify content if (memcmp(src, decompressed, src_size) != 0) { printf("Failed: Content mismatch after decompression\n"); free(src); free(compressed); free(decompressed); return 0; } // 5. Test error case: Destination too small size_t small_capacity = (size_t)(compressed_size / 2); zxc_compress_opts_t _co25 = {.level = 3, .checksum_enabled = checksum_enabled}; int64_t small_res = zxc_compress(src, src_size, compressed, small_capacity, &_co25); if (small_res >= 0) { printf("Failed: zxc_compress should fail with small buffer (returned %lld)\n", (long long)small_res); free(src); free(compressed); free(decompressed); return 0; } printf("PASS\n\n"); free(src); free(compressed); free(decompressed); return 1; } /* * Test for zxc_br_init and zxc_br_ensure */ int test_bit_reader() { printf("=== TEST: Unit - Bit Reader (zxc_br_init / zxc_br_ensure) ===\n"); // Case 1: Normal initialization uint8_t buffer[16]; for (int i = 0; i < 16; i++) buffer[i] = (uint8_t)i; zxc_bit_reader_t br; zxc_br_init(&br, buffer, 16); if (br.bits != 64) return 0; if (br.ptr != buffer + 8) return 0; if (br.accum != zxc_le64(buffer)) return 0; printf(" [PASS] Normal init\n"); // Case 2: Small buffer initialization (should not crash) uint8_t small_buffer[4] = {0xAA, 0xBB, 0xCC, 0xDD}; zxc_br_init(&br, small_buffer, 4); // Should have read 4 bytes safely (in LE order, matching zxc_le_partial) uint64_t expected_accum = (uint64_t)small_buffer[0] | ((uint64_t)small_buffer[1] << 8) | ((uint64_t)small_buffer[2] << 16) | ((uint64_t)small_buffer[3] << 24); if (br.accum != expected_accum) return 0; if (br.ptr != small_buffer + 4) return 0; printf(" [PASS] Small buffer init\n"); // Case 3: zxc_br_ensure (Normal refill) zxc_br_init(&br, buffer, 16); br.bits = 10; // Simulate consumption br.accum >>= 54; // Simulate shift zxc_br_ensure(&br, 32); // Should have refilled if (br.bits < 32) return 0; printf(" [PASS] Ensure normal refill\n"); // Case 4: zxc_br_ensure (End of stream) // Init with full buffer but advanced pointer near end zxc_br_init(&br, buffer, 16); br.ptr = buffer + 16; // At end br.bits = 0; // Try to ensure bits, should not read past end zxc_br_ensure(&br, 10); // The key is it didn't crash. printf(" [PASS] Ensure EOF safety\n"); printf("PASS\n\n"); return 1; } /* * Test for zxc_bitpack_stream_32 */ int test_bitpack() { printf("=== TEST: Unit - Bit Packing (zxc_bitpack_stream_32) ===\n"); const uint32_t src[4] = {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF}; uint8_t dst[16]; // Pack 4 values with 4 bits each. // Input is 0xFFFFFFFF, but should be masked to 0xF (1111). // Result should be 2 bytes: 0xFF, 0xFF int len = zxc_bitpack_stream_32(src, 4, dst, 16, 4); if (len != 2) return 0; if (dst[0] != 0xFF || dst[1] != 0xFF) return 0; printf(" [PASS] Bitpack overflow masking\n"); // Edge case: bits = 32 const uint32_t src32[1] = {0x12345678}; len = zxc_bitpack_stream_32(src32, 1, dst, 16, 32); if (len != 4) return 0; if (zxc_le32(dst) != 0x12345678) return 0; printf(" [PASS] Bitpack 32 bits\n"); printf("PASS\n\n"); return 1; } // Checks that the EOF block is correctly appended int test_eof_block_structure() { printf("=== TEST: Unit - EOF Block Structure ===\n"); const char* input = "test"; size_t src_size = 4; size_t max_dst_size = (size_t)zxc_compress_bound(src_size); uint8_t* compressed = malloc(max_dst_size); if (!compressed) return 0; zxc_compress_opts_t _co26 = {.level = 1, .checksum_enabled = 0}; int64_t comp_size = zxc_compress(input, src_size, compressed, max_dst_size, &_co26); if (comp_size <= 0) { printf("Failed: Compression returned 0\n"); free(compressed); return 0; } // Validating Footer and EOF Block // Total Overhead: 12 bytes (Footer) + 8 bytes (EOF Header) = 20 bytes if (comp_size < 20) { printf("Failed: Compressed size too small for Footer + EOF (%lld)\n", (long long)comp_size); free(compressed); return 0; } // 1. Verify 12-byte Footer // Structure: [SrcSize (8)] + [Hash (4)] const uint8_t* footer_ptr = compressed + comp_size - 12; uint32_t f_src_low = zxc_le32(footer_ptr); // Should be 4 uint32_t f_src_high = zxc_le32(footer_ptr + 4); // Should be 0 uint32_t f_hash = zxc_le32(footer_ptr + 8); // Should be 0 (checksum disabled) if (f_src_low != 4 || f_src_high != 0 || f_hash != 0) { printf("Failed: Footer mismatch. Src: %u, Hash: %u\n", f_src_low, f_hash); free(compressed); return 0; } // 2. Verify EOF Block Header (8 bytes) // Should be immediately before the footer const uint8_t* eof_ptr = compressed + comp_size - 20; uint8_t expected[8] = {0xFF, 0, 0, 0, 0, 0, 0, 0}; expected[7] = zxc_hash8(expected); if (memcmp(eof_ptr, expected, 8) != 0) { printf( "Failed: EOF block mismatch.\nExpected: %02X %02X %02X ... %02X\nGot: %02X %02X " "%02X ... %02X\n", expected[0], expected[1], expected[2], expected[7], eof_ptr[0], eof_ptr[1], eof_ptr[2], eof_ptr[7]); free(compressed); return 0; } printf("PASS\n\n"); free(compressed); return 1; } int test_header_checksum() { printf("Running test_header_checksum...\n"); uint8_t header_buf[ZXC_BLOCK_HEADER_SIZE]; zxc_block_header_t bh_in = {.block_type = ZXC_BLOCK_GLO, .block_flags = 0, .reserved = 0, .header_crc = 0, .comp_size = 1024}; // 1. Write Header if (zxc_write_block_header(header_buf, ZXC_BLOCK_HEADER_SIZE, &bh_in) != ZXC_BLOCK_HEADER_SIZE) { printf(" [FAIL] zxc_write_block_header failed\n"); return 0; } // Verify manually that checksum byte is non-zero (highly likely) if (header_buf[7] == 0) { // It's technically possible but very unlikely with a good hash printf(" [WARN] Checksum is 0 (unlikely but possible)\n"); } // 2. Read Header (Valid) zxc_block_header_t bh_out; if (zxc_read_block_header(header_buf, ZXC_BLOCK_HEADER_SIZE, &bh_out) != 0) { printf(" [FAIL] zxc_read_block_header failed on valid input\n"); return 0; } if (bh_out.block_type != bh_in.block_type || bh_out.comp_size != bh_in.comp_size || bh_out.header_crc != header_buf[7]) { printf(" [FAIL] Read data mismatch\n"); return 0; } // 3. Corrupt Header Checksum uint8_t original_crc = header_buf[7]; header_buf[7] = ~original_crc; // Flip bits if (zxc_read_block_header(header_buf, ZXC_BLOCK_HEADER_SIZE, &bh_out) == 0) { printf(" [FAIL] zxc_read_block_header should have failed on corrupted CRC\n"); return 0; } header_buf[7] = original_crc; // Restore // 4. Corrupt Header Content header_buf[0] = ZXC_BLOCK_RAW; // Change type if (zxc_read_block_header(header_buf, ZXC_BLOCK_HEADER_SIZE, &bh_out) == 0) { printf(" [FAIL] zxc_read_block_header should have failed on corrupted content\n"); return 0; } printf("PASS\n\n"); return 1; } // 5. Test Global Checksum Order Sensitivity // Ensures that swapping two blocks (even if valid individually) triggers a global checksum failure. int test_global_checksum_order() { printf("TEST: Global Checksum Order Sensitivity... "); // 1. Create input data withDISTINCT patterns for 2 blocks (so blocks are different) // ZXC_BLOCK_SIZE_DEFAULT is 256KB. We need > 256KB. Let's use 600KB. size_t input_sz = 600 * 1024; uint8_t* val_buf = malloc(input_sz); if (!val_buf) return 0; // Fill Block 1 with 0xAA, Block 2 with 0xBB, Block 3 with 0xCC... memset(val_buf, 0xAA, 256 * 1024); memset(val_buf + 256 * 1024, 0xBB, 256 * 1024); memset(val_buf + 512 * 1024, 0xCC, input_sz - 512 * 1024); FILE* f_in = tmpfile(); FILE* f_comp = tmpfile(); fwrite(val_buf, 1, input_sz, f_in); rewind(f_in); // 2. Compress with Checksum Enabled zxc_compress_opts_t _sco27 = {.n_threads = 1, .level = 1, .checksum_enabled = 1}; zxc_stream_compress(f_in, f_comp, &_sco27); // 3. Read compressed data to memory long comp_sz = ftell(f_comp); rewind(f_comp); uint8_t* comp_buf = malloc((size_t)comp_sz); if (fread(comp_buf, 1, comp_sz, f_comp) != (size_t)comp_sz) { printf("[FAIL] Failed to read compressed data\n"); free(val_buf); free(comp_buf); fclose(f_in); fclose(f_comp); return 0; } // 4. Parse Blocks to identify Block 1 and Block 2 // File Header: ZXC_FILE_HEADER_SIZE bytes size_t off1 = ZXC_FILE_HEADER_SIZE; // Parse Block 1 Header zxc_block_header_t bh1; zxc_read_block_header(comp_buf + off1, ZXC_BLOCK_HEADER_SIZE, &bh1); size_t len1 = ZXC_BLOCK_HEADER_SIZE + bh1.comp_size + ZXC_BLOCK_CHECKSUM_SIZE; size_t off2 = off1 + len1; // Parse Block 2 Header zxc_block_header_t bh2; zxc_read_block_header(comp_buf + off2, ZXC_BLOCK_HEADER_SIZE, &bh2); size_t len2 = ZXC_BLOCK_HEADER_SIZE + bh2.comp_size + ZXC_BLOCK_CHECKSUM_SIZE; // Ensure we have at least 2 full blocks + EOF + Global Checksum if (off2 + len2 > (size_t)comp_sz) { printf("[FAIL] Compressed size too small for test\n"); free(val_buf); free(comp_buf); fclose(f_in); fclose(f_comp); return 0; } // 5. Swap Block 1 and Block 2 // To safely swap, we need a new buffer uint8_t* swapped_buf = malloc((size_t)comp_sz); // Copy File Header // Copy File Header memcpy(swapped_buf, comp_buf, ZXC_FILE_HEADER_SIZE); size_t w_off = ZXC_FILE_HEADER_SIZE; // Write Block 2 first memcpy(swapped_buf + w_off, comp_buf + off2, len2); w_off += len2; // Write Block 1 second memcpy(swapped_buf + w_off, comp_buf + off1, len1); w_off += len1; // Write remaining data (EOF block + Global Checksum) size_t remaining_off = off2 + len2; size_t remaining_len = comp_sz - remaining_off; memcpy(swapped_buf + w_off, comp_buf + remaining_off, remaining_len); // 6. Write to File for Decompression FILE* f_bad = tmpfile(); fwrite(swapped_buf, 1, (size_t)comp_sz, f_bad); rewind(f_bad); // 7. Attempt Decompression FILE* f_out = tmpfile(); zxc_decompress_opts_t _sdo28 = {.n_threads = 1, .checksum_enabled = 1}; int64_t res = zxc_stream_decompress(f_bad, f_out, &_sdo28); fclose(f_in); fclose(f_comp); fclose(f_bad); fclose(f_out); free(val_buf); free(comp_buf); free(swapped_buf); if (res >= 0) { printf(" [FAIL] zxc_stream_decompress unexpectedly succeeded on swapped blocks\n"); return 0; } printf("PASS\n\n"); return 1; } // Test zxc_get_decompressed_size int test_get_decompressed_size() { printf("=== TEST: Unit - zxc_get_decompressed_size ===\n"); // 1. Compress some data, then check decompressed size size_t src_size = 64 * 1024; uint8_t* src = malloc(src_size); gen_lz_data(src, src_size); size_t max_dst = (size_t)zxc_compress_bound(src_size); uint8_t* compressed = malloc(max_dst); zxc_compress_opts_t _co29 = {.level = 3, .checksum_enabled = 0}; int64_t comp_size = zxc_compress(src, src_size, compressed, max_dst, &_co29); if (comp_size <= 0) { printf("Failed: Compression returned 0\n"); free(src); free(compressed); return 0; } size_t reported = (size_t)zxc_get_decompressed_size(compressed, comp_size); if (reported != src_size) { printf("Failed: Expected %zu, got %zu\n", src_size, reported); free(src); free(compressed); return 0; } printf(" [PASS] Valid compressed data\n"); // 2. Too-small buffer if (zxc_get_decompressed_size(compressed, 4) != 0) { printf("Failed: Should return 0 for too-small buffer\n"); free(src); free(compressed); return 0; } printf(" [PASS] Too-small buffer\n"); // 3. Invalid magic word uint8_t bad_buf[64] = {0}; if (zxc_get_decompressed_size(bad_buf, sizeof(bad_buf)) != 0) { printf("Failed: Should return 0 for invalid magic\n"); free(src); free(compressed); return 0; } printf(" [PASS] Invalid magic word\n"); printf("PASS\n\n"); free(src); free(compressed); return 1; } int test_error_name() { printf("--- Test: zxc_error_name ---\n"); struct { int code; const char* expected; } cases[] = { {ZXC_OK, "ZXC_OK"}, {ZXC_ERROR_MEMORY, "ZXC_ERROR_MEMORY"}, {ZXC_ERROR_DST_TOO_SMALL, "ZXC_ERROR_DST_TOO_SMALL"}, {ZXC_ERROR_SRC_TOO_SMALL, "ZXC_ERROR_SRC_TOO_SMALL"}, {ZXC_ERROR_BAD_MAGIC, "ZXC_ERROR_BAD_MAGIC"}, {ZXC_ERROR_BAD_VERSION, "ZXC_ERROR_BAD_VERSION"}, {ZXC_ERROR_BAD_HEADER, "ZXC_ERROR_BAD_HEADER"}, {ZXC_ERROR_BAD_CHECKSUM, "ZXC_ERROR_BAD_CHECKSUM"}, {ZXC_ERROR_CORRUPT_DATA, "ZXC_ERROR_CORRUPT_DATA"}, {ZXC_ERROR_BAD_OFFSET, "ZXC_ERROR_BAD_OFFSET"}, {ZXC_ERROR_OVERFLOW, "ZXC_ERROR_OVERFLOW"}, {ZXC_ERROR_IO, "ZXC_ERROR_IO"}, {ZXC_ERROR_NULL_INPUT, "ZXC_ERROR_NULL_INPUT"}, {ZXC_ERROR_BAD_BLOCK_TYPE, "ZXC_ERROR_BAD_BLOCK_TYPE"}, {ZXC_ERROR_BAD_BLOCK_SIZE, "ZXC_ERROR_BAD_BLOCK_SIZE"}, }; const int n = sizeof(cases) / sizeof(cases[0]); for (int i = 0; i < n; i++) { const char* name = zxc_error_name(cases[i].code); if (strcmp(name, cases[i].expected) != 0) { printf(" [FAIL] zxc_error_name(%d) = \"%s\", expected \"%s\"\n", cases[i].code, name, cases[i].expected); return 0; } } printf(" [PASS] All %d known error codes\n", n); // Unknown codes should return "ZXC_UNKNOWN_ERROR" const char* unk = zxc_error_name(-999); if (strcmp(unk, "ZXC_UNKNOWN_ERROR") != 0) { printf(" [FAIL] zxc_error_name(-999) = \"%s\", expected \"ZXC_UNKNOWN_ERROR\"\n", unk); return 0; } unk = zxc_error_name(42); if (strcmp(unk, "ZXC_UNKNOWN_ERROR") != 0) { printf(" [FAIL] zxc_error_name(42) = \"%s\", expected \"ZXC_UNKNOWN_ERROR\"\n", unk); return 0; } printf(" [PASS] Unknown error codes\n"); printf("PASS\n\n"); return 1; } int test_legacy_header() { printf("=== TEST: Legacy header (chunk_size_code=64) ===\n"); // Build a valid file header with legacy chunk_size_code = 64 (= 256 KB) uint8_t hdr[ZXC_FILE_HEADER_SIZE]; memset(hdr, 0, sizeof(hdr)); // Magic word (LE) hdr[0] = 0xF5; hdr[1] = 0x2E; hdr[2] = 0xB0; hdr[3] = 0x9C; // Version hdr[4] = ZXC_FILE_FORMAT_VERSION; // Legacy chunk size code hdr[5] = 64; // Flags: no checksum hdr[6] = 0; // Compute CRC16 (bytes 14-15 zeroed, then hash) hdr[14] = 0; hdr[15] = 0; uint16_t crc = zxc_hash16(hdr); hdr[14] = (uint8_t)(crc & 0xFF); hdr[15] = (uint8_t)(crc >> 8); size_t block_size = 0; int has_checksum = -1; int rc = zxc_read_file_header(hdr, sizeof(hdr), &block_size, &has_checksum); if (rc != ZXC_OK) { printf(" [FAIL] zxc_read_file_header returned %d (%s)\n", rc, zxc_error_name(rc)); return 0; } if (block_size != 256 * 1024) { printf(" [FAIL] block_size = %zu, expected %d\n", block_size, 256 * 1024); return 0; } if (has_checksum != 0) { printf(" [FAIL] has_checksum = %d, expected 0\n", has_checksum); return 0; } printf(" [PASS] Legacy code 64 -> block_size = 256 KB\n"); // Verify that invalid codes are rejected hdr[5] = 99; // Not a valid exponent nor legacy value hdr[14] = 0; hdr[15] = 0; crc = zxc_hash16(hdr); hdr[14] = (uint8_t)(crc & 0xFF); hdr[15] = (uint8_t)(crc >> 8); rc = zxc_read_file_header(hdr, sizeof(hdr), &block_size, &has_checksum); if (rc != ZXC_ERROR_BAD_BLOCK_SIZE) { printf(" [FAIL] invalid code 99: expected %d, got %d\n", ZXC_ERROR_BAD_BLOCK_SIZE, rc); return 0; } printf(" [PASS] Invalid code 99 -> ZXC_ERROR_BAD_BLOCK_SIZE\n"); printf("PASS\n\n"); return 1; } int test_buffer_error_codes() { printf("=== TEST: Unit - Buffer API Error Codes ===\n"); /* ------------------------------------------------------------------ */ /* zxc_compress error paths */ /* ------------------------------------------------------------------ */ // 1. NULL src zxc_compress_opts_t _co30 = {.level = 3, .checksum_enabled = 0}; int64_t r = zxc_compress(NULL, 100, (void*)1, 100, &_co30); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] NULL src: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] zxc_compress NULL src -> ZXC_ERROR_NULL_INPUT\n"); // 2. NULL dst zxc_compress_opts_t _co31 = {.level = 3, .checksum_enabled = 0}; r = zxc_compress((void*)1, 100, NULL, 100, &_co31); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] NULL dst: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] zxc_compress NULL dst -> ZXC_ERROR_NULL_INPUT\n"); // 3. src_size == 0 uint8_t dummy[16]; zxc_compress_opts_t _co32 = {.level = 3, .checksum_enabled = 0}; r = zxc_compress(dummy, 0, dummy, sizeof(dummy), &_co32); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] src_size==0: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] zxc_compress src_size==0 -> ZXC_ERROR_NULL_INPUT\n"); // 4. dst_capacity == 0 zxc_compress_opts_t _co33 = {.level = 3, .checksum_enabled = 0}; r = zxc_compress(dummy, sizeof(dummy), dummy, 0, &_co33); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] dst_cap==0: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] zxc_compress dst_capacity==0 -> ZXC_ERROR_NULL_INPUT\n"); // 5. dst too small for file header (< 16 bytes) { uint8_t src[64]; uint8_t dst[8]; // Too small for file header (16 bytes) gen_lz_data(src, sizeof(src)); zxc_compress_opts_t _co34 = {.level = 3, .checksum_enabled = 0}; r = zxc_compress(src, sizeof(src), dst, sizeof(dst), &_co34); if (r >= 0) { printf(" [FAIL] dst too small for header: expected < 0, got %lld\n", (long long)r); return 0; } } printf(" [PASS] zxc_compress dst too small for header -> negative\n"); // 6. dst too small for data (fits header but not chunk) { const size_t src_sz = 4096; uint8_t* src = malloc(src_sz); const size_t small_dst = 128; uint8_t* dst = malloc(small_dst); gen_lz_data(src, src_sz); zxc_compress_opts_t _co35 = {.level = 3, .checksum_enabled = 0}; r = zxc_compress(src, src_sz, dst, small_dst, &_co35); if (r >= 0) { printf(" [FAIL] dst too small for chunk: expected < 0, got %lld\n", (long long)r); free(src); free(dst); return 0; } free(src); free(dst); } printf(" [PASS] zxc_compress dst too small for chunk -> negative\n"); // 7. dst too small for EOF + footer { // Compress first to find the exact compressed size, then retry with // just enough for the data blocks but not for the EOF + footer. const size_t src_sz = 256; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); const size_t full_cap = (size_t)zxc_compress_bound(src_sz); uint8_t* full_dst = malloc(full_cap); zxc_compress_opts_t _co36 = {.level = 3, .checksum_enabled = 0}; const int64_t full_sz = zxc_compress(src, src_sz, full_dst, full_cap, &_co36); if (full_sz <= 0) { printf(" [SKIP] Cannot prepare for EOF test\n"); free(src); free(full_dst); } else { // EOF header(8) + footer(12) = 20 bytes at the end. // Try with a buffer that's just a few bytes too small. const size_t tight = (size_t)full_sz - 5; uint8_t* tight_dst = malloc(tight); zxc_compress_opts_t _co37 = {.level = 3, .checksum_enabled = 0}; r = zxc_compress(src, src_sz, tight_dst, tight, &_co37); if (r >= 0) { printf(" [FAIL] dst too small for EOF+footer: expected < 0, got %lld\n", (long long)r); free(src); free(full_dst); free(tight_dst); return 0; } free(src); free(full_dst); free(tight_dst); } } printf(" [PASS] zxc_compress dst too small for EOF+footer -> negative\n"); /* ------------------------------------------------------------------ */ /* zxc_decompress error paths */ /* ------------------------------------------------------------------ */ // 8. NULL src zxc_decompress_opts_t _do38 = {.checksum_enabled = 0}; r = zxc_decompress(NULL, 100, (void*)1, 100, &_do38); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] decompress NULL src: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] zxc_decompress NULL src -> ZXC_ERROR_NULL_INPUT\n"); // 9. NULL dst zxc_decompress_opts_t _do39 = {.checksum_enabled = 0}; r = zxc_decompress((void*)1, 100, NULL, 100, &_do39); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] decompress NULL dst: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] zxc_decompress NULL dst -> ZXC_ERROR_NULL_INPUT\n"); // 10. src too small for file header { uint8_t tiny[4] = {0}; uint8_t out[64]; zxc_decompress_opts_t _do40 = {.checksum_enabled = 0}; r = zxc_decompress(tiny, sizeof(tiny), out, sizeof(out), &_do40); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] src too small: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } } printf(" [PASS] zxc_decompress src too small -> ZXC_ERROR_NULL_INPUT\n"); // 11. Bad file header (invalid magic) { uint8_t bad_src[64]; memset(bad_src, 0, sizeof(bad_src)); uint8_t out[64]; zxc_decompress_opts_t _do41 = {.checksum_enabled = 0}; r = zxc_decompress(bad_src, sizeof(bad_src), out, sizeof(out), &_do41); if (r != ZXC_ERROR_BAD_HEADER) { printf(" [FAIL] bad magic: expected %d, got %lld\n", ZXC_ERROR_BAD_HEADER, (long long)r); return 0; } } printf(" [PASS] zxc_decompress bad magic -> ZXC_ERROR_BAD_HEADER\n"); // Prepare a valid compressed buffer for subsequent decompress error tests const size_t test_src_sz = 1024; uint8_t* test_src = malloc(test_src_sz); gen_lz_data(test_src, test_src_sz); const size_t comp_cap = (size_t)zxc_compress_bound(test_src_sz); uint8_t* comp_buf = malloc(comp_cap); zxc_compress_opts_t _co42 = {.level = 3, .checksum_enabled = 1}; const int64_t comp_sz = zxc_compress(test_src, test_src_sz, comp_buf, comp_cap, &_co42); if (comp_sz <= 0) { printf(" [FAIL] Could not prepare compressed data\n"); free(test_src); free(comp_buf); return 0; } // 12. Corrupt block header (damage the first block header byte after file header) { uint8_t* corrupt = malloc((size_t)comp_sz); memcpy(corrupt, comp_buf, (size_t)comp_sz); // Corrupt the block type byte at offset ZXC_FILE_HEADER_SIZE corrupt[ZXC_FILE_HEADER_SIZE] = 0xFF; // Invalid block type uint8_t* out = malloc(test_src_sz); zxc_decompress_opts_t _do43 = {.checksum_enabled = 1}; r = zxc_decompress(corrupt, (size_t)comp_sz, out, test_src_sz, &_do43); if (r >= 0) { printf(" [FAIL] corrupt block header: expected < 0, got %lld\n", (long long)r); free(corrupt); free(out); free(test_src); free(comp_buf); return 0; } free(corrupt); free(out); } printf(" [PASS] zxc_decompress corrupt block header -> negative\n"); // 13. Truncated at EOF (missing footer) { // Find the EOF block: it ends with the footer(12 bytes) // Truncate so the footer is missing const size_t trunc_sz = (size_t)comp_sz - ZXC_FILE_FOOTER_SIZE + 2; // Cut most of footer uint8_t* out = malloc(test_src_sz); zxc_decompress_opts_t _do44 = {.checksum_enabled = 1}; r = zxc_decompress(comp_buf, trunc_sz, out, test_src_sz, &_do44); if (r >= 0) { printf(" [FAIL] truncated footer: expected < 0, got %lld\n", (long long)r); free(out); free(test_src); free(comp_buf); return 0; } free(out); } printf(" [PASS] zxc_decompress truncated footer -> negative\n"); // 14. Stored size mismatch (corrupt the source size in footer) { uint8_t* corrupt = malloc((size_t)comp_sz); memcpy(corrupt, comp_buf, (size_t)comp_sz); // Footer is at end: last 12 bytes = [src_size(8)] + [global_hash(4)] // Corrupt the source size field (add 1 to the first byte) const size_t footer_offset = (size_t)comp_sz - ZXC_FILE_FOOTER_SIZE; corrupt[footer_offset] ^= 0x01; // Flip a bit in the stored source size uint8_t* out = malloc(test_src_sz); zxc_decompress_opts_t _do45 = {.checksum_enabled = 1}; r = zxc_decompress(corrupt, (size_t)comp_sz, out, test_src_sz, &_do45); if (r >= 0) { printf(" [FAIL] size mismatch: expected < 0, got %lld\n", (long long)r); free(corrupt); free(out); free(test_src); free(comp_buf); return 0; } free(corrupt); free(out); } printf(" [PASS] zxc_decompress stored size mismatch -> negative\n"); // 15. Global checksum failure (corrupt the global hash in footer) { uint8_t* corrupt = malloc((size_t)comp_sz); memcpy(corrupt, comp_buf, (size_t)comp_sz); // Global hash is the last 4 bytes of the file corrupt[comp_sz - 1] ^= 0xFF; uint8_t* out = malloc(test_src_sz); zxc_decompress_opts_t _do46 = {.checksum_enabled = 1}; r = zxc_decompress(corrupt, (size_t)comp_sz, out, test_src_sz, &_do46); if (r != ZXC_ERROR_BAD_CHECKSUM) { printf(" [FAIL] bad global checksum: expected %d, got %lld\n", ZXC_ERROR_BAD_CHECKSUM, (long long)r); free(corrupt); free(out); free(test_src); free(comp_buf); return 0; } free(corrupt); free(out); } printf(" [PASS] zxc_decompress global checksum -> ZXC_ERROR_BAD_CHECKSUM\n"); // 16. dst too small for decompression { uint8_t* out = malloc(test_src_sz / 4); // Way too small zxc_decompress_opts_t _do47 = {.checksum_enabled = 0}; r = zxc_decompress(comp_buf, (size_t)comp_sz, out, test_src_sz / 4, &_do47); if (r >= 0) { printf(" [FAIL] dst too small for decompress: expected < 0, got %lld\n", (long long)r); free(out); free(test_src); free(comp_buf); return 0; } free(out); } printf(" [PASS] zxc_decompress dst too small -> negative\n"); free(test_src); free(comp_buf); printf("PASS\n\n"); return 1; } int test_stream_get_decompressed_size_errors() { printf("=== TEST: Unit - zxc_stream_get_decompressed_size Error Codes ===\n"); // 1. NULL FILE* int64_t r = zxc_stream_get_decompressed_size(NULL); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] NULL FILE*: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); return 0; } printf(" [PASS] NULL FILE* -> ZXC_ERROR_NULL_INPUT\n"); // 2. File too small (less than header + footer) { FILE* f = tmpfile(); if (!f) { printf(" [SKIP] tmpfile failed\n"); return 0; } fwrite("tiny", 1, 4, f); fseek(f, 0, SEEK_SET); r = zxc_stream_get_decompressed_size(f); if (r != ZXC_ERROR_SRC_TOO_SMALL) { printf(" [FAIL] file too small: expected %d, got %lld\n", ZXC_ERROR_SRC_TOO_SMALL, (long long)r); fclose(f); return 0; } fclose(f); } printf(" [PASS] file too small -> ZXC_ERROR_SRC_TOO_SMALL\n"); // 3. Bad magic word { FILE* f = tmpfile(); if (!f) { printf(" [SKIP] tmpfile failed\n"); return 0; } // Write enough bytes but with wrong magic uint8_t garbage[ZXC_FILE_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE]; memset(garbage, 0, sizeof(garbage)); fwrite(garbage, 1, sizeof(garbage), f); fseek(f, 0, SEEK_SET); r = zxc_stream_get_decompressed_size(f); if (r != ZXC_ERROR_BAD_MAGIC) { printf(" [FAIL] bad magic: expected %d, got %lld\n", ZXC_ERROR_BAD_MAGIC, (long long)r); fclose(f); return 0; } fclose(f); } printf(" [PASS] bad magic -> ZXC_ERROR_BAD_MAGIC\n"); // 4. Valid file returns correct size { // Create a valid compressed file in memory const size_t src_sz = 512; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); const size_t cap = (size_t)zxc_compress_bound(src_sz); uint8_t* comp = malloc(cap); zxc_compress_opts_t _co48 = {.level = 3, .checksum_enabled = 0}; int64_t comp_sz = zxc_compress(src, src_sz, comp, cap, &_co48); if (comp_sz <= 0) { printf(" [SKIP] compress failed\n"); free(src); free(comp); return 0; } FILE* f = tmpfile(); fwrite(comp, 1, (size_t)comp_sz, f); fseek(f, 0, SEEK_SET); r = zxc_stream_get_decompressed_size(f); if (r != (int64_t)src_sz) { printf(" [FAIL] valid file: expected %zu, got %lld\n", src_sz, (long long)r); fclose(f); free(src); free(comp); return 0; } fclose(f); free(src); free(comp); } printf(" [PASS] valid file -> correct size\n"); printf("PASS\n\n"); return 1; } int test_stream_engine_errors() { printf("=== TEST: Unit - Stream Engine Error Codes ===\n"); // 1. zxc_stream_compress with NULL f_in { FILE* f_out = tmpfile(); zxc_compress_opts_t _sco49 = {.n_threads = 1, .level = 3, .checksum_enabled = 0}; int64_t r = zxc_stream_compress(NULL, f_out, &_sco49); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] compress NULL f_in: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); if (f_out) fclose(f_out); return 0; } if (f_out) fclose(f_out); } printf(" [PASS] zxc_stream_compress NULL f_in -> ZXC_ERROR_NULL_INPUT\n"); // 2. zxc_stream_decompress with NULL f_in { FILE* f_out = tmpfile(); zxc_decompress_opts_t _sdo50 = {.n_threads = 1, .checksum_enabled = 0}; int64_t r = zxc_stream_decompress(NULL, f_out, &_sdo50); if (r != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] decompress NULL f_in: expected %d, got %lld\n", ZXC_ERROR_NULL_INPUT, (long long)r); if (f_out) fclose(f_out); return 0; } if (f_out) fclose(f_out); } printf(" [PASS] zxc_stream_decompress NULL f_in -> ZXC_ERROR_NULL_INPUT\n"); // 3. zxc_stream_decompress with bad header (invalid file) { FILE* f_in = tmpfile(); FILE* f_out = tmpfile(); if (!f_in || !f_out) { if (f_in) fclose(f_in); if (f_out) fclose(f_out); printf(" [SKIP] tmpfile failed\n"); return 0; } // Write garbage data (bad magic) uint8_t garbage[64]; memset(garbage, 0xAA, sizeof(garbage)); fwrite(garbage, 1, sizeof(garbage), f_in); fseek(f_in, 0, SEEK_SET); zxc_decompress_opts_t _sdo51 = {.n_threads = 1, .checksum_enabled = 0}; int64_t r = zxc_stream_decompress(f_in, f_out, &_sdo51); if (r != ZXC_ERROR_BAD_HEADER) { printf(" [FAIL] bad header: expected %d, got %lld\n", ZXC_ERROR_BAD_HEADER, (long long)r); fclose(f_in); fclose(f_out); return 0; } fclose(f_in); fclose(f_out); } printf(" [PASS] zxc_stream_decompress bad header -> ZXC_ERROR_BAD_HEADER\n"); // 4. Stream decompress with corrupted footer (stored size mismatch) { // First, create a valid compressed stream const size_t src_sz = 4096; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); FILE* f_comp_in = tmpfile(); FILE* f_comp_out = tmpfile(); fwrite(src, 1, src_sz, f_comp_in); fseek(f_comp_in, 0, SEEK_SET); zxc_compress_opts_t _sco52 = {.n_threads = 1, .level = 3, .checksum_enabled = 1}; int64_t comp_sz = zxc_stream_compress(f_comp_in, f_comp_out, &_sco52); fclose(f_comp_in); if (comp_sz <= 0) { printf(" [SKIP] stream compress failed\n"); fclose(f_comp_out); free(src); return 0; } // Read the compressed data, corrupt the footer source size, rewrite fseek(f_comp_out, 0, SEEK_END); const long comp_file_sz = ftell(f_comp_out); uint8_t* comp_data = malloc(comp_file_sz); fseek(f_comp_out, 0, SEEK_SET); if (fread(comp_data, 1, comp_file_sz, f_comp_out) != (size_t)comp_file_sz) { printf(" [FAIL] fread failed\n"); fclose(f_comp_out); free(comp_data); free(src); return 0; } fclose(f_comp_out); // Corrupt the stored source size in footer (last 12 bytes: [src_size(8)] + [hash(4)]) const size_t footer_off = comp_file_sz - ZXC_FILE_FOOTER_SIZE; comp_data[footer_off] ^= 0x01; // Flip a bit in stored source size FILE* f_corrupt = tmpfile(); FILE* f_dec_out = tmpfile(); fwrite(comp_data, 1, comp_file_sz, f_corrupt); fseek(f_corrupt, 0, SEEK_SET); free(comp_data); zxc_decompress_opts_t _sdo53 = {.n_threads = 1, .checksum_enabled = 1}; int64_t r = zxc_stream_decompress(f_corrupt, f_dec_out, &_sdo53); fclose(f_corrupt); fclose(f_dec_out); free(src); if (r >= 0) { printf(" [FAIL] corrupt footer size: expected < 0, got %lld\n", (long long)r); return 0; } } printf(" [PASS] zxc_stream_decompress corrupt footer -> negative\n"); // 5. Stream decompress with corrupted global checksum { const size_t src_sz = 4096; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); FILE* f_comp_in = tmpfile(); FILE* f_comp_out = tmpfile(); fwrite(src, 1, src_sz, f_comp_in); fseek(f_comp_in, 0, SEEK_SET); zxc_compress_opts_t _sco54 = {.n_threads = 1, .level = 3, .checksum_enabled = 1}; int64_t comp_sz = zxc_stream_compress(f_comp_in, f_comp_out, &_sco54); fclose(f_comp_in); if (comp_sz <= 0) { printf(" [SKIP] stream compress failed\n"); fclose(f_comp_out); free(src); return 0; } fseek(f_comp_out, 0, SEEK_END); const long comp_file_sz = ftell(f_comp_out); uint8_t* comp_data = malloc(comp_file_sz); fseek(f_comp_out, 0, SEEK_SET); if (fread(comp_data, 1, comp_file_sz, f_comp_out) != (size_t)comp_file_sz) { printf(" [FAIL] fread failed\n"); fclose(f_comp_out); free(comp_data); free(src); return 0; } fclose(f_comp_out); // Corrupt the global checksum (last 4 bytes) comp_data[comp_file_sz - 1] ^= 0xFF; FILE* f_corrupt = tmpfile(); FILE* f_dec_out = tmpfile(); fwrite(comp_data, 1, comp_file_sz, f_corrupt); fseek(f_corrupt, 0, SEEK_SET); free(comp_data); zxc_decompress_opts_t _sdo55 = {.n_threads = 1, .checksum_enabled = 1}; int64_t r = zxc_stream_decompress(f_corrupt, f_dec_out, &_sdo55); fclose(f_corrupt); fclose(f_dec_out); free(src); if (r >= 0) { printf(" [FAIL] corrupt global checksum: expected < 0, got %lld\n", (long long)r); return 0; } } printf(" [PASS] zxc_stream_decompress corrupt checksum -> negative\n"); // 6. Stream decompress truncated file (missing EOF + footer) { const size_t src_sz = 4096; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); FILE* f_comp_in = tmpfile(); FILE* f_comp_out = tmpfile(); fwrite(src, 1, src_sz, f_comp_in); fseek(f_comp_in, 0, SEEK_SET); zxc_compress_opts_t _sco56 = {.n_threads = 1, .level = 3, .checksum_enabled = 0}; int64_t comp_sz = zxc_stream_compress(f_comp_in, f_comp_out, &_sco56); fclose(f_comp_in); free(src); if (comp_sz <= 0) { printf(" [SKIP] stream compress failed\n"); fclose(f_comp_out); return 0; } fseek(f_comp_out, 0, SEEK_END); const long comp_file_sz = ftell(f_comp_out); // Truncate: remove the EOF block header + footer const long trunc_sz = comp_file_sz - (ZXC_BLOCK_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE); uint8_t* comp_data = malloc(trunc_sz); fseek(f_comp_out, 0, SEEK_SET); if (fread(comp_data, 1, trunc_sz, f_comp_out) != (size_t)trunc_sz) { printf(" [FAIL] fread failed\n"); fclose(f_comp_out); free(comp_data); return 0; } fclose(f_comp_out); FILE* f_corrupt = tmpfile(); FILE* f_dec_out = tmpfile(); fwrite(comp_data, 1, trunc_sz, f_corrupt); fseek(f_corrupt, 0, SEEK_SET); free(comp_data); zxc_decompress_opts_t _sdo57 = {.n_threads = 1, .checksum_enabled = 0}; int64_t r = zxc_stream_decompress(f_corrupt, f_dec_out, &_sdo57); fclose(f_corrupt); fclose(f_dec_out); // Should fail: missing EOF/footer means io_error or bad read if (r >= 0) { printf(" [FAIL] truncated stream: expected < 0, got %lld\n", (long long)r); return 0; } } printf(" [PASS] zxc_stream_decompress truncated -> negative\n"); // 7. Stream decompress with mid-block body truncation { const size_t src_sz = 64 * 1024; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); FILE* f_comp_in = tmpfile(); FILE* f_comp_out = tmpfile(); fwrite(src, 1, src_sz, f_comp_in); fseek(f_comp_in, 0, SEEK_SET); zxc_compress_opts_t sco_mb = {.n_threads = 1, .level = 3, .checksum_enabled = 0}; int64_t comp_sz = zxc_stream_compress(f_comp_in, f_comp_out, &sco_mb); fclose(f_comp_in); free(src); if (comp_sz <= 0) { printf(" [SKIP] stream compress failed\n"); fclose(f_comp_out); return 0; } fseek(f_comp_out, 0, SEEK_END); const long comp_file_sz = ftell(f_comp_out); // Truncate mid-block: keep header + first block header + partial body const long trunc_sz = ZXC_FILE_HEADER_SIZE + ZXC_BLOCK_HEADER_SIZE + 16; if (trunc_sz < comp_file_sz) { uint8_t* comp_data = malloc(trunc_sz); fseek(f_comp_out, 0, SEEK_SET); if (fread(comp_data, 1, trunc_sz, f_comp_out) == (size_t)trunc_sz) { FILE* f_trunc = tmpfile(); FILE* f_dec_out = tmpfile(); fwrite(comp_data, 1, trunc_sz, f_trunc); fseek(f_trunc, 0, SEEK_SET); zxc_decompress_opts_t sdo_mb = {.n_threads = 1, .checksum_enabled = 0}; int64_t r = zxc_stream_decompress(f_trunc, f_dec_out, &sdo_mb); fclose(f_trunc); fclose(f_dec_out); if (r >= 0) { printf(" [FAIL] mid-block truncated: expected < 0, got %lld\n", (long long)r); free(comp_data); fclose(f_comp_out); return 0; } } free(comp_data); } fclose(f_comp_out); } printf(" [PASS] zxc_stream_decompress mid-block truncated -> negative\n"); // 8. Streaming fwrite error: compress real data, then decompress to a read-only file { const size_t src_sz = 64 * 1024; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); FILE* f_comp_in = tmpfile(); FILE* f_comp_out = tmpfile(); fwrite(src, 1, src_sz, f_comp_in); fseek(f_comp_in, 0, SEEK_SET); free(src); zxc_compress_opts_t sco_io = {.n_threads = 1, .level = 1, .checksum_enabled = 0}; int64_t comp_sz = zxc_stream_compress(f_comp_in, f_comp_out, &sco_io); fclose(f_comp_in); if (comp_sz <= 0) { printf(" [SKIP] compress failed\n"); fclose(f_comp_out); return 0; } fseek(f_comp_out, 0, SEEK_SET); // Open a read-only file as the output: fwrite will fail const char* ro_file = "zxc_test_stream_readonly.tmp"; FILE* f_ro = create_restricted_file(ro_file); if (f_ro) fclose(f_ro); FILE* f_bad_out = fopen(ro_file, "rb"); if (f_bad_out) { zxc_decompress_opts_t sdo_io = {.n_threads = 1, .checksum_enabled = 0}; int64_t r = zxc_stream_decompress(f_comp_out, f_bad_out, &sdo_io); fclose(f_bad_out); if (r >= 0) { printf(" [FAIL] fwrite error: expected < 0, got %lld\n", (long long)r); fclose(f_comp_out); remove(ro_file); return 0; } } fclose(f_comp_out); remove(ro_file); } printf(" [PASS] zxc_stream_decompress fwrite error -> negative\n"); // 9. Multi-threaded streaming I/O failure (writer fwrite error with multiple workers) { const size_t src_sz = 256 * 1024; uint8_t* src = malloc(src_sz); gen_lz_data(src, src_sz); FILE* f_comp_in = tmpfile(); FILE* f_comp_out = tmpfile(); fwrite(src, 1, src_sz, f_comp_in); fseek(f_comp_in, 0, SEEK_SET); free(src); zxc_compress_opts_t sco_mt = {.n_threads = 4, .level = 1, .checksum_enabled = 0}; int64_t comp_sz = zxc_stream_compress(f_comp_in, f_comp_out, &sco_mt); fclose(f_comp_in); if (comp_sz <= 0) { printf(" [SKIP] mt compress failed\n"); fclose(f_comp_out); return 0; } fseek(f_comp_out, 0, SEEK_SET); const char* ro_file2 = "zxc_test_stream_mt_readonly.tmp"; FILE* f_ro2 = create_restricted_file(ro_file2); if (f_ro2) fclose(f_ro2); FILE* f_bad_out2 = fopen(ro_file2, "rb"); if (f_bad_out2) { zxc_decompress_opts_t sdo_mt = {.n_threads = 4, .checksum_enabled = 0}; int64_t r = zxc_stream_decompress(f_comp_out, f_bad_out2, &sdo_mt); fclose(f_bad_out2); if (r >= 0) { printf(" [FAIL] mt fwrite error: expected < 0, got %lld\n", (long long)r); fclose(f_comp_out); remove(ro_file2); return 0; } } fclose(f_comp_out); remove(ro_file2); } printf(" [PASS] zxc_stream_decompress mt fwrite error -> negative\n"); printf("PASS\n\n"); return 1; } // Tests the buffer API scratch buffer (work_buf) used to safely absorb // zxc_copy32 wild-copy overshoot during decompression. int test_buffer_api_scratch_buf() { printf("=== TEST: Unit - Buffer API Scratch Buffer (work_buf) ===\n"); // 1. Small data roundtrip (177 bytes) { const size_t sz = 177; uint8_t src[177]; gen_lz_data(src, sz); const size_t comp_cap = (size_t)zxc_compress_bound(sz); uint8_t* comp = malloc(comp_cap); zxc_compress_opts_t _co58 = {.level = 3, .checksum_enabled = 0}; const int64_t comp_sz = zxc_compress(src, sz, comp, comp_cap, &_co58); if (comp_sz <= 0) { printf(" [FAIL] compress 177B\n"); free(comp); return 0; } uint8_t dec[177]; zxc_decompress_opts_t _do59 = {.checksum_enabled = 0}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, dec, sz, &_do59); if (dec_sz != (int64_t)sz || memcmp(src, dec, sz) != 0) { printf(" [FAIL] roundtrip 177B\n"); free(comp); return 0; } free(comp); printf(" [PASS] small data roundtrip (177 bytes)\n"); } // 2. Exact-fit destination (dst_capacity == decompressed size, no slack) { const size_t sz = 1024; uint8_t* src = malloc(sz); gen_lz_data(src, sz); const size_t comp_cap = (size_t)zxc_compress_bound(sz); uint8_t* comp = malloc(comp_cap); zxc_compress_opts_t _co60 = {.level = 1, .checksum_enabled = 1}; const int64_t comp_sz = zxc_compress(src, sz, comp, comp_cap, &_co60); if (comp_sz <= 0) { printf(" [FAIL] compress 1KB\n"); free(src); free(comp); return 0; } uint8_t* dec = malloc(sz); // exactly sz, no extra room zxc_decompress_opts_t _do61 = {.checksum_enabled = 1}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, dec, sz, &_do61); if (dec_sz != (int64_t)sz || memcmp(src, dec, sz) != 0) { printf(" [FAIL] exact-fit 1KB\n"); free(src); free(comp); free(dec); return 0; } free(src); free(comp); free(dec); printf(" [PASS] exact-fit destination (1KB)\n"); } // 3. Tiny data (1 byte) { const uint8_t src = 0x42; const size_t comp_cap = (size_t)zxc_compress_bound(1); uint8_t* comp = malloc(comp_cap); zxc_compress_opts_t _co62 = {.level = 1, .checksum_enabled = 0}; const int64_t comp_sz = zxc_compress(&src, 1, comp, comp_cap, &_co62); if (comp_sz <= 0) { printf(" [FAIL] compress 1B\n"); free(comp); return 0; } uint8_t dec = 0; zxc_decompress_opts_t _do63 = {.checksum_enabled = 0}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, &dec, 1, &_do63); if (dec_sz != 1 || dec != 0x42) { printf(" [FAIL] roundtrip 1B\n"); free(comp); return 0; } free(comp); printf(" [PASS] tiny data roundtrip (1 byte)\n"); } // 4. Malformed input must not crash (safe error return) { uint8_t garbage[64]; for (int i = 0; i < 64; i++) garbage[i] = (uint8_t)(i * 37); uint8_t out[256]; zxc_decompress_opts_t _do64 = {.checksum_enabled = 0}; const int64_t r = zxc_decompress(garbage, sizeof(garbage), out, sizeof(out), &_do64); if (r >= 0) { printf(" [FAIL] malformed input should return < 0\n"); return 0; } printf(" [PASS] malformed input -> error %lld (no crash)\n", (long long)r); } // 5. Destination too small { const size_t sz = 512; uint8_t* src = malloc(sz); gen_lz_data(src, sz); const size_t comp_cap = (size_t)zxc_compress_bound(sz); uint8_t* comp = malloc(comp_cap); zxc_compress_opts_t _co65 = {.level = 1, .checksum_enabled = 0}; const int64_t comp_sz = zxc_compress(src, sz, comp, comp_cap, &_co65); if (comp_sz <= 0) { printf(" [FAIL] compress 512B\n"); free(src); free(comp); return 0; } uint8_t tiny_dst[8]; zxc_decompress_opts_t _do66 = {.checksum_enabled = 0}; const int64_t r = zxc_decompress(comp, (size_t)comp_sz, tiny_dst, sizeof(tiny_dst), &_do66); if (r >= 0) { printf(" [FAIL] dst too small should return < 0\n"); free(src); free(comp); return 0; } free(src); free(comp); printf(" [PASS] zxc_decompress dst too small -> negative\n"); } printf("PASS\n\n"); return 1; } // Tests that the two decompression paths in zxc_decompress() produce // identical results: // - Fast path: rem_cap >= runtime_chunk_size + ZXC_PAD_SIZE // -> decompress directly into dst (enough padding for wild copies). // - Safe path: rem_cap < runtime_chunk_size + ZXC_PAD_SIZE // -> decompress into bounce buffer (work_buf), then memcpy exact result. // int test_decompress_fast_vs_safe_path() { printf("=== TEST: Unit - Decompress Fast Path vs Safe Path ===\n"); // Use a multi-block input: ZXC_BLOCK_SIZE_DEFAULT + extra so we get at least 2 blocks. // Block size = 256KB (ZXC_BLOCK_SIZE_DEFAULT). Second block is small. const size_t src_sz = ZXC_BLOCK_SIZE_DEFAULT + 4096; // 256KB + 4KB -> 2 blocks uint8_t* src = malloc(src_sz); if (!src) return 0; gen_lz_data(src, src_sz); const size_t comp_cap = (size_t)zxc_compress_bound(src_sz); uint8_t* comp = malloc(comp_cap); zxc_compress_opts_t _co67 = {.level = 3, .checksum_enabled = 1}; const int64_t comp_sz = zxc_compress(src, src_sz, comp, comp_cap, &_co67); if (comp_sz <= 0) { printf(" [FAIL] compression failed\n"); free(src); free(comp); return 0; } // ----- Sub-test 1: Fast path ----- // Provide a very large dst buffer so all chunks decompress directly into // dst (rem_cap >= runtime_chunk_size + ZXC_PAD_SIZE at every iteration). { const size_t big_cap = src_sz + ZXC_BLOCK_SIZE_DEFAULT; // way more than enough uint8_t* dst = malloc(big_cap); zxc_decompress_opts_t _do68 = {.checksum_enabled = 1}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, dst, big_cap, &_do68); if (dec_sz != (int64_t)src_sz) { printf(" [FAIL] fast path size: expected %zu, got %lld\n", src_sz, (long long)dec_sz); free(dst); free(src); free(comp); return 0; } if (memcmp(src, dst, src_sz) != 0) { printf(" [FAIL] fast path content mismatch\n"); free(dst); free(src); free(comp); return 0; } free(dst); printf(" [PASS] fast path (oversized dst)\n"); } // ----- Sub-test 2: Safe path (exact-fit) ----- // Provide dst_capacity == src_sz exactly. After the first 256KB block is // written, rem_cap for the second block (4KB) is exactly 4KB which is // < runtime_chunk_size (256KB) + ZXC_PAD_SIZE (32). This forces the // safe path (bounce buffer) for the second block. { uint8_t* dst = malloc(src_sz); // no slack at all zxc_decompress_opts_t _do69 = {.checksum_enabled = 1}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, dst, src_sz, &_do69); if (dec_sz != (int64_t)src_sz) { printf(" [FAIL] safe path size: expected %zu, got %lld\n", src_sz, (long long)dec_sz); free(dst); free(src); free(comp); return 0; } if (memcmp(src, dst, src_sz) != 0) { printf(" [FAIL] safe path content mismatch\n"); free(dst); free(src); free(comp); return 0; } free(dst); printf(" [PASS] safe path (exact-fit dst)\n"); } // ----- Sub-test 3: Boundary ----- // dst_capacity = src_sz + ZXC_PAD_SIZE - 1 (just below the fast path // threshold for the LAST chunk). The last chunk should still fall into // the safe path here. { const size_t tight_cap = src_sz + ZXC_PAD_SIZE - 1; uint8_t* dst = malloc(tight_cap); zxc_decompress_opts_t _do70 = {.checksum_enabled = 1}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, dst, tight_cap, &_do70); if (dec_sz != (int64_t)src_sz) { printf(" [FAIL] boundary size: expected %zu, got %lld\n", src_sz, (long long)dec_sz); free(dst); free(src); free(comp); return 0; } if (memcmp(src, dst, src_sz) != 0) { printf(" [FAIL] boundary content mismatch\n"); free(dst); free(src); free(comp); return 0; } free(dst); printf(" [PASS] boundary (dst = src_sz + PAD - 1)\n"); } // ----- Sub-test 4: Safe path with dst too small ----- // The safe path detects that the decompressed chunk doesn't fit and // returns ZXC_ERROR_DST_TOO_SMALL (covers the res > rem_cap guard). { const size_t tiny_cap = ZXC_BLOCK_SIZE_DEFAULT / 2; // Enough for half a block uint8_t* dst = malloc(tiny_cap); zxc_decompress_opts_t _do71 = {.checksum_enabled = 0}; const int64_t dec_sz = zxc_decompress(comp, (size_t)comp_sz, dst, tiny_cap, &_do71); if (dec_sz >= 0) { printf(" [FAIL] safe path dst-too-small should fail, got %lld\n", (long long)dec_sz); free(dst); free(src); free(comp); return 0; } free(dst); printf(" [PASS] safe path dst too small -> negative\n"); } free(src); free(comp); printf("PASS\n\n"); return 1; } int test_block_api() { printf("=== TEST: Unit - Block API (zxc_compress_block/zxc_decompress_block) ===\n"); const size_t src_size = 128 * 1024; // 128 KB int result = 0; /* All resources initialized to NULL for centralized cleanup. */ uint8_t* src = NULL; uint8_t* compressed = NULL; uint8_t* decompressed = NULL; zxc_cctx* cctx = NULL; zxc_dctx* dctx = NULL; src = malloc(src_size); if (!src) goto cleanup; gen_lz_data(src, src_size); // 1. zxc_compress_block_bound const uint64_t block_bound = zxc_compress_block_bound(src_size); const uint64_t file_bound = zxc_compress_bound(src_size); if (block_bound == 0 || block_bound >= file_bound) { printf("Failed: block_bound=%llu should be >0 and < file_bound=%llu\n", (unsigned long long)block_bound, (unsigned long long)file_bound); goto cleanup; } printf(" [PASS] block_bound=%llu < file_bound=%llu\n", (unsigned long long)block_bound, (unsigned long long)file_bound); // 2. Allocate buffers and contexts compressed = malloc((size_t)block_bound); decompressed = malloc(src_size); cctx = zxc_create_cctx(NULL); dctx = zxc_create_dctx(); if (!compressed || !decompressed || !cctx || !dctx) { printf("Failed: Allocation failed\n"); goto cleanup; } // 3. Compress block (no checksum) zxc_compress_opts_t copts = {.level = 3, .checksum_enabled = 0}; int64_t csize = zxc_compress_block(cctx, src, src_size, compressed, (size_t)block_bound, &copts); if (csize <= 0) { printf("Failed: zxc_compress_block returned %lld\n", (long long)csize); goto cleanup; } printf(" [PASS] Compress block: %zu -> %lld bytes\n", src_size, (long long)csize); // 4. Decompress block (no checksum) zxc_decompress_opts_t dopts = {.checksum_enabled = 0}; int64_t dsize = zxc_decompress_block(dctx, compressed, (size_t)csize, decompressed, src_size, &dopts); if (dsize != (int64_t)src_size) { printf("Failed: zxc_decompress_block returned %lld, expected %zu\n", (long long)dsize, src_size); goto cleanup; } if (memcmp(src, decompressed, src_size) != 0) { printf("Failed: Content mismatch after block decompression\n"); goto cleanup; } printf(" [PASS] Decompress block: roundtrip OK (no checksum)\n"); // 5. With checksum enabled copts.checksum_enabled = 1; csize = zxc_compress_block(cctx, src, src_size, compressed, (size_t)block_bound, &copts); if (csize <= 0) { printf("Failed: zxc_compress_block with checksum returned %lld\n", (long long)csize); goto cleanup; } dopts.checksum_enabled = 1; dsize = zxc_decompress_block(dctx, compressed, (size_t)csize, decompressed, src_size, &dopts); if (dsize != (int64_t)src_size || memcmp(src, decompressed, src_size) != 0) { printf("Failed: Roundtrip with checksum failed\n"); goto cleanup; } printf(" [PASS] Decompress block: roundtrip OK (with checksum)\n"); // 6. Error cases if (zxc_compress_block(NULL, src, src_size, compressed, (size_t)block_bound, &copts) >= 0) { printf("Failed: Should fail with NULL cctx\n"); goto cleanup; } if (zxc_compress_block(cctx, NULL, src_size, compressed, (size_t)block_bound, &copts) >= 0) { printf("Failed: Should fail with NULL src\n"); goto cleanup; } if (zxc_decompress_block(NULL, compressed, (size_t)csize, decompressed, src_size, &dopts) >= 0) { printf("Failed: Should fail with NULL dctx\n"); goto cleanup; } printf(" [PASS] Error cases handled correctly\n"); // 7. Context reuse across multiple blocks for (int i = 0; i < 3; i++) { gen_lz_data(src, src_size); src[0] = (uint8_t)i; copts.checksum_enabled = 0; csize = zxc_compress_block(cctx, src, src_size, compressed, (size_t)block_bound, &copts); if (csize <= 0) { printf("Failed: Reuse iteration %d compress failed\n", i); goto cleanup; } dopts.checksum_enabled = 0; dsize = zxc_decompress_block(dctx, compressed, (size_t)csize, decompressed, src_size, &dopts); if (dsize != (int64_t)src_size || memcmp(src, decompressed, src_size) != 0) { printf("Failed: Reuse iteration %d roundtrip failed\n", i); goto cleanup; } } printf(" [PASS] Context reuse: 3 independent blocks OK\n"); printf("PASS\n\n"); result = 1; cleanup: zxc_free_cctx(cctx); /* safe with NULL */ zxc_free_dctx(dctx); /* safe with NULL */ free(compressed); free(decompressed); free(src); return result; } int test_opaque_context_api() { printf("=== TEST: Opaque Context API (zxc_create_cctx / zxc_create_dctx) ===\n"); /* 1. NULL context -> ZXC_ERROR_NULL_INPUT */ { uint8_t d[64]; zxc_compress_opts_t co = {.level = 3, .checksum_enabled = 0}; if (zxc_compress_cctx(NULL, d, sizeof(d), d, sizeof(d), &co) != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] compress_cctx NULL ctx\n"); return 0; } zxc_decompress_opts_t do_ = {.checksum_enabled = 0}; if (zxc_decompress_dctx(NULL, d, sizeof(d), d, sizeof(d), &do_) != ZXC_ERROR_NULL_INPUT) { printf(" [FAIL] decompress_dctx NULL ctx\n"); return 0; } printf(" [PASS] NULL context -> ZXC_ERROR_NULL_INPUT\n"); } /* 2. Create with eager init, multi-call reuse, free */ zxc_compress_opts_t create_opts = {.level = 3, .checksum_enabled = 0}; zxc_cctx* cctx = zxc_create_cctx(&create_opts); zxc_dctx* dctx = zxc_create_dctx(); if (!cctx || !dctx) { printf(" [FAIL] create returned NULL\n"); zxc_free_cctx(cctx); zxc_free_dctx(dctx); return 0; } const size_t src_sz = 8192; uint8_t* src = malloc(src_sz); const size_t comp_cap = (size_t)zxc_compress_bound(src_sz); uint8_t* comp = malloc(comp_cap); uint8_t* dec = malloc(src_sz); /* 3. Three calls with the SAME cctx: level 1, 3, 5 */ for (int lvl = 1; lvl <= 5; lvl += 2) { gen_lz_data(src, src_sz); zxc_compress_opts_t co = {.level = lvl, .checksum_enabled = (lvl == 3)}; const int64_t csz = zxc_compress_cctx(cctx, src, src_sz, comp, comp_cap, &co); if (csz <= 0) { printf(" [FAIL] compress_cctx level %d returned %lld\n", lvl, (long long)csz); goto fail; } zxc_decompress_opts_t do_ = {.checksum_enabled = (lvl == 3)}; const int64_t dsz = zxc_decompress_dctx(dctx, comp, (size_t)csz, dec, src_sz, &do_); if (dsz != (int64_t)src_sz || memcmp(src, dec, src_sz) != 0) { printf(" [FAIL] roundtrip level %d (dsz=%lld)\n", lvl, (long long)dsz); goto fail; } } printf(" [PASS] Multi-call reuse (level 1, 3, 5)\n"); /* 4. Free is safe to call multiple times / on NULL */ zxc_free_cctx(cctx); cctx = NULL; zxc_free_cctx(NULL); /* no-op */ zxc_free_dctx(dctx); dctx = NULL; zxc_free_dctx(NULL); printf(" [PASS] Free + double-free + NULL safety\n"); free(src); free(comp); free(dec); printf("PASS\n\n"); return 1; fail: zxc_free_cctx(cctx); zxc_free_dctx(dctx); free(src); free(comp); free(dec); return 0; } int test_library_info_api() { printf("=== TEST: Unit - Library Info API (zxc_min/max/default_level, zxc_version_string) ===\n"); // 1. Min level must match compile-time constant int min = zxc_min_level(); if (min != ZXC_LEVEL_FASTEST) { printf("Failed: zxc_min_level() returned %d, expected %d\n", min, ZXC_LEVEL_FASTEST); return 0; } printf(" [PASS] zxc_min_level() == %d\n", min); // 2. Max level must match compile-time constant int max = zxc_max_level(); if (max != ZXC_LEVEL_COMPACT) { printf("Failed: zxc_max_level() returned %d, expected %d\n", max, ZXC_LEVEL_COMPACT); return 0; } printf(" [PASS] zxc_max_level() == %d\n", max); // 3. Default level must be within [min, max] int def = zxc_default_level(); if (def < min || def > max) { printf("Failed: zxc_default_level() returned %d, not in [%d, %d]\n", def, min, max); return 0; } if (def != ZXC_LEVEL_DEFAULT) { printf("Failed: zxc_default_level() returned %d, expected %d\n", def, ZXC_LEVEL_DEFAULT); return 0; } printf(" [PASS] zxc_default_level() == %d\n", def); // 4. Version string must be non-NULL and match compile-time version const char* ver = zxc_version_string(); if (!ver) { printf("Failed: zxc_version_string() returned NULL\n"); return 0; } if (strcmp(ver, ZXC_LIB_VERSION_STR) != 0) { printf("Failed: zxc_version_string() returned \"%s\", expected \"%s\"\n", ver, ZXC_LIB_VERSION_STR); return 0; } printf(" [PASS] zxc_version_string() == \"%s\"\n", ver); printf("PASS\n\n"); return 1; } /* ========================================================================= */ /* Seekable Tests */ /* ========================================================================= */ static void fill_seek_data(uint8_t* buf, size_t size, uint8_t seed) { for (size_t i = 0; i < size; i++) { buf[i] = (uint8_t)(seed + (i * 17) + (i >> 8)); } } int test_seekable_table_sizes() { printf("=== TEST: Seekable - Table Sizes ===\n"); /* block_header(8) + 10*4 = 48 */ if (zxc_seek_table_size(10) != 48) { printf("Failed: size for 10 blocks\n"); return 0; } /* block_header(8) + 0 = 8 */ if (zxc_seek_table_size(0) != 8) { printf("Failed: zero blocks size\n"); return 0; } printf("PASS\n\n"); return 1; } int test_seekable_table_write() { printf("=== TEST: Seekable - Table Write/Validate ===\n"); const uint32_t comp[] = {100, 200, 150}; const size_t sz = zxc_seek_table_size(3); uint8_t* buf = malloc(sz); if (!buf) return 0; const int64_t written = zxc_write_seek_table(buf, sz, comp, 3); if (written != (int64_t)sz) { printf("Failed: write size mismatch\n"); free(buf); return 0; } /* Validate block_type == SEK in the block header */ if (buf[0] != ZXC_BLOCK_SEK) { printf("Failed: bad block_type (%u)\n", buf[0]); free(buf); return 0; } /* Validate first comp_size entry (after 8-byte block header) */ if (zxc_le32(buf + 8) != 100) { printf("Failed: bad first comp_size\n"); free(buf); return 0; } free(buf); printf("PASS\n\n"); return 1; } int test_seekable_roundtrip() { printf("=== TEST: Seekable - Compress/Decompress Roundtrip ===\n"); const size_t SRC_SIZE = 256 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 42); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } zxc_compress_opts_t opts = {.level = ZXC_LEVEL_DEFAULT, .block_size = 64 * 1024, .checksum_enabled = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); free(dec); return 0; } /* Sub-test A: single block (data < block_size) roundtrip */ { const size_t SMALL = 60 * 1024; /* fits in one 64KB block */ memset(dec, 0, SMALL); zxc_compress_opts_t opts_a = {.level = ZXC_LEVEL_DEFAULT, .block_size = 64 * 1024, .checksum_enabled = 1, .seekable = 0}; const int64_t a_csize = zxc_compress(src, SMALL, dst, dst_cap, &opts_a); if (a_csize <= 0) { printf("Failed: single-block 64KB compress (%lld)\n", (long long)a_csize); free(src); free(dst); free(dec); return 0; } zxc_decompress_opts_t ad = {.checksum_enabled = 1}; const int64_t a_dsize = zxc_decompress(dst, (size_t)a_csize, dec, SMALL, &ad); if (a_dsize != (int64_t)SMALL || memcmp(src, dec, SMALL) != 0) { printf("Failed: single-block 64KB roundtrip (dsize=%lld)\n", (long long)a_dsize); if (a_dsize == (int64_t)SMALL) { for (size_t i = 0; i < SMALL; i++) { if (src[i] != dec[i]) { printf(" first diff at byte %zu: src=0x%02x dec=0x%02x\n", i, src[i], dec[i]); break; } } } free(src); free(dst); free(dec); return 0; } printf(" sub-test A (single block, 60KB): OK\n"); } /* Sub-test B: exactly 2 blocks (128KB with 64KB block_size) */ { const size_t TWO = 128 * 1024; memset(dec, 0, TWO); zxc_compress_opts_t opts_b = {.level = ZXC_LEVEL_DEFAULT, .block_size = 64 * 1024, .checksum_enabled = 1, .seekable = 0}; const int64_t b_csize = zxc_compress(src, TWO, dst, dst_cap, &opts_b); if (b_csize <= 0) { printf("Failed: 2-block 64KB compress (%lld)\n", (long long)b_csize); free(src); free(dst); free(dec); return 0; } zxc_decompress_opts_t bd = {.checksum_enabled = 1}; const int64_t b_dsize = zxc_decompress(dst, (size_t)b_csize, dec, TWO, &bd); if (b_dsize != (int64_t)TWO || memcmp(src, dec, TWO) != 0) { printf("Failed: 2-block 64KB roundtrip (dsize=%lld)\n", (long long)b_dsize); if (b_dsize == (int64_t)TWO) { for (size_t i = 0; i < TWO; i++) { if (src[i] != dec[i]) { printf(" first diff at byte %zu: src=0x%02x dec=0x%02x\n", i, src[i], dec[i]); break; } } } free(src); free(dst); free(dec); return 0; } printf(" sub-test B (2 blocks, 128KB): OK\n"); } /* Sub-test C: full 256KB (4 blocks x 64KB) */ { memset(dec, 0, SRC_SIZE); zxc_compress_opts_t opts_ns = {.level = ZXC_LEVEL_DEFAULT, .block_size = 64 * 1024, .checksum_enabled = 1, .seekable = 0}; const int64_t ns_csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts_ns); if (ns_csize <= 0) { printf("Failed: non-seekable compress (%lld)\n", (long long)ns_csize); free(src); free(dst); free(dec); return 0; } zxc_decompress_opts_t nd = {.checksum_enabled = 1}; const int64_t ns_dsize = zxc_decompress(dst, (size_t)ns_csize, dec, SRC_SIZE, &nd); if (ns_dsize != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: non-seekable 64KB block_size roundtrip (dsize=%lld)\n", (long long)ns_dsize); if (ns_dsize == (int64_t)SRC_SIZE) { for (size_t i = 0; i < SRC_SIZE; i++) { if (src[i] != dec[i]) { printf(" first diff at byte %zu: src=0x%02x dec=0x%02x\n", i, src[i], dec[i]); break; } } } free(src); free(dst); free(dec); return 0; } printf(" sub-test C (4 blocks, 256KB): OK\n"); } /* Re-compress with seekable=1 for the actual test */ memset(dec, 0, SRC_SIZE); const int64_t csize2 = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize2 <= 0) { printf("Failed: seekable re-compress (%lld)\n", (long long)csize2); free(src); free(dst); free(dec); return 0; } /* Full decompression with standard API (backward compatibility) */ zxc_decompress_opts_t dopts = {.checksum_enabled = 1}; const int64_t dsize = zxc_decompress(dst, (size_t)csize2, dec, SRC_SIZE, &dopts); if (dsize != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: decompress mismatch (csize=%lld dsize=%lld expected=%zu)\n", (long long)csize2, (long long)dsize, SRC_SIZE); if (dsize == (int64_t)SRC_SIZE) { for (size_t i = 0; i < SRC_SIZE; i++) { if (src[i] != dec[i]) { printf(" first diff at byte %zu: src=0x%02x dec=0x%02x\n", i, src[i], dec[i]); break; } } } free(src); free(dst); free(dec); return 0; } free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } int test_seekable_open_query() { printf("=== TEST: Seekable - Open and Query ===\n"); const size_t SRC_SIZE = 200 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 99); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 2, .block_size = 64 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } const uint32_t nb = zxc_seekable_get_num_blocks(s); if (nb < 3) { printf("Failed: expected >= 3 blocks, got %u\n", nb); zxc_seekable_free(s); free(src); free(dst); return 0; } const uint64_t total = zxc_seekable_get_decompressed_size(s); if (total != SRC_SIZE) { printf("Failed: decomp size\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } uint64_t sum = 0; for (uint32_t i = 0; i < nb; i++) { sum += zxc_seekable_get_block_decomp_size(s, i); if (zxc_seekable_get_block_comp_size(s, i) == 0) { printf("Failed: zero comp size\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } } if (sum != SRC_SIZE) { printf("Failed: block sizes sum\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } int test_seekable_random_access() { printf("=== TEST: Seekable - Random Access ===\n"); const size_t SRC_SIZE = 300 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 77); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 3, .block_size = 64 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } /* First 1000 bytes */ uint8_t out1[1000]; int64_t r = zxc_seekable_decompress_range(s, out1, 1000, 0, 1000); if (r != 1000 || memcmp(src, out1, 1000) != 0) { printf("Failed: first 1000 bytes (r=%lld)\n", (long long)r); if (r == 1000) { for (int i = 0; i < 1000; i++) { if (src[i] != out1[i]) { printf(" first diff at byte %d: src=0x%02x out=0x%02x\n", i, src[i], out1[i]); break; } } } zxc_seekable_free(s); free(src); free(dst); return 0; } /* Middle range spanning multiple blocks */ const uint64_t off = 100 * 1024; const size_t len = 80 * 1024; uint8_t* out2 = malloc(len); if (!out2) { zxc_seekable_free(s); free(src); free(dst); return 0; } r = zxc_seekable_decompress_range(s, out2, len, off, len); if (r != (int64_t)len || memcmp(src + off, out2, len) != 0) { printf("Failed: mid-range\n"); free(out2); zxc_seekable_free(s); free(src); free(dst); return 0; } free(out2); /* Last bytes */ uint8_t out3[512]; r = zxc_seekable_decompress_range(s, out3, 512, SRC_SIZE - 512, 512); if (r != 512 || memcmp(src + SRC_SIZE - 512, out3, 512) != 0) { printf("Failed: tail\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Entire file */ uint8_t* out4 = malloc(SRC_SIZE); if (!out4) { zxc_seekable_free(s); free(src); free(dst); return 0; } r = zxc_seekable_decompress_range(s, out4, SRC_SIZE, 0, SRC_SIZE); if (r != (int64_t)SRC_SIZE || memcmp(src, out4, SRC_SIZE) != 0) { printf("Failed: full range\n"); free(out4); zxc_seekable_free(s); free(src); free(dst); return 0; } free(out4); /* Zero length */ uint8_t dummy; r = zxc_seekable_decompress_range(s, &dummy, 1, 0, 0); if (r != 0) { printf("Failed: zero-length\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } int test_seekable_non_seekable_reject() { printf("=== TEST: Seekable - Non-Seekable Archive Rejected ===\n"); const size_t SRC_SIZE = 10000; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 11); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE); uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 1, .seekable = 0}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (s != NULL) { printf("Failed: expected NULL for non-seekable\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } free(src); free(dst); printf("PASS\n\n"); return 1; } int test_seekable_single_block() { printf("=== TEST: Seekable - Single Block ===\n"); const size_t SRC_SIZE = 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 55); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 256; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } if (zxc_seekable_get_num_blocks(s) != 1) { printf("Failed: expected 1 block\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } uint8_t out[100]; int64_t r = zxc_seekable_decompress_range(s, out, 100, 500, 100); if (r != 100 || memcmp(src + 500, out, 100) != 0) { printf("Failed: range data\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } int test_seekable_all_levels() { printf("=== TEST: Seekable - All Compression Levels ===\n"); const size_t SRC_SIZE = 128 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 33); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } for (int lvl = 1; lvl <= 5; lvl++) { zxc_compress_opts_t opts = {.level = lvl, .block_size = 32 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress level %d\n", lvl); free(src); free(dst); free(dec); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open level %d\n", lvl); free(src); free(dst); free(dec); return 0; } int64_t r = zxc_seekable_decompress_range(s, dec, SRC_SIZE, 0, SRC_SIZE); if (r != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: level %d data mismatch (r=%lld expected=%zu)\n", lvl, (long long)r, SRC_SIZE); if (r == (int64_t)SRC_SIZE) { for (size_t i = 0; i < SRC_SIZE; i++) { if (src[i] != dec[i]) { printf(" first diff at byte %zu: src=0x%02x dec=0x%02x\n", i, src[i], dec[i]); break; } } } zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } zxc_seekable_free(s); } free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } int test_seekable_many_blocks() { printf("=== TEST: Seekable - Many Small Blocks ===\n"); /* Use minimum block size (4KB) with 256KB data => 64 blocks. * This stresses the seekable block tracking array (dispatch lines 410-424) * and ensures the seek table handles high block counts correctly. */ const size_t SRC_SIZE = 256 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 77); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 4096; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } /* Compress with minimum block_size = 4096 */ zxc_compress_opts_t opts = {.level = ZXC_LEVEL_DEFAULT, .block_size = 4096, .checksum_enabled = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress (%lld)\n", (long long)csize); free(src); free(dst); free(dec); return 0; } /* Open and verify block count = 64 */ zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); free(dec); return 0; } const uint32_t n_blocks = zxc_seekable_get_num_blocks(s); if (n_blocks != 64) { printf("Failed: expected 64 blocks, got %u\n", n_blocks); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } /* Full decompress via seekable API */ int64_t r = zxc_seekable_decompress_range(s, dec, SRC_SIZE, 0, SRC_SIZE); if (r != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: full decompress mismatch (r=%lld)\n", (long long)r); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } /* Random access: read 100 bytes from the middle of block 32 */ const uint64_t mid_off = 32 * 4096 + 2000; uint8_t spot[100]; r = zxc_seekable_decompress_range(s, spot, 100, mid_off, 100); if (r != 100 || memcmp(src + mid_off, spot, 100) != 0) { printf("Failed: random access at offset %llu\n", (unsigned long long)mid_off); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } /* Cross-block read: span block boundary (last 50B of block 15 + first 50B of block 16) */ const uint64_t cross_off = 16 * 4096 - 50; uint8_t cross[100]; r = zxc_seekable_decompress_range(s, cross, 100, cross_off, 100); if (r != 100 || memcmp(src + cross_off, cross, 100) != 0) { printf("Failed: cross-block read at offset %llu\n", (unsigned long long)cross_off); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } zxc_seekable_free(s); free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } int test_seekable_open_file() { printf("=== TEST: Seekable - Open File ===\n"); /* Compress seekable data into a buffer, write to tmpfile, then open via file API */ const size_t SRC_SIZE = 128 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 99); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } zxc_compress_opts_t opts = {.level = ZXC_LEVEL_DEFAULT, .block_size = 32 * 1024, .checksum_enabled = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress (%lld)\n", (long long)csize); free(src); free(dst); free(dec); return 0; } /* Write compressed data to a temp file */ FILE* tf = tmpfile(); if (!tf) { printf("Failed: tmpfile\n"); free(src); free(dst); free(dec); return 0; } if (fwrite(dst, 1, (size_t)csize, tf) != (size_t)csize) { printf("Failed: fwrite\n"); fclose(tf); free(src); free(dst); free(dec); return 0; } fflush(tf); /* Open via file API */ zxc_seekable* s = zxc_seekable_open_file(tf); if (!s) { printf("Failed: zxc_seekable_open_file returned NULL\n"); fclose(tf); free(src); free(dst); free(dec); return 0; } /* Verify block count: 128KB / 32KB = 4 blocks */ const uint32_t n_blocks = zxc_seekable_get_num_blocks(s); if (n_blocks != 4) { printf("Failed: expected 4 blocks, got %u\n", n_blocks); zxc_seekable_free(s); fclose(tf); free(src); free(dst); free(dec); return 0; } /* Full decompress from file */ int64_t r = zxc_seekable_decompress_range(s, dec, SRC_SIZE, 0, SRC_SIZE); if (r != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: full decompress from file (r=%lld)\n", (long long)r); zxc_seekable_free(s); fclose(tf); free(src); free(dst); free(dec); return 0; } /* Random access from file: read 200 bytes spanning block boundary */ const uint64_t cross_off = 32 * 1024 - 100; /* last 100B of block 0 + first 100B of block 1 */ uint8_t cross[200]; r = zxc_seekable_decompress_range(s, cross, 200, cross_off, 200); if (r != 200 || memcmp(src + cross_off, cross, 200) != 0) { printf("Failed: cross-block read from file\n"); zxc_seekable_free(s); fclose(tf); free(src); free(dst); free(dec); return 0; } zxc_seekable_free(s); fclose(tf); /* NULL input rejection */ if (zxc_seekable_open_file(NULL) != NULL) { printf("Failed: NULL not rejected\n"); free(src); free(dst); free(dec); return 0; } free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } /* ========================================================================= */ /* Multi-Threaded Seekable Tests */ /* ========================================================================= */ int test_seekable_mt_roundtrip() { printf("=== TEST: Seekable MT - Roundtrip (4 threads) ===\n"); const size_t SRC_SIZE = 512 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 55); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } /* Compress with small blocks to create many blocks */ zxc_compress_opts_t opts = {.level = 3, .block_size = 64 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); free(dec); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); free(dec); return 0; } /* Full decompression with 4 threads */ int64_t r = zxc_seekable_decompress_range_mt(s, dec, SRC_SIZE, 0, SRC_SIZE, 4); if (r != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: MT decompress mismatch\n"); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } zxc_seekable_free(s); free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } int test_seekable_mt_single_block() { printf("=== TEST: Seekable MT - Single Block Fallback ===\n"); const size_t SRC_SIZE = 32 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 88); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } /* Compress with block_size >= SRC_SIZE => only 1 block */ zxc_compress_opts_t opts = {.level = 3, .block_size = 64 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); free(dec); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); free(dec); return 0; } /* Should fallback to ST since only 1 block */ int64_t r = zxc_seekable_decompress_range_mt(s, dec, SRC_SIZE, 0, SRC_SIZE, 4); if (r != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: single-block MT mismatch\n"); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } zxc_seekable_free(s); free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } int test_seekable_mt_random_access() { printf("=== TEST: Seekable MT - Random Access ===\n"); const size_t SRC_SIZE = 512 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 11); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 3, .block_size = 64 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } /* Mid-range spanning 3+ blocks with MT */ const uint64_t off = 100 * 1024; const size_t len = 200 * 1024; uint8_t* out = malloc(len); if (!out) { zxc_seekable_free(s); free(src); free(dst); return 0; } int64_t r = zxc_seekable_decompress_range_mt(s, out, len, off, len, 4); if (r != (int64_t)len || memcmp(src + off, out, len) != 0) { printf("Failed: MT random access mismatch\n"); free(out); zxc_seekable_free(s); free(src); free(dst); return 0; } free(out); zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } int test_seekable_mt_full_file() { printf("=== TEST: Seekable MT - Full File (auto threads) ===\n"); const size_t SRC_SIZE = 1024 * 1024; /* 1 MB */ uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 7); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); uint8_t* dec = malloc(SRC_SIZE); if (!dst || !dec) { free(src); free(dst); free(dec); return 0; } /* 16 blocks of 64K */ zxc_compress_opts_t opts = {.level = 3, .block_size = 64 * 1024, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); free(dec); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); free(dec); return 0; } /* Decompress with auto-detect threads (n_threads=0) */ int64_t r = zxc_seekable_decompress_range_mt(s, dec, SRC_SIZE, 0, SRC_SIZE, 0); if (r != (int64_t)SRC_SIZE || memcmp(src, dec, SRC_SIZE) != 0) { printf("Failed: MT full file mismatch\n"); zxc_seekable_free(s); free(src); free(dst); free(dec); return 0; } zxc_seekable_free(s); free(src); free(dst); free(dec); printf("PASS\n\n"); return 1; } /* Cross-boundary range: decompresses bytes that span exactly two blocks */ int test_seekable_cross_boundary() { printf("=== TEST: Seekable - Cross-Boundary Range ===\n"); const size_t BLK = 64 * 1024; const size_t SRC_SIZE = BLK * 4; /* 4 blocks */ uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 123); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 3, .block_size = BLK, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } /* Read 200 bytes starting 100 bytes before the block 0/1 boundary */ const uint64_t boundary = BLK; const uint64_t off = boundary - 100; const size_t len = 200; uint8_t out[200]; int64_t r = zxc_seekable_decompress_range(s, out, sizeof(out), off, len); if (r != (int64_t)len) { printf("Failed: cross-boundary range returned %lld\n", (long long)r); zxc_seekable_free(s); free(src); free(dst); return 0; } if (memcmp(out, src + off, len) != 0) { printf("Failed: cross-boundary data mismatch\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Also test a range spanning 3 blocks */ const uint64_t off3 = BLK * 2 - 100; const size_t len3 = BLK + 200; uint8_t* out3 = malloc(len3); if (!out3) { zxc_seekable_free(s); free(src); free(dst); return 0; } r = zxc_seekable_decompress_range(s, out3, len3, off3, len3); if (r != (int64_t)len3 || memcmp(out3, src + off3, len3) != 0) { printf("Failed: 3-block span mismatch\n"); free(out3); zxc_seekable_free(s); free(src); free(dst); return 0; } free(out3); zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } /* Open with truncated data should return NULL */ int test_seekable_truncated_input() { printf("=== TEST: Seekable - Truncated Input Rejected ===\n"); const size_t SRC_SIZE = 64 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 44); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 256; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } /* Truncate to half */ zxc_seekable* s = zxc_seekable_open(dst, (size_t)(csize / 2)); if (s != NULL) { printf("Failed: should reject truncated data\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Truncate to just header */ s = zxc_seekable_open(dst, 16); if (s != NULL) { printf("Failed: should reject header-only data\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Zero bytes */ s = zxc_seekable_open(dst, 0); if (s != NULL) { printf("Failed: should reject zero-length data\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } free(src); free(dst); printf("PASS\n\n"); return 1; } /* Corrupted SEK block: ensure no crash (no UB) */ int test_seekable_corrupted_sek() { printf("=== TEST: Seekable - Corrupted SEK Block ===\n"); const size_t SRC_SIZE = 64 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 66); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 256; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } /* Corrupt a byte in the SEK payload area (before footer) */ uint8_t* corrupt = malloc((size_t)csize); if (!corrupt) { free(src); free(dst); return 0; } memcpy(corrupt, dst, (size_t)csize); corrupt[csize - 14] ^= 0xFF; zxc_seekable* s = zxc_seekable_open(corrupt, (size_t)csize); /* May succeed or fail - just ensure no crash */ if (s) { uint8_t out[100]; (void)zxc_seekable_decompress_range(s, out, sizeof(out), 0, 100); zxc_seekable_free(s); } free(corrupt); free(src); free(dst); printf("PASS\n\n"); return 1; } /* Range beyond file end should return error */ int test_seekable_range_out_of_bounds() { printf("=== TEST: Seekable - Out-of-Bounds Range ===\n"); const size_t SRC_SIZE = 32 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 22); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 256; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } uint8_t out[256]; /* offset past EOF */ int64_t r = zxc_seekable_decompress_range(s, out, sizeof(out), SRC_SIZE + 100, 100); if (r > 0) { printf("Failed: should reject offset past EOF (got %lld)\n", (long long)r); zxc_seekable_free(s); free(src); free(dst); return 0; } /* offset valid but length extends past EOF */ r = zxc_seekable_decompress_range(s, out, sizeof(out), SRC_SIZE - 50, 200); if (r > 0) { printf("Failed: should reject range extending past EOF (got %lld)\n", (long long)r); zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } /* dst_capacity too small for requested range */ int test_seekable_dst_too_small() { printf("=== TEST: Seekable - Dst Too Small ===\n"); const size_t SRC_SIZE = 32 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 91); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 256; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = {.level = 1, .seekable = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } uint8_t out[10]; int64_t r = zxc_seekable_decompress_range(s, out, 10, 0, 1000); if (r > 0) { printf("Failed: should reject insufficient dst capacity (got %lld)\n", (long long)r); zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } /* Empty file with seekable=1 */ /* Empty file with seekable=1: buffer API rejects NULL src, verify graceful rejection. * Also verify via streaming API (which supports empty files). */ int test_seekable_empty_file() { printf("=== TEST: Seekable - Empty File ===\n"); const size_t dst_cap = (size_t)zxc_compress_bound(0) + 256; uint8_t* dst = malloc(dst_cap); if (!dst) return 0; /* Buffer API: NULL src with size 0 is rejected with ZXC_ERROR_NULL_INPUT */ zxc_compress_opts_t opts = {.level = 3, .seekable = 1}; const int64_t csize = zxc_compress(NULL, 0, dst, dst_cap, &opts); if (csize >= 0) { printf("Failed: expected NULL_INPUT rejection (got %lld)\n", (long long)csize); free(dst); return 0; } /* Streaming API: empty file via tmpfile() should work */ FILE* fin = tmpfile(); FILE* fout = tmpfile(); if (fin && fout) { int64_t stream_sz = zxc_stream_compress(fin, fout, &opts); if (stream_sz < 0) { printf("Failed: stream compress empty (got %lld)\n", (long long)stream_sz); fclose(fin); fclose(fout); free(dst); return 0; } /* Decompress the stream output */ rewind(fout); FILE* fdec = tmpfile(); if (fdec) { zxc_decompress_opts_t dopts = {.checksum_enabled = 0}; int64_t dsz = zxc_stream_decompress(fout, fdec, &dopts); if (dsz != 0) { printf("Failed: stream decompress empty should return 0 (got %lld)\n", (long long)dsz); fclose(fin); fclose(fout); fclose(fdec); free(dst); return 0; } fclose(fdec); } fclose(fin); fclose(fout); } free(dst); printf("PASS\n\n"); return 1; } /* Seekable without checksum (seekable=1, checksum_enabled=0) */ int test_seekable_no_checksum() { printf("=== TEST: Seekable - No Checksum ===\n"); const size_t SRC_SIZE = 256 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 31); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = { .level = 3, .block_size = 64 * 1024, .seekable = 1, .checksum_enabled = 0}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } uint8_t out[512]; const uint64_t off = 64 * 1024 + 100; int64_t r = zxc_seekable_decompress_range(s, out, sizeof(out), off, 512); if (r != 512 || memcmp(out, src + off, 512) != 0) { printf("Failed: no-checksum range mismatch\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Full decompress also works */ uint8_t* full = malloc(SRC_SIZE); if (!full) { zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_decompress_opts_t dopts = {.checksum_enabled = 0}; int64_t dsize = zxc_decompress(dst, (size_t)csize, full, SRC_SIZE, &dopts); if (dsize != (int64_t)SRC_SIZE || memcmp(src, full, SRC_SIZE) != 0) { printf("Failed: full decompress mismatch\n"); free(full); zxc_seekable_free(s); free(src); free(dst); return 0; } free(full); zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } /* Seekable with checksum (seekable=1, checksum_enabled=1) */ int test_seekable_with_checksum() { printf("=== TEST: Seekable - With Checksum ===\n"); const size_t SRC_SIZE = 256 * 1024; uint8_t* src = malloc(SRC_SIZE); if (!src) return 0; fill_seek_data(src, SRC_SIZE, 47); const size_t dst_cap = (size_t)zxc_compress_bound(SRC_SIZE) + 1024; uint8_t* dst = malloc(dst_cap); if (!dst) { free(src); return 0; } zxc_compress_opts_t opts = { .level = 3, .block_size = 64 * 1024, .seekable = 1, .checksum_enabled = 1}; const int64_t csize = zxc_compress(src, SRC_SIZE, dst, dst_cap, &opts); if (csize <= 0) { printf("Failed: compress\n"); free(src); free(dst); return 0; } /* Seekable random access */ zxc_seekable* s = zxc_seekable_open(dst, (size_t)csize); if (!s) { printf("Failed: open\n"); free(src); free(dst); return 0; } /* Range from block 0 */ uint8_t out[512]; int64_t r = zxc_seekable_decompress_range(s, out, sizeof(out), 0, 512); if (r != 512 || memcmp(out, src, 512) != 0) { printf("Failed: checksum range head mismatch\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Range spanning blocks 1-2 */ const uint64_t off = 64 * 1024 + 100; r = zxc_seekable_decompress_range(s, out, sizeof(out), off, 512); if (r != 512 || memcmp(out, src + off, 512) != 0) { printf("Failed: checksum range mid mismatch\n"); zxc_seekable_free(s); free(src); free(dst); return 0; } /* Full decompress with checksum verification */ uint8_t* full = malloc(SRC_SIZE); if (!full) { zxc_seekable_free(s); free(src); free(dst); return 0; } zxc_decompress_opts_t dopts = {.checksum_enabled = 1}; int64_t dsize = zxc_decompress(dst, (size_t)csize, full, SRC_SIZE, &dopts); if (dsize != (int64_t)SRC_SIZE || memcmp(src, full, SRC_SIZE) != 0) { printf("Failed: full decompress with checksum mismatch\n"); free(full); zxc_seekable_free(s); free(src); free(dst); return 0; } free(full); zxc_seekable_free(s); free(src); free(dst); printf("PASS\n\n"); return 1; } int main() { srand(42); // Fixed seed for reproducibility int total_failures = 0; // Standard size for blocks const size_t BUF_SIZE = 256 * 1024; uint8_t* buffer = malloc(BUF_SIZE); if (!buffer) { printf("Memory allocation failed!\n"); return 1; } gen_random_data(buffer, BUF_SIZE); if (!test_round_trip("RAW Block (Random Data)", buffer, BUF_SIZE, 3, 0)) total_failures++; gen_lz_data(buffer, BUF_SIZE); if (!test_round_trip("GHI Block (Text Pattern)", buffer, BUF_SIZE, 2, 0)) total_failures++; gen_lz_data(buffer, BUF_SIZE); if (!test_round_trip("GLO Block (Text Pattern)", buffer, BUF_SIZE, 4, 0)) total_failures++; gen_num_data(buffer, BUF_SIZE); if (!test_round_trip("NUM Block (Integer Sequence)", buffer, BUF_SIZE, 3, 0)) total_failures++; gen_num_data_zero(buffer, BUF_SIZE); if (!test_round_trip("NUM Block (Zero Deltas)", buffer, BUF_SIZE, 3, 0)) total_failures++; gen_num_data_small(buffer, BUF_SIZE); if (!test_round_trip("NUM Block (Small Deltas)", buffer, BUF_SIZE, 3, 0)) total_failures++; gen_num_data_large(buffer, BUF_SIZE); if (!test_round_trip("NUM Block (Large Deltas)", buffer, BUF_SIZE, 3, 0)) total_failures++; gen_random_data(buffer, 50); if (!test_round_trip("Small Input (50 bytes)", buffer, 50, 3, 0)) total_failures++; if (!test_round_trip("Empty Input (0 bytes)", buffer, 0, 3, 0)) total_failures++; // Edge Cases: 1-byte file gen_random_data(buffer, 1); if (!test_round_trip("1-byte Input", buffer, 1, 3, 0)) total_failures++; if (!test_round_trip("1-byte Input (with checksum)", buffer, 1, 3, 1)) total_failures++; // Large File Case: Cross block boundaries const size_t LARGE_BUF_SIZE = 15 * 1024 * 1024; // 15 MB uint8_t* large_buffer = malloc(LARGE_BUF_SIZE); if (large_buffer) { gen_lz_data(large_buffer, LARGE_BUF_SIZE); // Good mix of repetitive data if (!test_round_trip("Large File (15MB Multi-Block)", large_buffer, LARGE_BUF_SIZE, 3, 1)) { total_failures++; } // Test NUM block specifically over a large size to stress boundaries gen_num_data(large_buffer, LARGE_BUF_SIZE); if (!test_round_trip("Large File NUM (15MB Multi-Block)", large_buffer, LARGE_BUF_SIZE, 3, 1)) { total_failures++; } free(large_buffer); } else { printf("Failed to allocate 15MB buffer for large file test.\n"); } printf("\n--- Test Coverage: Checksum ---\n"); gen_lz_data(buffer, BUF_SIZE); if (!test_round_trip("Checksum Disabled", buffer, BUF_SIZE, 3, 0)) total_failures++; if (!test_round_trip("Checksum Enabled", buffer, BUF_SIZE, 31, 1)) total_failures++; printf("\n--- Test Coverage: Compression Levels ---\n"); gen_lz_data(buffer, BUF_SIZE); if (!test_round_trip("Level 1", buffer, BUF_SIZE, 1, 1)) total_failures++; if (!test_round_trip("Level 2", buffer, BUF_SIZE, 2, 1)) total_failures++; if (!test_round_trip("Level 3", buffer, BUF_SIZE, 3, 1)) total_failures++; if (!test_round_trip("Level 4", buffer, BUF_SIZE, 4, 1)) total_failures++; if (!test_round_trip("Level 5", buffer, BUF_SIZE, 5, 1)) total_failures++; printf("\n--- Test Coverage: Binary Data Preservation ---\n"); gen_binary_data(buffer, BUF_SIZE); if (!test_round_trip("Binary Data (0x00, 0x0A, 0x0D, 0xFF)", buffer, BUF_SIZE, 3, 0)) total_failures++; if (!test_round_trip("Binary Data with Checksum", buffer, BUF_SIZE, 3, 1)) total_failures++; // Test with small binary data to ensure even small payloads are preserved gen_binary_data(buffer, 128); if (!test_round_trip("Small Binary Data (128 bytes)", buffer, 128, 3, 0)) total_failures++; printf("\n--- Test Coverage: Repetitive Pattern Encoding ---\n"); // Test 8-bit offset mode (enc_off=1): patterns with all offsets <= 255 gen_small_offset_data(buffer, BUF_SIZE); if (!test_round_trip("8-bit Offsets (Small Pattern)", buffer, BUF_SIZE, 3, 1)) total_failures++; if (!test_round_trip("8-bit Offsets (Level 5)", buffer, BUF_SIZE, 5, 1)) total_failures++; // Test 16-bit offset mode (enc_off=0): patterns with offsets > 255 gen_large_offset_data(buffer, BUF_SIZE); if (!test_round_trip("16-bit Offsets (Large Distance)", buffer, BUF_SIZE, 3, 1)) total_failures++; if (!test_round_trip("16-bit Offsets (Level 5)", buffer, BUF_SIZE, 5, 1)) total_failures++; // Edge case: Mixed buffer that should trigger 16-bit mode // (even one large offset forces 16-bit mode) gen_small_offset_data(buffer, BUF_SIZE / 2); gen_large_offset_data(buffer + BUF_SIZE / 2, BUF_SIZE / 2); if (!test_round_trip("Mixed Offsets (Hybrid)", buffer, BUF_SIZE, 3, 1)) total_failures++; free(buffer); // --- UNIT TESTS (ROBUSTNESS/API) --- if (!test_buffer_api()) total_failures++; if (!test_multithread_roundtrip()) total_failures++; if (!test_null_output_decompression()) total_failures++; if (!test_max_compressed_size_logic()) total_failures++; if (!test_invalid_arguments()) total_failures++; if (!test_truncated_input()) total_failures++; if (!test_io_failures()) total_failures++; if (!test_thread_params()) total_failures++; if (!test_bit_reader()) total_failures++; if (!test_bitpack()) total_failures++; if (!test_eof_block_structure()) total_failures++; if (!test_header_checksum()) total_failures++; if (!test_global_checksum_order()) total_failures++; if (!test_get_decompressed_size()) total_failures++; if (!test_error_name()) total_failures++; if (!test_legacy_header()) total_failures++; if (!test_buffer_error_codes()) total_failures++; if (!test_stream_get_decompressed_size_errors()) total_failures++; if (!test_stream_engine_errors()) total_failures++; if (!test_buffer_api_scratch_buf()) total_failures++; if (!test_decompress_fast_vs_safe_path()) total_failures++; if (!test_opaque_context_api()) total_failures++; if (!test_block_api()) total_failures++; if (!test_library_info_api()) total_failures++; // --- SEEKABLE TESTS --- if (!test_seekable_table_sizes()) total_failures++; if (!test_seekable_table_write()) total_failures++; if (!test_seekable_roundtrip()) total_failures++; if (!test_seekable_open_query()) total_failures++; if (!test_seekable_random_access()) total_failures++; if (!test_seekable_non_seekable_reject()) total_failures++; if (!test_seekable_single_block()) total_failures++; if (!test_seekable_all_levels()) total_failures++; if (!test_seekable_many_blocks()) total_failures++; if (!test_seekable_open_file()) total_failures++; // --- SEEKABLE MT TESTS --- if (!test_seekable_mt_roundtrip()) total_failures++; if (!test_seekable_mt_single_block()) total_failures++; if (!test_seekable_mt_random_access()) total_failures++; if (!test_seekable_mt_full_file()) total_failures++; // --- SEEKABLE EDGE-CASE TESTS --- if (!test_seekable_cross_boundary()) total_failures++; if (!test_seekable_truncated_input()) total_failures++; if (!test_seekable_corrupted_sek()) total_failures++; if (!test_seekable_range_out_of_bounds()) total_failures++; if (!test_seekable_dst_too_small()) total_failures++; if (!test_seekable_empty_file()) total_failures++; if (!test_seekable_no_checksum()) total_failures++; if (!test_seekable_with_checksum()) total_failures++; if (total_failures > 0) { printf("FAILED: %d tests failed.\n", total_failures); return 1; } printf("ALL TESTS PASSED SUCCESSFULLY.\n"); return 0; }zxc-0.10.0/tests/test_cli.sh000077500000000000000000000774401517010557200157220ustar00rootroot00000000000000#!/bin/bash # ZXC - High-performance lossless compression # # Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. # SPDX-License-Identifier: BSD-3-Clause # # Test script for ZXC CLI # Usage: ./tests/test_cli.sh [path_to_zxc_binary] set -e # Default binary path ZXC_BIN=${1:-"../build/zxc"} # Test Directory (to isolate test files) TEST_DIR="./test_tmp" mkdir -p "$TEST_DIR" # Test Files TEST_FILE="$TEST_DIR/testdata" TEST_FILE_XC="${TEST_FILE}.zxc" TEST_FILE_DEC="${TEST_FILE}.dec" PIPE_XC="$TEST_DIR/test_pipe.zxc" PIPE_DEC="$TEST_DIR/test_pipe.dec" # Variables for checking file existence TEST_FILE_XC_BASH="$TEST_FILE_XC" TEST_FILE_DEC_BASH="${TEST_FILE}.dec" # Arguments passed to ZXC (relative to test_tmp) TEST_FILE_ARG="$TEST_FILE" TEST_FILE_XC_ARG="$TEST_FILE_XC" RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color log_pass() { echo -e "${GREEN}[PASS]${NC} $1" } log_fail() { echo -e "${RED}[FAIL]${NC} $1" exit 1 } # cleanup on exit cleanup() { echo "Cleaning up..." rm -rf "$TEST_DIR" } trap cleanup EXIT # 0. Check binary if [ ! -f "$ZXC_BIN" ]; then log_fail "Binary not found at $ZXC_BIN. Please build the project first." fi echo "Using binary: $ZXC_BIN" # 1. Generate Test File (Lorem Ipsum) echo "Generating test file..." cat > "$TEST_FILE" <> "${TEST_FILE}.tmp" done mv "${TEST_FILE}.tmp" "$TEST_FILE" FILE_SIZE=$(wc -c < "$TEST_FILE" | tr -d ' ') echo "Test file generated: $TEST_FILE ($FILE_SIZE bytes)" # Helper: Wait for file to be ready and readable wait_for_file() { local file="$1" local retries=10 local count=0 # On Windows, file locking can cause race conditions immediately after creation. while [ $count -lt $retries ]; do if [ -f "$file" ]; then # Try to read a byte to ensure it's not exclusively locked if head -c 1 "$file" >/dev/null 2>&1; then return 0 fi fi sleep 1 count=$((count + 1)) done echo "Timeout waiting for file '$file' to be readable." ls -l "$file" 2>/dev/null || echo "File not found in ls." return 1 } # 2. Basic Round-Trip echo "Testing Basic Round-Trip..." if ! "$ZXC_BIN" -z -k "$TEST_FILE_ARG"; then log_fail "Compression command failed (exit code $?)" fi # Wait for output if ! wait_for_file "$TEST_FILE_XC_BASH"; then log_fail "Compression succeeded but output file '$TEST_FILE_XC_BASH' is not accessible." fi # Decompress to stdout echo "Attempting decompression of: $TEST_FILE_XC_ARG" if ! "$ZXC_BIN" -d -c "$TEST_FILE_XC_ARG" > "$TEST_FILE_DEC_BASH"; then echo "Decompress to stdout failed. Retrying once..." sleep 1 if ! "$ZXC_BIN" -d -c "$TEST_FILE_XC_ARG" > "$TEST_FILE_DEC_BASH"; then log_fail "Decompression to stdout failed." fi fi # Decompress to file rm -f "$TEST_FILE" sleep 1 if ! "$ZXC_BIN" -d -k "$TEST_FILE_XC_ARG"; then echo "Decompress to file failed. Retrying once..." sleep 1 if ! "$ZXC_BIN" -d -k "$TEST_FILE_XC_ARG"; then log_fail "Decompression to file failed." fi fi if ! wait_for_file "$TEST_FILE"; then log_fail "Decompression failed to recreate original file." fi mv "$TEST_FILE" "$TEST_FILE_DEC_BASH" # Re-generate source for valid comparison cat > "$TEST_FILE" <> "${TEST_FILE}.tmp" done mv "${TEST_FILE}.tmp" "$TEST_FILE" if cmp -s "$TEST_FILE" "$TEST_FILE_DEC_BASH"; then log_pass "Basic Round-Trip" else log_fail "Basic Round-Trip content mismatch" fi # 3. Piping echo "Testing Piping..." rm -f "$PIPE_XC" "$PIPE_DEC" cat "$TEST_FILE" | "$ZXC_BIN" > "$PIPE_XC" if [ ! -s "$PIPE_XC" ]; then log_fail "Piping compression failed (empty output)" fi cat "$PIPE_XC" | "$ZXC_BIN" -d > "$PIPE_DEC" if [ ! -s "$PIPE_DEC" ]; then log_fail "Piping decompression failed (empty output)" fi if cmp -s "$TEST_FILE" "$PIPE_DEC"; then log_pass "Piping" else log_fail "Piping content mismatch" fi # 4. Flags echo "Testing Flags..." # Level "$ZXC_BIN" -1 -k -f "$TEST_FILE_ARG" if [ ! -f "$TEST_FILE_XC_BASH" ]; then log_fail "Level 1 flag failed"; fi log_pass "Flag -1" # Force Overwrite (-f) touch "$TEST_DIR/out.zxc" touch "${TEST_FILE_XC_BASH}" set +e "$ZXC_BIN" -z -k "$TEST_FILE_ARG" > /dev/null 2>&1 RET=$? set -e if [ $RET -eq 0 ]; then log_fail "Should have failed to overwrite without -f" else log_pass "Overwrite protection verified" fi "$ZXC_BIN" -z -k -f "$TEST_FILE_ARG" if [ $? -eq 0 ]; then log_pass "Force overwrite (-f)" else log_fail "Force overwrite failed" fi # 5. Benchmark echo "Testing Benchmark..." "$ZXC_BIN" -b1 "$TEST_FILE_ARG" > /dev/null if [ $? -eq 0 ]; then log_pass "Benchmark mode" else log_fail "Benchmark mode failed" fi # 6. Error Handling echo "Testing Error Handling..." set +e "$ZXC_BIN" "nonexistent_file" > /dev/null 2>&1 RET=$? set -e if [ $RET -ne 0 ]; then log_pass "Missing file error handled" else log_fail "Missing file should return error" fi # 7. Version echo "Testing Version..." OUT_VER=$("$ZXC_BIN" -V) if [[ "$OUT_VER" == *"zxc"* ]]; then log_pass "Version flag" else log_fail "Version flag failed" fi # 8. Checksum echo "Testing Checksum..." "$ZXC_BIN" -C -k -f "$TEST_FILE_ARG" if [ ! -f "$TEST_FILE_XC_BASH" ]; then log_fail "Checksum compression failed"; fi rm -f "$TEST_FILE" "$ZXC_BIN" -d "$TEST_FILE_XC_ARG" if [ ! -f "$TEST_FILE" ]; then log_fail "Checksum decompression failed"; fi log_pass "Checksum enabled (-C)" "$ZXC_BIN" -N -k -f "$TEST_FILE_ARG" if [ ! -f "$TEST_FILE_XC_BASH" ]; then log_fail "No-Checksum compression failed"; fi rm -f "$TEST_FILE" "$ZXC_BIN" -d "$TEST_FILE_XC_ARG" if [ ! -f "$TEST_FILE" ]; then log_fail "No-Checksum decompression failed"; fi log_pass "Checksum disabled (-N)" # 9. Integrity Test (-t) echo "Testing Integrity Check (-t)..." "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" # Valid file should pass and show "OK" OUT=$("$ZXC_BIN" -t "$TEST_FILE_XC_ARG") if [[ "$OUT" == *": OK"* ]]; then log_pass "Integrity check passed on valid file" else log_fail "Integrity check failed on valid file (expected OK output)" fi # Verbose test mode OUT=$("$ZXC_BIN" -t -v "$TEST_FILE_XC_ARG") if [[ "$OUT" == *": OK"* ]] && [[ "$OUT" == *"Checksum:"* ]]; then log_pass "Integrity check verbose mode" else log_fail "Integrity check verbose mode failed" fi # Corrupt file should fail and show "FAILED" # Corrupt a byte in the middle of the file (after header) printf '\xff' | dd of="$TEST_FILE_XC_ARG" bs=1 seek=100 count=1 conv=notrunc 2>/dev/null set +e OUT=$("$ZXC_BIN" -t "$TEST_FILE_XC_ARG" 2>&1) RET=$? set -e if [ $RET -ne 0 ] && [[ "$OUT" == *": FAILED"* ]]; then log_pass "Integrity check correctly failed on corrupt file" else log_fail "Integrity check PASSED on corrupt file (False Negative)" fi # 10. Global Checksum Integrity echo "Testing Global Checksum Integrity..." "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" # Corrupt the last byte (part of Global Checksum) FILE_SZ=$(wc -c < "$TEST_FILE_XC_ARG" | tr -d ' ') LAST_BYTE_OFFSET=$((FILE_SZ - 1)) printf '\x00' | dd of="$TEST_FILE_XC_ARG" bs=1 seek=$LAST_BYTE_OFFSET count=1 conv=notrunc 2>/dev/null set +e OUT=$("$ZXC_BIN" -t "$TEST_FILE_XC_ARG" 2>&1) RET=$? set -e if [ $RET -ne 0 ] && [[ "$OUT" == *": FAILED"* ]]; then log_pass "Integrity check correctly failed on corrupt Global Checksum" else log_fail "Integrity check PASSED on corrupt Global Checksum (False Negative)" fi # Ensure no output file is created if [ -f "${TEST_FILE}.zxc.zxc" ] || [ -f "${TEST_FILE}.zxc.dec" ]; then log_fail "Integrity check created output file unexpectedly" fi # 11. List Command (-l) echo "Testing List Command (-l)..." "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" # Normal list mode OUT=$("$ZXC_BIN" -l "$TEST_FILE_XC_ARG") if [[ "$OUT" == *"Compressed"* ]] && [[ "$OUT" == *"Uncompressed"* ]] && [[ "$OUT" == *"Ratio"* ]]; then log_pass "List command basic output" else log_fail "List command basic output failed" fi # Verbose list mode OUT=$("$ZXC_BIN" -l -v "$TEST_FILE_XC_ARG") if [[ "$OUT" == *"Block Format:"* ]] && [[ "$OUT" == *"Block Units:"* ]] && [[ "$OUT" == *"Checksum Method:"* ]]; then log_pass "List command verbose output" else log_fail "List command verbose output failed" fi # List with invalid file should fail set +e "$ZXC_BIN" -l "nonexistent_file" > /dev/null 2>&1 RET=$? set -e if [ $RET -ne 0 ]; then log_pass "List command error handling" else log_fail "List command should fail on nonexistent file" fi # 12. Compression Levels (All) echo "Testing All Compression Levels..." for LEVEL in 1 2 3 4 5; do "$ZXC_BIN" -$LEVEL -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_lvl${LEVEL}.zxc" if [ ! -f "$TEST_DIR/test_lvl${LEVEL}.zxc" ]; then log_fail "Compression level $LEVEL failed" fi # Decompress and verify "$ZXC_BIN" -d -c "$TEST_DIR/test_lvl${LEVEL}.zxc" > "$TEST_DIR/test_lvl${LEVEL}.dec" if ! cmp -s "$TEST_FILE" "$TEST_DIR/test_lvl${LEVEL}.dec"; then log_fail "Level $LEVEL decompression mismatch" fi SIZE=$(wc -c < "$TEST_DIR/test_lvl${LEVEL}.zxc" | tr -d ' ') log_pass "Level $LEVEL (Size: $SIZE bytes)" done # 13. Data Type Tests echo "Testing Different Data Types..." # 13.1 Highly Repetitive Text (Best Compression) echo " Testing repetitive data..." yes "Lorem ipsum dolor sit amet" | head -n 1000 > "$TEST_DIR/test_repetitive.txt" "$ZXC_BIN" -3 -k "$TEST_DIR/test_repetitive.txt" if [ ! -f "$TEST_DIR/test_repetitive.txt.zxc" ]; then log_fail "Repetitive data compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_repetitive.txt.zxc" > "$TEST_DIR/test_repetitive.dec" if cmp -s "$TEST_DIR/test_repetitive.txt" "$TEST_DIR/test_repetitive.dec"; then SIZE_ORIG=$(wc -c < "$TEST_DIR/test_repetitive.txt" | tr -d ' ') SIZE_COMP=$(wc -c < "$TEST_DIR/test_repetitive.txt.zxc" | tr -d ' ') RATIO=$((SIZE_ORIG / SIZE_COMP)) log_pass "Repetitive text (Ratio: ${RATIO}:1)" else log_fail "Repetitive data round-trip failed" fi # 13.2 Binary Zeros (Highly Compressible) echo " Testing binary zeros..." dd if=/dev/zero bs=1024 count=100 of="$TEST_DIR/test_zeros.bin" 2>/dev/null "$ZXC_BIN" -3 -k "$TEST_DIR/test_zeros.bin" if [ ! -f "$TEST_DIR/test_zeros.bin.zxc" ]; then log_fail "Binary zeros compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_zeros.bin.zxc" > "$TEST_DIR/test_zeros.dec" if cmp -s "$TEST_DIR/test_zeros.bin" "$TEST_DIR/test_zeros.dec"; then SIZE_ORIG=$(wc -c < "$TEST_DIR/test_zeros.bin" | tr -d ' ') SIZE_COMP=$(wc -c < "$TEST_DIR/test_zeros.bin.zxc" | tr -d ' ') RATIO=$((SIZE_ORIG / SIZE_COMP)) log_pass "Binary zeros (Ratio: ${RATIO}:1)" else log_fail "Binary zeros round-trip failed" fi # 13.3 Random Data (Incompressible - Should use RAW blocks) echo " Testing random data (incompressible)..." dd if=/dev/urandom bs=1024 count=100 of="$TEST_DIR/test_random.bin" 2>/dev/null "$ZXC_BIN" -3 -k "$TEST_DIR/test_random.bin" if [ ! -f "$TEST_DIR/test_random.bin.zxc" ]; then log_fail "Random data compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_random.bin.zxc" > "$TEST_DIR/test_random.dec" if cmp -s "$TEST_DIR/test_random.bin" "$TEST_DIR/test_random.dec"; then SIZE_ORIG=$(wc -c < "$TEST_DIR/test_random.bin" | tr -d ' ') SIZE_COMP=$(wc -c < "$TEST_DIR/test_random.bin.zxc" | tr -d ' ') # Random data should expand slightly (RAW blocks + headers) if [ $SIZE_COMP -le $((SIZE_ORIG + 200)) ]; then log_pass "Random data (RAW blocks, minimal expansion)" else log_fail "Random data expanded too much" fi else log_fail "Random data round-trip failed" fi # 14. Large File Test (Performance Validation) echo "Testing Large Files..." dd if=/dev/zero bs=1M count=10 of="$TEST_DIR/test_large.bin" 2>/dev/null if ! "$ZXC_BIN" -3 -k "$TEST_DIR/test_large.bin"; then log_fail "Large file compression failed" fi if ! "$ZXC_BIN" -d -c "$TEST_DIR/test_large.bin.zxc" > "$TEST_DIR/test_large.dec"; then log_fail "Large file decompression failed" fi if cmp -s "$TEST_DIR/test_large.bin" "$TEST_DIR/test_large.dec"; then log_pass "Large file (10 MB) round-trip" else log_fail "Large file content mismatch" fi # 15. One-Pass Pipe Round-Trip (Critical for Streaming) echo "Testing One-Pass Pipe Round-Trip..." cat "$TEST_FILE" | "$ZXC_BIN" -c | "$ZXC_BIN" -dc > "$TEST_DIR/test_pipe_onepass.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_pipe_onepass.dec"; then log_pass "One-pass pipe round-trip" else log_fail "One-pass pipe round-trip content mismatch" fi # 16. Empty File Edge Case echo "Testing Empty File..." touch "$TEST_DIR/test_empty.txt" "$ZXC_BIN" -3 -k "$TEST_DIR/test_empty.txt" if [ ! -f "$TEST_DIR/test_empty.txt.zxc" ]; then log_fail "Empty file compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_empty.txt.zxc" > "$TEST_DIR/test_empty.dec" if [ ! -s "$TEST_DIR/test_empty.dec" ]; then log_pass "Empty file round-trip" else log_fail "Empty file produced non-empty output" fi # 17. Stdin Detection (No -c flag needed for compression) echo "Testing Stdin Auto-Detection..." cat "$TEST_FILE" | "$ZXC_BIN" > "$TEST_DIR/test_stdin_auto.zxc" 2>/dev/null if [ -s "$TEST_DIR/test_stdin_auto.zxc" ]; then "$ZXC_BIN" -d -c "$TEST_DIR/test_stdin_auto.zxc" > "$TEST_DIR/test_stdin_auto.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_stdin_auto.dec"; then log_pass "Stdin auto-detection (compression)" else log_fail "Stdin auto-detection content mismatch" fi else log_fail "Stdin auto-detection failed (empty output)" fi # 18. Keep Source File (-k) echo "Testing Keep Source (-k)..." cp "$TEST_FILE" "$TEST_DIR/test_keep.txt" "$ZXC_BIN" -k "$TEST_DIR/test_keep.txt" if [ -f "$TEST_DIR/test_keep.txt" ] && [ -f "$TEST_DIR/test_keep.txt.zxc" ]; then log_pass "Keep source file (-k)" else log_fail "Keep source file failed (source was deleted)" fi # 19. Multi-Threading Tests (-T) echo "Testing Multi-Threading..." # 19.1 Single Thread (baseline) echo " Testing single thread (-T1)..." "$ZXC_BIN" -3 -T1 -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_T1.zxc" if [ ! -f "$TEST_DIR/test_T1.zxc" ]; then log_fail "Single thread compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_T1.zxc" > "$TEST_DIR/test_T1.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_T1.dec"; then SIZE_T1=$(wc -c < "$TEST_DIR/test_T1.zxc" | tr -d ' ') log_pass "Single thread (-T1, Size: $SIZE_T1)" else log_fail "Single thread round-trip failed" fi # 19.2 Multi-Thread (2 threads) echo " Testing 2 threads (-T2)..." "$ZXC_BIN" -3 -T2 -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_T2.zxc" if [ ! -f "$TEST_DIR/test_T2.zxc" ]; then log_fail "2-thread compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_T2.zxc" > "$TEST_DIR/test_T2.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_T2.dec"; then SIZE_T2=$(wc -c < "$TEST_DIR/test_T2.zxc" | tr -d ' ') log_pass "2 threads (-T2, Size: $SIZE_T2)" else log_fail "2-thread round-trip failed" fi # 19.3 Multi-Thread (all threads) echo " Testing all threads (-T0)..." "$ZXC_BIN" -3 -T0 -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_T0.zxc" if [ ! -f "$TEST_DIR/test_T0.zxc" ]; then log_fail "all-thread compression failed" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_T0.zxc" > "$TEST_DIR/test_T0.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_T0.dec"; then SIZE_T0=$(wc -c < "$TEST_DIR/test_T0.zxc" | tr -d ' ') log_pass "all threads (-T0, Size: $SIZE_T0)" else log_fail "all-thread round-trip failed" fi # 19.4 Verify determinism: All outputs should decompress to same content echo " Verifying thread-count independence..." if cmp -s "$TEST_DIR/test_T1.dec" "$TEST_DIR/test_T2.dec" && cmp -s "$TEST_DIR/test_T2.dec" "$TEST_DIR/test_T0.dec"; then log_pass "Deterministic output (all thread counts produce valid results)" else log_fail "Thread outputs differ (non-deterministic)" fi # 19.5 Cross-compatibility: File compressed with -T2 should decompress without -T flag echo " Testing cross-compatibility..." "$ZXC_BIN" -d -c "$TEST_DIR/test_T2.zxc" > "$TEST_DIR/test_T2_compat.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_T2_compat.dec"; then log_pass "Cross-compatible decompression" else log_fail "Multi-threaded file not compatible with standard decompression" fi # 19.6 Large file with threads (performance validation) echo " Testing large file with threads..." dd if=/dev/zero bs=1M count=5 of="$TEST_DIR/test_large_mt.bin" 2>/dev/null "$ZXC_BIN" -3 -T4 -c -k "$TEST_DIR/test_large_mt.bin" > "$TEST_DIR/test_large_mt.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/test_large_mt.zxc" > "$TEST_DIR/test_large_mt.dec" if cmp -s "$TEST_DIR/test_large_mt.bin" "$TEST_DIR/test_large_mt.dec"; then log_pass "Large file multi-threading" else log_fail "Large file multi-threading round-trip failed" fi # 20. JSON Output Tests echo "Testing JSON Output (-j, --json)..." # 20.1 List mode with JSON (single file) echo " Testing list mode with JSON (single file)..." "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" JSON_OUT=$("$ZXC_BIN" -l -j "$TEST_FILE_XC_ARG") if [[ "$JSON_OUT" == *'"filename"'* ]] && \ [[ "$JSON_OUT" == *'"compressed_size_bytes"'* ]] && \ [[ "$JSON_OUT" == *'"uncompressed_size_bytes"'* ]] && \ [[ "$JSON_OUT" == *'"compression_ratio"'* ]] && \ [[ "$JSON_OUT" == *'"format_version"'* ]] && \ [[ "$JSON_OUT" == *'"checksum_method"'* ]]; then log_pass "List mode JSON output (single file)" else log_fail "List mode JSON output missing expected fields" fi # 20.2 List mode with JSON (multiple files) echo " Testing list mode with JSON (multiple files)..." cp "$TEST_FILE_XC_ARG" "$TEST_DIR/test2.zxc" JSON_OUT=$("$ZXC_BIN" -l -j "$TEST_FILE_XC_ARG" "$TEST_DIR/test2.zxc") if [[ "$JSON_OUT" == "["* ]] && \ [[ "$JSON_OUT" == *"]" ]] && \ [[ "$JSON_OUT" == *'"filename"'* ]] && \ [[ "$JSON_OUT" == *","* ]]; then log_pass "List mode JSON output (multiple files - array)" else log_fail "List mode JSON output should produce array for multiple files" fi # 20.3 Benchmark mode with JSON echo " Testing benchmark mode with JSON..." JSON_OUT=$("$ZXC_BIN" -b1 -j "$TEST_FILE_ARG" 2>/dev/null) if [[ "$JSON_OUT" == *'"input_file"'* ]] && \ [[ "$JSON_OUT" == *'"input_size_bytes"'* ]] && \ [[ "$JSON_OUT" == *'"compressed_size_bytes"'* ]] && \ [[ "$JSON_OUT" == *'"compression_ratio"'* ]] && \ [[ "$JSON_OUT" == *'"duration_seconds"'* ]] && \ [[ "$JSON_OUT" == *'"compress_iterations"'* ]] && \ [[ "$JSON_OUT" == *'"decompress_iterations"'* ]] && \ [[ "$JSON_OUT" == *'"threads"'* ]] && \ [[ "$JSON_OUT" == *'"level"'* ]] && \ [[ "$JSON_OUT" == *'"checksum_enabled"'* ]] && \ [[ "$JSON_OUT" == *'"compress_speed_mbps"'* ]] && \ [[ "$JSON_OUT" == *'"decompress_speed_mbps"'* ]]; then log_pass "Benchmark mode JSON output" else log_fail "Benchmark mode JSON output missing expected fields" fi # 20.4 Integrity check with JSON (valid file) echo " Testing integrity check with JSON (valid file)..." "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" JSON_OUT=$("$ZXC_BIN" -t -j "$TEST_FILE_XC_ARG") if [[ "$JSON_OUT" == *'"filename"'* ]] && \ [[ "$JSON_OUT" == *'"status": "ok"'* ]] && \ [[ "$JSON_OUT" == *'"checksum_verified"'* ]]; then log_pass "Integrity check JSON output (valid file)" else log_fail "Integrity check JSON output missing expected fields" fi # 20.5 Integrity check with JSON (corrupt file) echo " Testing integrity check with JSON (corrupt file)..." "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" # Corrupt a byte printf '\xff' | dd of="$TEST_FILE_XC_ARG" bs=1 seek=100 count=1 conv=notrunc 2>/dev/null set +e JSON_OUT=$("$ZXC_BIN" -t -j "$TEST_FILE_XC_ARG" 2>&1) RET=$? set -e if [ $RET -ne 0 ] && \ [[ "$JSON_OUT" == *'"filename"'* ]] && \ [[ "$JSON_OUT" == *'"status": "failed"'* ]] && \ [[ "$JSON_OUT" == *'"error"'* ]]; then log_pass "Integrity check JSON output (corrupt file)" else log_fail "Integrity check JSON output for corrupt file incorrect" fi # 20.6 Verify JSON is parseable (if jq is available) echo " Checking JSON validity (if jq available)..." if command -v jq &> /dev/null; then "$ZXC_BIN" -z -k -f -C "$TEST_FILE_ARG" JSON_OUT=$("$ZXC_BIN" -l -j "$TEST_FILE_XC_ARG") if echo "$JSON_OUT" | jq . > /dev/null 2>&1; then log_pass "JSON output is valid (verified with jq)" else log_fail "JSON output is not valid JSON" fi else echo " [SKIP] jq not available, skipping JSON validation" fi # 21. Multiple Mode (-m) Tests echo "Testing Multiple Mode (-m)..." # 21.1 Compress multiple files echo " Testing compress multiple files..." cp "$TEST_FILE" "$TEST_DIR/multi1.txt" cp "$TEST_FILE" "$TEST_DIR/multi2.txt" "$ZXC_BIN" -m -3 "$TEST_DIR/multi1.txt" "$TEST_DIR/multi2.txt" if [ -f "$TEST_DIR/multi1.txt.zxc" ] && [ -f "$TEST_DIR/multi2.txt.zxc" ]; then log_pass "Compress multiple files (-m)" else log_fail "Compress multiple files failed" fi # 21.2 Decompress multiple files echo " Testing decompress multiple files..." rm -f "$TEST_DIR/multi1.txt" "$TEST_DIR/multi2.txt" "$ZXC_BIN" -d -m "$TEST_DIR/multi1.txt.zxc" "$TEST_DIR/multi2.txt.zxc" if cmp -s "$TEST_FILE" "$TEST_DIR/multi1.txt" && cmp -s "$TEST_FILE" "$TEST_DIR/multi2.txt"; then log_pass "Decompress multiple files (-d -m)" else log_fail "Decompress multiple files failed (content mismatch or missing files)" fi # 21.3 Error on multiple and stdout echo " Testing stdout restriction with multiple mode..." set +e "$ZXC_BIN" -m -c "$TEST_DIR/multi1.txt" "$TEST_DIR/multi2.txt" > /dev/null 2>&1 RET=$? set -e if [ $RET -ne 0 ]; then log_pass "Stdout rejected with multiple mode" else log_fail "Stdout should be rejected with multiple mode" fi # 22. Recursive Mode (-r) Tests echo "Testing Recursive Mode (-r)..." # Create a nested directory structure mkdir -p "$TEST_DIR/rec_test/subdir1" mkdir -p "$TEST_DIR/rec_test/subdir2" cp "$TEST_FILE" "$TEST_DIR/rec_test/fileA.txt" cp "$TEST_FILE" "$TEST_DIR/rec_test/subdir1/fileB.txt" cp "$TEST_FILE" "$TEST_DIR/rec_test/subdir2/fileC.txt" # 22.1 Compress recursively echo " Testing compress recursive directory..." "$ZXC_BIN" -r -3 "$TEST_DIR/rec_test" if [ -f "$TEST_DIR/rec_test/fileA.txt.zxc" ] && \ [ -f "$TEST_DIR/rec_test/subdir1/fileB.txt.zxc" ] && \ [ -f "$TEST_DIR/rec_test/subdir2/fileC.txt.zxc" ]; then log_pass "Compress recursive directory (-r)" else log_fail "Compress recursive directory failed" fi # 22.2 Decompress recursively echo " Testing decompress recursive directory..." rm -f "$TEST_DIR/rec_test/fileA.txt" "$TEST_DIR/rec_test/subdir1/fileB.txt" "$TEST_DIR/rec_test/subdir2/fileC.txt" "$ZXC_BIN" -d -r "$TEST_DIR/rec_test" if cmp -s "$TEST_FILE" "$TEST_DIR/rec_test/fileA.txt" && \ cmp -s "$TEST_FILE" "$TEST_DIR/rec_test/subdir1/fileB.txt" && \ cmp -s "$TEST_FILE" "$TEST_DIR/rec_test/subdir2/fileC.txt"; then log_pass "Decompress recursive directory (-d -r)" else log_fail "Decompress recursive directory failed (content mismatch or missing files)" fi # 23. Block Size Tests (-B) echo "Testing Block Size (-B)..." # 23.1 Round-trip with different block sizes for BS in 4K 64K 512K 1M; do echo " Testing block size -B $BS..." "$ZXC_BIN" -3 -B "$BS" -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_bs_${BS}.zxc" if [ ! -s "$TEST_DIR/test_bs_${BS}.zxc" ]; then log_fail "Block size $BS compression produced empty output" fi "$ZXC_BIN" -d -c "$TEST_DIR/test_bs_${BS}.zxc" > "$TEST_DIR/test_bs_${BS}.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_bs_${BS}.dec"; then SIZE_BS=$(wc -c < "$TEST_DIR/test_bs_${BS}.zxc" | tr -d ' ') log_pass "Block size -B $BS (Size: $SIZE_BS)" else log_fail "Block size $BS round-trip failed" fi done # 23.2 Test suffix formats (KB, M, MB) echo " Testing block size suffix formats..." "$ZXC_BIN" -3 -B 128KB -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_bs_128KB.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/test_bs_128KB.zxc" > "$TEST_DIR/test_bs_128KB.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_bs_128KB.dec"; then log_pass "Block size suffix KB" else log_fail "Block size suffix KB round-trip failed" fi "$ZXC_BIN" -3 -B 2MB -c -k "$TEST_FILE_ARG" > "$TEST_DIR/test_bs_2MB.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/test_bs_2MB.zxc" > "$TEST_DIR/test_bs_2MB.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/test_bs_2MB.dec"; then log_pass "Block size suffix MB" else log_fail "Block size suffix MB round-trip failed" fi # 23.3 Error: invalid block size (not power of 2) echo " Testing invalid block size..." set +e "$ZXC_BIN" -3 -B 100K "$TEST_FILE_ARG" > /dev/null 2>&1 RET=$? set -e if [ $RET -ne 0 ]; then log_pass "Invalid block size rejected (100K)" else log_fail "Invalid block size should be rejected (100K is not power of 2)" fi # 23.4 Error: block size too small set +e "$ZXC_BIN" -3 -B 2K "$TEST_FILE_ARG" > /dev/null 2>&1 RET=$? set -e if [ $RET -ne 0 ]; then log_pass "Block size too small rejected (2K)" else log_fail "Block size 2K should be rejected (min is 4K)" fi # 23.5 Error: block size too large set +e "$ZXC_BIN" -3 -B 4M "$TEST_FILE_ARG" > /dev/null 2>&1 RET=$? set -e if [ $RET -ne 0 ]; then log_pass "Block size too large rejected (4M)" else log_fail "Block size 4M should be rejected (max is 2M)" fi # 24. Seekable Format (-S) echo "Testing Seekable Format (-S)..." # 24.1 Basic seekable round-trip echo " Testing basic seekable round-trip..." rm -f "$TEST_FILE_XC_ARG" "$TEST_FILE_DEC_BASH" "$ZXC_BIN" -S -c "$TEST_FILE_ARG" > "$TEST_FILE_XC_ARG" if [ ! -s "$TEST_FILE_XC_ARG" ]; then log_fail "Seekable compression failed" fi "$ZXC_BIN" -d -c "$TEST_FILE_XC_ARG" > "$TEST_FILE_DEC_BASH" if cmp -s "$TEST_FILE" "$TEST_FILE_DEC_BASH"; then log_pass "Seekable basic round-trip (-S)" else log_fail "Seekable decompression mismatch" fi # 24.2 Seekable file must be larger than normal (SEK block overhead) echo " Testing seekable overhead..." "$ZXC_BIN" -3 -c -k "$TEST_FILE_ARG" > "$TEST_DIR/normal.zxc" "$ZXC_BIN" -3 -S -c -k "$TEST_FILE_ARG" > "$TEST_DIR/seekable.zxc" SIZE_NORMAL=$(wc -c < "$TEST_DIR/normal.zxc" | tr -d ' ') SIZE_SEEKABLE=$(wc -c < "$TEST_DIR/seekable.zxc" | tr -d ' ') if [ "$SIZE_SEEKABLE" -gt "$SIZE_NORMAL" ]; then OVERHEAD=$((SIZE_SEEKABLE - SIZE_NORMAL)) log_pass "Seekable overhead verified (+${OVERHEAD} bytes)" else log_fail "Seekable file should be larger than normal (SEK block missing?)" fi # 24.3 Seekable with small block size (many blocks = larger seek table) echo " Testing seekable with small blocks (-B 4K)..." "$ZXC_BIN" -3 -S -B 4K -c -k "$TEST_FILE_ARG" > "$TEST_DIR/seekable_4k.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/seekable_4k.zxc" > "$TEST_DIR/seekable_4k.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/seekable_4k.dec"; then SIZE_4K=$(wc -c < "$TEST_DIR/seekable_4k.zxc" | tr -d ' ') # With 4K blocks, overhead should be larger than with default 256K blocks if [ "$SIZE_4K" -gt "$SIZE_SEEKABLE" ]; then log_pass "Seekable small blocks (-B 4K, Size: $SIZE_4K, more entries)" else log_pass "Seekable small blocks (-B 4K, Size: $SIZE_4K)" fi else log_fail "Seekable small blocks round-trip failed" fi # 24.4 Seekable with checksum echo " Testing seekable + checksum (-S -C)..." "$ZXC_BIN" -3 -S -C -c -k "$TEST_FILE_ARG" > "$TEST_DIR/seekable_chk.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/seekable_chk.zxc" > "$TEST_DIR/seekable_chk.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/seekable_chk.dec"; then log_pass "Seekable + checksum (-S -C)" else log_fail "Seekable + checksum round-trip failed" fi # 24.5 Seekable with multi-threading echo " Testing seekable + threads (-S -T2)..." "$ZXC_BIN" -3 -S -T2 -c -k "$TEST_FILE_ARG" > "$TEST_DIR/seekable_mt.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/seekable_mt.zxc" > "$TEST_DIR/seekable_mt.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/seekable_mt.dec"; then log_pass "Seekable + multi-threading (-S -T2)" else log_fail "Seekable + multi-threading round-trip failed" fi # 24.6 Seekable across all levels echo " Testing seekable across all levels..." SEEK_ALL_OK=1 for LEVEL in 1 2 3 4 5; do "$ZXC_BIN" -$LEVEL -S -c -k "$TEST_FILE_ARG" > "$TEST_DIR/seekable_lvl${LEVEL}.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/seekable_lvl${LEVEL}.zxc" > "$TEST_DIR/seekable_lvl${LEVEL}.dec" if ! cmp -s "$TEST_FILE" "$TEST_DIR/seekable_lvl${LEVEL}.dec"; then SEEK_ALL_OK=0 log_fail "Seekable level $LEVEL round-trip failed" fi done if [ "$SEEK_ALL_OK" -eq 1 ]; then log_pass "Seekable across all levels (1-5)" fi # 24.7 Seekable pipe round-trip (no fseeko - validates SEK skip on stdin) echo " Testing seekable pipe round-trip..." cat "$TEST_FILE" | "$ZXC_BIN" -S -c | "$ZXC_BIN" -dc > "$TEST_DIR/seekable_pipe.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/seekable_pipe.dec"; then log_pass "Seekable pipe round-trip" else log_fail "Seekable pipe round-trip content mismatch" fi # 24.8 Seekable + no-checksum echo " Testing seekable + no-checksum (-S -N)..." "$ZXC_BIN" -3 -S -N -c -k "$TEST_FILE_ARG" > "$TEST_DIR/seekable_nochk.zxc" "$ZXC_BIN" -d -c "$TEST_DIR/seekable_nochk.zxc" > "$TEST_DIR/seekable_nochk.dec" if cmp -s "$TEST_FILE" "$TEST_DIR/seekable_nochk.dec"; then log_pass "Seekable + no-checksum (-S -N)" else log_fail "Seekable + no-checksum round-trip failed" fi # 24.9 List command on seekable archive echo " Testing list command on seekable archive..." "$ZXC_BIN" -3 -S -C -k -f "$TEST_FILE_ARG" OUT=$("$ZXC_BIN" -l "$TEST_FILE_XC_ARG") if [[ "$OUT" == *"Compressed"* ]] && [[ "$OUT" == *"Uncompressed"* ]]; then log_pass "List command on seekable archive" else log_fail "List command on seekable archive failed" fi echo "All tests passed!" exit 0 zxc-0.10.0/wrappers/000077500000000000000000000000001517010557200142425ustar00rootroot00000000000000zxc-0.10.0/wrappers/go/000077500000000000000000000000001517010557200146475ustar00rootroot00000000000000zxc-0.10.0/wrappers/go/README.md000066400000000000000000000042371517010557200161340ustar00rootroot00000000000000# ZXC Go Bindings High-performance Go bindings for the **ZXC** asymmetric compressor, optimised for **fast decompression**. Designed for *Write Once, Read Many* workloads like ML datasets, game assets, and caches. ## Features - **Blazing fast decompression** - ZXC is specifically optimised for read-heavy workloads. - **Buffer API** - compress/decompress `[]byte` slices in a single call. - **Streaming API** - multi-threaded file compression/decompression. - **Functional options** - clean, composable configuration (`WithLevel`, `WithChecksum`, `WithThreads`). - **Typed errors** - sentinel error values for every ZXC error code. ## Prerequisites Build the ZXC core library as a static library: ```bash cd /path/to/zxc cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ -DZXC_BUILD_CLI=OFF -DZXC_BUILD_TESTS=OFF cmake --build build ``` ## Installation (from source) ```bash git clone https://github.com/hellobertrand/zxc.git cd zxc/wrappers/go CGO_ENABLED=1 CGO_LDFLAGS="-L../../build -lzxc -lpthread" go build ./... ``` ## Quick Start ```go package main import ( "fmt" "log" zxc "github.com/hellobertrand/zxc/wrappers/go" ) func main() { data := []byte("Hello, ZXC from Go!") // Compress compressed, err := zxc.Compress(data, zxc.WithLevel(zxc.LevelDefault)) if err != nil { log.Fatal(err) } fmt.Printf("Compressed %d => %d bytes\n", len(data), len(compressed)) // Decompress original, err := zxc.Decompress(compressed) if err != nil { log.Fatal(err) } fmt.Printf("Decompressed: %s\n", original) } ``` ## Streaming Files ```go // Multi-threaded file compression n, err := zxc.CompressFile("input.bin", "output.zxc", zxc.WithLevel(zxc.LevelCompact), zxc.WithThreads(4), zxc.WithChecksum(true), ) // Decompress n, err = zxc.DecompressFile("output.zxc", "restored.bin") ``` ## Testing ```bash cd zxc/wrappers/go CGO_ENABLED=1 CGO_LDFLAGS="-L../../build -lzxc -lpthread" go test -v -count=1 ./... ``` ## Benchmarks ```bash CGO_ENABLED=1 CGO_LDFLAGS="-L../../build -lzxc -lpthread" go test -bench=. -benchmem ./... ``` ## License BSD-3-Clause - see [LICENSE](../../LICENSE). zxc-0.10.0/wrappers/go/go.mod000066400000000000000000000000711517010557200157530ustar00rootroot00000000000000module github.com/hellobertrand/zxc/wrappers/go go 1.21 zxc-0.10.0/wrappers/go/zxc.go000066400000000000000000000320101517010557200157760ustar00rootroot00000000000000/* ZXC - High-performance lossless compression Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. SPDX-License-Identifier: BSD-3-Clause */ // Package zxc provides Go bindings to the ZXC high-performance lossless // compression library. // // ZXC is optimised for fast decompression speed: it is designed for // "Write Once, Read Many" workloads such as ML datasets, game assets, // firmware images and caches. // // # Quick Start // // compressed, err := zxc.Compress(data) // if err != nil { log.Fatal(err) } // // original, err := zxc.Decompress(compressed) // if err != nil { log.Fatal(err) } // // # Streaming (file-based) // // n, err := zxc.CompressFile("input.bin", "output.zxc") // n, err = zxc.DecompressFile("output.zxc", "restored.bin") package zxc /* #cgo CFLAGS: -I${SRCDIR}/../../include -DZXC_STATIC_DEFINE #cgo LDFLAGS: -lpthread #include #include #include "zxc.h" */ import "C" import ( "errors" "fmt" "os" "unsafe" ) // ============================================================================ // Compression Levels // ============================================================================ // Level represents a ZXC compression level. type Level int const ( // LevelFastest provides the fastest compression, best for real-time // applications (level 1). LevelFastest Level = C.ZXC_LEVEL_FASTEST // LevelFast provides fast compression, good for real-time applications // (level 2). LevelFast Level = C.ZXC_LEVEL_FAST // LevelDefault is the recommended default: ratio > LZ4, decode speed > LZ4 // (level 3). LevelDefault Level = C.ZXC_LEVEL_DEFAULT // LevelBalanced provides good ratio with good decode speed (level 4). LevelBalanced Level = C.ZXC_LEVEL_BALANCED // LevelCompact provides the highest density. Best for storage, firmware // and assets (level 5). LevelCompact Level = C.ZXC_LEVEL_COMPACT ) // AllLevels returns all available compression levels from fastest to most // compact. func AllLevels() []Level { return []Level{LevelFastest, LevelFast, LevelDefault, LevelBalanced, LevelCompact} } // ============================================================================ // Version // ============================================================================ const ( // VersionMajor is the major version of the underlying C library. VersionMajor = C.ZXC_VERSION_MAJOR // VersionMinor is the minor version of the underlying C library. VersionMinor = C.ZXC_VERSION_MINOR // VersionPatch is the patch version of the underlying C library. VersionPatch = C.ZXC_VERSION_PATCH ) // Version returns the library version as (major, minor, patch). func Version() (int, int, int) { return int(VersionMajor), int(VersionMinor), int(VersionPatch) } // VersionString returns the library version as a "major.minor.patch" string. func VersionString() string { return fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch) } // ============================================================================ // Error Handling // ============================================================================ // Error represents a ZXC library error. type Error struct { Code int Name string } func (e *Error) Error() string { return fmt.Sprintf("zxc: %s (code %d)", e.Name, e.Code) } // Sentinel errors for each ZXC error code. var ( ErrMemory = &Error{Code: int(C.ZXC_ERROR_MEMORY), Name: "memory allocation failed"} ErrDstTooSmall = &Error{Code: int(C.ZXC_ERROR_DST_TOO_SMALL), Name: "destination buffer too small"} ErrSrcTooSmall = &Error{Code: int(C.ZXC_ERROR_SRC_TOO_SMALL), Name: "source buffer too small"} ErrBadMagic = &Error{Code: int(C.ZXC_ERROR_BAD_MAGIC), Name: "invalid magic word"} ErrBadVersion = &Error{Code: int(C.ZXC_ERROR_BAD_VERSION), Name: "unsupported format version"} ErrBadHeader = &Error{Code: int(C.ZXC_ERROR_BAD_HEADER), Name: "corrupted header"} ErrBadChecksum = &Error{Code: int(C.ZXC_ERROR_BAD_CHECKSUM), Name: "checksum verification failed"} ErrCorruptData = &Error{Code: int(C.ZXC_ERROR_CORRUPT_DATA), Name: "corrupted compressed data"} ErrBadOffset = &Error{Code: int(C.ZXC_ERROR_BAD_OFFSET), Name: "invalid match offset"} ErrOverflow = &Error{Code: int(C.ZXC_ERROR_OVERFLOW), Name: "buffer overflow detected"} ErrIO = &Error{Code: int(C.ZXC_ERROR_IO), Name: "I/O error"} ErrNullInput = &Error{Code: int(C.ZXC_ERROR_NULL_INPUT), Name: "null input pointer"} ErrBadBlockType = &Error{Code: int(C.ZXC_ERROR_BAD_BLOCK_TYPE), Name: "unknown block type"} ErrBadBlockSize = &Error{Code: int(C.ZXC_ERROR_BAD_BLOCK_SIZE), Name: "invalid block size"} ErrInvalidData = errors.New("zxc: invalid compressed data") ) // errorFromCode converts a negative C error code to a Go error. func errorFromCode(code C.int64_t) error { switch int(code) { case int(C.ZXC_ERROR_MEMORY): return ErrMemory case int(C.ZXC_ERROR_DST_TOO_SMALL): return ErrDstTooSmall case int(C.ZXC_ERROR_SRC_TOO_SMALL): return ErrSrcTooSmall case int(C.ZXC_ERROR_BAD_MAGIC): return ErrBadMagic case int(C.ZXC_ERROR_BAD_VERSION): return ErrBadVersion case int(C.ZXC_ERROR_BAD_HEADER): return ErrBadHeader case int(C.ZXC_ERROR_BAD_CHECKSUM): return ErrBadChecksum case int(C.ZXC_ERROR_CORRUPT_DATA): return ErrCorruptData case int(C.ZXC_ERROR_BAD_OFFSET): return ErrBadOffset case int(C.ZXC_ERROR_OVERFLOW): return ErrOverflow case int(C.ZXC_ERROR_IO): return ErrIO case int(C.ZXC_ERROR_NULL_INPUT): return ErrNullInput case int(C.ZXC_ERROR_BAD_BLOCK_TYPE): return ErrBadBlockType case int(C.ZXC_ERROR_BAD_BLOCK_SIZE): return ErrBadBlockSize default: return fmt.Errorf("zxc: unknown error (code %d)", int(code)) } } // ============================================================================ // Options (functional options pattern) // ============================================================================ // options holds all configurable parameters. type options struct { level Level checksum bool seekable bool threads int } func defaultOptions() options { return options{ level: LevelDefault, checksum: false, threads: 0, // auto-detect } } // Option configures compression or decompression. type Option func(*options) // WithLevel sets the compression level. func WithLevel(l Level) Option { return func(o *options) { o.level = l } } // WithChecksum enables checksum computation and verification. func WithChecksum(enabled bool) Option { return func(o *options) { o.checksum = enabled } } // WithThreads sets the number of worker threads for streaming operations. // A value of 0 means auto-detect the CPU core count. func WithThreads(n int) Option { return func(o *options) { o.threads = n } } // WithSeekable enables seek table generation for random-access decompression. func WithSeekable(enabled bool) Option { return func(o *options) { o.seekable = enabled } } func applyOptions(opts []Option) options { o := defaultOptions() for _, fn := range opts { fn(&o) } return o } // ============================================================================ // Buffer API // ============================================================================ // CompressBound returns the maximum compressed size for an input of the given // size. Use this to pre-allocate output buffers. func CompressBound(inputSize int) uint64 { return uint64(C.zxc_compress_bound(C.size_t(inputSize))) } // Compress compresses data using the ZXC algorithm. // // Options: [WithLevel], [WithChecksum]. // // out, err := zxc.Compress(data, zxc.WithLevel(zxc.LevelCompact)) func Compress(data []byte, opts ...Option) ([]byte, error) { if len(data) == 0 { return nil, ErrSrcTooSmall } o := applyOptions(opts) bound := CompressBound(len(data)) dst := make([]byte, bound) var copts C.zxc_compress_opts_t copts.level = C.int(o.level) if o.checksum { copts.checksum_enabled = 1 } written := C.zxc_compress( unsafe.Pointer(&data[0]), C.size_t(len(data)), unsafe.Pointer(&dst[0]), C.size_t(bound), &copts, ) if written < 0 { return nil, errorFromCode(written) } if written == 0 { return nil, ErrInvalidData } return dst[:int(written)], nil } // CompressTo compresses data into a pre-allocated output buffer. // Returns the number of bytes written. func CompressTo(data []byte, output []byte, opts ...Option) (int, error) { if len(data) == 0 { return 0, ErrSrcTooSmall } if len(output) == 0 { return 0, ErrDstTooSmall } o := applyOptions(opts) var copts C.zxc_compress_opts_t copts.level = C.int(o.level) if o.checksum { copts.checksum_enabled = 1 } written := C.zxc_compress( unsafe.Pointer(&data[0]), C.size_t(len(data)), unsafe.Pointer(&output[0]), C.size_t(len(output)), &copts, ) if written < 0 { return 0, errorFromCode(written) } if written == 0 { return 0, ErrInvalidData } return int(written), nil } // DecompressedSize returns the original uncompressed size stored in the // compressed data footer. Returns 0, ErrInvalidData if the data is too small // or invalid. func DecompressedSize(data []byte) (uint64, error) { if len(data) == 0 { return 0, ErrInvalidData } size := C.zxc_get_decompressed_size( unsafe.Pointer(&data[0]), C.size_t(len(data)), ) if size == 0 { return 0, ErrInvalidData } return uint64(size), nil } // Decompress decompresses ZXC-compressed data. // // The output size is read from the compressed data footer. For pre-allocated // buffers, use [DecompressTo]. // // Options: [WithChecksum]. func Decompress(data []byte, opts ...Option) ([]byte, error) { if len(data) == 0 { return nil, ErrInvalidData } size, err := DecompressedSize(data) if err != nil { return nil, err } if size == 0 { return []byte{}, nil } o := applyOptions(opts) dst := make([]byte, size) var dopts C.zxc_decompress_opts_t if o.checksum { dopts.checksum_enabled = 1 } written := C.zxc_decompress( unsafe.Pointer(&data[0]), C.size_t(len(data)), unsafe.Pointer(&dst[0]), C.size_t(size), &dopts, ) if written < 0 { return nil, errorFromCode(written) } if uint64(written) != size { return nil, ErrInvalidData } return dst[:int(written)], nil } // DecompressTo decompresses data into a pre-allocated output buffer. // Returns the number of bytes written. func DecompressTo(data []byte, output []byte, opts ...Option) (int, error) { if len(data) == 0 { return 0, ErrInvalidData } if len(output) == 0 { return 0, ErrDstTooSmall } o := applyOptions(opts) var dopts C.zxc_decompress_opts_t if o.checksum { dopts.checksum_enabled = 1 } written := C.zxc_decompress( unsafe.Pointer(&data[0]), C.size_t(len(data)), unsafe.Pointer(&output[0]), C.size_t(len(output)), &dopts, ) if written < 0 { return 0, errorFromCode(written) } return int(written), nil } // ============================================================================ // Streaming API (file-based) // ============================================================================ // CompressFile compresses src to dst using multi-threaded streaming. // // This is the recommended method for large files. It uses an asynchronous // pipeline with separate reader, worker, and writer threads. // // Options: [WithLevel], [WithChecksum], [WithThreads]. // // n, err := zxc.CompressFile("input.bin", "output.zxc", // zxc.WithLevel(zxc.LevelCompact), zxc.WithThreads(4)) func CompressFile(input, output string, opts ...Option) (int64, error) { o := applyOptions(opts) fIn, err := os.Open(input) if err != nil { return 0, fmt.Errorf("zxc: open input: %w", err) } defer fIn.Close() fOut, err := os.Create(output) if err != nil { return 0, fmt.Errorf("zxc: create output: %w", err) } defer fOut.Close() // Duplicate file descriptors so the C FILE* owns its own fd. cIn, err := dupFileRead(fIn) if err != nil { return 0, err } defer C.fclose(cIn) cOut, err := dupFileWrite(fOut) if err != nil { return 0, err } defer C.fclose(cOut) var copts C.zxc_compress_opts_t copts.n_threads = C.int(o.threads) copts.level = C.int(o.level) if o.checksum { copts.checksum_enabled = 1 } if o.seekable { copts.seekable = 1 } result := C.zxc_stream_compress(cIn, cOut, &copts) if result < 0 { return 0, errorFromCode(result) } return int64(result), nil } // DecompressFile decompresses src to dst using multi-threaded streaming. // // Options: [WithChecksum], [WithThreads]. // // n, err := zxc.DecompressFile("compressed.zxc", "output.bin") func DecompressFile(input, output string, opts ...Option) (int64, error) { o := applyOptions(opts) fIn, err := os.Open(input) if err != nil { return 0, fmt.Errorf("zxc: open input: %w", err) } defer fIn.Close() fOut, err := os.Create(output) if err != nil { return 0, fmt.Errorf("zxc: create output: %w", err) } defer fOut.Close() cIn, err := dupFileRead(fIn) if err != nil { return 0, err } defer C.fclose(cIn) cOut, err := dupFileWrite(fOut) if err != nil { return 0, err } defer C.fclose(cOut) var dopts C.zxc_decompress_opts_t dopts.n_threads = C.int(o.threads) if o.checksum { dopts.checksum_enabled = 1 } result := C.zxc_stream_decompress(cIn, cOut, &dopts) if result < 0 { return 0, errorFromCode(result) } return int64(result), nil } zxc-0.10.0/wrappers/go/zxc_dup_unix.go000066400000000000000000000025101517010557200177130ustar00rootroot00000000000000//go:build unix package zxc /* #include #include */ import "C" import ( "fmt" "os" "runtime" ) // C mode strings - allocated once, never freed (intentional; they live for the // process lifetime and avoid per-call C.CString/C.free overhead). var ( cModeRead = C.CString("rb") cModeWrite = C.CString("wb") ) // dupFileRead duplicates a Go *os.File's fd and wraps it in a C FILE* for // reading. The returned FILE* owns its own fd and must be closed with // C.fclose(). func dupFileRead(f *os.File) (*C.FILE, error) { fd := C.int(f.Fd()) dupFd := C.dup(fd) runtime.KeepAlive(f) if dupFd < 0 { return nil, fmt.Errorf("zxc: dup failed for read fd") } cFile := C.fdopen(dupFd, cModeRead) if cFile == nil { C.close(dupFd) return nil, fmt.Errorf("zxc: fdopen failed for read fd") } return cFile, nil } // dupFileWrite duplicates a Go *os.File's fd and wraps it in a C FILE* for // writing. The returned FILE* owns its own fd and must be closed with // C.fclose(). func dupFileWrite(f *os.File) (*C.FILE, error) { fd := C.int(f.Fd()) dupFd := C.dup(fd) runtime.KeepAlive(f) if dupFd < 0 { return nil, fmt.Errorf("zxc: dup failed for write fd") } cFile := C.fdopen(dupFd, cModeWrite) if cFile == nil { C.close(dupFd) return nil, fmt.Errorf("zxc: fdopen failed for write fd") } return cFile, nil } zxc-0.10.0/wrappers/go/zxc_dup_windows.go000066400000000000000000000036641517010557200204350ustar00rootroot00000000000000//go:build windows package zxc /* #include #include #include */ import "C" import ( "fmt" "os" "runtime" ) // C mode strings - allocated once, never freed (intentional; they live for the // process lifetime and avoid per-call C.CString/C.free overhead). var ( cModeRead = C.CString("rb") cModeWrite = C.CString("wb") ) // dupFileRead converts a Go *os.File to a C FILE* for reading on Windows. // // os.File.Fd() returns a Windows HANDLE (not a CRT fd), so we must use // _open_osfhandle() to wrap it as a CRT fd, then _dup() to own it // independently, since _open_osfhandle() transfers ownership of the HANDLE. func dupFileRead(f *os.File) (*C.FILE, error) { handle := C.intptr_t(f.Fd()) runtime.KeepAlive(f) // Wrap the HANDLE as a read-only CRT fd. crtFd := C._open_osfhandle(handle, C._O_RDONLY) if crtFd < 0 { return nil, fmt.Errorf("zxc: _open_osfhandle failed for read fd") } // Dup so we own this fd independently from Go's *os.File. // Do NOT close crtFd: _open_osfhandle transfers HANDLE ownership to the // CRT; closing it would also close the HANDLE still owned by Go. dupFd := C._dup(crtFd) if dupFd < 0 { return nil, fmt.Errorf("zxc: _dup failed for read fd") } cFile := C._fdopen(dupFd, cModeRead) if cFile == nil { C._close(dupFd) return nil, fmt.Errorf("zxc: _fdopen failed for read fd") } return cFile, nil } // dupFileWrite converts a Go *os.File to a C FILE* for writing on Windows. func dupFileWrite(f *os.File) (*C.FILE, error) { handle := C.intptr_t(f.Fd()) runtime.KeepAlive(f) crtFd := C._open_osfhandle(handle, C._O_WRONLY) if crtFd < 0 { return nil, fmt.Errorf("zxc: _open_osfhandle failed for write fd") } dupFd := C._dup(crtFd) if dupFd < 0 { return nil, fmt.Errorf("zxc: _dup failed for write fd") } cFile := C._fdopen(dupFd, cModeWrite) if cFile == nil { C._close(dupFd) return nil, fmt.Errorf("zxc: _fdopen failed for write fd") } return cFile, nil } zxc-0.10.0/wrappers/go/zxc_test.go000066400000000000000000000211231517010557200170400ustar00rootroot00000000000000/* ZXC - High-performance lossless compression Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. SPDX-License-Identifier: BSD-3-Clause */ package zxc import ( "bytes" "fmt" "os" "path/filepath" "testing" ) // ============================================================================ // Buffer API Tests // ============================================================================ func TestRoundtrip(t *testing.T) { data := []byte("Hello, ZXC! This is a test of the Go wrapper.") compressed, err := Compress(data) if err != nil { t.Fatalf("Compress: %v", err) } decompressed, err := Decompress(compressed) if err != nil { t.Fatalf("Decompress: %v", err) } if !bytes.Equal(data, decompressed) { t.Fatal("roundtrip mismatch") } } func TestAllLevels(t *testing.T) { data := []byte("Test data with repetition: CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC") for _, level := range AllLevels() { compressed, err := Compress(data, WithLevel(level)) if err != nil { t.Fatalf("Compress at level %d: %v", level, err) } decompressed, err := Decompress(compressed) if err != nil { t.Fatalf("Decompress at level %d: %v", level, err) } if !bytes.Equal(data, decompressed) { t.Fatalf("roundtrip mismatch at level %d", level) } } } func TestLargeData(t *testing.T) { // 1 MB of compressible data data := make([]byte, 1024*1024) for i := range data { data[i] = byte((i % 256) ^ ((i / 256) % 256)) } compressed, err := Compress(data) if err != nil { t.Fatalf("Compress: %v", err) } if len(compressed) >= len(data) { t.Fatalf("expected compression, got %d >= %d", len(compressed), len(data)) } decompressed, err := Decompress(compressed) if err != nil { t.Fatalf("Decompress: %v", err) } if !bytes.Equal(data, decompressed) { t.Fatal("large data roundtrip mismatch") } } func TestDecompressedSize(t *testing.T) { data := []byte("Hello, world! Testing DecompressedSize function.") compressed, err := Compress(data) if err != nil { t.Fatalf("Compress: %v", err) } size, err := DecompressedSize(compressed) if err != nil { t.Fatalf("DecompressedSize: %v", err) } if size != uint64(len(data)) { t.Fatalf("DecompressedSize = %d, want %d", size, len(data)) } } func TestCompressBound(t *testing.T) { bound := CompressBound(1024) if bound <= 1024 { t.Fatalf("CompressBound(1024) = %d, want > 1024", bound) } } func TestInvalidData(t *testing.T) { _, err := Decompress([]byte("not valid zxc data")) if err == nil { t.Fatal("expected error on invalid data") } } func TestEmptyInput(t *testing.T) { _, err := Compress(nil) if err == nil { t.Fatal("expected error on nil input") } _, err = Compress([]byte{}) if err == nil { t.Fatal("expected error on empty input") } } func TestChecksumRoundtrip(t *testing.T) { data := []byte("Test with checksum enabled for data integrity verification") compressed, err := Compress(data, WithChecksum(true)) if err != nil { t.Fatalf("Compress with checksum: %v", err) } decompressed, err := Decompress(compressed, WithChecksum(true)) if err != nil { t.Fatalf("Decompress with checksum: %v", err) } if !bytes.Equal(data, decompressed) { t.Fatal("checksum roundtrip mismatch") } } func TestCompressTo(t *testing.T) { data := []byte("Testing CompressTo with pre-allocated buffer") output := make([]byte, CompressBound(len(data))) n, err := CompressTo(data, output) if err != nil { t.Fatalf("CompressTo: %v", err) } decompressed, err := Decompress(output[:n]) if err != nil { t.Fatalf("Decompress: %v", err) } if !bytes.Equal(data, decompressed) { t.Fatal("CompressTo roundtrip mismatch") } } func TestVersion(t *testing.T) { major, minor, patch := Version() s := VersionString() if s == "" { t.Fatal("VersionString returned empty") } expected := fmt.Sprintf("%d.%d.%d", major, minor, patch) if s != expected { t.Fatalf("VersionString() = %q, want %q", s, expected) } t.Logf("ZXC version: %s", s) } func TestErrorMessages(t *testing.T) { errors := []*Error{ ErrMemory, ErrDstTooSmall, ErrCorruptData, ErrBadChecksum, } for _, e := range errors { msg := e.Error() if msg == "" { t.Fatalf("expected non-empty error message for code %d", e.Code) } } } // ============================================================================ // Streaming API Tests // ============================================================================ func tempPath(t *testing.T, name string) string { t.Helper() dir := filepath.Join(os.TempDir(), "zxc_go_test") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } return filepath.Join(dir, name) } func TestFileRoundtrip(t *testing.T) { inputPath := tempPath(t, "roundtrip_input.bin") compressedPath := tempPath(t, "roundtrip_compressed.zxc") outputPath := tempPath(t, "roundtrip_output.bin") defer os.Remove(inputPath) defer os.Remove(compressedPath) defer os.Remove(outputPath) // 64 KB of compressible data data := make([]byte, 64*1024) for i := range data { data[i] = byte((i % 256) ^ ((i / 256) % 256)) } if err := os.WriteFile(inputPath, data, 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } csize, err := CompressFile(inputPath, compressedPath) if err != nil { t.Fatalf("CompressFile: %v", err) } if csize <= 0 { t.Fatalf("CompressFile returned %d bytes", csize) } dsize, err := DecompressFile(compressedPath, outputPath) if err != nil { t.Fatalf("DecompressFile: %v", err) } if dsize != int64(len(data)) { t.Fatalf("DecompressFile: got %d bytes, want %d", dsize, len(data)) } result, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("ReadFile: %v", err) } if !bytes.Equal(data, result) { t.Fatal("file roundtrip mismatch") } } func TestFileAllLevels(t *testing.T) { inputPath := tempPath(t, "levels_input.bin") defer os.Remove(inputPath) data := make([]byte, 32*1024) for i := range data { data[i] = byte((i % 256) ^ ((i / 256) % 256)) } if err := os.WriteFile(inputPath, data, 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } for _, level := range AllLevels() { compressedPath := tempPath(t, "levels_compressed.zxc") outputPath := tempPath(t, "levels_output.bin") _, err := CompressFile(inputPath, compressedPath, WithLevel(level)) if err != nil { t.Fatalf("CompressFile at level %d: %v", level, err) } _, err = DecompressFile(compressedPath, outputPath) if err != nil { t.Fatalf("DecompressFile at level %d: %v", level, err) } result, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("ReadFile: %v", err) } if !bytes.Equal(data, result) { t.Fatalf("file roundtrip mismatch at level %d", level) } os.Remove(compressedPath) os.Remove(outputPath) } } func TestFileMultithreaded(t *testing.T) { inputPath := tempPath(t, "mt_input.bin") compressedPath := tempPath(t, "mt_compressed.zxc") outputPath := tempPath(t, "mt_output.bin") defer os.Remove(inputPath) defer os.Remove(compressedPath) defer os.Remove(outputPath) // 1 MB of data data := make([]byte, 1024*1024) for i := range data { data[i] = byte((i % 256) ^ ((i / 256) % 256)) } if err := os.WriteFile(inputPath, data, 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } for _, threads := range []int{1, 2, 4} { _, err := CompressFile(inputPath, compressedPath, WithThreads(threads)) if err != nil { t.Fatalf("CompressFile with %d threads: %v", threads, err) } dsize, err := DecompressFile(compressedPath, outputPath, WithThreads(threads)) if err != nil { t.Fatalf("DecompressFile with %d threads: %v", threads, err) } if dsize != int64(len(data)) { t.Fatalf("got %d bytes with %d threads, want %d", dsize, threads, len(data)) } result, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("ReadFile: %v", err) } if !bytes.Equal(data, result) { t.Fatalf("mismatch with %d threads", threads) } } } // ============================================================================ // Benchmarks // ============================================================================ func BenchmarkCompress(b *testing.B) { data := make([]byte, 1024*1024) for i := range data { data[i] = byte((i % 256) ^ ((i / 256) % 256)) } b.SetBytes(int64(len(data))) b.ResetTimer() for i := 0; i < b.N; i++ { _, err := Compress(data) if err != nil { b.Fatal(err) } } } func BenchmarkDecompress(b *testing.B) { data := make([]byte, 1024*1024) for i := range data { data[i] = byte((i % 256) ^ ((i / 256) % 256)) } compressed, err := Compress(data) if err != nil { b.Fatal(err) } b.SetBytes(int64(len(data))) b.ResetTimer() for i := 0; i < b.N; i++ { _, err := Decompress(compressed) if err != nil { b.Fatal(err) } } } zxc-0.10.0/wrappers/nodejs/000077500000000000000000000000001517010557200155245ustar00rootroot00000000000000zxc-0.10.0/wrappers/nodejs/.gitignore000066400000000000000000000000711517010557200175120ustar00rootroot00000000000000node_modules/ build/ prebuilds/ *.node package-lock.json zxc-0.10.0/wrappers/nodejs/.nvmrc000066400000000000000000000000021517010557200166420ustar00rootroot0000000000000022zxc-0.10.0/wrappers/nodejs/CMakeLists.txt000066400000000000000000000027741517010557200202760ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.14) project(zxc_nodejs C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # ---- Core ZXC library ---- add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../ ${CMAKE_CURRENT_BINARY_DIR}/zxc_core_build) # ---- Node.js addon ---- include_directories(${CMAKE_JS_INC}) file(GLOB SOURCE_FILES "src/zxc_addon.cc") add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node" ) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../include ${CMAKE_JS_INC} ) # Generate node.lib on Windows (cmake-js provides .def but not .lib) if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) endif() target_link_libraries(${PROJECT_NAME} PRIVATE zxc_lib ${CMAKE_JS_LIB} ) # node-addon-api (header-only, no exceptions by default) execute_process( COMMAND node -p "require('node-addon-api').include" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NAPI_INCLUDE_DIR OUTPUT_STRIP_TRAILING_WHITESPACE ) # Strip surrounding quotes if present string(REPLACE "\"" "" NAPI_INCLUDE_DIR ${NAPI_INCLUDE_DIR}) target_include_directories(${PROJECT_NAME} PRIVATE ${NAPI_INCLUDE_DIR}) target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) zxc-0.10.0/wrappers/nodejs/README.md000066400000000000000000000050031517010557200170010ustar00rootroot00000000000000# ZXC Node.js Bindings High-performance Node.js bindings for the **ZXC** asymmetric compressor, optimized for **fast decompression**. Designed for *Write Once, Read Many* workloads like ML datasets, game assets, and caches. ## Features - **Blazing fast decompression** - ZXC is specifically optimized for read-heavy workloads. - **Native N-API addon** - compiled C code via cmake-js for maximum performance. - **Buffer support** - works directly with Node.js `Buffer` objects. - **TypeScript declarations** - full type definitions included. ## Installation (from source) ```bash git clone https://github.com/hellobertrand/zxc.git cd zxc/wrappers/nodejs npm install ``` ### Prerequisites - Node.js >= 16.0.0 - CMake >= 3.14 - A C17/C++17 compiler (GCC, Clang, or MSVC) ## Usage ```javascript const zxc = require('zxc-compress'); // Compress const data = Buffer.from('Hello, World!'.repeat(1_000)); const compressed = zxc.compress(data, { level: zxc.LEVEL_DEFAULT }); // Decompress (auto-detects size) const decompressed = zxc.decompress(compressed); console.log(`Original: ${data.length} bytes`); console.log(`Compressed: ${compressed.length} bytes`); console.log(`Ratio: ${(compressed.length / data.length * 100).toFixed(1)}%`); ``` ## API ### `compress(data, options?)` Compress a Buffer. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `data` | `Buffer` | - | Input data | | `options.level` | `number` | `LEVEL_DEFAULT` | Compression level (1–5) | | `options.checksum` | `boolean` | `false` | Enable checksum | Returns: `Buffer` - compressed data. ### `decompress(data, options?)` Decompress a ZXC compressed Buffer. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `data` | `Buffer` | - | Compressed data | | `options.size` | `number` | auto | Expected decompressed size | | `options.checksum` | `boolean` | `false` | Verify checksum | Returns: `Buffer` - decompressed data. ### `compressBound(inputSize)` Returns the maximum compressed size for a given input size. ### `getDecompressedSize(data)` Returns the original size from a ZXC compressed buffer (reads footer only). ### Constants | Constant | Value | Description | |----------|-------|-------------| | `LEVEL_FASTEST` | 1 | Fastest compression | | `LEVEL_FAST` | 2 | Fast compression | | `LEVEL_DEFAULT` | 3 | Recommended balance | | `LEVEL_BALANCED` | 4 | Good ratio, good speed | | `LEVEL_COMPACT` | 5 | Highest density | ## Testing ```bash npm test ``` ## License BSD-3-Clause zxc-0.10.0/wrappers/nodejs/lib/000077500000000000000000000000001517010557200162725ustar00rootroot00000000000000zxc-0.10.0/wrappers/nodejs/lib/index.d.ts000066400000000000000000000057021517010557200201770ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** Fastest compression, best for real-time applications. */ export const LEVEL_FASTEST: number; /** Fast compression, good for real-time applications. */ export const LEVEL_FAST: number; /** Recommended: ratio > LZ4, decode speed > LZ4. */ export const LEVEL_DEFAULT: number; /** Good ratio, good decode speed. */ export const LEVEL_BALANCED: number; /** High density. Best for storage/firmware/assets. */ export const LEVEL_COMPACT: number; /** Memory allocation failure. */ export const ERROR_MEMORY: number; /** Destination buffer too small. */ export const ERROR_DST_TOO_SMALL: number; /** Source buffer too small or truncated input. */ export const ERROR_SRC_TOO_SMALL: number; /** Invalid magic word in file header. */ export const ERROR_BAD_MAGIC: number; /** Unsupported file format version. */ export const ERROR_BAD_VERSION: number; /** Corrupted or invalid header (CRC mismatch). */ export const ERROR_BAD_HEADER: number; /** Block or global checksum verification failed. */ export const ERROR_BAD_CHECKSUM: number; /** Corrupted compressed data. */ export const ERROR_CORRUPT_DATA: number; /** Invalid match offset during decompression. */ export const ERROR_BAD_OFFSET: number; /** Buffer overflow detected during processing. */ export const ERROR_OVERFLOW: number; /** Read/write/seek failure on file. */ export const ERROR_IO: number; /** Required input pointer is NULL. */ export const ERROR_NULL_INPUT: number; /** Unknown or unexpected block type. */ export const ERROR_BAD_BLOCK_TYPE: number; /** Invalid block size. */ export const ERROR_BAD_BLOCK_SIZE: number; export interface CompressOptions { /** Compression level (1-5). Defaults to LEVEL_DEFAULT. */ level?: number; /** Enable checksum verification. Defaults to false. */ checksum?: boolean; /** Enable seek table for random-access decompression. Defaults to false. */ seekable?: boolean; } export interface DecompressOptions { /** Expected decompressed size. If omitted, read from header. */ size?: number; /** Enable checksum verification. Defaults to false. */ checksum?: boolean; } /** * Returns the maximum compressed size for a given input size. * Useful for pre-allocating output buffers. */ export function compressBound(inputSize: number): number; /** * Compress a Buffer using the ZXC algorithm. */ export function compress(data: Buffer, options?: CompressOptions): Buffer; /** * Returns the original decompressed size from a ZXC compressed buffer. * Reads the footer without performing decompression. */ export function getDecompressedSize(data: Buffer): number; /** * Decompress a ZXC compressed Buffer. */ export function decompress(data: Buffer, options?: DecompressOptions): Buffer; /** * Returns a human-readable name for a given error code. */ export function errorName(code: number): string; zxc-0.10.0/wrappers/nodejs/lib/index.js000066400000000000000000000116301517010557200177400ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ 'use strict'; const native = require('../build/Release/zxc_nodejs.node'); // Re-export compression level constants const LEVEL_FASTEST = native.LEVEL_FASTEST; const LEVEL_FAST = native.LEVEL_FAST; const LEVEL_DEFAULT = native.LEVEL_DEFAULT; const LEVEL_BALANCED = native.LEVEL_BALANCED; const LEVEL_COMPACT = native.LEVEL_COMPACT; // Re-export error constants const ERROR_MEMORY = native.ERROR_MEMORY; const ERROR_DST_TOO_SMALL = native.ERROR_DST_TOO_SMALL; const ERROR_SRC_TOO_SMALL = native.ERROR_SRC_TOO_SMALL; const ERROR_BAD_MAGIC = native.ERROR_BAD_MAGIC; const ERROR_BAD_VERSION = native.ERROR_BAD_VERSION; const ERROR_BAD_HEADER = native.ERROR_BAD_HEADER; const ERROR_BAD_CHECKSUM = native.ERROR_BAD_CHECKSUM; const ERROR_CORRUPT_DATA = native.ERROR_CORRUPT_DATA; const ERROR_BAD_OFFSET = native.ERROR_BAD_OFFSET; const ERROR_OVERFLOW = native.ERROR_OVERFLOW; const ERROR_IO = native.ERROR_IO; const ERROR_NULL_INPUT = native.ERROR_NULL_INPUT; const ERROR_BAD_BLOCK_TYPE = native.ERROR_BAD_BLOCK_TYPE; const ERROR_BAD_BLOCK_SIZE = native.ERROR_BAD_BLOCK_SIZE; /** * Returns a human-readable name for a given error code. * * @param {number} code - ZXC error code. * @returns {string} Error name (e.g., "ZXC_ERROR_DST_TOO_SMALL"). */ function errorName(code) { if (typeof code !== 'number') { throw new TypeError('code must be a number'); } return native.errorName(code); } /** * Returns the maximum compressed size for a given input size. * Useful for pre-allocating output buffers. * * @param {number} inputSize - Size of the input data in bytes. * @returns {number} Maximum required buffer size in bytes. */ function compressBound(inputSize) { if (typeof inputSize !== 'number' || inputSize < 0) { throw new TypeError('inputSize must be a non-negative number'); } return native.compressBound(inputSize); } /** * Compress a Buffer using the ZXC algorithm. * * @param {Buffer} data - Buffer to compress. * @param {object} [options] - Compression options. * @param {number} [options.level=LEVEL_DEFAULT] - Compression level (1-5). * @param {boolean} [options.checksum=false] - Enable checksum verification. * @param {boolean} [options.seekable=false] - Enable seek table for random-access decompression. * @returns {Buffer} Compressed data. */ function compress(data, options = {}) { if (!Buffer.isBuffer(data)) { throw new TypeError('data must be a Buffer'); } const level = options.level !== undefined ? options.level : LEVEL_DEFAULT; const checksum = options.checksum !== undefined ? options.checksum : false; const seekable = options.seekable !== undefined ? options.seekable : false; if (data.length === 0 && !checksum) { return Buffer.alloc(0); } return native.compress(data, level, checksum, seekable); } /** * Returns the original decompressed size from a ZXC compressed buffer. * Reads the footer without performing decompression. * * @param {Buffer} data - Compressed data buffer. * @returns {number} Original uncompressed size in bytes, or 0 if invalid. */ function getDecompressedSize(data) { if (!Buffer.isBuffer(data)) { throw new TypeError('data must be a Buffer'); } return native.getDecompressedSize(data); } /** * Decompress a ZXC compressed Buffer. * * @param {Buffer} data - Compressed data. * @param {object} [options] - Decompression options. * @param {number} [options.size] - Expected decompressed size. If omitted, read from header. * @param {boolean} [options.checksum=false] - Enable checksum verification. * @returns {Buffer} Decompressed data. */ function decompress(data, options = {}) { if (!Buffer.isBuffer(data)) { throw new TypeError('data must be a Buffer'); } const checksum = options.checksum !== undefined ? options.checksum : false; if (data.length === 0 && !checksum) { return Buffer.alloc(0); } let size = options.size; if (size === undefined) { size = getDecompressedSize(data); if (size === 0) { throw new Error('Invalid ZXC header or data too short to determine size'); } } return native.decompress(data, size, checksum); } module.exports = { // Functions compress, decompress, compressBound, getDecompressedSize, // Constants LEVEL_FASTEST, LEVEL_FAST, LEVEL_DEFAULT, LEVEL_BALANCED, LEVEL_COMPACT, // Error handling errorName, ERROR_MEMORY, ERROR_DST_TOO_SMALL, ERROR_SRC_TOO_SMALL, ERROR_BAD_MAGIC, ERROR_BAD_VERSION, ERROR_BAD_HEADER, ERROR_BAD_CHECKSUM, ERROR_CORRUPT_DATA, ERROR_BAD_OFFSET, ERROR_OVERFLOW, ERROR_IO, ERROR_NULL_INPUT, ERROR_BAD_BLOCK_TYPE, ERROR_BAD_BLOCK_SIZE, }; zxc-0.10.0/wrappers/nodejs/package.json000066400000000000000000000017521517010557200200170ustar00rootroot00000000000000{ "name": "zxc-compress", "version": "0.10.0", "description": "ZXC: High-performance lossless asymmetric compression for Node.js", "main": "lib/index.js", "types": "lib/index.d.ts", "files": [ "lib/", "src/", "CMakeLists.txt" ], "scripts": { "install": "cmake-js compile", "build": "cmake-js compile", "rebuild": "cmake-js rebuild", "test": "vitest run" }, "keywords": [ "compression", "decompression", "lossless", "zxc", "performance", "lz77" ], "author": "Bertrand Lebonnois", "license": "BSD-3-Clause", "repository": { "type": "git", "url": "https://github.com/hellobertrand/zxc.git", "directory": "wrappers/nodejs" }, "homepage": "https://github.com/hellobertrand/zxc", "engines": { "node": ">=16.0.0" }, "dependencies": { "node-addon-api": "^8.5.0", "cmake-js": "^8.0.0" }, "devDependencies": { "vitest": "^4.0.0" }, "binary": { "napi_versions": [ 8 ] } }zxc-0.10.0/wrappers/nodejs/src/000077500000000000000000000000001517010557200163135ustar00rootroot00000000000000zxc-0.10.0/wrappers/nodejs/src/zxc_addon.cc000066400000000000000000000204501517010557200205740ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ #include extern "C" { #include "zxc.h" } // ============================================================================= // compressBound(inputSize: number): number // ============================================================================= static Napi::Value CompressBound(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Expected a number (inputSize)").ThrowAsJavaScriptException(); return env.Undefined(); } uint64_t input_size = info[0].As().Int64Value(); uint64_t bound = zxc_compress_bound(static_cast(input_size)); return Napi::Number::New(env, static_cast(bound)); } // ============================================================================= // compress(buffer: Buffer, level?: number, checksum?: boolean, seekable?: boolean): Buffer // ============================================================================= static Napi::Value Compress(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsBuffer()) { Napi::TypeError::New(env, "Expected a Buffer as first argument") .ThrowAsJavaScriptException(); return env.Undefined(); } Napi::Buffer src_buf = info[0].As>(); const void* src = src_buf.Data(); size_t src_size = src_buf.Length(); int level = ZXC_LEVEL_DEFAULT; if (info.Length() >= 2 && info[1].IsNumber()) { level = info[1].As().Int32Value(); } int checksum = 0; if (info.Length() >= 3 && info[2].IsBoolean()) { checksum = info[2].As().Value() ? 1 : 0; } int seekable = 0; if (info.Length() >= 4 && info[3].IsBoolean()) { seekable = info[3].As().Value() ? 1 : 0; } // Handle empty input if (src_size == 0 && !checksum) { return Napi::Buffer::New(env, 0); } uint64_t bound = zxc_compress_bound(src_size); Napi::Buffer dst_buf = Napi::Buffer::New(env, static_cast(bound)); zxc_compress_opts_t opts = {0}; opts.level = level; opts.checksum_enabled = checksum; opts.seekable = seekable; int64_t nwritten = zxc_compress(src, src_size, dst_buf.Data(), static_cast(bound), &opts); if (nwritten < 0) { int err_code = static_cast(nwritten); Napi::Error err = Napi::Error::New(env, zxc_error_name(err_code)); err.Set("code", Napi::Number::New(env, err_code)); err.ThrowAsJavaScriptException(); return env.Undefined(); } if (nwritten == 0) { Napi::Error::New(env, "Input is too small to be compressed").ThrowAsJavaScriptException(); return env.Undefined(); } // Return a slice of the buffer with the actual size return Napi::Buffer::Copy(env, dst_buf.Data(), static_cast(nwritten)); } // ============================================================================= // decompress(buffer: Buffer, decompressSize: number, checksum?: boolean): Buffer // ============================================================================= static Napi::Value Decompress(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (info.Length() < 2 || !info[0].IsBuffer() || !info[1].IsNumber()) { Napi::TypeError::New(env, "Expected (Buffer, number) as arguments") .ThrowAsJavaScriptException(); return env.Undefined(); } Napi::Buffer src_buf = info[0].As>(); const void* src = src_buf.Data(); size_t src_size = src_buf.Length(); size_t decompress_size = static_cast(info[1].As().Int64Value()); int checksum = 0; if (info.Length() >= 3 && info[2].IsBoolean()) { checksum = info[2].As().Value() ? 1 : 0; } Napi::Buffer dst_buf = Napi::Buffer::New(env, decompress_size); zxc_decompress_opts_t dopts = {0}; dopts.checksum_enabled = checksum; int64_t nwritten = zxc_decompress(src, src_size, dst_buf.Data(), decompress_size, &dopts); if (nwritten < 0) { int err_code = static_cast(nwritten); Napi::Error err = Napi::Error::New(env, zxc_error_name(err_code)); err.Set("code", Napi::Number::New(env, err_code)); err.ThrowAsJavaScriptException(); return env.Undefined(); } return dst_buf; } // ============================================================================= // getDecompressedSize(buffer: Buffer): number // ============================================================================= static Napi::Value GetDecompressedSize(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsBuffer()) { Napi::TypeError::New(env, "Expected a Buffer as first argument") .ThrowAsJavaScriptException(); return env.Undefined(); } Napi::Buffer src_buf = info[0].As>(); uint64_t size = zxc_get_decompressed_size(src_buf.Data(), src_buf.Length()); return Napi::Number::New(env, static_cast(size)); } // ============================================================================= // errorName(code: number): string // ============================================================================= static Napi::Value ErrorName(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Expected a number (error code)").ThrowAsJavaScriptException(); return env.Undefined(); } int code = info[0].As().Int32Value(); const char* name = zxc_error_name(code); return Napi::String::New(env, name); } // ============================================================================= // Module initialization // ============================================================================= static Napi::Object Init(Napi::Env env, Napi::Object exports) { // Functions exports.Set("compressBound", Napi::Function::New(env, CompressBound, "compressBound")); exports.Set("compress", Napi::Function::New(env, Compress, "compress")); exports.Set("decompress", Napi::Function::New(env, Decompress, "decompress")); exports.Set("getDecompressedSize", Napi::Function::New(env, GetDecompressedSize, "getDecompressedSize")); exports.Set("errorName", Napi::Function::New(env, ErrorName, "errorName")); // Compression level constants exports.Set("LEVEL_FASTEST", Napi::Number::New(env, ZXC_LEVEL_FASTEST)); exports.Set("LEVEL_FAST", Napi::Number::New(env, ZXC_LEVEL_FAST)); exports.Set("LEVEL_DEFAULT", Napi::Number::New(env, ZXC_LEVEL_DEFAULT)); exports.Set("LEVEL_BALANCED", Napi::Number::New(env, ZXC_LEVEL_BALANCED)); exports.Set("LEVEL_COMPACT", Napi::Number::New(env, ZXC_LEVEL_COMPACT)); // Error constants exports.Set("ERROR_MEMORY", Napi::Number::New(env, ZXC_ERROR_MEMORY)); exports.Set("ERROR_DST_TOO_SMALL", Napi::Number::New(env, ZXC_ERROR_DST_TOO_SMALL)); exports.Set("ERROR_SRC_TOO_SMALL", Napi::Number::New(env, ZXC_ERROR_SRC_TOO_SMALL)); exports.Set("ERROR_BAD_MAGIC", Napi::Number::New(env, ZXC_ERROR_BAD_MAGIC)); exports.Set("ERROR_BAD_VERSION", Napi::Number::New(env, ZXC_ERROR_BAD_VERSION)); exports.Set("ERROR_BAD_HEADER", Napi::Number::New(env, ZXC_ERROR_BAD_HEADER)); exports.Set("ERROR_BAD_CHECKSUM", Napi::Number::New(env, ZXC_ERROR_BAD_CHECKSUM)); exports.Set("ERROR_CORRUPT_DATA", Napi::Number::New(env, ZXC_ERROR_CORRUPT_DATA)); exports.Set("ERROR_BAD_OFFSET", Napi::Number::New(env, ZXC_ERROR_BAD_OFFSET)); exports.Set("ERROR_OVERFLOW", Napi::Number::New(env, ZXC_ERROR_OVERFLOW)); exports.Set("ERROR_IO", Napi::Number::New(env, ZXC_ERROR_IO)); exports.Set("ERROR_NULL_INPUT", Napi::Number::New(env, ZXC_ERROR_NULL_INPUT)); exports.Set("ERROR_BAD_BLOCK_TYPE", Napi::Number::New(env, ZXC_ERROR_BAD_BLOCK_TYPE)); exports.Set("ERROR_BAD_BLOCK_SIZE", Napi::Number::New(env, ZXC_ERROR_BAD_BLOCK_SIZE)); return exports; } NODE_API_MODULE(zxc_nodejs, Init) zxc-0.10.0/wrappers/nodejs/test/000077500000000000000000000000001517010557200165035ustar00rootroot00000000000000zxc-0.10.0/wrappers/nodejs/test/zxc.test.js000066400000000000000000000144111517010557200206240ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ 'use strict'; const zxc = require('../lib/index'); // ============================================================================= // Roundtrip tests // ============================================================================= describe('compress/decompress roundtrip', () => { const testCases = [ { name: 'normal data', data: Buffer.from('hello world'.repeat(10)) }, { name: 'single byte', data: Buffer.from('a') }, { name: 'empty buffer', data: Buffer.alloc(0) }, { name: 'large 10 MB', data: Buffer.alloc(10_000_000, 0x42) }, ]; for (const { name, data } of testCases) { test(`roundtrip: ${name}`, () => { for (let level = zxc.LEVEL_FASTEST; level <= zxc.LEVEL_COMPACT; level++) { const compressed = zxc.compress(data, { level }); const size = zxc.getDecompressedSize(compressed); const decompressed = zxc.decompress(compressed, { size }); expect(decompressed.length).toBe(data.length); expect(Buffer.compare(decompressed, data)).toBe(0); } }); } test('roundtrip with auto-size detection', () => { const data = Buffer.from('auto size detection test'.repeat(100)); const compressed = zxc.compress(data); const decompressed = zxc.decompress(compressed); expect(decompressed.length).toBe(data.length); expect(Buffer.compare(decompressed, data)).toBe(0); }); }); // ============================================================================= // compressBound // ============================================================================= describe('compressBound', () => { test('returns a value >= input size', () => { const bound = zxc.compressBound(1024); expect(bound).toBeGreaterThanOrEqual(1024); }); test('returns 0-based bound for size 0', () => { const bound = zxc.compressBound(0); expect(bound).toBeGreaterThanOrEqual(0); }); }); // ============================================================================= // Corruption detection // ============================================================================= describe('corruption detection', () => { test('detects corrupted data with checksum', () => { const data = Buffer.from('hello world'.repeat(10)); const compressed = zxc.compress(data, { checksum: true }); // Corrupt last byte const corrupted = Buffer.from(compressed); corrupted[corrupted.length - 1] ^= 0x01; expect(() => { zxc.decompress(corrupted, { size: data.length, checksum: true }); }).toThrow(); }); test('throws on truncated compressed input', () => { expect(() => { zxc.decompress(Buffer.from([0x01, 0x02, 0x03]), { size: 100 }); }).toThrow(); }); }); // ============================================================================= // Invalid input types // ============================================================================= describe('invalid input handling', () => { test('compress rejects non-Buffer', () => { expect(() => zxc.compress('string')).toThrow(TypeError); expect(() => zxc.compress(123)).toThrow(TypeError); expect(() => zxc.compress(null)).toThrow(TypeError); }); test('decompress rejects non-Buffer', () => { expect(() => zxc.decompress('string')).toThrow(TypeError); expect(() => zxc.decompress(123)).toThrow(TypeError); }); test('getDecompressedSize rejects non-Buffer', () => { expect(() => zxc.getDecompressedSize('string')).toThrow(TypeError); }); test('compressBound rejects non-number', () => { expect(() => zxc.compressBound('abc')).toThrow(TypeError); }); }); // ============================================================================= // Constants // ============================================================================= describe('constants', () => { test('compression levels are defined', () => { expect(zxc.LEVEL_FASTEST).toBe(1); expect(zxc.LEVEL_FAST).toBe(2); expect(zxc.LEVEL_DEFAULT).toBe(3); expect(zxc.LEVEL_BALANCED).toBe(4); expect(zxc.LEVEL_COMPACT).toBe(5); }); }); // ============================================================================= // Error Handling // ============================================================================= describe('error handling', () => { test('error constants are defined', () => { expect(zxc.ERROR_MEMORY).toBe(-1); expect(zxc.ERROR_DST_TOO_SMALL).toBe(-2); expect(zxc.ERROR_SRC_TOO_SMALL).toBe(-3); expect(zxc.ERROR_BAD_MAGIC).toBe(-4); expect(zxc.ERROR_BAD_VERSION).toBe(-5); expect(zxc.ERROR_BAD_HEADER).toBe(-6); expect(zxc.ERROR_BAD_CHECKSUM).toBe(-7); expect(zxc.ERROR_CORRUPT_DATA).toBe(-8); expect(zxc.ERROR_BAD_OFFSET).toBe(-9); expect(zxc.ERROR_OVERFLOW).toBe(-10); expect(zxc.ERROR_IO).toBe(-11); expect(zxc.ERROR_NULL_INPUT).toBe(-12); expect(zxc.ERROR_BAD_BLOCK_TYPE).toBe(-13); expect(zxc.ERROR_BAD_BLOCK_SIZE).toBe(-14); }); test('errorName works', () => { expect(zxc.errorName(zxc.ERROR_DST_TOO_SMALL)).toBe('ZXC_ERROR_DST_TOO_SMALL'); expect(zxc.errorName(-999)).toBe('ZXC_UNKNOWN_ERROR'); }); test('thrown errors have code property', () => { try { zxc.decompress(Buffer.from([0x01, 0x02, 0x03]), { size: 100 }); throw new Error('Should have thrown'); } catch (err) { expect(err.code).toBe(zxc.ERROR_NULL_INPUT); expect(err.message).toBe('ZXC_ERROR_NULL_INPUT'); } }); test('detects bad magic with correct code', () => { try { // Provide enough bytes (16 for header) but fake magic const fakeHeader = Buffer.alloc(32); fakeHeader.write('FAKE', 0, 'utf8'); zxc.decompress(fakeHeader, { size: 100 }); throw new Error('Should have thrown'); } catch (err) { expect(err.code).toBe(zxc.ERROR_BAD_HEADER); } }); }); zxc-0.10.0/wrappers/nodejs/vitest.config.mjs000066400000000000000000000001631517010557200210210ustar00rootroot00000000000000import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, }, }); zxc-0.10.0/wrappers/python/000077500000000000000000000000001517010557200155635ustar00rootroot00000000000000zxc-0.10.0/wrappers/python/.gitignore000066400000000000000000000014011517010557200175470ustar00rootroot00000000000000# Editor temporary/working/backup files # ######################################### .#* [#]*# *~ *$ *.bak *.diff .idea/ *.iml *.ipr *.iws *.org .project pmip *.rej .settings/ .*.sw[nop] .sw[nop] *.tmp *.vim .vscode tags cscope.out # gnu global GPATH GRTAGS GSYMS GTAGS .cache .mypy_cache/ # Compiled source # ################### *.a *.com *.class *.dll *.exe *.o *.o.d *.py[ocd] *.so *.mod # Compiled source # ################### build build-* _build _version.py # Egg metadata *.egg-info venv/ # OS generated files # ###################### .DS_Store* .VolumeIcon.icns .fseventsd Icon? .gdb_history ehthumbs.db Thumbs.db .directory # pytest generated files # ########################## /.pytest_cache # benchmarking # ########################## heaptrack* *.out zxc-0.10.0/wrappers/python/CMakeLists.txt000066400000000000000000000010751517010557200203260ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.20) project(zxc_python C) find_package(Python COMPONENTS Development.Module REQUIRED) # ---- Core ---- add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../ ${CMAKE_CURRENT_BINARY_DIR}/zxc_core_build) # ---- Python extension ---- Python_add_library(_zxc MODULE src/zxc/_zxc.c) # Without "lib" prefix set_target_properties(_zxc PROPERTIES PREFIX "") target_include_directories(_zxc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) target_link_libraries(_zxc PRIVATE zxc_lib) install(TARGETS _zxc DESTINATION zxc)zxc-0.10.0/wrappers/python/README.md000066400000000000000000000013751517010557200170500ustar00rootroot00000000000000# ZXC Python Bindings High-performance Python bindings for the **ZXC** asymmetric compressor, optimized for **fast decompression**. Designed for *Write Once, Read Many* workloads like ML datasets, game assets, and caches. ## Features - **Blazing fast decompression** - ZXC is specifically optimized for read-heavy workloads. - **Buffer protocol support** - works with `bytes`, `bytearray`, `memoryview`, and even NumPy arrays. - **Releases the GIL** during compression/decompression - true parallelism with Python threads. - **Stream helpers** - compress/decompress file-like objects. ## Installation (from source) ```bash git clone https://github.com/hellobertrand/zxc.git cd zxc/wrappers/python python -m venv .venv source .venv/bin/activate pip install .zxc-0.10.0/wrappers/python/pyproject.toml000066400000000000000000000011341517010557200204760ustar00rootroot00000000000000[build-system] requires = ["scikit-build-core>=0.9", "setuptools_scm[toml]>=7.0"] build-backend = "scikit_build_core.build" [project] name = "zxc-compress" dynamic = ["version"] description = "ZXC: Package for high-performance, lossless, asymmetric compressions" readme = "README.md" license = "BSD-3-Clause" [project.urls] Homepage = "https://github.com/hellobertrand/zxc" [tool.scikit-build] metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" wheel.packages = ["src/zxc"] [tool.setuptools_scm] root = "../../" version_scheme = "only-version" local_scheme = "no-local-version"zxc-0.10.0/wrappers/python/src/000077500000000000000000000000001517010557200163525ustar00rootroot00000000000000zxc-0.10.0/wrappers/python/src/zxc/000077500000000000000000000000001517010557200171565ustar00rootroot00000000000000zxc-0.10.0/wrappers/python/src/zxc/__init__.py000066400000000000000000000133371517010557200212760ustar00rootroot00000000000000""" ZXC - High-performance lossless compression Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. SPDX-License-Identifier: BSD-3-Clause """ from ._zxc import ( pyzxc_compress, pyzxc_decompress, pyzxc_stream_compress, pyzxc_stream_decompress, pyzxc_get_decompressed_size, LEVEL_FASTEST, LEVEL_FAST, LEVEL_DEFAULT, LEVEL_BALANCED, LEVEL_COMPACT, ERROR_MEMORY, ERROR_DST_TOO_SMALL, ERROR_SRC_TOO_SMALL, ERROR_BAD_MAGIC, ERROR_BAD_VERSION, ERROR_BAD_HEADER, ERROR_BAD_CHECKSUM, ERROR_CORRUPT_DATA, ERROR_BAD_OFFSET, ERROR_OVERFLOW, ERROR_IO, ERROR_NULL_INPUT, ERROR_BAD_BLOCK_TYPE, ERROR_BAD_BLOCK_SIZE, ) try: from ._version import __version__ except ImportError: __version__ = "0.0.0-dev" __all__ = [ # Functions "compress", "decompress", "stream_compress", "stream_decompress", "pyzxc_get_decompressed_size", # Constants "LEVEL_FASTEST", "LEVEL_FAST", "LEVEL_DEFAULT", "LEVEL_BALANCED", "LEVEL_COMPACT", # Error Constants "ERROR_MEMORY", "ERROR_DST_TOO_SMALL", "ERROR_SRC_TOO_SMALL", "ERROR_BAD_MAGIC", "ERROR_BAD_VERSION", "ERROR_BAD_HEADER", "ERROR_BAD_CHECKSUM", "ERROR_CORRUPT_DATA", "ERROR_BAD_OFFSET", "ERROR_OVERFLOW", "ERROR_IO", "ERROR_NULL_INPUT", "ERROR_BAD_BLOCK_TYPE", "ERROR_BAD_BLOCK_SIZE", ] def compress(data, level = LEVEL_DEFAULT, checksum = False) -> bytes: """Compress a bytes object. Args: data: Bytes-like object to compress. level: Compression level. Use constants like LEVEL_FASTEST, LEVEL_DEFAULT, etc. checksum: If True, append a checksum for integrity verification. Returns: Compressed bytes. Note: This function operates entirely in-memory. For streaming files, use `stream_compress`. """ if len(data) == 0 and not checksum: return data return pyzxc_compress(data, level, checksum) def get_decompressed_size(data: bytes) -> int: """Get the original decompressed size of a ZXC compressed buffer. Args: data (bytes): Compressed bytes buffer. Returns: int: Original uncompressed size in bytes, or 0 if the buffer is invalid or too small. Note: This function does not decompress the data, it only reads the footer for size info. """ return pyzxc_get_decompressed_size(data) def decompress(data, decompress_size=None, checksum=False) -> bytes: """Decompress a bytes object. Args: data: Compressed bytes. decompress_size: Expected size. If None, read from header (slower/safer). checksum: If True, verify the checksum appended during compression. Returns: Decompressed bytes. """ if len(data) == 0 and not checksum: return data if decompress_size is None: decompress_size = get_decompressed_size(data) if decompress_size == 0: raise ValueError( "Invalid ZXC header or data too short to determine size" ) return pyzxc_decompress(data, decompress_size, checksum) def stream_compress(src, dst, n_threads=0, level=LEVEL_DEFAULT, checksum=False, seekable=False) -> int: """Compress data from src to dst (file-like objects). Args: src: Readable file-like object with `fileno()` support (e.g., open file). dst: Writable file-like object with `fileno()` support. n_threads: Number of threads to use for compression. 0 uses default. level: Compression level. Use constants like LEVEL_FASTEST, LEVEL_DEFAULT, etc. checksum: If True, append a checksum for integrity verification. seekable: If True, append a seek table for random-access decompression. Returns: Number of bytes written to `dst`. Note: In-memory streams like `io.BytesIO` are not supported. Use the in-memory `compress`/`decompress` functions for buffers. """ if not hasattr(src, "fileno") or not hasattr(dst, "fileno"): raise ValueError("src and dst must be open file-like objects") if not src.readable(): raise ValueError("Source file must be readable") if not dst.writable(): raise ValueError("Destination file must be writable") # CRITICAL: Flush Python buffers before passing FDs to C # to prevent data reordering/corruption. if hasattr(src, "flush"): src.flush() if hasattr(dst, "flush"): dst.flush() return pyzxc_stream_compress(src, dst, n_threads, level, checksum, seekable) def stream_decompress(src, dst, n_threads=0, checksum=False) -> int: """Decompress data from src to dst (file-like objects). Args: src: Readable file-like object with `fileno()` support. dst: Writable file-like object with `fileno()` support. n_threads: Number of threads to use for decompression. 0 uses default. checksum: If True, verify the checksum appended during compression. Returns: Number of bytes written to `dst`. Note: In-memory streams like `io.BytesIO` are not supported. Use the in-memory `compress`/`decompress` functions for buffers. """ if not hasattr(src, "fileno") or not hasattr(dst, "fileno"): raise ValueError("src and dst must be open file-like objects") if not src.readable(): raise ValueError("Source file must be readable") if not dst.writable(): raise ValueError("Destination file must be writable") # CRITICAL: Flush Python buffers before passing FDs to C # to prevent data reordering/corruption. if hasattr(src, "flush"): src.flush() if hasattr(dst, "flush"): dst.flush() return pyzxc_stream_decompress(src, dst, n_threads, checksum)zxc-0.10.0/wrappers/python/src/zxc/__init__.pyi000066400000000000000000000023741517010557200214460ustar00rootroot00000000000000""" ZXC - High-performance lossless compression Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. SPDX-License-Identifier: BSD-3-Clause """ from typing import Protocol, Optional # ---------- constants ---------- LEVEL_FASTEST: int LEVEL_FAST: int LEVEL_DEFAULT: int LEVEL_BALANCED: int LEVEL_COMPACT: int # ---------- types ---------- class FileLike(Protocol): """File-like object with a file descriptor. Note: This excludes in-memory streams like io.BytesIO. Use real file objects or objects wrapping OS file descriptors. """ def fileno(self) -> int: ... def readable(self) -> bool: ... def writable(self) -> bool: ... # ---------- functions ---------- def compress( data: bytes, level: int = LEVEL_DEFAULT, checksum: bool = False ) -> bytes: ... def decompress( data: bytes, decompress_size: Optional[int] = None, checksum: bool = False ) -> bytes: ... def stream_compress( src: FileLike, dst: FileLike, n_threads: int = 0, level: int = LEVEL_DEFAULT, checksum: bool = False ) -> int: ... def stream_decompress( src: FileLike, dst: FileLike, n_threads: int = 0, checksum: bool = False ) -> int: ... def get_decompressed_size(data: bytes) -> int: ...zxc-0.10.0/wrappers/python/src/zxc/_zxc.c000066400000000000000000000306201517010557200202660ustar00rootroot00000000000000/* * Copyright (c) 2025-2026, Bertrand Lebonnois * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #define PY_SSIZE_T_CLEAN #include #include "zxc.h" #define Py_Return_Errno(err) \ do { \ PyErr_SetFromErrno(err); \ return NULL; \ } while (0) #define Py_Return_Err(err, str) \ do { \ PyErr_SetString(err, str); \ return NULL; \ } while (0) // ============================================================================= // Platform // ============================================================================= static inline int zxc_dup(int fd) { #ifdef _WIN32 return _dup(fd); #else return dup(fd); #endif } static inline FILE* zxc_fdopen(int fd, const char* mode) { #ifdef _WIN32 return _fdopen(fd, mode); #else return fdopen(fd, mode); #endif } static inline int zxc_close(int fd) { #ifdef _WIN32 return _close(fd); #else return close(fd); #endif } // ============================================================================= // Wrapper functions // ============================================================================= static PyObject* pyzxc_compress(PyObject* self, PyObject* args, PyObject* kwargs); static PyObject* pyzxc_decompress(PyObject* self, PyObject* args, PyObject* kwargs); static PyObject* pyzxc_stream_compress(PyObject* self, PyObject* args, PyObject* kwargs); static PyObject* pyzxc_stream_decompress(PyObject* self, PyObject* args, PyObject* kwargs); static PyObject* pyzxc_get_decompressed_size(PyObject* self, PyObject* args, PyObject* kwargs); // ============================================================================= // Initialize python module // ============================================================================= PyDoc_STRVAR( zxc_doc, "ZXC: High-performance, lossless asymmetric compression.\n" "\n" "Functions:\n" " compress(data: bytes, level: int = LEVEL_DEFAULT, checksum: bool = False) -> bytes\n" " Compress a bytes object.\n" "\n" " decompress(data: bytes, decompress_size: int, checksum: bool = False) -> bytes\n" " Decompress a bytes object to its original size.\n" "\n" " stream_compress(src: file-like, dst: file-like, n_threads: int = 0, level: int = " "LEVEL_DEFAULT, checksum: bool = False) -> None\n" " Compress data from a readable file-like object to a writable file-like object.\n" "\n" " stream_decompress(src: file-like, dst: file-like, n_threads: int = 0, checksum: bool = " "False) -> None\n" " Decompress data from a readable file-like object to a writable file-like object.\n" "\n" "Notes:\n" " - File-like objects must support fileno(), readable(), and writable().\n" " - Stream functions release the GIL for multi-threaded compression/decompression.\n"); static PyMethodDef zxc_methods[] = { {"pyzxc_compress", (PyCFunction)pyzxc_compress, METH_VARARGS | METH_KEYWORDS, NULL}, {"pyzxc_decompress", (PyCFunction)pyzxc_decompress, METH_VARARGS | METH_KEYWORDS, NULL}, {"pyzxc_stream_compress", (PyCFunction)pyzxc_stream_compress, METH_VARARGS | METH_KEYWORDS, NULL}, {"pyzxc_stream_decompress", (PyCFunction)pyzxc_stream_decompress, METH_VARARGS | METH_KEYWORDS, NULL}, {"pyzxc_get_decompressed_size", (PyCFunction)pyzxc_get_decompressed_size, METH_VARARGS | METH_KEYWORDS, NULL}, {NULL, NULL, 0, NULL} // sentinel }; static struct PyModuleDef zxc_module = {PyModuleDef_HEAD_INIT, "_zxc", zxc_doc, 0, zxc_methods}; PyMODINIT_FUNC PyInit__zxc(void) { PyObject* m = PyModule_Create(&zxc_module); if (!m) return NULL; PyModule_AddIntConstant(m, "LEVEL_FASTEST", ZXC_LEVEL_FASTEST); PyModule_AddIntConstant(m, "LEVEL_FAST", ZXC_LEVEL_FAST); PyModule_AddIntConstant(m, "LEVEL_DEFAULT", ZXC_LEVEL_DEFAULT); PyModule_AddIntConstant(m, "LEVEL_BALANCED", ZXC_LEVEL_BALANCED); PyModule_AddIntConstant(m, "LEVEL_COMPACT", ZXC_LEVEL_COMPACT); /* Error Enums */ PyModule_AddIntConstant(m, "ERROR_MEMORY", ZXC_ERROR_MEMORY); PyModule_AddIntConstant(m, "ERROR_DST_TOO_SMALL", ZXC_ERROR_DST_TOO_SMALL); PyModule_AddIntConstant(m, "ERROR_SRC_TOO_SMALL", ZXC_ERROR_SRC_TOO_SMALL); PyModule_AddIntConstant(m, "ERROR_BAD_MAGIC", ZXC_ERROR_BAD_MAGIC); PyModule_AddIntConstant(m, "ERROR_BAD_VERSION", ZXC_ERROR_BAD_VERSION); PyModule_AddIntConstant(m, "ERROR_BAD_HEADER", ZXC_ERROR_BAD_HEADER); PyModule_AddIntConstant(m, "ERROR_BAD_CHECKSUM", ZXC_ERROR_BAD_CHECKSUM); PyModule_AddIntConstant(m, "ERROR_CORRUPT_DATA", ZXC_ERROR_CORRUPT_DATA); PyModule_AddIntConstant(m, "ERROR_BAD_OFFSET", ZXC_ERROR_BAD_OFFSET); PyModule_AddIntConstant(m, "ERROR_OVERFLOW", ZXC_ERROR_OVERFLOW); PyModule_AddIntConstant(m, "ERROR_IO", ZXC_ERROR_IO); PyModule_AddIntConstant(m, "ERROR_NULL_INPUT", ZXC_ERROR_NULL_INPUT); PyModule_AddIntConstant(m, "ERROR_BAD_BLOCK_TYPE", ZXC_ERROR_BAD_BLOCK_TYPE); PyModule_AddIntConstant(m, "ERROR_BAD_BLOCK_SIZE", ZXC_ERROR_BAD_BLOCK_SIZE); return m; } // ============================================================================= // Functions definitions // ============================================================================= static PyObject* pyzxc_compress(PyObject* self, PyObject* args, PyObject* kwargs) { Py_buffer view; int level = ZXC_LEVEL_DEFAULT; int checksum = 0; static char* kwlist[] = {"data", "level", "checksum", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y*|ip", kwlist, &view, &level, &checksum)) { return NULL; } if (view.itemsize != 1) { PyBuffer_Release(&view); PyErr_SetString(PyExc_TypeError, "expected a byte buffer (itemsize==1)"); return NULL; } size_t src_size = (size_t)view.len; uint64_t bound = zxc_compress_bound(src_size); PyObject* out = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)bound); if (!out) { PyBuffer_Release(&view); return NULL; } char* dst = PyBytes_AsString(out); // Return a pointer to the contents int64_t nwritten; // The number of bytes written to dst zxc_compress_opts_t copts = {0}; copts.level = level; copts.checksum_enabled = checksum; Py_BEGIN_ALLOW_THREADS nwritten = zxc_compress(view.buf, // Source buffer src_size, // Source size dst, // Destination buffer bound, // Destination capacity &copts // Options ); Py_END_ALLOW_THREADS PyBuffer_Release(&view); if (nwritten < 0) { Py_DECREF(out); Py_Return_Err(PyExc_RuntimeError, zxc_error_name((int)nwritten)); } if (nwritten == 0) { Py_DECREF(out); Py_Return_Err(PyExc_ValueError, "input is too small to be compressed"); } if (_PyBytes_Resize(&out, (Py_ssize_t)nwritten) < 0) // Realloc return NULL; return out; } static PyObject* pyzxc_get_decompressed_size(PyObject* self, PyObject* args, PyObject* kwargs) { Py_buffer view; if (!PyArg_ParseTuple(args, "y*", &view)) { return NULL; } uint64_t n = zxc_get_decompressed_size(view.buf, view.len); PyBuffer_Release(&view); return Py_BuildValue("K", n); } static PyObject* pyzxc_decompress(PyObject* self, PyObject* args, PyObject* kwargs) { Py_buffer view; int checksum = 0; Py_ssize_t decompress_size; static char* kwlist[] = {"data", "decompress_size", "checksum", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y*n|p", kwlist, &view, &decompress_size, &checksum)) { return NULL; } if (view.itemsize != 1) { PyBuffer_Release(&view); PyErr_SetString(PyExc_TypeError, "expected a byte buffer (itemsize==1)"); return NULL; } size_t src_size = (size_t)view.len; PyObject* out = PyBytes_FromStringAndSize(NULL, (Py_ssize_t)decompress_size); if (!out) { PyBuffer_Release(&view); return NULL; } char* dst = PyBytes_AsString(out); // Return a pointer to the contents int64_t nwritten; // The number of bytes written to dst zxc_decompress_opts_t dopts = {0}; dopts.checksum_enabled = checksum; Py_BEGIN_ALLOW_THREADS nwritten = zxc_decompress(view.buf, // Source buffer src_size, // Source size dst, // Destination buffer decompress_size, // Destination capacity &dopts // Options ); Py_END_ALLOW_THREADS PyBuffer_Release(&view); if (nwritten < 0) { Py_DECREF(out); Py_Return_Err(PyExc_RuntimeError, zxc_error_name((int)nwritten)); } return out; } static PyObject* pyzxc_stream_compress(PyObject* self, PyObject* args, PyObject* kwargs) { PyObject *src, *dst; int nthreads = 0; int level = ZXC_LEVEL_DEFAULT; int checksum = 0; int seekable = 0; static char* kwlist[] = {"src", "dst", "n_threads", "level", "checksum", "seekable", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|iipp", kwlist, &src, &dst, &nthreads, &level, &checksum, &seekable)) { return NULL; } int src_fd = PyObject_AsFileDescriptor(src); int dst_fd = PyObject_AsFileDescriptor(dst); if (src_fd == -1 || dst_fd == -1) Py_Return_Err(PyExc_RuntimeError, "couldn't get file descriptor"); int src_dup = zxc_dup(src_fd); if (src_dup == -1) { Py_Return_Errno(PyExc_OSError); } int dst_dup = zxc_dup(dst_fd); if (dst_dup == -1) { zxc_close(src_dup); Py_Return_Errno(PyExc_OSError); } FILE* fsrc = zxc_fdopen(src_dup, "rb"); if (!fsrc) { zxc_close(src_dup); zxc_close(dst_dup); Py_Return_Errno(PyExc_OSError); } FILE* fdst = zxc_fdopen(dst_dup, "wb"); if (!fdst) { fclose(fsrc); zxc_close(dst_dup); Py_Return_Errno(PyExc_OSError); } int64_t nwritten; zxc_compress_opts_t scopts = {0}; scopts.n_threads = nthreads; scopts.level = level; scopts.checksum_enabled = checksum; scopts.seekable = seekable; Py_BEGIN_ALLOW_THREADS nwritten = zxc_stream_compress(fsrc, fdst, &scopts); Py_END_ALLOW_THREADS fclose(fdst); fclose(fsrc); if (nwritten < 0) Py_Return_Err(PyExc_RuntimeError, zxc_error_name((int)nwritten)); return Py_BuildValue("L", nwritten); } static PyObject* pyzxc_stream_decompress(PyObject* self, PyObject* args, PyObject* kwargs) { PyObject *src, *dst; int nthreads = 0; int checksum = 0; static char* kwlist[] = {"src", "dst", "n_threads", "checksum", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO|ip", kwlist, &src, &dst, &nthreads, &checksum)) { return NULL; } int src_fd = PyObject_AsFileDescriptor(src); int dst_fd = PyObject_AsFileDescriptor(dst); if (src_fd == -1 || dst_fd == -1) Py_Return_Err(PyExc_RuntimeError, "couldn't get file descriptor"); int src_dup = zxc_dup(src_fd); if (src_dup == -1) { Py_Return_Errno(PyExc_OSError); } int dst_dup = zxc_dup(dst_fd); if (dst_dup == -1) { zxc_close(src_dup); Py_Return_Errno(PyExc_OSError); } FILE* fsrc = zxc_fdopen(src_dup, "rb"); if (!fsrc) { zxc_close(src_dup); zxc_close(dst_dup); Py_Return_Errno(PyExc_OSError); } FILE* fdst = zxc_fdopen(dst_dup, "wb"); if (!fdst) { fclose(fsrc); zxc_close(dst_dup); Py_Return_Errno(PyExc_OSError); } int64_t nwritten; zxc_decompress_opts_t sdopts = {0}; sdopts.n_threads = nthreads; sdopts.checksum_enabled = checksum; Py_BEGIN_ALLOW_THREADS nwritten = zxc_stream_decompress(fsrc, fdst, &sdopts); Py_END_ALLOW_THREADS fclose(fdst); fclose(fsrc); if (nwritten < 0) Py_Return_Err(PyExc_RuntimeError, zxc_error_name((int)nwritten)); return Py_BuildValue("L", nwritten); } zxc-0.10.0/wrappers/python/tests/000077500000000000000000000000001517010557200167255ustar00rootroot00000000000000zxc-0.10.0/wrappers/python/tests/test_buffer.py000066400000000000000000000030561517010557200216130ustar00rootroot00000000000000""" ZXC - High-performance lossless compression Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. SPDX-License-Identifier: BSD-3-Clause """ import pytest import zxc @pytest.mark.parametrize("data", [ None, "string", 123, 12.5, object(), ]) def test_compress_invalid_type(data): with pytest.raises(TypeError): zxc.compress(data) @pytest.mark.parametrize( "data,corrupt_func,exc", [ (b"hello world" * 10, lambda x: x[:-1] + b"\x01", RuntimeError), (b"a" * 10, lambda x: b"", RuntimeError), ], ids=["corrupted_data", "invalid_header"], ) def test_compress_corruption(data, corrupt_func, exc): compressed = zxc.compress(data, checksum=True) corrupted = corrupt_func(compressed) with pytest.raises(exc): zxc.decompress(corrupted, len(data), checksum=True) @pytest.mark.parametrize( "data", [ b"hello world" * 10, # default b"a", # single byte b"", b"a" * 10_000_000, # large data ], ids=[ "normal_data", "single_byte", "empty", "large_10mb", ], ) def test_compress_roundtrip(data): # test all compression levels levels_cnt = zxc.LEVEL_COMPACT for level in range(levels_cnt + 1): compressed = zxc.compress(data, level) out_size = zxc.get_decompressed_size(compressed) decompressed = zxc.decompress(compressed, out_size) assert len(data) == len(decompressed) assert data == decompressed zxc-0.10.0/wrappers/python/tests/test_stream.py000066400000000000000000000077301517010557200216400ustar00rootroot00000000000000""" ZXC - High-performance lossless compression Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. SPDX-License-Identifier: BSD-3-Clause """ import pytest import zxc import io @pytest.mark.parametrize( "src,dst,expected_error,match", [ ("not a file", io.BytesIO(), ValueError, "src and dst must be open file-like objects"), (io.BytesIO(b"data"), "not a file", ValueError, "src and dst must be open file-like objects"), (io.BytesIO(b"data"), io.BytesIO(), ValueError, "Source file must be readable"), (io.BytesIO(b"data"), io.BytesIO(), ValueError, "Destination file must be writable"), (None, None, None, "valid_file"), ], ids=[ "src_not_file", "dst_not_file", "src_not_readable", "dst_not_writable", "valid_file", ] ) def test_stream_invalid_src_dst(tmp_path, src, dst, expected_error, match): # Helper class to mock fileno behavior class MockFile(io.BytesIO): def fileno(self): return 1 class NotReadable(MockFile): def readable(self): return False class NotWritable(MockFile): def writable(self): return False if match == "Source file must be readable": src = NotReadable(b"data") dst = MockFile() elif match == "Destination file must be writable": src = MockFile(b"data") dst = NotWritable() elif match == "valid_file": src_path = tmp_path / "src.txt" dst_path = tmp_path / "dst.zxc" src_path.write_bytes(b"hello world") src = open(src_path, "rb") dst = open(dst_path, "wb") try: if not expected_error: _ = zxc.stream_compress(src, dst) else: with pytest.raises(expected_error, match=match): zxc.stream_compress(src, dst) finally: if hasattr(src, "close"): src.close() if hasattr(dst, "close"): dst.close() @pytest.mark.parametrize( "data,corrupt_func,exc", [ (b"hello world" * 10, lambda x: x[:-1] + b"\x01", RuntimeError), (b"a" * 10, lambda x: b"", RuntimeError), ], ids=["corrupted_data", "invalid_header"], ) def test_stream_compress_corruption(tmp_path, data, corrupt_func, exc): src_file_path = tmp_path / "src.bin" compressed_file_path = tmp_path / "compressed.zxc" decompressed_file_path = tmp_path / "decompressed.bin" src_file_path.write_bytes(data) with open(src_file_path, "rb") as src, open(compressed_file_path, "wb") as dst: zxc.stream_compress(src, dst, checksum=True) compressed_bytes = compressed_file_path.read_bytes() corrupted_bytes = corrupt_func(compressed_bytes) compressed_file_path.write_bytes(corrupted_bytes) with pytest.raises(exc): with open(compressed_file_path, "rb") as src, open(decompressed_file_path, "wb") as dst: zxc.stream_decompress(src, dst, checksum=True) @pytest.mark.parametrize("data", [ b"hello world" * 10, # normal b"a", # single byte b"", # empty b"a" * 10_000_000, # large ], ids=["normal", "single_byte", "empty", "large_10mb"]) def test_stream_roundtrip(tmp_path, data): src_file_path = tmp_path / "src.txt" compressed_file_path = tmp_path / "compressed.zxc" decompressed_file_path = tmp_path / "decompressed.txt" src_file_path.write_bytes(data) levels_cnt = zxc.LEVEL_COMPACT for level in range(levels_cnt + 1): with open(src_file_path, "rb") as src, \ open(compressed_file_path, "wb") as compressed: zxc.stream_compress(src, compressed, level=level) with open(compressed_file_path, "rb") as compressed, \ open(decompressed_file_path, "wb") as decompressed: zxc.stream_decompress(compressed, decompressed) decompressed_data = decompressed_file_path.read_bytes() assert decompressed_data == data assert len(decompressed_data) == len(data)zxc-0.10.0/wrappers/rust/000077500000000000000000000000001517010557200152375ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/.gitignore000066400000000000000000000004501517010557200172260ustar00rootroot00000000000000# Rust build artifacts target/ # Cargo.lock for library crates (keep it for binaries) # Since zxc-sys and zxc are libraries, we ignore Cargo.lock Cargo.lock # Debug/release build output debug/ release/ # IDE files .vscode/ .idea/ *.swp *.swo *~ # macOS .DS_Store # Backup files *.bak *.orig zxc-0.10.0/wrappers/rust/Cargo.toml000066400000000000000000000006011517010557200171640ustar00rootroot00000000000000[workspace] members = ["zxc-sys", "zxc"] resolver = "2" [workspace.package] version = "0.10.0" authors = ["Bertrand Lebonnois"] edition = "2024" rust-version = "1.85" license = "BSD-3-Clause" repository = "https://github.com/hellobertrand/zxc" keywords = [ "zxc", "compression", "lossless", "high-performance", "data-compression", ] categories = ["compression", "encoding"] zxc-0.10.0/wrappers/rust/zxc-sys/000077500000000000000000000000001517010557200166575ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc-sys/Cargo.toml000066400000000000000000000010001517010557200205760ustar00rootroot00000000000000[package] name = "zxc-compress-sys" description = "Low-level FFI bindings to the ZXC compression library" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true keywords.workspace = true categories.workspace = true links = "zxc" build = "build.rs" [lib] name = "zxc_sys" [dependencies] libc = "0.2" [build-dependencies] cc = "1.2" [features] default = [] # Enable if you want to link against a system-installed ZXC library system = [] zxc-0.10.0/wrappers/rust/zxc-sys/build.rs000066400000000000000000000267221517010557200203350ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ //! Build script for zxc-sys //! //! This script compiles the ZXC C library with Function Multi-Versioning (FMV) //! to support runtime CPU feature detection and optimized code paths. //! //! On ARM64: Compiles `_default` and `_neon` variants //! On x86_64: Compiles `_default`, `_avx2`, and `_avx512` variants use std::env; use std::fs; use std::path::{Path, PathBuf}; /// Extract version constants from zxc_constants.h fn extract_version(include_dir: &Path) -> (u32, u32, u32) { let header_path = include_dir.join("zxc_constants.h"); let content = fs::read_to_string(&header_path) .expect("Failed to read zxc_constants.h"); let mut major = None; let mut minor = None; let mut patch = None; for line in content.lines() { let trimmed = line.trim(); // Parse lines like: #define ZXC_VERSION_MAJOR 0 if trimmed.starts_with("#define") { let parts: Vec<&str> = trimmed.split_whitespace().collect(); if parts.len() >= 3 { match parts[1] { "ZXC_VERSION_MAJOR" => major = parts[2].parse().ok(), "ZXC_VERSION_MINOR" => minor = parts[2].parse().ok(), "ZXC_VERSION_PATCH" => patch = parts[2].parse().ok(), _ => {} } } } } ( major.expect("ZXC_VERSION_MAJOR not found"), minor.expect("ZXC_VERSION_MINOR not found"), patch.expect("ZXC_VERSION_PATCH not found"), ) } /// Extract compression level constants from zxc_constants.h fn extract_compression_levels(include_dir: &Path) -> (i32, i32, i32, i32, i32) { let header_path = include_dir.join("zxc_constants.h"); let content = fs::read_to_string(&header_path) .expect("Failed to read zxc_constants.h"); let mut fastest = None; let mut fast = None; let mut default = None; let mut balanced = None; let mut compact = None; for line in content.lines() { let trimmed = line.trim(); // Parse lines like: ZXC_LEVEL_FASTEST = 1, if trimmed.starts_with("ZXC_LEVEL_") { let parts: Vec<&str> = trimmed.split('=').collect(); if parts.len() >= 2 { let name = parts[0].trim(); // Extract number, removing comma and comments let value_str = parts[1].split(&[',', '/'][..]).next().unwrap().trim(); let value: Option = value_str.parse().ok(); match name { "ZXC_LEVEL_FASTEST" => fastest = value, "ZXC_LEVEL_FAST" => fast = value, "ZXC_LEVEL_DEFAULT" => default = value, "ZXC_LEVEL_BALANCED" => balanced = value, "ZXC_LEVEL_COMPACT" => compact = value, _ => {} } } } } ( fastest.expect("ZXC_LEVEL_FASTEST not found"), fast.expect("ZXC_LEVEL_FAST not found"), default.expect("ZXC_LEVEL_DEFAULT not found"), balanced.expect("ZXC_LEVEL_BALANCED not found"), compact.expect("ZXC_LEVEL_COMPACT not found"), ) } fn main() { // Check if we should use system library instead of compiling from source if env::var("CARGO_FEATURE_SYSTEM").is_ok() { println!("cargo:rustc-link-lib=zxc"); return; } // Path to ZXC source files let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); // We use mirrored symlinks under zxc/ to preserve relative include paths // expected by the C source code (../../include/...). // During `cargo publish`, these symlinks are followed and real files are packaged. let src_lib = manifest_dir.join("zxc/src/lib"); let include_dir = manifest_dir.join("zxc/include"); // Fallback for local development if symlinks are broken or missing let (src_lib, include_dir) = if src_lib.exists() && include_dir.exists() { (src_lib, include_dir) } else { // Try direct path from workspace root let zxc_root = manifest_dir.join("../../..").canonicalize() .expect("Failed to find ZXC root directory"); (zxc_root.join("src/lib"), zxc_root.join("include")) }; // Verify paths exist assert!( src_lib.exists(), "ZXC source directory not found: {:?}", src_lib ); assert!( include_dir.exists(), "ZXC include directory not found: {:?}", include_dir ); // Extract version from header and make it available to lib.rs let (major, minor, patch) = extract_version(&include_dir); println!("cargo:rustc-env=ZXC_VERSION_MAJOR={}", major); println!("cargo:rustc-env=ZXC_VERSION_MINOR={}", minor); println!("cargo:rustc-env=ZXC_VERSION_PATCH={}", patch); // Extract compression levels from header and make them available to lib.rs let (fastest, fast, default, balanced, compact) = extract_compression_levels(&include_dir); println!("cargo:rustc-env=ZXC_LEVEL_FASTEST={}", fastest); println!("cargo:rustc-env=ZXC_LEVEL_FAST={}", fast); println!("cargo:rustc-env=ZXC_LEVEL_DEFAULT={}", default); println!("cargo:rustc-env=ZXC_LEVEL_BALANCED={}", balanced); println!("cargo:rustc-env=ZXC_LEVEL_COMPACT={}", compact); let target = env::var("TARGET").unwrap_or_default(); let is_arm64 = target.contains("aarch64") || target.contains("arm64"); let is_x86_64 = target.contains("x86_64") || target.contains("i686"); // ========================================================================= // Core library files (common to all architectures) // ========================================================================= let mut core_build = cc::Build::new(); core_build .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_common.c")) .file(src_lib.join("zxc_driver.c")) .file(src_lib.join("zxc_dispatch.c")) .file(src_lib.join("zxc_seekable.c")) .opt_level(3) .warnings(false) .flag_if_supported("-pthread"); // ========================================================================= // Function Multi-Versioning: Compile variants with different suffixes // ========================================================================= // --- Default variant (baseline, always compiled) --- let mut default_compress = cc::Build::new(); default_compress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_compress.c")) .define("ZXC_FUNCTION_SUFFIX", "_default") .opt_level(3) .warnings(false); let mut default_decompress = cc::Build::new(); default_decompress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_decompress.c")) .define("ZXC_FUNCTION_SUFFIX", "_default") .opt_level(3) .warnings(false); // Add architecture-specific flags to core build BEFORE compiling if is_arm64 { core_build.flag_if_supported("-march=armv8-a+crc"); } else if is_x86_64 { core_build.flag_if_supported("-msse4.2"); core_build.flag_if_supported("-mpclmul"); } core_build.compile("zxc_core"); // Compile defaults default_compress.compile("zxc_compress_default"); default_decompress.compile("zxc_decompress_default"); // --- Architecture-specific variants --- if is_arm64 { // NEON variant for ARM64 let mut neon_compress = cc::Build::new(); neon_compress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_compress.c")) .define("ZXC_FUNCTION_SUFFIX", "_neon") .flag_if_supported("-march=armv8-a+simd") .opt_level(3) .warnings(false); let mut neon_decompress = cc::Build::new(); neon_decompress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_decompress.c")) .define("ZXC_FUNCTION_SUFFIX", "_neon") .flag_if_supported("-march=armv8-a+simd") .opt_level(3) .warnings(false); neon_compress.compile("zxc_compress_neon"); neon_decompress.compile("zxc_decompress_neon"); } else if is_x86_64 { // AVX2 variant let mut avx2_compress = cc::Build::new(); avx2_compress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_compress.c")) .define("ZXC_FUNCTION_SUFFIX", "_avx2") .flag_if_supported("-mavx2") .flag_if_supported("-mfma") .flag_if_supported("-mbmi2") .opt_level(3) .warnings(false); let mut avx2_decompress = cc::Build::new(); avx2_decompress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_decompress.c")) .define("ZXC_FUNCTION_SUFFIX", "_avx2") .flag_if_supported("-mavx2") .flag_if_supported("-mfma") .flag_if_supported("-mbmi2") .opt_level(3) .warnings(false); avx2_compress.compile("zxc_compress_avx2"); avx2_decompress.compile("zxc_decompress_avx2"); // AVX512 variant let mut avx512_compress = cc::Build::new(); avx512_compress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_compress.c")) .define("ZXC_FUNCTION_SUFFIX", "_avx512") .flag_if_supported("-mavx512f") .flag_if_supported("-mavx512bw") .flag_if_supported("-mbmi2") .opt_level(3) .warnings(false); let mut avx512_decompress = cc::Build::new(); avx512_decompress .include(&include_dir) .include(&src_lib) .include(src_lib.join("vendors")) .define("ZXC_STATIC_DEFINE", None) .file(src_lib.join("zxc_decompress.c")) .define("ZXC_FUNCTION_SUFFIX", "_avx512") .flag_if_supported("-mavx512f") .flag_if_supported("-mavx512bw") .flag_if_supported("-mbmi2") .opt_level(3) .warnings(false); avx512_compress.compile("zxc_compress_avx512"); avx512_decompress.compile("zxc_decompress_avx512"); } // Threading support (not needed on Windows, which uses kernel32) if !target.contains("windows") { println!("cargo:rustc-link-lib=pthread"); } // Re-run build script if source files change println!("cargo:rerun-if-changed={}", src_lib.display()); println!("cargo:rerun-if-changed={}", include_dir.display()); } zxc-0.10.0/wrappers/rust/zxc-sys/src/000077500000000000000000000000001517010557200174465ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc-sys/src/lib.rs000066400000000000000000000346511517010557200205730ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ //! Low-level FFI bindings to the ZXC compression library. //! //! This crate provides raw, unsafe bindings to the ZXC C library. //! For a safe, idiomatic Rust API, use the `zxc` crate instead. //! //! # Example //! //! ```rust,ignore //! use zxc_sys::*; //! //! unsafe { //! let bound = zxc_compress_bound(1024); //! // ... allocate buffer and compress //! } //! ``` #![allow(non_camel_case_types)] #![allow(non_upper_case_globals)] use std::ffi::c_int; use std::os::raw::c_void; // ============================================================================= // ZXC Version Constants // ============================================================================= // Version constants - automatically extracted from zxc_constants.h by build.rs const fn parse_version(s: &str) -> u32 { let bytes = s.as_bytes(); let mut result = 0u32; let mut i = 0; while i < bytes.len() { let digit = bytes[i]; if digit >= b'0' && digit <= b'9' { result = result * 10 + (digit - b'0') as u32; } i += 1; } result } pub const ZXC_VERSION_MAJOR: u32 = parse_version(env!("ZXC_VERSION_MAJOR")); pub const ZXC_VERSION_MINOR: u32 = parse_version(env!("ZXC_VERSION_MINOR")); pub const ZXC_VERSION_PATCH: u32 = parse_version(env!("ZXC_VERSION_PATCH")); // ============================================================================= // Compression Levels // ============================================================================= // Helper function to parse integer from string literal at compile time const fn parse_i32(s: &str) -> i32 { let bytes = s.as_bytes(); let mut result = 0i32; let mut i = 0; let mut sign = 1; if i < bytes.len() && bytes[i] == b'-' { sign = -1; i += 1; } while i < bytes.len() { let digit = bytes[i]; if digit >= b'0' && digit <= b'9' { result = result * 10 + (digit - b'0') as i32; } i += 1; } result * sign } // Compression level constants - automatically extracted from zxc_constants.h by build.rs /// Fastest compression, best for real-time applications pub const ZXC_LEVEL_FASTEST: i32 = parse_i32(env!("ZXC_LEVEL_FASTEST")); /// Fast compression, good for real-time applications pub const ZXC_LEVEL_FAST: i32 = parse_i32(env!("ZXC_LEVEL_FAST")); /// Recommended: ratio > LZ4, decode speed > LZ4 pub const ZXC_LEVEL_DEFAULT: i32 = parse_i32(env!("ZXC_LEVEL_DEFAULT")); /// Good ratio, good decode speed pub const ZXC_LEVEL_BALANCED: i32 = parse_i32(env!("ZXC_LEVEL_BALANCED")); /// High density. Best for storage/firmware/assets. pub const ZXC_LEVEL_COMPACT: i32 = parse_i32(env!("ZXC_LEVEL_COMPACT")); // ============================================================================= // Error Codes // ============================================================================= /// Success (no error) pub const ZXC_OK: i32 = 0; /// Memory allocation failure pub const ZXC_ERROR_MEMORY: i32 = -1; /// Destination buffer too small pub const ZXC_ERROR_DST_TOO_SMALL: i32 = -2; /// Source buffer too small or truncated input pub const ZXC_ERROR_SRC_TOO_SMALL: i32 = -3; /// Invalid magic word in file header pub const ZXC_ERROR_BAD_MAGIC: i32 = -4; /// Unsupported file format version pub const ZXC_ERROR_BAD_VERSION: i32 = -5; /// Corrupted or invalid header (CRC mismatch) pub const ZXC_ERROR_BAD_HEADER: i32 = -6; /// Block or global checksum verification failed pub const ZXC_ERROR_BAD_CHECKSUM: i32 = -7; /// Corrupted compressed data pub const ZXC_ERROR_CORRUPT_DATA: i32 = -8; /// Invalid match offset during decompression pub const ZXC_ERROR_BAD_OFFSET: i32 = -9; /// Buffer overflow detected during processing pub const ZXC_ERROR_OVERFLOW: i32 = -10; /// Read/write/seek failure on file pub const ZXC_ERROR_IO: i32 = -11; /// Required input pointer is NULL pub const ZXC_ERROR_NULL_INPUT: i32 = -12; /// Unknown or unexpected block type pub const ZXC_ERROR_BAD_BLOCK_TYPE: i32 = -13; /// Invalid block size pub const ZXC_ERROR_BAD_BLOCK_SIZE: i32 = -14; // ============================================================================= // Options Structs (mirroring C API) // ============================================================================= /// Compression options (mirrors `zxc_compress_opts_t` from C API). #[repr(C)] #[derive(Debug, Clone)] pub struct zxc_compress_opts_t { /// Worker thread count (0 = auto-detect CPU cores). pub n_threads: c_int, /// Compression level 1-5 (0 = default). pub level: c_int, /// Block size in bytes (0 = default 256 KB). Must be power of 2, 4 KB – 2 MB. pub block_size: usize, /// 1 to enable per-block and global checksums, 0 to disable. pub checksum_enabled: c_int, /// 1 to append a seek table for random-access decompression, 0 to disable. pub seekable: c_int, /// Progress callback (NULL to disable). pub progress_cb: *const c_void, /// User context pointer passed to progress_cb. pub user_data: *mut c_void, } impl Default for zxc_compress_opts_t { fn default() -> Self { Self { n_threads: 0, level: 0, block_size: 0, checksum_enabled: 0, seekable: 0, progress_cb: std::ptr::null(), user_data: std::ptr::null_mut(), } } } /// Decompression options (mirrors `zxc_decompress_opts_t` from C API). #[repr(C)] #[derive(Debug, Clone)] pub struct zxc_decompress_opts_t { /// Worker thread count (0 = auto-detect CPU cores). pub n_threads: c_int, /// 1 to verify per-block and global checksums, 0 to skip. pub checksum_enabled: c_int, /// Progress callback (NULL to disable). pub progress_cb: *const c_void, /// User context pointer passed to progress_cb. pub user_data: *mut c_void, } impl Default for zxc_decompress_opts_t { fn default() -> Self { Self { n_threads: 0, checksum_enabled: 0, progress_cb: std::ptr::null(), user_data: std::ptr::null_mut(), } } } // ============================================================================= // Buffer-Based API // ============================================================================= unsafe extern "C" { /// Calculates the maximum compressed size for a given input. /// /// Useful for allocating output buffers before compression. pub fn zxc_compress_bound(input_size: usize) -> u64; /// Compresses a data buffer using the ZXC algorithm. /// /// # Safety /// /// - `src` must be a valid pointer to `src_size` bytes. /// - `dst` must be a valid pointer to at least `dst_capacity` bytes. /// - The caller must ensure no data races on the buffers. /// /// # Returns /// /// Number of bytes written to `dst` (>0 on success), or a negative error code. pub fn zxc_compress( src: *const c_void, src_size: usize, dst: *mut c_void, dst_capacity: usize, opts: *const zxc_compress_opts_t, ) -> i64; /// Decompresses a ZXC compressed buffer. /// /// # Safety /// /// - `src` must be a valid pointer to `src_size` bytes of compressed data. /// - `dst` must be a valid pointer to at least `dst_capacity` bytes. /// /// # Returns /// /// Number of decompressed bytes written to `dst` (>0 on success), or a negative error code. pub fn zxc_decompress( src: *const c_void, src_size: usize, dst: *mut c_void, dst_capacity: usize, opts: *const zxc_decompress_opts_t, ) -> i64; /// Returns the decompressed size stored in a ZXC compressed buffer. /// /// Reads the file footer without performing decompression. /// /// # Returns /// /// Original uncompressed size in bytes, or 0 if invalid. pub fn zxc_get_decompressed_size(src: *const c_void, src_size: usize) -> u64; /// Returns a human-readable name for the given error code. /// /// # Arguments /// /// * `code` - An error code from zxc_error_t (or any integer) /// /// # Returns /// /// A constant string such as "ZXC_OK" or "ZXC_ERROR_MEMORY". /// Returns "ZXC_UNKNOWN_ERROR" for unrecognized codes. pub fn zxc_error_name(code: c_int) -> *const std::os::raw::c_char; } // ============================================================================= // Streaming API (FILE-based) // ============================================================================= unsafe extern "C" { /// Compresses data from an input stream to an output stream. /// /// Uses a multi-threaded pipeline architecture for high throughput /// on large files. /// /// # Safety /// /// - `f_in` must be a valid FILE* opened in "rb" mode /// - `f_out` must be a valid FILE* opened in "wb" mode /// - File handles must remain valid for the duration of the call /// /// # Arguments /// /// * `f_in` - Input file stream /// * `f_out` - Output file stream /// * `n_threads` - Number of worker threads (0 = auto-detect CPU cores) /// * `level` - Compression level (1-5) /// * `checksum_enabled` - If non-zero, enables checksum verification /// /// # Returns /// /// Total compressed bytes written, or -1 on error. pub fn zxc_stream_compress( f_in: *mut libc::FILE, f_out: *mut libc::FILE, opts: *const zxc_compress_opts_t, ) -> i64; /// Decompresses data from an input stream to an output stream. /// /// Uses the same pipeline architecture as compression for maximum throughput. /// /// # Safety /// /// - `f_in` must be a valid FILE* opened in "rb" mode /// - `f_out` must be a valid FILE* opened in "wb" mode /// /// # Arguments /// /// * `f_in` - Input file stream (compressed data) /// * `f_out` - Output file stream (decompressed data) /// * `n_threads` - Number of worker threads (0 = auto-detect) /// * `checksum_enabled` - If non-zero, verifies checksums /// /// # Returns /// /// Total decompressed bytes written, or -1 on error. pub fn zxc_stream_decompress( f_in: *mut libc::FILE, f_out: *mut libc::FILE, opts: *const zxc_decompress_opts_t, ) -> i64; /// Returns the decompressed size stored in a ZXC compressed file. /// /// Reads the file footer to extract the original size without decompressing. /// The file position is restored after reading. /// /// # Safety /// /// - `f_in` must be a valid FILE* opened in "rb" mode /// /// # Returns /// /// Original uncompressed size in bytes, or -1 on error. pub fn zxc_stream_get_decompressed_size(f_in: *mut libc::FILE) -> i64; } // ============================================================================= // Tests // ============================================================================= #[cfg(test)] mod tests { use super::*; #[test] fn test_compress_bound() { unsafe { let bound = zxc_compress_bound(1024); // Should return a reasonable bound (input + overhead) assert!(bound > 1024); assert!(bound < 1024 * 2); // Should not be excessively large } } #[test] fn test_roundtrip() { // Use highly repetitive data that definitely compresses well let input: Vec = (0..4096) .map(|i| ((i % 16) as u8).wrapping_add(b'A')) .collect(); unsafe { // Allocate compression buffer let bound = zxc_compress_bound(input.len()) as usize; let mut compressed = vec![0u8; bound]; // Compress let copts = zxc_compress_opts_t { level: ZXC_LEVEL_DEFAULT, checksum_enabled: 1, ..Default::default() }; let compressed_size = zxc_compress( input.as_ptr() as *const c_void, input.len(), compressed.as_mut_ptr() as *mut c_void, compressed.len(), &copts, ); assert!(compressed_size > 0, "Compression failed"); // Highly repetitive data should compress significantly assert!((compressed_size as usize) < input.len() / 2, "Data should compress well"); // Get decompressed size let decompressed_size = zxc_get_decompressed_size( compressed.as_ptr() as *const c_void, compressed_size as usize, ); assert_eq!(decompressed_size as usize, input.len()); // Decompress let mut decompressed = vec![0u8; decompressed_size as usize]; let dopts = zxc_decompress_opts_t { checksum_enabled: 1, ..Default::default() }; let result_size = zxc_decompress( compressed.as_ptr() as *const c_void, compressed_size as usize, decompressed.as_mut_ptr() as *mut c_void, decompressed.len(), &dopts, ); assert_eq!(result_size, input.len() as i64); assert_eq!(&decompressed[..], &input[..]); } } #[test] fn test_all_levels() { let input = b"Test data for all compression levels - with some repetition \ to ensure compression works: BBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; for level in [ ZXC_LEVEL_FASTEST, ZXC_LEVEL_FAST, ZXC_LEVEL_DEFAULT, ZXC_LEVEL_BALANCED, ZXC_LEVEL_COMPACT, ] { unsafe { let bound = zxc_compress_bound(input.len()) as usize; let mut compressed = vec![0u8; bound]; let copts = zxc_compress_opts_t { level, checksum_enabled: 1, ..Default::default() }; let compressed_size = zxc_compress( input.as_ptr() as *const c_void, input.len(), compressed.as_mut_ptr() as *mut c_void, compressed.len(), &copts, ); assert!( compressed_size > 0, "Compression failed at level {}", level ); } } } } zxc-0.10.0/wrappers/rust/zxc-sys/zxc/000077500000000000000000000000001517010557200174635ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc-sys/zxc/include000077700000000000000000000000001517010557200234462../../../../includeustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc-sys/zxc/src/000077500000000000000000000000001517010557200202525ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc-sys/zxc/src/lib000077700000000000000000000000001517010557200235052../../../../../src/libustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc/000077500000000000000000000000001517010557200160435ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc/Cargo.toml000066400000000000000000000012141517010557200177710ustar00rootroot00000000000000[package] name = "zxc-compress" description = "Safe Rust bindings to the ZXC compression library" version.workspace = true authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true keywords.workspace = true categories.workspace = true readme = "README.md" [lib] name = "zxc" [dependencies] zxc_sys = { path = "../zxc-sys", version = "0.10.0", package = "zxc-compress-sys" } thiserror = "2.0" libc = "0.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = [ "Win32_Foundation", "Win32_System_Threading", ] } [dev-dependencies] rand = "0.10" [features] default = [] zxc-0.10.0/wrappers/rust/zxc/README.md000066400000000000000000000054741517010557200173340ustar00rootroot00000000000000# zxc Safe Rust bindings to the **ZXC compression library** - a fast LZ77-based compressor optimized for high decompression speed. [![Crates.io](https://img.shields.io/crates/v/zxc.svg)](https://crates.io/crates/zxc) [![Documentation](https://docs.rs/zxc/badge.svg)](https://docs.rs/zxc) [![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE) ## Quick Start ```rust use zxc::{compress, decompress, Level}; fn main() -> Result<(), zxc::Error> { let data = b"Hello, ZXC! This is some data to compress."; // Compress (no checksum for max speed) let compressed = compress(data, Level::Default, None)?; println!("Compressed {} -> {} bytes", data.len(), compressed.len()); // Decompress let decompressed = decompress(&compressed)?; assert_eq!(&decompressed[..], &data[..]); Ok(()) } ``` ## Compression Levels | Level | Speed | Ratio | Use Case | |-------|-------|-------|----------| | `Level::Fastest` | ★★★★★ | ★★☆☆☆ | Real-time, gaming | | `Level::Fast` | ★★★★☆ | ★★★☆☆ | Network, streaming | | `Level::Default` | ★★★☆☆ | ★★★★☆ | General purpose | | `Level::Balanced` | ★★☆☆☆ | ★★★★☆ | Archives | | `Level::Compact` | ★☆☆☆☆ | ★★★★★ | Storage, firmware | ## Features - **Fast decompression**: Optimized for read-heavy workloads - **5 compression levels**: Trade off speed vs ratio - **Optional checksums**: Disabled by default for maximum performance, enable for data integrity - **File streaming**: Multi-threaded compression/decompression for large files - **Zero-allocation API**: `compress_to` and `decompress_to` for buffer reuse - **Pure Rust API**: Safe, idiomatic interface over the C library ## Advanced Usage ### Pre-allocated Buffers ```rust use zxc::{compress_to, decompress_to, compress_bound, CompressOptions, DecompressOptions}; let data = b"Hello, world!"; // Compression let mut output = vec![0u8; compress_bound(data.len())]; let size = compress_to(data, &mut output, &CompressOptions::default())?; output.truncate(size); // Decompression let mut decompressed = vec![0u8; data.len()]; decompress_to(&output, &mut decompressed, &DecompressOptions::default())?; ``` ### Disable Checksum ```rust use zxc::{compress_with_options, decompress_with_options, CompressOptions, DecompressOptions, Level}; let opts = CompressOptions::with_level(Level::Fastest).without_checksum(); let compressed = compress_with_options(data, &opts)?; let decompressed = decompress_with_options(&compressed, &DecompressOptions::skip_checksum())?; ``` ### Query Decompressed Size ```rust use zxc::decompressed_size; if let Some(size) = decompressed_size(&compressed) { let mut buffer = vec![0u8; size]; // ... } ``` ## License BSD-3-Clause - see [LICENSE](../../LICENSE) for details. zxc-0.10.0/wrappers/rust/zxc/examples/000077500000000000000000000000001517010557200176615ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc/examples/file_compression.rs000066400000000000000000000050071517010557200235710ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ //! Example demonstrating file-based streaming compression and decompression. //! //! Run with: `cargo run --example file_compression` use std::fs; use std::io::Write; fn main() -> Result<(), Box> { println!("ZXC File Streaming Example\n"); // Create a test file with compressible data let test_data: Vec = (0..1024 * 1024) // 1 MB .map(|i| ((i % 256) ^ ((i / 256) % 256)) as u8) .collect(); let input_path = "/tmp/zxc_test_input.bin"; let compressed_path = "/tmp/zxc_test_compressed.zxc"; let output_path = "/tmp/zxc_test_output.bin"; // Write test data to file { let mut file = fs::File::create(input_path)?; file.write_all(&test_data)?; } // Get original file size let original_size = fs::metadata(input_path)?.len(); println!("Original file size: {} bytes", original_size); // Compress the file with multi-threading println!("\nCompressing with auto-detected threads..."); let compressed_bytes = zxc::compress_file( input_path, compressed_path, zxc::Level::Default, None, // Auto-detect CPU cores None, // Maximum performance (no checksum) )?; println!(" Compressed bytes written: {}", compressed_bytes); // Get compressed file size let compressed_size = fs::metadata(compressed_path)?.len(); let ratio = 100.0 * compressed_size as f64 / original_size as f64; println!(" Compressed file size: {} bytes ({:.1}%)", compressed_size, ratio); // Query the decompressed size from the file let reported_size = zxc::file_decompressed_size(compressed_path)?; println!("\n Reported decompressed size: {} bytes", reported_size); // Decompress the file println!("\nDecompressing..."); let decompressed_bytes = zxc::decompress_file( compressed_path, output_path, Some(4), // Use 4 threads )?; println!(" Decompressed bytes written: {}", decompressed_bytes); // Verify the result let output_data = fs::read(output_path)?; if output_data == test_data { println!("\n✓ Data integrity verified! Files match."); } else { println!("\n✗ ERROR: Data mismatch!"); } // Cleanup fs::remove_file(input_path)?; fs::remove_file(compressed_path)?; fs::remove_file(output_path)?; println!("\nDone."); Ok(()) } zxc-0.10.0/wrappers/rust/zxc/examples/simple.rs000066400000000000000000000034321517010557200215220ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ //! Simple example demonstrating ZXC compression and decompression. //! //! Run with: `cargo run --example simple` use zxc::{compress, decompress, decompressed_size, version_string, Level}; fn main() -> Result<(), zxc::Error> { println!("ZXC Rust Wrapper v{}\n", version_string()); // Sample data with some repetition (compresses well) let original = b"Hello, ZXC! This is a demonstration of the Rust wrapper. \ Let's add some repetitive content to show compression: \ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; println!("Original size: {} bytes", original.len()); // Test all compression levels for level in Level::all() { let compressed = compress(original, *level, None)?; let ratio = (compressed.len() as f64 / original.len() as f64) * 100.0; println!( " Level {:?}: {} bytes ({:.1}%)", level, compressed.len(), ratio ); } // Full roundtrip demonstration println!("\nRoundtrip test:"); let compressed = compress(original, Level::Default, None)?; // Query size before decompression let size = decompressed_size(&compressed).expect("valid compressed data"); println!(" Reported decompressed size: {} bytes", size); let decompressed = decompress(&compressed)?; println!(" Actual decompressed size: {} bytes", decompressed.len()); // Verify data integrity assert_eq!(&decompressed[..], &original[..]); println!(" ✓ Data integrity verified!"); Ok(()) } zxc-0.10.0/wrappers/rust/zxc/src/000077500000000000000000000000001517010557200166325ustar00rootroot00000000000000zxc-0.10.0/wrappers/rust/zxc/src/lib.rs000066400000000000000000001151341517010557200177530ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ //! Safe Rust bindings to the ZXC compression library. //! //! ZXC is a fast compression library optimized for high decompression speed. //! This crate provides a safe, idiomatic Rust API. //! //! # Quick Start //! //! ```rust //! use zxc::{compress, decompress, Level}; //! //! // Compress some data //! let data = b"Hello, ZXC! This is some data to compress."; //! let compressed = compress(data, Level::Default, None).expect("compression failed"); //! //! // Decompress it back //! let decompressed = decompress(&compressed).expect("decompression failed"); //! assert_eq!(&decompressed[..], &data[..]); //! ``` //! //! # Compression Levels //! //! ZXC provides 5 compression levels trading off speed vs ratio: //! //! | Level | Speed | Ratio | Use Case | //! |-------|-------|-------|----------| //! | `Fastest` | ★★★★★ | ★★☆☆☆ | Real-time, gaming | //! | `Fast` | ★★★★☆ | ★★★☆☆ | Network, streaming | //! | `Default` | ★★★☆☆ | ★★★★☆ | General purpose | //! | `Balanced` | ★★☆☆☆ | ★★★★☆ | Archives | //! | `Compact` | ★☆☆☆☆ | ★★★★★ | Storage, firmware | //! //! # Features //! //! - **Checksum verification**: Optional, disabled by default for maximum performance //! - **Zero-copy decompression bound**: Query the output size before decompressing #![warn(missing_docs)] #![warn(rust_2018_idioms)] use std::ffi::c_void; pub use zxc_sys::{ ZXC_LEVEL_BALANCED, ZXC_LEVEL_COMPACT, ZXC_LEVEL_DEFAULT, ZXC_LEVEL_FAST, ZXC_LEVEL_FASTEST, ZXC_VERSION_MAJOR, ZXC_VERSION_MINOR, ZXC_VERSION_PATCH, // Error codes ZXC_OK, ZXC_ERROR_MEMORY, ZXC_ERROR_DST_TOO_SMALL, ZXC_ERROR_SRC_TOO_SMALL, ZXC_ERROR_BAD_MAGIC, ZXC_ERROR_BAD_VERSION, ZXC_ERROR_BAD_HEADER, ZXC_ERROR_BAD_CHECKSUM, ZXC_ERROR_CORRUPT_DATA, ZXC_ERROR_BAD_OFFSET, ZXC_ERROR_OVERFLOW, ZXC_ERROR_IO, ZXC_ERROR_NULL_INPUT, ZXC_ERROR_BAD_BLOCK_TYPE, ZXC_ERROR_BAD_BLOCK_SIZE, }; // ============================================================================= // Error Types // ============================================================================= /// Errors that can occur during ZXC operations. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { /// Memory allocation failure #[error("memory allocation failed")] Memory, /// Destination buffer too small #[error("destination buffer too small")] DstTooSmall, /// Source buffer too small or truncated input #[error("source buffer too small or truncated")] SrcTooSmall, /// Invalid magic word in file header #[error("invalid magic word in header")] BadMagic, /// Unsupported file format version #[error("unsupported file format version")] BadVersion, /// Corrupted or invalid header (CRC mismatch) #[error("corrupted or invalid header")] BadHeader, /// Block or global checksum verification failed #[error("checksum verification failed")] BadChecksum, /// Corrupted compressed data #[error("corrupted compressed data")] CorruptData, /// Invalid match offset during decompression #[error("invalid match offset")] BadOffset, /// Buffer overflow detected during processing #[error("buffer overflow detected")] Overflow, /// Read/write/seek failure on file #[error("I/O error")] Io, /// Required input pointer is NULL #[error("null input pointer")] NullInput, /// Unknown or unexpected block type #[error("unknown block type")] BadBlockType, /// Invalid block size #[error("invalid block size")] BadBlockSize, /// The compressed data appears to be invalid or truncated #[error("invalid compressed data")] InvalidData, /// Unknown error code from C library #[error("unknown error (code: {0})")] Unknown(i32), } /// Convert a negative error code from the C library to a Rust Error. fn error_from_code(code: i64) -> Error { match code as i32 { ZXC_ERROR_MEMORY => Error::Memory, ZXC_ERROR_DST_TOO_SMALL => Error::DstTooSmall, ZXC_ERROR_SRC_TOO_SMALL => Error::SrcTooSmall, ZXC_ERROR_BAD_MAGIC => Error::BadMagic, ZXC_ERROR_BAD_VERSION => Error::BadVersion, ZXC_ERROR_BAD_HEADER => Error::BadHeader, ZXC_ERROR_BAD_CHECKSUM => Error::BadChecksum, ZXC_ERROR_CORRUPT_DATA => Error::CorruptData, ZXC_ERROR_BAD_OFFSET => Error::BadOffset, ZXC_ERROR_OVERFLOW => Error::Overflow, ZXC_ERROR_IO => Error::Io, ZXC_ERROR_NULL_INPUT => Error::NullInput, ZXC_ERROR_BAD_BLOCK_TYPE => Error::BadBlockType, ZXC_ERROR_BAD_BLOCK_SIZE => Error::BadBlockSize, _ => Error::Unknown(code as i32), } } /// Result type for ZXC operations. pub type Result = std::result::Result; // ============================================================================= // Compression Levels // ============================================================================= /// Compression level presets. /// /// Higher levels produce smaller output but compress more slowly. /// Decompression speed is similar across all levels. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[repr(i32)] pub enum Level { /// Fastest compression, best for real-time applications (level 1) Fastest = 1, /// Fast compression, good for real-time applications (level 2) Fast = 2, /// Recommended default: ratio > LZ4, decode speed > LZ4 (level 3) #[default] Default = 3, /// Good ratio, good decode speed (level 4) Balanced = 4, /// Highest density. Best for storage/firmware/assets (level 5) Compact = 5, } impl Level { /// Returns all available compression levels. pub fn all() -> &'static [Level] { &[ Level::Fastest, Level::Fast, Level::Default, Level::Balanced, Level::Compact, ] } } impl From for i32 { fn from(level: Level) -> i32 { level as i32 } } // ============================================================================= // Compression Options // ============================================================================= /// Options for compression operations. #[derive(Debug, Clone)] pub struct CompressOptions { /// Compression level (default: `Level::Default`) pub level: Level, /// Enable checksum for data integrity (default: `true`) pub checksum: bool, /// Enable seek table for random-access decompression (default: `false`) pub seekable: bool, } impl Default for CompressOptions { fn default() -> Self { Self { level: Level::Default, checksum: true, seekable: false, } } } impl CompressOptions { /// Create options with the specified compression level. pub fn with_level(level: Level) -> Self { Self { level, ..Default::default() } } /// Disable checksum computation for faster compression. pub fn without_checksum(mut self) -> Self { self.checksum = false; self } /// Enable seek table for random-access decompression. pub fn with_seekable(mut self) -> Self { self.seekable = true; self } } // ============================================================================= // Decompression Options // ============================================================================= /// Options for decompression operations. #[derive(Debug, Clone, Default)] pub struct DecompressOptions { /// Verify checksum during decompression (default: `true`) pub verify_checksum: bool, } impl DecompressOptions { /// Create options that skip checksum verification. pub fn skip_checksum() -> Self { Self { verify_checksum: false, } } } // ============================================================================= // Public API // ============================================================================= /// Returns the maximum compressed size for an input of the given size. /// /// Use this to allocate a buffer before calling [`compress_to`]. /// /// # Example /// /// ```rust /// let bound = zxc::compress_bound(1024); /// assert!(bound > 1024); // Accounts for headers and worst-case expansion /// ``` #[inline] pub fn compress_bound(input_size: usize) -> u64 { unsafe { zxc_sys::zxc_compress_bound(input_size) } } /// Compresses data with the specified level. /// /// This is a convenience function that allocates the output buffer automatically. /// For zero-allocation usage, see [`compress_to`]. /// /// # Arguments /// /// * `data` - The data to compress /// * `level` - Compression level /// * `checksum` - Optional checksum for data integrity (`None` = disabled for maximum performance) /// /// # Example /// /// ```rust /// use zxc::{compress, Level}; /// /// let data = b"Hello, world!"; /// /// // Maximum performance (no checksum) /// let compressed = compress(data, Level::Default, None)?; /// /// // With data integrity verification /// let compressed = compress(data, Level::Default, Some(true))?; /// # Ok::<(), zxc::Error>(()) /// ``` pub fn compress(data: &[u8], level: Level, checksum: Option) -> Result> { let opts = CompressOptions { level, checksum: checksum.unwrap_or(false), seekable: false, }; compress_with_options(data, &opts) } /// Compresses data with full options control. /// /// # Example /// /// ```rust /// use zxc::{compress_with_options, CompressOptions, Level}; /// /// let data = b"Hello, world!"; /// let opts = CompressOptions::with_level(Level::Compact).without_checksum(); /// let compressed = compress_with_options(data, &opts)?; /// # Ok::<(), zxc::Error>(()) /// ``` pub fn compress_with_options(data: &[u8], options: &CompressOptions) -> Result> { let bound = compress_bound(data.len()) as usize; let mut output = Vec::with_capacity(bound); let written = unsafe { impl_compress( data, output.as_mut_ptr(), output.capacity(), options )? }; unsafe { output.set_len(written); } Ok(output) } /// Helper to handle the raw compression call. /// /// # Safety /// /// `dst_ptr` must be valid for writes up to `dst_cap` bytes. #[inline(always)] unsafe fn impl_compress( data: &[u8], dst_ptr: *mut u8, dst_cap: usize, options: &CompressOptions, ) -> Result { let written = unsafe { let copts = zxc_sys::zxc_compress_opts_t { level: options.level as i32, checksum_enabled: options.checksum as i32, seekable: options.seekable as i32, ..Default::default() }; zxc_sys::zxc_compress( data.as_ptr() as *const c_void, data.len(), dst_ptr as *mut c_void, dst_cap, &copts, ) }; if written < 0 { return Err(error_from_code(written)); } if written == 0 && !data.is_empty() { return Err(Error::InvalidData); } Ok(written as usize) } /// Compresses data into a pre-allocated buffer. /// /// Returns the number of bytes written to `output`. /// /// # Errors /// /// Returns [`Error::CompressionFailed`] if the output buffer is too small /// or an internal error occurs. /// /// # Example /// /// ```rust /// use zxc::{compress_to, compress_bound, CompressOptions}; /// /// let data = b"Hello, world!"; /// let mut output = vec![0u8; compress_bound(data.len()) as usize]; /// let size = compress_to(data, &mut output, &CompressOptions::default())?; /// output.truncate(size); /// # Ok::<(), zxc::Error>(()) /// ``` pub fn compress_to(data: &[u8], output: &mut [u8], options: &CompressOptions) -> Result { unsafe { impl_compress(data, output.as_mut_ptr(), output.len(), options) } } /// Returns the original uncompressed size from compressed data. /// /// This reads the footer without performing decompression. /// Returns `None` if the data is invalid or truncated. /// /// # Example /// /// ```rust /// use zxc::{compress, decompressed_size, Level}; /// /// let data = b"Hello, world!"; /// let compressed = compress(data, Level::Default, None)?; /// let size = decompressed_size(&compressed); /// assert_eq!(size, Some(data.len() as u64)); /// # Ok::<(), zxc::Error>(()) /// ``` pub fn decompressed_size(compressed: &[u8]) -> Option { let size = unsafe { zxc_sys::zxc_get_decompressed_size(compressed.as_ptr() as *const c_void, compressed.len()) }; if size == 0 && !compressed.is_empty() { None } else { Some(size) } } /// Decompresses ZXC-compressed data. /// /// This is a convenience function that queries the output size and allocates /// the buffer automatically. For zero-allocation usage, see [`decompress_to`]. /// /// # Example /// /// ```rust /// use zxc::{compress, decompress, Level}; /// /// let data = b"Hello, world!"; /// let compressed = compress(data, Level::Default, None)?; /// let decompressed = decompress(&compressed)?; /// assert_eq!(&decompressed[..], &data[..]); /// # Ok::<(), zxc::Error>(()) /// ``` pub fn decompress(compressed: &[u8]) -> Result> { decompress_with_options(compressed, &DecompressOptions::default()) } /// Decompresses data with full options control. pub fn decompress_with_options(compressed: &[u8], options: &DecompressOptions) -> Result> { let size = decompressed_size(compressed).ok_or(Error::InvalidData)? as usize; let mut output = Vec::with_capacity(size); let written = unsafe { impl_decompress( compressed, output.as_mut_ptr(), output.capacity(), options )? }; if written != size { return Err(Error::InvalidData); } unsafe { output.set_len(written); } Ok(output) } /// Helper to handle the raw decompression call. /// /// # Safety /// /// `dst_ptr` must be valid for writes up to `dst_cap` bytes. #[inline(always)] unsafe fn impl_decompress( compressed: &[u8], dst_ptr: *mut u8, dst_cap: usize, options: &DecompressOptions, ) -> Result { let written = unsafe { let dopts = zxc_sys::zxc_decompress_opts_t { checksum_enabled: if options.verify_checksum { 1 } else { 0 }, ..Default::default() }; zxc_sys::zxc_decompress( compressed.as_ptr() as *const c_void, compressed.len(), dst_ptr as *mut c_void, dst_cap, &dopts, ) }; if written < 0 { return Err(error_from_code(written)); } if written == 0 && !compressed.is_empty() { return Err(Error::InvalidData); } Ok(written as usize) } /// Decompresses data into a pre-allocated buffer. /// /// Returns the number of bytes written to `output`. /// /// # Errors /// /// Returns an error if decompression fails due to invalid data, corruption, /// or insufficient output buffer size. pub fn decompress_to( compressed: &[u8], output: &mut [u8], options: &DecompressOptions, ) -> Result { unsafe { impl_decompress(compressed, output.as_mut_ptr(), output.len(), options) } } /// Returns the library version as a tuple (major, minor, patch). pub fn version() -> (u32, u32, u32) { (ZXC_VERSION_MAJOR, ZXC_VERSION_MINOR, ZXC_VERSION_PATCH) } /// Returns the library version as a string. pub fn version_string() -> String { format!( "{}.{}.{}", ZXC_VERSION_MAJOR, ZXC_VERSION_MINOR, ZXC_VERSION_PATCH ) } // ============================================================================= // Streaming API (File-based) // ============================================================================= use std::path::Path; use std::fs::File; use std::io; #[cfg(unix)] use std::os::unix::io::AsRawFd; /// Options for streaming compression operations. #[derive(Debug, Clone)] pub struct StreamCompressOptions { /// Compression level (default: `Level::Default`) pub level: Level, /// Number of worker threads (default: `None` = auto-detect CPU cores) pub threads: Option, /// Enable checksum for data integrity (default: `true`) pub checksum: bool, /// Enable seek table for random-access decompression (default: `false`) pub seekable: bool, } impl Default for StreamCompressOptions { fn default() -> Self { Self { level: Level::Default, threads: None, checksum: true, seekable: false, } } } impl StreamCompressOptions { /// Create options with the specified compression level. pub fn with_level(level: Level) -> Self { Self { level, ..Default::default() } } /// Set the number of worker threads. pub fn threads(mut self, n: usize) -> Self { self.threads = Some(n); self } /// Disable checksum computation. pub fn without_checksum(mut self) -> Self { self.checksum = false; self } /// Enable seek table for random-access decompression. pub fn with_seekable(mut self) -> Self { self.seekable = true; self } } /// Options for streaming decompression operations. #[derive(Debug, Clone, Default)] pub struct StreamDecompressOptions { /// Number of worker threads (default: `None` = auto-detect CPU cores) pub threads: Option, /// Verify checksum during decompression (default: `true`) pub verify_checksum: bool, } impl StreamDecompressOptions { /// Set the number of worker threads. pub fn threads(mut self, n: usize) -> Self { self.threads = Some(n); self } /// Skip checksum verification. pub fn skip_checksum(mut self) -> Self { self.verify_checksum = false; self } } /// Extend Error enum for I/O errors #[derive(Debug, thiserror::Error)] pub enum StreamError { /// I/O error during file operations #[error("I/O error: {0}")] Io(#[from] io::Error), /// Error from buffer operations #[error("buffer error: {0}")] BufferError(#[from] Error), /// Streaming compression failed #[error("stream compression failed")] CompressionFailed, /// Streaming decompression failed #[error("stream decompression failed")] DecompressionFailed, /// Invalid compressed file #[error("invalid compressed file")] InvalidFile, } /// Result type for streaming operations. pub type StreamResult = std::result::Result; /// Convert a Rust File to a C FILE* for read operations. /// /// This function duplicates the file descriptor before passing it to fdopen, /// so the returned FILE* owns its own fd and must be closed with fclose(). #[cfg(unix)] unsafe fn file_to_c_file_read(file: &File) -> *mut libc::FILE { let fd = file.as_raw_fd(); // Duplicate the fd so C FILE* has its own ownership let dup_fd = unsafe { libc::dup(fd) }; if dup_fd < 0 { return std::ptr::null_mut(); } let file_ptr = unsafe { libc::fdopen(dup_fd, c"rb".as_ptr()) }; if file_ptr.is_null() { // fdopen failed, close the duplicated fd to avoid leak unsafe { libc::close(dup_fd); } } file_ptr } /// Convert a Rust File to a C FILE* for write operations. /// /// This function duplicates the file descriptor before passing it to fdopen, /// so the returned FILE* owns its own fd and must be closed with fclose(). #[cfg(unix)] unsafe fn file_to_c_file_write(file: &File) -> *mut libc::FILE { let fd = file.as_raw_fd(); // Duplicate the fd so C FILE* has its own ownership let dup_fd = unsafe { libc::dup(fd) }; if dup_fd < 0 { return std::ptr::null_mut(); } let file_ptr = unsafe { libc::fdopen(dup_fd, c"wb".as_ptr()) }; if file_ptr.is_null() { // fdopen failed, close the duplicated fd to avoid leak unsafe { libc::close(dup_fd); } } file_ptr } /// Convert a Rust File to a C FILE* for read operations (Windows). /// /// This function duplicates the file handle before passing it to the C runtime, /// so the returned FILE* owns its own handle and must be closed with fclose(). #[cfg(windows)] unsafe fn file_to_c_file_read(file: &File) -> *mut libc::FILE { use std::os::windows::io::AsRawHandle; let handle = file.as_raw_handle(); // Duplicate the handle so C FILE* has its own ownership let mut dup_handle: *mut std::ffi::c_void = std::ptr::null_mut(); let result = unsafe { windows_sys::Win32::Foundation::DuplicateHandle( windows_sys::Win32::System::Threading::GetCurrentProcess(), handle as *mut std::ffi::c_void, windows_sys::Win32::System::Threading::GetCurrentProcess(), &mut dup_handle, 0, 0, windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS, ) }; if result == 0 { return std::ptr::null_mut(); } let fd = unsafe { libc::open_osfhandle(dup_handle as libc::intptr_t, libc::O_RDONLY) }; if fd < 0 { // open_osfhandle failed, close the duplicated handle to avoid leak unsafe { windows_sys::Win32::Foundation::CloseHandle(dup_handle); } return std::ptr::null_mut(); } let file_ptr = unsafe { libc::fdopen(fd, c"rb".as_ptr()) }; if file_ptr.is_null() { // fdopen failed, close the fd (which will close the handle) unsafe { libc::close(fd); } } file_ptr } /// Convert a Rust File to a C FILE* for write operations (Windows). /// /// This function duplicates the file handle before passing it to the C runtime, /// so the returned FILE* owns its own handle and must be closed with fclose(). #[cfg(windows)] unsafe fn file_to_c_file_write(file: &File) -> *mut libc::FILE { use std::os::windows::io::AsRawHandle; let handle = file.as_raw_handle(); // Duplicate the handle so C FILE* has its own ownership let mut dup_handle: *mut std::ffi::c_void = std::ptr::null_mut(); let result = unsafe { windows_sys::Win32::Foundation::DuplicateHandle( windows_sys::Win32::System::Threading::GetCurrentProcess(), handle as *mut std::ffi::c_void, windows_sys::Win32::System::Threading::GetCurrentProcess(), &mut dup_handle, 0, 0, windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS, ) }; if result == 0 { return std::ptr::null_mut(); } let fd = unsafe { libc::open_osfhandle(dup_handle as libc::intptr_t, libc::O_WRONLY) }; if fd < 0 { // open_osfhandle failed, close the duplicated handle to avoid leak unsafe { windows_sys::Win32::Foundation::CloseHandle(dup_handle); } return std::ptr::null_mut(); } let file_ptr = unsafe { libc::fdopen(fd, c"wb".as_ptr()) }; if file_ptr.is_null() { // fdopen failed, close the fd (which will close the handle) unsafe { libc::close(fd); } } file_ptr } /// Compresses a file using multi-threaded streaming. /// /// This is the recommended method for compressing large files, as it: /// - Processes data in chunks without loading the entire file into memory /// - Uses multiple CPU cores for parallel compression /// - Provides better throughput for files larger than a few MB /// /// # Arguments /// /// * `input` - Path to the input file /// * `output` - Path to the output file /// * `level` - Compression level /// * `threads` - Number of threads (`None` = auto-detect CPU cores) /// * `checksum` - Optional checksum for data integrity (`None` = disabled for maximum performance) /// /// # Example /// /// ```rust,no_run /// use zxc::{compress_file, Level}; /// /// // Maximum performance (no checksum, auto threads) /// let bytes = compress_file("input.bin", "output.zxc", Level::Default, None, None)?; /// /// // With data integrity verification /// let bytes = compress_file("input.bin", "output.zxc", Level::Default, None, Some(true))?; /// /// // Custom configuration /// let bytes = compress_file("input.bin", "output.zxc", Level::Compact, Some(4), Some(true))?; /// # Ok::<(), zxc::StreamError>(()) /// ``` pub fn compress_file>( input: P, output: P, level: Level, threads: Option, checksum: Option, ) -> StreamResult { let f_in = File::open(input)?; let f_out = File::create(output)?; let n_threads = threads.unwrap_or(0) as i32; let checksum_enabled = if checksum.unwrap_or(false) { 1 } else { 0 }; unsafe { let c_in = file_to_c_file_read(&f_in); let c_out = file_to_c_file_write(&f_out); // Check for errors and cleanup on failure if c_in.is_null() { if !c_out.is_null() { libc::fclose(c_out); } return Err(StreamError::Io(io::Error::last_os_error())); } if c_out.is_null() { libc::fclose(c_in); return Err(StreamError::Io(io::Error::last_os_error())); } let result = zxc_sys::zxc_stream_compress( c_in, c_out, &zxc_sys::zxc_compress_opts_t { n_threads: n_threads, level: level as i32, checksum_enabled: checksum_enabled, ..Default::default() }, ); // Always close C FILE handles (they own duplicated fds) libc::fclose(c_in); libc::fclose(c_out); if result < 0 { Err(StreamError::BufferError(error_from_code(result))) } else { Ok(result as u64) } } } /// Decompresses a file using multi-threaded streaming. /// /// # Example /// /// ```rust,no_run /// use zxc::decompress_file; /// /// // Decompress with auto-detected thread count /// let bytes = decompress_file("compressed.zxc", "output.bin", None)?; /// println!("Decompressed {} bytes", bytes); /// # Ok::<(), zxc::StreamError>(()) /// ``` pub fn decompress_file>( input: P, output: P, threads: Option, ) -> StreamResult { let f_in = File::open(input)?; let f_out = File::create(output)?; let n_threads = threads.unwrap_or(0) as i32; let checksum_enabled = 1; // Default to verify unsafe { let c_in = file_to_c_file_read(&f_in); let c_out = file_to_c_file_write(&f_out); // Check for errors and cleanup on failure if c_in.is_null() { if !c_out.is_null() { libc::fclose(c_out); } return Err(StreamError::Io(io::Error::last_os_error())); } if c_out.is_null() { libc::fclose(c_in); return Err(StreamError::Io(io::Error::last_os_error())); } let result = zxc_sys::zxc_stream_decompress( c_in, c_out, &zxc_sys::zxc_decompress_opts_t { n_threads: n_threads, checksum_enabled: checksum_enabled, ..Default::default() }, ); // Always close C FILE handles (they own duplicated fds) libc::fclose(c_in); libc::fclose(c_out); if result < 0 { Err(StreamError::BufferError(error_from_code(result))) } else { Ok(result as u64) } } } /// Returns the decompressed size stored in a compressed file. /// /// This reads the file footer without performing decompression, /// useful for pre-allocating buffers or showing progress. /// /// # Example /// /// ```rust,no_run /// use zxc::file_decompressed_size; /// /// let size = file_decompressed_size("compressed.zxc")?; /// println!("Original size: {} bytes", size); /// # Ok::<(), zxc::StreamError>(()) /// ``` pub fn file_decompressed_size>(path: P) -> StreamResult { let f = File::open(path)?; unsafe { let c_file = file_to_c_file_read(&f); if c_file.is_null() { return Err(StreamError::Io(io::Error::last_os_error())); } let result = zxc_sys::zxc_stream_get_decompressed_size(c_file); if result < 0 { Err(StreamError::InvalidFile) } else { Ok(result as u64) } } } // ============================================================================= // Tests // ============================================================================= #[cfg(test)] mod tests { use super::*; #[test] fn test_roundtrip() { let data = b"Hello, ZXC! This is a test of the safe Rust wrapper."; let compressed = compress(data, Level::Default, None).unwrap(); let decompressed = decompress(&compressed).unwrap(); assert_eq!(&decompressed[..], &data[..]); } #[test] fn test_all_levels() { let data = b"Test data with repetition: CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; for level in Level::all() { let compressed = compress(data, *level, None).unwrap(); let decompressed = decompress(&compressed).unwrap(); assert_eq!( &decompressed[..], &data[..], "Roundtrip failed at level {:?}", level ); } } #[test] fn test_empty() { let data: &[u8] = b""; let result = compress(data, Level::Default, None); assert!(result.is_err(), "Compressing empty data should return an error"); } #[test] fn test_checksum_options() { let data = b"Test with and without checksum"; // With checksum let opts_with = CompressOptions::with_level(Level::Default); let compressed = compress_with_options(data, &opts_with).unwrap(); let decompressed = decompress(&compressed).unwrap(); assert_eq!(&decompressed[..], &data[..]); // Without checksum let opts_without = CompressOptions::with_level(Level::Fast).without_checksum(); let compressed = compress_with_options(data, &opts_without).unwrap(); let decompressed = decompress_with_options(&compressed, &DecompressOptions::skip_checksum()).unwrap(); assert_eq!(&decompressed[..], &data[..]); } #[test] fn test_decompressed_size() { let data = b"Hello, world! Testing decompressed_size function."; let compressed = compress(data, Level::Default, None).unwrap(); let size = decompressed_size(&compressed); assert_eq!(size, Some(data.len() as u64)); } #[test] fn test_version() { let (major, minor, patch) = version(); let expected = format!("{}.{}.{}", major, minor, patch); assert_eq!(version_string(), expected); let cargo_version = env!("CARGO_PKG_VERSION"); assert_eq!(expected, cargo_version); } #[test] fn test_invalid_data() { let garbage = b"not valid zxc data"; let result = decompress(garbage); assert!(result.is_err()); } #[test] fn test_specific_error_codes() { // Test invalid magic - size detection should fail first and return InvalidData let invalid_magic = b"INVALID_DATA_NOT_ZXC"; let result = decompress(invalid_magic); assert!(result.is_err(), "Should fail on invalid data"); // Test truncated data - should produce specific error let data = b"Hello, world! Testing error codes with enough data to compress well."; let compressed = compress(data, Level::Default, None).unwrap(); let truncated = &compressed[..10]; // Too short to be valid match decompress(truncated) { Err(Error::InvalidData) | Err(Error::SrcTooSmall) | Err(Error::BadHeader) | Err(Error::BadMagic) => { // Any of these errors are acceptable for truncated data } other => panic!("Expected specific error for truncated data, got: {:?}", other), } } #[test] fn test_error_messages() { // Verify error messages are descriptive let errors = vec![ (Error::Memory, "memory allocation failed"), (Error::BadChecksum, "checksum verification failed"), (Error::CorruptData, "corrupted compressed data"), (Error::DstTooSmall, "destination buffer too small"), ]; for (error, expected_msg) in errors { let msg = error.to_string(); assert!( msg.contains(expected_msg), "Error message '{}' should contain '{}'", msg, expected_msg ); } } #[test] fn test_compress_to_buffer() { let data = b"Testing compress_to with pre-allocated buffer"; let mut output = vec![0u8; compress_bound(data.len()) as usize]; let size = compress_to(data, &mut output, &CompressOptions::default()).unwrap(); output.truncate(size); let decompressed = decompress(&output).unwrap(); assert_eq!(&decompressed[..], &data[..]); } #[test] fn test_large_data() { // 1 MB of random-ish but compressible data let data: Vec = (0..1024 * 1024) .map(|i| ((i % 256) ^ ((i / 256) % 256)) as u8) .collect(); let compressed = compress(&data, Level::Default, None).unwrap(); assert!(compressed.len() < data.len()); // Should compress let decompressed = decompress(&compressed).unwrap(); assert_eq!(decompressed, data); } } // ============================================================================= // Streaming API Tests // ============================================================================= #[cfg(test)] mod streaming_tests { use super::*; use std::fs; use std::io::Write; fn temp_path(name: &str) -> String { let dir_name = format!("zxc_test_{}", std::process::id()); // Try the system temp directory first let mut path = std::env::temp_dir(); path.push(&dir_name); // If we can't create the directory in the system temp, fall back // to a subdirectory relative to the current working directory. // This happens on some Windows CI runners where TEMP is missing or // points to a path the process cannot access. if fs::create_dir_all(&path).is_err() { path = std::env::current_dir().expect("cannot determine current directory"); path.push(&dir_name); fs::create_dir_all(&path).expect("failed to create temp directory in current dir"); } path.push(name); path.to_string_lossy().into_owned() } #[test] fn test_file_roundtrip() { let input_path = temp_path("roundtrip_input.bin"); let compressed_path = temp_path("roundtrip_compressed.zxc"); let output_path = temp_path("roundtrip_output.bin"); // Create test data let data: Vec = (0..64 * 1024) // 64 KB .map(|i| ((i % 256) ^ ((i / 256) % 256)) as u8) .collect(); // Write test file { let mut f = fs::File::create(&input_path).unwrap(); f.write_all(&data).unwrap(); } // Compress let compressed_size = compress_file(&input_path, &compressed_path, Level::Default, None, None).unwrap(); assert!(compressed_size > 0); // Decompress let decompressed_size = decompress_file(&compressed_path, &output_path, None).unwrap(); assert_eq!(decompressed_size, data.len() as u64); // Verify content let result = fs::read(&output_path).unwrap(); assert_eq!(result, data); // Cleanup let _ = fs::remove_file(&input_path); let _ = fs::remove_file(&compressed_path); let _ = fs::remove_file(&output_path); } #[test] fn test_file_decompressed_size_query() { let input_path = temp_path("size_input.bin"); let compressed_path = temp_path("size_compressed.zxc"); // Create test data let data: Vec = (0..128 * 1024) // 128 KB .map(|i| (i % 256) as u8) .collect(); // Write and compress { let mut f = fs::File::create(&input_path).unwrap(); f.write_all(&data).unwrap(); } compress_file(&input_path, &compressed_path, Level::Default, None, None).unwrap(); // Query size let reported_size = file_decompressed_size(&compressed_path).unwrap(); assert_eq!(reported_size, data.len() as u64); // Cleanup let _ = fs::remove_file(&input_path); let _ = fs::remove_file(&compressed_path); } #[test] fn test_file_all_levels() { let input_path = temp_path("levels_input.bin"); // Create test data let data: Vec = (0..32 * 1024) // 32 KB .map(|i| ((i % 256) ^ ((i / 256) % 256)) as u8) .collect(); { let mut f = fs::File::create(&input_path).unwrap(); f.write_all(&data).unwrap(); } for level in Level::all() { let compressed_path = temp_path(&format!("levels_{:?}.zxc", level)); let output_path = temp_path(&format!("levels_{:?}_out.bin", level)); // Compress with this level compress_file(&input_path, &compressed_path, *level, Some(2), None).unwrap(); // Decompress decompress_file(&compressed_path, &output_path, Some(2)).unwrap(); // Verify let result = fs::read(&output_path).unwrap(); assert_eq!(result, data, "Data mismatch at level {:?}", level); // Cleanup let _ = fs::remove_file(&compressed_path); let _ = fs::remove_file(&output_path); } let _ = fs::remove_file(&input_path); } #[test] fn test_file_multithreaded() { let input_path = temp_path("mt_input.bin"); let compressed_path = temp_path("mt_compressed.zxc"); let output_path = temp_path("mt_output.bin"); // Create larger test data (1 MB) let data: Vec = (0..1024 * 1024) .map(|i| ((i % 256) ^ ((i / 256) % 256)) as u8) .collect(); { let mut f = fs::File::create(&input_path).unwrap(); f.write_all(&data).unwrap(); } // Test with different thread counts for threads in [1, 2, 4] { // Compress compress_file(&input_path, &compressed_path, Level::Default, Some(threads), None).unwrap(); // Decompress let size = decompress_file(&compressed_path, &output_path, Some(threads)).unwrap(); assert_eq!(size, data.len() as u64); // Verify let result = fs::read(&output_path).unwrap(); assert_eq!(result, data, "Mismatch with {} threads", threads); } // Cleanup let _ = fs::remove_file(&input_path); let _ = fs::remove_file(&compressed_path); let _ = fs::remove_file(&output_path); } } zxc-0.10.0/wrappers/wasm/000077500000000000000000000000001517010557200152115ustar00rootroot00000000000000zxc-0.10.0/wrappers/wasm/README.md000066400000000000000000000062011517010557200164670ustar00rootroot00000000000000# ZXC WebAssembly High-performance lossless compression for the browser and Node.js via WebAssembly. ## Features - **Buffer API**: Compress and decompress `Uint8Array` buffers - **Reusable Contexts**: Amortise allocation overhead across multiple operations - **All Levels**: Compression levels 1–5 - **Checksum Support**: Optional integrity verification - **Tiny Footprint**: ~60 KB `.wasm` file (scalar build, no SIMD) ## Quick Start ### Browser (ES Module) ```js import createZXC from './zxc_wasm.js'; const zxc = await createZXC(); // Compress const input = new TextEncoder().encode('Hello, World!'); const compressed = zxc.compress(input, { level: 3 }); // Decompress const output = zxc.decompress(compressed); console.log(new TextDecoder().decode(output)); // "Hello, World!" ``` ### Node.js ```js import createZXC from 'zxc-wasm'; import { readFileSync } from 'fs'; const zxc = await createZXC(); const data = new Uint8Array(readFileSync('input.bin')); const compressed = zxc.compress(data, { level: 5, checksum: true }); const decompressed = zxc.decompress(compressed, { checksum: true }); ``` ## API Reference ### `createZXC(moduleOverrides?) → Promise` Initialise the WASM module. Returns a frozen API object. ### `zxc.compress(data, opts?) → Uint8Array` | Option | Type | Default | Description | |--------|------|---------|-------------| | `level` | `number` | `3` | Compression level (1–5) | | `checksum` | `boolean` | `false` | Enable integrity checksums | ### `zxc.decompress(data, opts?) → Uint8Array` | Option | Type | Default | Description | |--------|------|---------|-------------| | `checksum` | `boolean` | `false` | Verify integrity checksums | ### `zxc.compressBound(inputSize) → number` Maximum compressed output size for a given input size. ### `zxc.getDecompressedSize(data) → number` Read the original size from a compressed buffer without decompressing. ### `zxc.createCompressContext(opts?) → CompressContext` Create a reusable compression context (avoids per-call allocation). ```js const ctx = zxc.createCompressContext({ level: 3 }); const c1 = ctx.compress(data1); const c2 = ctx.compress(data2); ctx.free(); // Release WASM memory ``` ### `zxc.createDecompressContext() → DecompressContext` Create a reusable decompression context. ```js const ctx = zxc.createDecompressContext(); const d1 = ctx.decompress(compressed1); const d2 = ctx.decompress(compressed2); ctx.free(); ``` ### Properties | Property | Type | Description | |----------|------|-------------| | `version` | `string` | Library version (e.g. `"0.10.0"`) | | `minLevel` | `number` | Minimum compression level (`1`) | | `maxLevel` | `number` | Maximum compression level (`5`) | | `defaultLevel` | `number` | Default compression level (`3`) | ## Building from Source Requires [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html). ```bash # Configure emcmake cmake -B build-wasm -DCMAKE_BUILD_TYPE=Release # Build cmake --build build-wasm # Test BUILD_DIR=build-wasm node wrappers/wasm/test.mjs ``` The build produces `build-wasm/zxc.js` and `build-wasm/zxc.wasm`. ## License BSD 3-Clause. See [LICENSE](../../LICENSE). zxc-0.10.0/wrappers/wasm/package.json000066400000000000000000000012741517010557200175030ustar00rootroot00000000000000{ "name": "zxc-wasm", "version": "0.10.0", "description": "ZXC high-performance lossless compression — WebAssembly build", "type": "module", "main": "zxc_wasm.js", "exports": { ".": "./zxc_wasm.js" }, "files": [ "zxc_wasm.js", "zxc.js", "zxc.wasm", "README.md" ], "scripts": { "test": "node test.mjs" }, "keywords": [ "compression", "decompression", "lz77", "wasm", "webassembly", "zxc", "lossless", "high-performance" ], "author": "Bertrand Lebonnois", "license": "BSD-3-Clause", "repository": { "type": "git", "url": "https://github.com/hellobertrand/zxc.git", "directory": "wrappers/wasm" } } zxc-0.10.0/wrappers/wasm/test.mjs000066400000000000000000000235161517010557200167120ustar00rootroot00000000000000/** * ZXC WASM Roundtrip Test * * Validates compress → decompress correctness via Node.js. * Run: node wrappers/wasm/test.mjs * * Expects the built zxc.js + zxc.wasm to be in the build directory. * The BUILD_DIR environment variable can override the default path. * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ import { createRequire } from 'module'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); // Resolve BUILD_DIR relative to CWD (not the test file location) import { resolve } from 'path'; const buildDir = process.env.BUILD_DIR ? resolve(process.cwd(), process.env.BUILD_DIR) : join(__dirname, '..', '..', 'build-wasm'); // Emscripten generates a CJS module; use createRequire to load it. const require = createRequire(import.meta.url); const ZXCModule = require(join(buildDir, 'zxc.js')); let passed = 0; let failed = 0; function assert(cond, msg) { if (!cond) { console.error(` ✗ FAIL: ${msg}`); failed++; } else { console.log(` ✓ ${msg}`); passed++; } } function arraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } async function main() { console.log('ZXC WASM Test Suite\n'); // --- 1. Module initialisation --- console.log('1. Module Initialisation'); const Module = await ZXCModule(); assert(!!Module, 'Module loaded successfully'); // Wrap core functions const _compress = Module.cwrap('zxc_compress', 'number', ['number', 'number', 'number', 'number', 'number']); const _decompress = Module.cwrap('zxc_decompress', 'number', ['number', 'number', 'number', 'number', 'number']); const _compress_bound = Module.cwrap('zxc_compress_bound', 'number', ['number']); const _get_decompressed_size = Module.cwrap('zxc_get_decompressed_size', 'number', ['number', 'number']); const _version_string = Module.cwrap('zxc_version_string', 'string', []); const _min_level = Module.cwrap('zxc_min_level', 'number', []); const _max_level = Module.cwrap('zxc_max_level', 'number', []); const _default_level = Module.cwrap('zxc_default_level', 'number', []); const _error_name = Module.cwrap('zxc_error_name', 'string', ['number']); // --- 2. Library info --- console.log('\n2. Library Info'); const version = _version_string(); assert(typeof version === 'string' && version.length > 0, `Version: ${version}`); assert(_min_level() === 1, `Min level: ${_min_level()}`); assert(_max_level() === 5, `Max level: ${_max_level()}`); assert(_default_level() === 3, `Default level: ${_default_level()}`); // --- 3. Compress bound --- console.log('\n3. Compress Bound'); const bound1k = _compress_bound(1024); assert(bound1k > 1024, `Bound for 1KB: ${bound1k}`); const bound0 = _compress_bound(0); assert(bound0 > 0, `Bound for 0 bytes: ${bound0}`); // --- 4. Roundtrip: simple string --- console.log('\n4. Roundtrip: Simple String'); { const input = new TextEncoder().encode('Hello, ZXC WebAssembly! '.repeat(100)); const bound = _compress_bound(input.length); const srcPtr = Module._malloc(input.length); const dstPtr = Module._malloc(bound); Module.HEAPU8.set(input, srcPtr); const csize = _compress(srcPtr, input.length, dstPtr, bound, 0); assert(csize > 0, `Compressed ${input.length} → ${csize} bytes`); assert(csize < input.length, `Compression ratio: ${(input.length / csize).toFixed(2)}x`); // Read back compressed data const compressed = new Uint8Array(Module.HEAPU8.buffer, dstPtr, csize).slice(); // Get decompressed size const csrcPtr = Module._malloc(compressed.length); Module.HEAPU8.set(compressed, csrcPtr); const origSize = _get_decompressed_size(csrcPtr, compressed.length); assert(origSize === input.length, `Decompressed size: ${origSize} (expected ${input.length})`); // Decompress const outPtr = Module._malloc(origSize); const dsize = _decompress(csrcPtr, compressed.length, outPtr, origSize, 0); assert(dsize === input.length, `Decompressed ${dsize} bytes`); const output = new Uint8Array(Module.HEAPU8.buffer, outPtr, dsize).slice(); assert(arraysEqual(input, output), 'Roundtrip byte-exact match'); Module._free(srcPtr); Module._free(dstPtr); Module._free(csrcPtr); Module._free(outPtr); } // --- 5. Roundtrip: all compression levels --- console.log('\n5. Roundtrip: All Compression Levels'); { const input = new Uint8Array(4096); // Fill with semi-compressible pattern for (let i = 0; i < input.length; i++) { input[i] = (i * 7 + 13) & 0xFF; } const bound = _compress_bound(input.length); for (let level = 1; level <= 5; level++) { const srcPtr = Module._malloc(input.length); const dstPtr = Module._malloc(bound); Module.HEAPU8.set(input, srcPtr); // Build opts struct: {n_threads=0, level, block_size=0, checksum=1, seekable=0, ...} const optsPtr = Module._malloc(28); for (let i = 0; i < 28; i++) Module.HEAPU8[optsPtr + i] = 0; Module.HEAP32[(optsPtr >> 2) + 1] = level; // level Module.HEAP32[(optsPtr >> 2) + 3] = 1; // checksum_enabled const csize = _compress(srcPtr, input.length, dstPtr, bound, optsPtr); assert(csize > 0, `Level ${level}: compressed to ${csize} bytes`); // Decompress with checksum verification const compressed = new Uint8Array(Module.HEAPU8.buffer, dstPtr, csize).slice(); const csrcPtr = Module._malloc(compressed.length); Module.HEAPU8.set(compressed, csrcPtr); const dOptsPtr = Module._malloc(16); for (let i = 0; i < 16; i++) Module.HEAPU8[dOptsPtr + i] = 0; Module.HEAP32[(dOptsPtr >> 2) + 1] = 1; // checksum_enabled const outPtr = Module._malloc(input.length); const dsize = _decompress(csrcPtr, compressed.length, outPtr, input.length, dOptsPtr); const output = new Uint8Array(Module.HEAPU8.buffer, outPtr, dsize).slice(); assert(arraysEqual(input, output), `Level ${level}: roundtrip OK`); Module._free(srcPtr); Module._free(dstPtr); Module._free(optsPtr); Module._free(csrcPtr); Module._free(dOptsPtr); Module._free(outPtr); } } // --- 6. Reusable context API --- console.log('\n6. Reusable Context API'); { const _create_cctx = Module.cwrap('zxc_create_cctx', 'number', ['number']); const _free_cctx = Module.cwrap('zxc_free_cctx', 'void', ['number']); const _compress_cctx = Module.cwrap('zxc_compress_cctx', 'number', ['number', 'number', 'number', 'number', 'number', 'number']); const _create_dctx = Module.cwrap('zxc_create_dctx', 'number', []); const _free_dctx = Module.cwrap('zxc_free_dctx', 'void', ['number']); const _decompress_dctx = Module.cwrap('zxc_decompress_dctx', 'number', ['number', 'number', 'number', 'number', 'number', 'number']); const cctx = _create_cctx(0); assert(cctx !== 0, 'Created compression context'); const dctx = _create_dctx(); assert(dctx !== 0, 'Created decompression context'); // Compress two different payloads with same context for (let trial = 0; trial < 3; trial++) { const input = new TextEncoder().encode(`Trial ${trial}: ${'ABCDEFGH'.repeat(200)}`); const bound = _compress_bound(input.length); const srcPtr = Module._malloc(input.length); const dstPtr = Module._malloc(bound); Module.HEAPU8.set(input, srcPtr); const csize = _compress_cctx(cctx, srcPtr, input.length, dstPtr, bound, 0); assert(csize > 0, `Context compress trial ${trial}: ${csize} bytes`); const compressed = new Uint8Array(Module.HEAPU8.buffer, dstPtr, csize).slice(); const csrcPtr = Module._malloc(compressed.length); Module.HEAPU8.set(compressed, csrcPtr); const outPtr = Module._malloc(input.length); const dsize = _decompress_dctx(dctx, csrcPtr, compressed.length, outPtr, input.length, 0); const output = new Uint8Array(Module.HEAPU8.buffer, outPtr, dsize).slice(); assert(arraysEqual(input, output), `Context roundtrip trial ${trial}: OK`); Module._free(srcPtr); Module._free(dstPtr); Module._free(csrcPtr); Module._free(outPtr); } _free_cctx(cctx); _free_dctx(dctx); console.log(' Contexts freed.'); } // --- 7. Error handling --- console.log('\n7. Error Handling'); { const errorName = _error_name(-1); assert(typeof errorName === 'string', `Error name for -1: ${errorName}`); // Attempt to decompress garbage const garbage = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); const gPtr = Module._malloc(garbage.length); const outPtr = Module._malloc(1024); Module.HEAPU8.set(garbage, gPtr); const result = _decompress(gPtr, garbage.length, outPtr, 1024, 0); assert(result < 0, `Garbage decompression returns error: ${result} (${_error_name(result)})`); Module._free(gPtr); Module._free(outPtr); } // --- Summary --- console.log(`\n${'='.repeat(40)}`); console.log(`Results: ${passed} passed, ${failed} failed`); process.exit(failed > 0 ? 1 : 0); } main().catch(err => { console.error('Fatal error:', err); process.exit(1); }); zxc-0.10.0/wrappers/wasm/wasm_entry.c000066400000000000000000000010261517010557200175440ustar00rootroot00000000000000/* * ZXC - High-performance lossless compression * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ /** * @file wasm_entry.c * @brief Minimal entry point for the WebAssembly build. * * Emscripten requires a main() symbol when producing a .js + .wasm pair * via add_executable(). All useful functionality is exported directly * from the library through -sEXPORTED_FUNCTIONS; this file simply * provides the mandatory entry point. */ int main(void) { return 0; } zxc-0.10.0/wrappers/wasm/zxc_wasm.js000066400000000000000000000303101517010557200173770ustar00rootroot00000000000000/** * ZXC WebAssembly Wrapper * * High-level JavaScript API for ZXC compression/decompression via WASM. * * @example * import createZXC from './zxc_wasm.js'; * const zxc = await createZXC(); * * const compressed = zxc.compress(inputUint8Array, { level: 3 }); * const decompressed = zxc.decompress(compressed); * * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. * SPDX-License-Identifier: BSD-3-Clause */ // The Emscripten-generated module loader (zxc.js) must be in the same directory. // It exports a factory function named ZXCModule (set by -sEXPORT_NAME). import ZXCModule from './zxc.js'; /** * Initialise the ZXC WASM module and return an ergonomic API object. * * @param {object} [moduleOverrides] - Optional Emscripten module overrides * (e.g. { locateFile: ... } for custom .wasm path resolution). * @returns {Promise} Resolved API object. */ export default async function createZXC(moduleOverrides) { const Module = await ZXCModule(moduleOverrides || {}); // --- Wrap C functions via cwrap ------------------------------------------ const _compress = Module.cwrap('zxc_compress', 'number', ['number', 'number', 'number', 'number', 'number']); const _decompress = Module.cwrap('zxc_decompress', 'number', ['number', 'number', 'number', 'number', 'number']); const _compress_bound = Module.cwrap('zxc_compress_bound', 'number', ['number']); const _get_decompressed_size = Module.cwrap('zxc_get_decompressed_size', 'number', ['number', 'number']); const _create_cctx = Module.cwrap('zxc_create_cctx', 'number', ['number']); const _free_cctx = Module.cwrap('zxc_free_cctx', 'void', ['number']); const _compress_cctx = Module.cwrap('zxc_compress_cctx', 'number', ['number', 'number', 'number', 'number', 'number', 'number']); const _create_dctx = Module.cwrap('zxc_create_dctx', 'number', []); const _free_dctx = Module.cwrap('zxc_free_dctx', 'void', ['number']); const _decompress_dctx = Module.cwrap('zxc_decompress_dctx', 'number', ['number', 'number', 'number', 'number', 'number', 'number']); const _version_string = Module.cwrap('zxc_version_string', 'string', []); const _error_name = Module.cwrap('zxc_error_name', 'string', ['number']); const _min_level = Module.cwrap('zxc_min_level', 'number', []); const _max_level = Module.cwrap('zxc_max_level', 'number', []); const _default_level = Module.cwrap('zxc_default_level', 'number', []); const _malloc = Module._malloc; const _free = Module._free; // --- Options struct layout ----------------------------------------------- // zxc_compress_opts_t (WASM32 layout): // int n_threads (4) | int level (4) | size_t block_size (4 in wasm32) // int checksum_enabled (4) | int seekable (4) // ptr progress_cb (4) | ptr user_data (4) // Total: 28 bytes in WASM32 const COMPRESS_OPTS_SIZE = 28; // zxc_decompress_opts_t: // int n_threads (4) | int checksum_enabled (4) | ptr progress_cb (4) | ptr user_data (4) // Total: 16 bytes in WASM32 const DECOMPRESS_OPTS_SIZE = 16; /** * Write a zxc_compress_opts_t struct into WASM memory. * @returns {number} Pointer to the struct (caller must free). */ function _writeCompressOpts(level, checksum, seekable) { const ptr = _malloc(COMPRESS_OPTS_SIZE); // Zero-fill first for (let i = 0; i < COMPRESS_OPTS_SIZE; i++) { Module.HEAPU8[ptr + i] = 0; } // n_threads = 0 (offset 0) Module.HEAP32[(ptr >> 2) + 0] = 0; // level (offset 4) Module.HEAP32[(ptr >> 2) + 1] = level; // block_size = 0 (default, offset 8) Module.HEAPU32[(ptr >> 2) + 2] = 0; // checksum_enabled (offset 12) Module.HEAP32[(ptr >> 2) + 3] = checksum ? 1 : 0; // seekable (offset 16) Module.HEAP32[(ptr >> 2) + 4] = seekable ? 1 : 0; // progress_cb = NULL (offset 20), user_data = NULL (offset 24) return ptr; } /** * Write a zxc_decompress_opts_t struct into WASM memory. * @returns {number} Pointer to the struct (caller must free). */ function _writeDecompressOpts(checksum) { const ptr = _malloc(DECOMPRESS_OPTS_SIZE); for (let i = 0; i < DECOMPRESS_OPTS_SIZE; i++) { Module.HEAPU8[ptr + i] = 0; } // n_threads = 0 (offset 0) Module.HEAP32[(ptr >> 2) + 0] = 0; // checksum_enabled (offset 4) Module.HEAP32[(ptr >> 2) + 1] = checksum ? 1 : 0; // progress_cb = NULL (offset 8), user_data = NULL (offset 12) return ptr; } // --- Public API ---------------------------------------------------------- /** * Compress a Uint8Array. * * @param {Uint8Array} data - Input data to compress. * @param {object} [opts] - Options. * @param {number} [opts.level=3] - Compression level (1-5). * @param {boolean} [opts.checksum=false] - Enable checksums. * @param {boolean} [opts.seekable=false] - Append seek table for random-access. * @returns {Uint8Array} Compressed data. * @throws {Error} On compression failure. */ function compress(data, opts) { const level = (opts && opts.level) || _default_level(); const checksum = (opts && opts.checksum) || false; const seekable = (opts && opts.seekable) || false; const bound = _compress_bound(data.length); if (bound === 0) throw new Error('ZXC: compress_bound returned 0'); const srcPtr = _malloc(data.length); const dstPtr = _malloc(bound); const optsPtr = _writeCompressOpts(level, checksum, seekable); try { Module.HEAPU8.set(data, srcPtr); const result = _compress(srcPtr, data.length, dstPtr, bound, optsPtr); if (result < 0) { throw new Error(`ZXC compress error: ${_error_name(result)} (${result})`); } return new Uint8Array(Module.HEAPU8.buffer, dstPtr, result).slice(); } finally { _free(srcPtr); _free(dstPtr); _free(optsPtr); } } /** * Decompress a Uint8Array. * * @param {Uint8Array} data - Compressed data. * @param {object} [opts] - Options. * @param {boolean} [opts.checksum=false] - Verify checksums. * @returns {Uint8Array} Decompressed data. * @throws {Error} On decompression failure. */ function decompress(data, opts) { const checksum = (opts && opts.checksum) || false; // Read decompressed size from footer const srcPtr = _malloc(data.length); Module.HEAPU8.set(data, srcPtr); const origSize = _get_decompressed_size(srcPtr, data.length); if (origSize === 0) { _free(srcPtr); throw new Error('ZXC: cannot read decompressed size (invalid archive?)'); } const dstPtr = _malloc(origSize); const optsPtr = _writeDecompressOpts(checksum); try { const result = _decompress(srcPtr, data.length, dstPtr, origSize, optsPtr); if (result < 0) { throw new Error(`ZXC decompress error: ${_error_name(result)} (${result})`); } return new Uint8Array(Module.HEAPU8.buffer, dstPtr, result).slice(); } finally { _free(srcPtr); _free(dstPtr); _free(optsPtr); } } /** * Returns the maximum compressed output size for a given input size. * @param {number} inputSize * @returns {number} */ function compressBound(inputSize) { return _compress_bound(inputSize); } /** * Reads the decompressed size from a compressed buffer without decompressing. * @param {Uint8Array} data - Compressed data. * @returns {number} Original uncompressed size, or 0 if invalid. */ function getDecompressedSize(data) { const ptr = _malloc(data.length); Module.HEAPU8.set(data, ptr); const size = _get_decompressed_size(ptr, data.length); _free(ptr); return size; } /** * Create a reusable compression context for high-frequency usage. * Call .free() when done to release WASM memory. * * @param {object} [opts] - Default options. * @param {number} [opts.level=3] - Default compression level. * @param {boolean} [opts.checksum=false] - Default checksum setting. * @returns {{ compress: Function, free: Function }} */ function createCompressContext(opts) { const level = (opts && opts.level) || _default_level(); const checksum = (opts && opts.checksum) || false; const seekable = (opts && opts.seekable) || false; const optsPtr = _writeCompressOpts(level, checksum, seekable); const cctx = _create_cctx(optsPtr); _free(optsPtr); if (cctx === 0) throw new Error('ZXC: failed to create compression context'); return { /** * Compress data using this reusable context. * @param {Uint8Array} data * @returns {Uint8Array} */ compress(data) { const bound = _compress_bound(data.length); const srcPtr = _malloc(data.length); const dstPtr = _malloc(bound); try { Module.HEAPU8.set(data, srcPtr); const result = _compress_cctx(cctx, srcPtr, data.length, dstPtr, bound, 0); if (result < 0) { throw new Error(`ZXC cctx compress error: ${_error_name(result)} (${result})`); } return new Uint8Array(Module.HEAPU8.buffer, dstPtr, result).slice(); } finally { _free(srcPtr); _free(dstPtr); } }, /** Free the context and release WASM memory. */ free() { _free_cctx(cctx); } }; } /** * Create a reusable decompression context for high-frequency usage. * Call .free() when done to release WASM memory. * * @returns {{ decompress: Function, free: Function }} */ function createDecompressContext() { const dctx = _create_dctx(); if (dctx === 0) throw new Error('ZXC: failed to create decompression context'); return { /** * Decompress data using this reusable context. * @param {Uint8Array} data * @returns {Uint8Array} */ decompress(data) { const srcPtr = _malloc(data.length); Module.HEAPU8.set(data, srcPtr); const origSize = _get_decompressed_size(srcPtr, data.length); if (origSize === 0) { _free(srcPtr); throw new Error('ZXC: cannot read decompressed size'); } const dstPtr = _malloc(origSize); try { const result = _decompress_dctx(dctx, srcPtr, data.length, dstPtr, origSize, 0); if (result < 0) { throw new Error(`ZXC dctx decompress error: ${_error_name(result)} (${result})`); } return new Uint8Array(Module.HEAPU8.buffer, dstPtr, result).slice(); } finally { _free(srcPtr); _free(dstPtr); } }, /** Free the context and release WASM memory. */ free() { _free_dctx(dctx); } }; } // --- Exposed API object -------------------------------------------------- return Object.freeze({ compress, decompress, compressBound, getDecompressedSize, createCompressContext, createDecompressContext, /** Library version string (e.g. "0.10.0"). */ version: _version_string(), /** Minimum compression level. */ minLevel: _min_level(), /** Maximum compression level. */ maxLevel: _max_level(), /** Default compression level. */ defaultLevel: _default_level(), /** Raw Emscripten Module (for advanced use). */ _module: Module, }); } zxc-0.10.0/zxcConfig.cmake.in000066400000000000000000000002431517010557200157370ustar00rootroot00000000000000@PACKAGE_INIT@ include(CMakeFindDependencyMacro) find_dependency(Threads) include("${CMAKE_CURRENT_LIST_DIR}/zxc-targets.cmake") check_required_components(zxc)