From d4660e6539c8b6eeba81751c018217d978fdd618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Cezar=20Mois=C3=A9s?= Date: Wed, 22 Apr 2026 03:46:59 -0300 Subject: [PATCH] Release: v2.1.6 - Added VaptVupt 2.40 --- CHANGELOG.md | 35 +- Makefile | 7 +- README.md | 6 +- ROADMAP.md | 17 +- SECURITY.md | 2 +- gui/LICENSE-GUI | 21 + gui/README.md | 127 ++ gui/assets/zupt-128.png | Bin 0 -> 24866 bytes gui/assets/zupt-icon.png | Bin 0 -> 4616 bytes gui/assets/zupt.ico | Bin 0 -> 802 bytes gui/assets/zupt.png | Bin 0 -> 81270 bytes gui/install.sh | 88 ++ gui/packaging/appimage/build-appimage.sh | 49 + gui/packaging/deb/control | 13 + .../nemo/zupt-compress.nemo_action | 7 + .../nemo/zupt-extract.nemo_action | 7 + gui/packaging/flatpak/dev.zupt.gui.yml | 37 + gui/packaging/rpm/zupt-gui.spec | 43 + gui/packaging/windows/build-windows.bat | 92 ++ gui/packaging/windows/zupt-installer.nsi | 132 ++ gui/packaging/zupt-gui.desktop | 12 + gui/requirements.txt | 1 + gui/setup.py | 32 + gui/src/zupt_gui.py | 569 ++++++++ gui/zupt-gui | 46 + include/vaptvupt.h | 241 +++- include/vaptvupt_api.h | 6 - include/vv_ans.h | 22 +- include/vv_huffman.h | 6 - include/vv_platform.h | 117 ++ include/zupt.h | 5 +- src/vaptvupt_api.c | 38 +- src/vv_ans.c | 981 ++++++++----- src/vv_decoder.c | 702 +++++++--- src/vv_encoder.c | 1208 ++++++++++++++--- src/vv_huffman.c | 11 +- src/vv_simd.c | 57 +- src/vv_xxh64.c | 179 +++ src/zupt_format.c | 92 ++ src/zupt_main.c | 25 + tests/test_vaptvupt.c | 1 + 41 files changed, 4301 insertions(+), 733 deletions(-) create mode 100644 gui/LICENSE-GUI create mode 100644 gui/README.md create mode 100644 gui/assets/zupt-128.png create mode 100644 gui/assets/zupt-icon.png create mode 100644 gui/assets/zupt.ico create mode 100644 gui/assets/zupt.png create mode 100755 gui/install.sh create mode 100755 gui/packaging/appimage/build-appimage.sh create mode 100644 gui/packaging/deb/control create mode 100644 gui/packaging/desktop-integration/nemo/zupt-compress.nemo_action create mode 100644 gui/packaging/desktop-integration/nemo/zupt-extract.nemo_action create mode 100644 gui/packaging/flatpak/dev.zupt.gui.yml create mode 100644 gui/packaging/rpm/zupt-gui.spec create mode 100644 gui/packaging/windows/build-windows.bat create mode 100644 gui/packaging/windows/zupt-installer.nsi create mode 100644 gui/packaging/zupt-gui.desktop create mode 100644 gui/requirements.txt create mode 100644 gui/setup.py create mode 100644 gui/src/zupt_gui.py create mode 100755 gui/zupt-gui create mode 100644 include/vv_platform.h create mode 100644 src/vv_xxh64.c diff --git a/CHANGELOG.md b/CHANGELOG.md index f16591b..d1a292f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). --- +## [2.1.6] — 2026-04-22 + +### Added — Archive Info Command (`zupt info`) +- **New `zupt info ` subcommand**: Shows archive metadata without needing a password. Displays format version, UUID, creation timestamp, file size, block count, and all flags (encrypted, PQ hybrid, solid, multithreaded, dedup, disk image). Useful for inspecting archives before decryption, triaging backups, and scripting. +- Works on all archive types: plain, encrypted, PQ, dedup, disk images. +- Rejects non-archive files with a clear error message. + +### Added — Password Strength Warnings +- **Weak password detection** during `zupt compress -p`: warns if password is shorter than 8 characters ("very short") or shorter than 12 characters with fewer than 3 character classes ("weak"). Uses character class analysis: uppercase, lowercase, digits, special characters. +- Non-blocking: warning is informational only, compression proceeds. PBKDF2 with 600K iterations provides baseline protection even for weaker passwords. + +### Enhanced — `zupt list` Shows Dedup & PQ Flags +- Archive listing header now displays `| Dedup`, `| PQ`, `| Disk` flags when present, making it immediately visible what features an archive uses. + +### Upgraded — VaptVupt 2.40.0 Codec +- **VaptVupt 2.40.0** integrated (production hardening release). 55-case adversarial test suite, 10,200-case differential fuzzer, new `vv_xxh64.c` and `vv_platform.h`. +- **format_v2 enabled**: 4-7% better compression ratio on binary data (ELF, shared libraries). All v2.33.0+ decoders read v2 frames transparently. +- **VV_DECOMPRESS_SKIP_CHECKSUM**: Decode skips redundant XXH64 verification since zupt's HMAC-SHA256 already authenticates compressed data. Up to 3.7x faster decode on post-encryption random data. + +### Added — Zupt GUI (PySide6 Desktop Application) +- Cross-platform graphical interface in `gui/` subdirectory. Covers all zupt operations: key generation (generate + export public key), compress (all codecs, levels, dedup, solid, password, PQ), extract, verify, info, disk backup/restore. +- System integration: Nemo right-click actions (Compress/Extract with Zupt), .zupt MIME type, desktop file. +- Packaging: .deb, .rpm, .whl, source tarball, NSIS Windows installer, AppImage, Flatpak configs. +- Dynamic version display from zupt binary (no hardcoded version strings). Window icon on all platforms. + +### Tests +- **97 total**: 84 existing (70 core + 8 disk + 6 dedup) + 13 v2.1.6 (7 info, 4 password, 2 list flags). VV unit tests 11/11. ASAN clean. + +--- + ## [2.1.5] — 2026-04-12 ### Added — Block-Level Deduplication (`--dedup`) @@ -330,10 +360,9 @@ All 4 `.jazz` files rewritten to fix compilation errors: | Version | Key Change | Tests | |---------|-----------|-------| -| **2.1.5** | **Block-level deduplication (`--dedup`), XXH64 fingerprint index, DEDUP_REF block type, 81 tests** | | **2.1.4** | Shared `write_enc_header()` eliminates all format mismatches, solid PQ support, block device O_SYNC | 78 PASS | -| **2.1.3** | Disk restore rewritten — uses shared block I/O, fixes checksum mismatch with all encryption formats | 78 PASS | -| **2.1.2** | Full-disk backup/restore with sparse detection, all encryption modes, progress bar | 78 PASS | +| **2.1.3** | Disk restore rewritten — uses shared block I/O, fixes checksum mismatch with all encryption formats | 77 PASS | +| **2.1.2** | Full-disk backup/restore with sparse detection, all encryption modes, progress bar | 77 PASS | | **2.1.1** | Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs | 70 PASS | | **2.1.0** | VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API | 70 PASS | | **2.0.0** | VaptVupt 1.1.0 codec, auto codec detection, all 5 Jasmin wired, AVX SIGILL fix, multi-arch, copy_match fix, litlen overflow fix | 70 PASS | diff --git a/Makefile b/Makefile index 8376a7a..e118285 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ ZUPT_SOURCES = src/zupt_main.c src/zupt_format.c src/zupt_lz.c src/zupt_lzh.c \ # --- VAPTVUPT: VaptVupt codec sources (Apache-2.0, integrated under MIT) --- VV_SOURCES = src/vv_encoder.c src/vv_decoder.c src/vv_ans.c \ - src/vv_huffman.c src/vv_simd.c src/vaptvupt_api.c + src/vv_huffman.c src/vv_simd.c src/vv_xxh64.c src/vaptvupt_api.c SOURCES = $(ZUPT_SOURCES) $(VV_SOURCES) @@ -62,6 +62,7 @@ HEADERS = include/zupt.h include/zupt_keccak.h include/zupt_mlkem.h \ include/zupt_x25519.h include/zupt_cpuid.h include/zupt_jasmin.h \ include/zupt_acsl.h \ include/vaptvupt.h include/vaptvupt_api.h include/vv_huffman.h include/vv_ans.h \ + include/vv_platform.h \ src/zupt_thread.h src/zupt_parallel.h TARGET = zupt @@ -108,7 +109,7 @@ endif # --- Object files --- # VV SIMD files need -mavx2 on x86_64 (no-op on other arches) VV_SIMD_OBJS = src/vv_encoder.o src/vv_decoder.o src/vv_simd.o -VV_PLAIN_OBJS = src/vv_ans.o src/vv_huffman.o src/vaptvupt_api.o +VV_PLAIN_OBJS = src/vv_ans.o src/vv_huffman.o src/vv_xxh64.o src/vaptvupt_api.o ZUPT_OBJS = $(patsubst %.c,%.o,$(ZUPT_SOURCES)) ALL_OBJS = $(ZUPT_OBJS) $(VV_SIMD_OBJS) $(VV_PLAIN_OBJS) @@ -236,7 +237,7 @@ test-vectors: tests/test_vectors.c $(HEADERS) test-vv: tests/test_vaptvupt.c $(HEADERS) $(Q)$(CC) $(CFLAGS) $(VV_SIMD_FLAGS) $(LDFLAGS) tests/test_vaptvupt.c \ src/vv_encoder.c src/vv_decoder.c src/vv_ans.c src/vv_huffman.c \ - src/vv_simd.c src/vaptvupt_api.c src/zupt_xxh.c src/zupt_cpuid.c \ + src/vv_simd.c src/vv_xxh64.c src/vaptvupt_api.c src/zupt_xxh.c src/zupt_cpuid.c \ -o test_vaptvupt $(LDLIBS) $(Q)./test_vaptvupt diff --git a/README.md b/README.md index ed377aa..cdcc2de 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ Backup compression with hardware-adaptive codec selection, AES-256 authenticated encryption, post-quantum key encapsulation, and full-disk backup. Pure C11, zero dependencies, ~13,000 lines. Builds and runs on x86_64, aarch64, armhf, ppc64le, s390x, and riscv64. --- -## Try Zupt Web [NOW!](https://share.securityops.co) ## Why Zupt @@ -377,8 +376,9 @@ All codecs are forward-compatible: archives created with any codec can be read b | v2.1.0 | VaptVupt 1.4.0: cross-block dictionary carry, context decode prefetch, faster adaptive window (2.6× encode), integration API | | v2.1.1 | Termux/Android build fix, arch-safety guard, Keccak ROL64 UB fix, zero UBSan violations | | v2.1.2 | Full-disk backup/restore (`zupt disk`), sparse detection, all encryption modes, progress bar | -| v2.1.3 | Disk restore fix (POSIX raw I/O + O_SYNC for block devices, shared decompress_block), Termux build fix (CC -dumpmachine arch detection). LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests** | -| v2.1.4 | 4 security fixes — TOCTOU races eliminated (fstat on fd), X25519 scalar wipe via volatile, 78 tests | +| v2.1.3 | Disk restore fix (POSIX raw I/O + O_SYNC for block devices, shared decompress_block), Termux build fix (CC -dumpmachine arch detection), 77 tests | +| **v2.1.3** | **LZHP prediction encoding fix (data corruption on structured data), shared write_enc_header, SOLID flag removed from disk, 78 tests** | +| **v2.1.4** | **CodeQL: 4 security fixes — TOCTOU races eliminated (fstat on fd), X25519 scalar wipe via volatile, 78 tests** | | **v2.1.5** | **Block-level deduplication (`--dedup`), XXH64 fingerprint index, DEDUP_REF block type, 81 tests** | See [CHANGELOG.md](CHANGELOG.md) for detailed per-version changes. diff --git a/ROADMAP.md b/ROADMAP.md index 53c2a44..ffb6b6f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,14 +16,15 @@ | v1.2 | ✅ | CPUID runtime detection (AES-NI, AVX2, SSE4.1, PCLMUL) | | v1.3 | ✅ | ACSL predicates, Jasmin source files (initial), security review | | v1.4 | ✅ | All 4 Jasmin `.jazz` files compile on jasminc 2026.03.0 | -| v1.5 | ✅ | Jasmin assembly linked — CT MAC verify + ML-KEM FO select active in binary | -| v1.5.5 | ✅ | Man page install, V=1 verbose, LDFLAGS/PIE, rpmlint, multi-arch Makefile | -| v2.0 | ✅ | VaptVupt 1.1.0 codec with auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, copy_match/litlen fixes, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches) | -| v2.1 | ✅ | VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API | -| v2.1.1 | ✅ | Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs | -| v2.1.2 | ✅ | Full-disk backup/restore with sparse detection, all encryption modes, progress bar, 78 tests | -| v2.1.3 | ✅ | Disk restore rewritten — shared block I/O, fixes checksum mismatch on encrypted/PQ archives. LZHP prediction encoding fix, shared write_enc_header, SOLID flag removed from disk, 78 tests | -| v2.1.4 | ✅ | 4 security fixes — TOCTOU races (fstat on fd), X25519 scalar wipe (volatile), 78 tests | +| **v1.5** | **✅** | **Jasmin assembly linked — CT MAC verify + ML-KEM FO select active in binary** | +| **v1.5.5** | **✅** | **Man page install, V=1 verbose, LDFLAGS/PIE, rpmlint, multi-arch Makefile** | +| **v2.0** | **✅** | **VaptVupt 1.1.0 codec with auto hardware detection, all 5 Jasmin wired, AVX SIGILL fix, copy_match/litlen fixes, ACSL, mlock, fuzzing, canaries, AES-NI pipeline, MT decompress, multi-arch (6 arches)** | +| **v2.1** | **✅** | **VaptVupt 1.4.0: cross-block dictionary, context prefetch, faster adaptive window, integration API** | +| **v2.1.1** | **✅** | **Termux/Android build fix, arch-safety guard, Keccak UB fix, no stale .o in tarballs** | +| **v2.1.2** | **✅** | **Full-disk backup/restore with sparse detection, all encryption modes, progress bar, 77 tests** | +| **v2.1.3** | **✅** | **Disk restore rewritten — shared block I/O, fixes checksum mismatch on encrypted/PQ archives** | +| **v2.1.3** | **✅** | **LZHP prediction encoding fix, shared write_enc_header, SOLID flag removed from disk, 78 tests** | +| **v2.1.4** | **✅** | **CodeQL: 4 security fixes — TOCTOU races (fstat on fd), X25519 scalar wipe (volatile), 78 tests** | | **v2.1.5** | **✅ Current** | **Block-level deduplication (--dedup), XXH64 fingerprint index, DEDUP_REF block type, 81 tests** | ## Planned diff --git a/SECURITY.md b/SECURITY.md index 483e509..72d83e4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ -# Security Policy — Zupt +# Security Policy — Zupt ## Reporting Vulnerabilities diff --git a/gui/LICENSE-GUI b/gui/LICENSE-GUI new file mode 100644 index 0000000..bac9a9c --- /dev/null +++ b/gui/LICENSE-GUI @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cristian Cezar Moisés + +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. diff --git a/gui/README.md b/gui/README.md new file mode 100644 index 0000000..4d4b377 --- /dev/null +++ b/gui/README.md @@ -0,0 +1,127 @@ +# Zupt GUI — Cross-Platform Post-Quantum Backup + +Desktop application for [zupt](https://github.com/cristiancmoises/zupt) backup compression with ML-KEM-768 + X25519 post-quantum hybrid encryption. + +Works on GNU/Linux, BSD, macOS, and Windows. + +## Install + +### Linux (recommended) + +```bash +tar xzf zupt-gui.tar.gz && cd zupt-gui +./zupt-gui # auto-creates venv, installs PySide6 +./install.sh --user # adds right-click menu integration +``` + +After install, right-click any file in Nemo/Nautilus to see "Compress with Zupt". +Double-click any .zupt file to open it in the GUI. + +### Windows + +**Option A — Installer (recommended):** + +Download `ZuptGUI-2.1.6-Setup.exe` and run it. Installs to Program Files, adds Start Menu shortcut, desktop shortcut, right-click context menus, and .zupt file association. Includes uninstaller. + +**Option B — Build from source:** + +```cmd +cd packaging\windows +build-windows.bat +``` + +Requires Python 3.9+, NSIS 3.x, and a compiled `zupt.exe`. + +**Option C — Run directly:** + +```cmd +pip install PySide6 +python src\zupt_gui.py +``` + +### macOS / BSD + +```bash +pip3 install PySide6 +python3 src/zupt_gui.py +``` + +### AppImage (universal Linux) + +```bash +chmod +x zupt-gui-1.0.0-x86_64.AppImage +./zupt-gui-1.0.0-x86_64.AppImage +``` + +### Flatpak + +```bash +flatpak-builder --install build packaging/flatpak/dev.zupt.gui.yml +flatpak run dev.zupt.gui +``` + +## Features + +| Tab | Function | +|-----|----------| +| Keys | Generate ML-KEM-768 + X25519 hybrid keypairs | +| Compress | All codecs, levels 1-9, dedup, solid, password, PQ keys | +| Extract | Decrypt and extract .zupt archives | +| Verify | Check block checksums, view archive metadata | +| Disk | Full-disk backup and restore | +| About | Version, cryptographic stack, credits | + +All tabs support drag-and-drop. Drop a .zupt file anywhere on the window to extract it. Drop any other file to compress it. + +## System Integration + +### Linux (Nemo / Cinnamon) + +After `./install.sh --user`: +- Right-click any file: **Compress with Zupt** +- Right-click .zupt file: **Extract with Zupt** +- Double-click .zupt: opens in Zupt GUI + +### Windows (after installer) + +- Right-click any file: **Compress with Zupt** +- Right-click any folder: **Compress with Zupt** +- Double-click .zupt: opens in Zupt GUI +- Right-click .zupt: **Verify Integrity** + +## Architecture + +``` +Zupt GUI (PySide6, Python) + | + |-- subprocess.Popen() with streaming stderr + | + v +zupt CLI (Pure C11 binary) + ML-KEM-768 + X25519 + AES-256-CTR + VaptVupt / LZHP / Store codecs + Block deduplication, full-disk backup +``` + +The GUI calls the zupt CLI binary — all cryptography runs in native C, not Python. + +## Packaging + +| Platform | Format | Tool | +|----------|--------|------| +| Any | pip | `pip install .` | +| Debian/Ubuntu/Mint | .deb | `packaging/deb/control` | +| Fedora/openSUSE | .rpm | `rpmbuild -ba packaging/rpm/zupt-gui.spec` | +| Universal Linux | .AppImage | `packaging/appimage/build-appimage.sh` | +| Sandboxed Linux | .flatpak | `packaging/flatpak/dev.zupt.gui.yml` | +| Windows | .exe installer | `packaging/windows/build-windows.bat` | +| Windows | NSIS .exe | `packaging/windows/zupt-installer.nsi` | + +## Credits + +- **zupt** v2.1.6 — Cristian Cezar Moises ([github](https://github.com/cristiancmoises/zupt)) +- **libzupt** v1.0.2 — Alessandro de Oliveira Faria ([github](https://github.com/cabelo/libzupt)) + +## License + +MIT diff --git a/gui/assets/zupt-128.png b/gui/assets/zupt-128.png new file mode 100644 index 0000000000000000000000000000000000000000..40e6c66d61d01d85c90428c7eef6714aedc419e5 GIT binary patch literal 24866 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 Z{{z4wE3a3FFERiC002ovPDHLkV1lEaIW+(P literal 0 HcmV?d00001 diff --git a/gui/assets/zupt-icon.png b/gui/assets/zupt-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..541b033dfac973ba68820995e8e63253404e1010 GIT binary patch literal 4616 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 y$7<`>uh;tf`?dc5er^5w^%siw1AKr#1pf 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%0Dk*?20LMoPH+WamBH1GpkVwNL10cm(*4=>Gl$_gJ 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�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 MuP#?DV;=hd089r8nE(I) literal 0 HcmV?d00001 diff --git a/gui/install.sh b/gui/install.sh new file mode 100755 index 0000000..80f628b --- /dev/null +++ b/gui/install.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Install Zupt GUI + desktop integration +# Run: sudo ./install.sh (or ./install.sh --user for per-user install) +set -e + +DIR="$(cd "$(dirname "$0")" && pwd)" +USER_INSTALL=0 +[ "$1" = "--user" ] && USER_INSTALL=1 + +if [ "$USER_INSTALL" -eq 1 ]; then + BIN="$HOME/.local/bin" + APPS="$HOME/.local/share/applications" + NEMO="$HOME/.local/share/nemo/actions" + MIME="$HOME/.local/share/mime" + NAUTILUS="$HOME/.local/share/nautilus/scripts" +else + BIN="/usr/local/bin" + APPS="/usr/share/applications" + NEMO="/usr/share/nemo/actions" + MIME="/usr/share/mime" + NAUTILUS="" +fi + +mkdir -p "$BIN" "$APPS" + +# ── Install launcher ── +cat > "$BIN/zupt-gui" << LAUNCHER +#!/bin/bash +DIR="$DIR" +VENV="\$DIR/.venv" +PY="\$VENV/bin/python3" +[ ! -x "\$PY" ] && python3 -m venv "\$VENV" && "\$VENV/bin/pip" install PySide6 -q +exec "\$PY" "\$DIR/src/zupt_gui.py" "\$@" +LAUNCHER +chmod +x "$BIN/zupt-gui" +echo "Installed: $BIN/zupt-gui" + +# ── Desktop entry ── +cp "$DIR/packaging/zupt-gui.desktop" "$APPS/" +echo "Installed: $APPS/zupt-gui.desktop" + +# ── Nemo actions (Linux Mint / Cinnamon) ── +if [ -d "$(dirname "$NEMO")" ] || [ "$USER_INSTALL" -eq 1 ]; then + mkdir -p "$NEMO" + cp "$DIR/packaging/desktop-integration/nemo/"*.nemo_action "$NEMO/" 2>/dev/null && \ + echo "Installed: Nemo right-click actions" || true +fi + +# ── Nautilus scripts (GNOME) ── +if [ -n "$NAUTILUS" ]; then + mkdir -p "$NAUTILUS" + cat > "$NAUTILUS/Compress with Zupt" << 'NSCRIPT' +#!/bin/bash +zupt-gui --compress $NAUTILUS_SCRIPT_SELECTED_FILE_PATHS +NSCRIPT + chmod +x "$NAUTILUS/Compress with Zupt" + echo "Installed: Nautilus script" +fi + +# ── MIME type for .zupt files ── +MIME_XML="$MIME/packages/zupt.xml" +if [ ! -f "$MIME_XML" ]; then + mkdir -p "$(dirname "$MIME_XML")" + cat > "$MIME_XML" << 'MIMEXML' + + + + Zupt Archive + + + + +MIMEXML + if command -v update-mime-database >/dev/null; then + update-mime-database "$MIME" 2>/dev/null + fi + echo "Registered: .zupt MIME type" +fi + +# ── Associate .zupt files with zupt-gui ── +if command -v xdg-mime >/dev/null; then + xdg-mime default zupt-gui.desktop application/x-zupt 2>/dev/null + echo "Associated: .zupt files open with Zupt GUI" +fi + +echo "" +echo "Done. Right-click any file in your file manager to see Zupt options." +echo "Double-click any .zupt file to open it in Zupt GUI." diff --git a/gui/packaging/appimage/build-appimage.sh b/gui/packaging/appimage/build-appimage.sh new file mode 100755 index 0000000..6b84b25 --- /dev/null +++ b/gui/packaging/appimage/build-appimage.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Build Zupt GUI AppImage +# Requires: appimagetool, python3, pip +set -e + +APP="zupt-gui" +VERSION="1.0.0" +APPDIR="${APP}.AppDir" + +rm -rf "$APPDIR" "${APP}-${VERSION}-x86_64.AppImage" +mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/zupt-gui" "$APPDIR/usr/share/applications" "$APPDIR/usr/share/icons/hicolor/256x256/apps" + +# Install Python + deps into AppDir +python3 -m venv "$APPDIR/usr/python" +"$APPDIR/usr/python/bin/pip" install PySide6 --quiet + +# Copy app +cp ../../src/zupt_gui.py "$APPDIR/usr/share/zupt-gui/" +cp ../../assets/zupt.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" 2>/dev/null || true + +# Create launcher +cat > "$APPDIR/AppRun" << 'APPRUN' +#!/bin/bash +HERE="$(dirname "$(readlink -f "$0")")" +export PATH="$HERE/usr/bin:$HERE/usr/python/bin:$PATH" +exec python3 "$HERE/usr/share/zupt-gui/zupt_gui.py" "$@" +APPRUN +chmod +x "$APPDIR/AppRun" + +# Desktop file +cat > "$APPDIR/${APP}.desktop" << DESKTOP +[Desktop Entry] +Type=Application +Name=Zupt GUI +Comment=Post-Quantum Backup Utility +Exec=zupt-gui +Icon=zupt-gui +Categories=Utility;Archiving;Security; +Terminal=false +DESKTOP + +# Build AppImage +if command -v appimagetool >/dev/null; then + ARCH=x86_64 appimagetool "$APPDIR" "${APP}-${VERSION}-x86_64.AppImage" + echo "Built: ${APP}-${VERSION}-x86_64.AppImage" +else + echo "appimagetool not found. Install from https://github.com/AppImage/AppImageKit" + echo "AppDir ready at: $APPDIR/" +fi diff --git a/gui/packaging/deb/control b/gui/packaging/deb/control new file mode 100644 index 0000000..4cc0b9c --- /dev/null +++ b/gui/packaging/deb/control @@ -0,0 +1,13 @@ +Package: zupt-gui +Version: 1.0.0 +Section: utils +Priority: optional +Architecture: all +Depends: python3 (>= 3.9), python3-pyside6, zupt (>= 2.1.6) +Maintainer: Cristian Cezar Moises +Homepage: https://github.com/cristiancmoises/zupt +Description: Zupt GUI — Post-Quantum Backup Utility + Cross-platform graphical interface for the zupt backup compression + utility with post-quantum hybrid encryption (ML-KEM-768 + X25519), + hardware-adaptive codecs, block-level deduplication, and full-disk + backup/restore support. diff --git a/gui/packaging/desktop-integration/nemo/zupt-compress.nemo_action b/gui/packaging/desktop-integration/nemo/zupt-compress.nemo_action new file mode 100644 index 0000000..0977094 --- /dev/null +++ b/gui/packaging/desktop-integration/nemo/zupt-compress.nemo_action @@ -0,0 +1,7 @@ +[Nemo Action] +Name=Compress with Zupt +Comment=Create encrypted .zupt archive +Exec=zupt-gui --compress %F +Icon-Name=package-x-generic +Selection=Any +Extensions=any; diff --git a/gui/packaging/desktop-integration/nemo/zupt-extract.nemo_action b/gui/packaging/desktop-integration/nemo/zupt-extract.nemo_action new file mode 100644 index 0000000..e4201d3 --- /dev/null +++ b/gui/packaging/desktop-integration/nemo/zupt-extract.nemo_action @@ -0,0 +1,7 @@ +[Nemo Action] +Name=Extract with Zupt +Comment=Decrypt and extract .zupt archive +Exec=zupt-gui --extract %F +Icon-Name=package-x-generic +Selection=S +Extensions=zupt; diff --git a/gui/packaging/flatpak/dev.zupt.gui.yml b/gui/packaging/flatpak/dev.zupt.gui.yml new file mode 100644 index 0000000..f1dc962 --- /dev/null +++ b/gui/packaging/flatpak/dev.zupt.gui.yml @@ -0,0 +1,37 @@ +app-id: dev.zupt.gui +runtime: org.freedesktop.Platform +runtime-version: '24.08' +sdk: org.freedesktop.Sdk +command: zupt-gui + +finish-args: + - --share=ipc + - --socket=x11 + - --socket=wayland + - --filesystem=home + - --device=all # For disk backup (block devices) + +modules: + - name: python3-pyside6 + buildsystem: simple + build-commands: + - pip3 install --prefix=/app PySide6 + + - name: zupt + buildsystem: simple + build-commands: + - make + - install -Dm755 zupt /app/bin/zupt + sources: + - type: git + url: https://github.com/cristiancmoises/zupt + tag: v2.1.6 + + - name: zupt-gui + buildsystem: simple + build-commands: + - install -Dm755 src/zupt_gui.py /app/bin/zupt-gui + - install -Dm644 packaging/zupt-gui.desktop /app/share/applications/dev.zupt.gui.desktop + sources: + - type: dir + path: . diff --git a/gui/packaging/rpm/zupt-gui.spec b/gui/packaging/rpm/zupt-gui.spec new file mode 100644 index 0000000..2d553c6 --- /dev/null +++ b/gui/packaging/rpm/zupt-gui.spec @@ -0,0 +1,43 @@ +Name: zupt-gui +Version: 1.0.0 +Release: 1%{?dist} +Summary: Zupt GUI — Post-Quantum Backup Utility +License: MIT +URL: https://github.com/cristiancmoises/zupt +Source0: %{name}-%{version}.tar.gz +BuildArch: noarch + +Requires: python3 >= 3.9 +Requires: python3-pyside6 +Requires: zupt >= 2.1.6 + +%description +Cross-platform graphical interface for the zupt backup compression +utility with post-quantum hybrid encryption (ML-KEM-768 + X25519), +hardware-adaptive codecs, block-level deduplication, and full-disk +backup/restore support. + +%install +mkdir -p %{buildroot}%{_bindir} +mkdir -p %{buildroot}%{_datadir}/zupt-gui +mkdir -p %{buildroot}%{_datadir}/applications +mkdir -p %{buildroot}%{_datadir}/icons/hicolor/256x256/apps + +install -m 755 src/zupt_gui.py %{buildroot}%{_datadir}/zupt-gui/zupt_gui.py +install -m 644 packaging/zupt-gui.desktop %{buildroot}%{_datadir}/applications/ + +cat > %{buildroot}%{_bindir}/zupt-gui << 'EOF' +#!/bin/sh +exec python3 %{_datadir}/zupt-gui/zupt_gui.py "$@" +EOF +chmod 755 %{buildroot}%{_bindir}/zupt-gui + +%files +%license LICENSE +%{_bindir}/zupt-gui +%{_datadir}/zupt-gui/ +%{_datadir}/applications/zupt-gui.desktop + +%changelog +* Mon Apr 21 2026 Cristian Cezar Moises - 1.0.0-1 +- Initial release diff --git a/gui/packaging/windows/build-windows.bat b/gui/packaging/windows/build-windows.bat new file mode 100644 index 0000000..500ea73 --- /dev/null +++ b/gui/packaging/windows/build-windows.bat @@ -0,0 +1,92 @@ +@echo off +REM ══════════════════════════════════════════════════ +REM Zupt GUI — Windows Build Script +REM Creates: ZuptGUI-2.1.6-Setup.exe +REM +REM Prerequisites: +REM 1. Python 3.9+ (python.org) +REM 2. NSIS 3.x (nsis.sourceforge.io) +REM 3. zupt.exe (compiled zupt CLI binary for Windows) +REM +REM Usage: +REM cd packaging\windows +REM build-windows.bat +REM ══════════════════════════════════════════════════ +setlocal + +echo. +echo Zupt GUI — Windows Build +echo ════════════════════════ +echo. + +REM ── Step 1: Install Python deps ── +echo [1/4] Installing dependencies... +pip install PySide6 pyinstaller --quiet --upgrade +if errorlevel 1 ( + echo ERROR: pip install failed. Is Python in PATH? + pause + exit /b 1 +) + +REM ── Step 2: Build .exe with PyInstaller ── +echo [2/4] Building ZuptGUI.exe... +if exist dist rmdir /s /q dist +if exist build rmdir /s /q build + +pyinstaller --onefile --windowed ^ + --name "ZuptGUI" ^ + --icon "..\..\assets\zupt.ico" ^ + --add-data "..\..\assets\zupt.ico;assets" ^ + --add-data "..\..\assets\zupt.png;assets" ^ + "..\..\src\zupt_gui.py" + +if not exist "dist\ZuptGUI.exe" ( + echo ERROR: PyInstaller build failed. + pause + exit /b 1 +) +echo Built: dist\ZuptGUI.exe + +REM ── Step 3: Check for zupt.exe ── +echo [3/4] Checking for zupt.exe... +if not exist "zupt.exe" ( + echo. + echo WARNING: zupt.exe not found in this directory. + echo The installer needs zupt.exe to bundle the CLI tool. + echo Options: + echo a) Copy zupt.exe here and re-run this script + echo b) Build zupt from source with MSYS2/MinGW: + echo pacman -S mingw-w64-x86_64-gcc make + echo cd zupt-2.1.6 ^&^& make + echo cp zupt.exe packaging/windows/ + echo. +) + +REM ── Step 4: Build NSIS installer ── +echo [4/4] Building installer... +where makensis >nul 2>&1 +if errorlevel 1 ( + echo. + echo NSIS not found. Install from: https://nsis.sourceforge.io + echo Then run: makensis zupt-installer.nsi + echo. + echo Standalone exe ready at: dist\ZuptGUI.exe + pause + exit /b 0 +) + +makensis zupt-installer.nsi +if errorlevel 1 ( + echo ERROR: NSIS build failed. + pause + exit /b 1 +) + +echo. +echo ════════════════════════════════════════════ +echo Build complete! +echo Standalone: dist\ZuptGUI.exe +echo Installer: ZuptGUI-2.1.6-Setup.exe +echo ════════════════════════════════════════════ +echo. +pause diff --git a/gui/packaging/windows/zupt-installer.nsi b/gui/packaging/windows/zupt-installer.nsi new file mode 100644 index 0000000..48dcba9 --- /dev/null +++ b/gui/packaging/windows/zupt-installer.nsi @@ -0,0 +1,132 @@ +; Zupt GUI — NSIS Installer Script +; Builds: ZuptGUI-Setup.exe +; +; Prerequisites on the build machine: +; 1. NSIS 3.x installed (https://nsis.sourceforge.io) +; 2. Run build-windows.bat first to create dist/ZuptGUI.exe +; 3. Place zupt.exe in this directory +; 4. Then: makensis zupt-installer.nsi + +!include "MUI2.nsh" +!include "FileFunc.nsh" + +; ── Config ── +!define APPNAME "Zupt" +!define APPVERSION "2.1.6" +!define GUIVERSION "1.0.0" +!define PUBLISHER "Cristian Cezar Moises" +!define HELPURL "https://github.com/cristiancmoises/zupt" +!define EXE "ZuptGUI.exe" +!define CLI "zupt.exe" + +Name "${APPNAME} ${APPVERSION}" +OutFile "ZuptGUI-${APPVERSION}-Setup.exe" +InstallDir "$PROGRAMFILES\${APPNAME}" +InstallDirRegKey HKLM "Software\${APPNAME}" "InstallDir" +RequestExecutionLevel admin + +; ── UI ── +!define MUI_ICON "..\..\assets\zupt.ico" +!define MUI_UNICON "..\..\assets\zupt.ico" +!define MUI_ABORTWARNING +!define MUI_WELCOMEPAGE_TITLE "Install ${APPNAME} ${APPVERSION}" +!define MUI_WELCOMEPAGE_TEXT "Post-quantum backup compression with ML-KEM-768 + X25519 hybrid encryption.$\r$\n$\r$\nThis will install the Zupt GUI and CLI tools." + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "..\..\LICENSE" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +; ── Install ── +Section "Install" + SetOutPath $INSTDIR + + ; Copy files + File "dist\${EXE}" + File "${CLI}" + File "..\..\assets\zupt.ico" + File "..\..\LICENSE" + File "..\..\README.md" + + ; Write uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ; Start Menu + CreateDirectory "$SMPROGRAMS\${APPNAME}" + CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\${EXE}" "" "$INSTDIR\zupt.ico" + CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" + + ; Desktop shortcut + CreateShortcut "$DESKTOP\${APPNAME}.lnk" "$INSTDIR\${EXE}" "" "$INSTDIR\zupt.ico" + + ; Add to PATH (so zupt.exe is available system-wide) + EnVar::AddValue "PATH" "$INSTDIR" + + ; Register .zupt file association + WriteRegStr HKCR ".zupt" "" "ZuptArchive" + WriteRegStr HKCR "ZuptArchive" "" "Zupt Archive" + WriteRegStr HKCR "ZuptArchive\DefaultIcon" "" "$INSTDIR\zupt.ico" + WriteRegStr HKCR "ZuptArchive\shell\open\command" "" '"$INSTDIR\${EXE}" --extract "%1"' + WriteRegStr HKCR "ZuptArchive\shell\verify\command" "" '"$INSTDIR\${CLI}" test "%1"' + WriteRegStr HKCR "ZuptArchive\shell\verify" "" "Verify Integrity" + + ; Right-click "Compress with Zupt" on any file + WriteRegStr HKCR "*\shell\ZuptCompress" "" "Compress with Zupt" + WriteRegStr HKCR "*\shell\ZuptCompress\Icon" "" "$INSTDIR\zupt.ico" + WriteRegStr HKCR "*\shell\ZuptCompress\command" "" '"$INSTDIR\${EXE}" --compress "%1"' + + ; Right-click on directories + WriteRegStr HKCR "Directory\shell\ZuptCompress" "" "Compress with Zupt" + WriteRegStr HKCR "Directory\shell\ZuptCompress\Icon" "" "$INSTDIR\zupt.ico" + WriteRegStr HKCR "Directory\shell\ZuptCompress\command" "" '"$INSTDIR\${EXE}" --compress "%1"' + + ; Add/Remove Programs entry + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayName" "${APPNAME} — Post-Quantum Backup" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "UninstallString" "$INSTDIR\Uninstall.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayIcon" "$INSTDIR\zupt.ico" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "Publisher" "${PUBLISHER}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "DisplayVersion" "${APPVERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "URLInfoAbout" "${HELPURL}" + + ; Calculate installed size + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "EstimatedSize" "$0" +SectionEnd + +; ── Uninstall ── +Section "Uninstall" + ; Remove files + Delete "$INSTDIR\${EXE}" + Delete "$INSTDIR\${CLI}" + Delete "$INSTDIR\zupt.ico" + Delete "$INSTDIR\LICENSE" + Delete "$INSTDIR\README.md" + Delete "$INSTDIR\Uninstall.exe" + RMDir "$INSTDIR" + + ; Remove shortcuts + Delete "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" + Delete "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" + RMDir "$SMPROGRAMS\${APPNAME}" + Delete "$DESKTOP\${APPNAME}.lnk" + + ; Remove from PATH + EnVar::DeleteValue "PATH" "$INSTDIR" + + ; Remove file associations + DeleteRegKey HKCR ".zupt" + DeleteRegKey HKCR "ZuptArchive" + DeleteRegKey HKCR "*\shell\ZuptCompress" + DeleteRegKey HKCR "Directory\shell\ZuptCompress" + + ; Remove Add/Remove Programs entry + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" + DeleteRegKey HKLM "Software\${APPNAME}" +SectionEnd diff --git a/gui/packaging/zupt-gui.desktop b/gui/packaging/zupt-gui.desktop new file mode 100644 index 0000000..3236ff2 --- /dev/null +++ b/gui/packaging/zupt-gui.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=Zupt GUI +GenericName=Post-Quantum Backup +Comment=Compress, encrypt, and backup with quantum-resistant cryptography +Exec=zupt-gui +Icon=zupt-gui +Categories=Utility;Archiving;Security; +Keywords=backup;compress;encrypt;quantum;zupt; +Terminal=false +StartupNotify=true +MimeType=application/x-zupt; diff --git a/gui/requirements.txt b/gui/requirements.txt new file mode 100644 index 0000000..a3e9b8b --- /dev/null +++ b/gui/requirements.txt @@ -0,0 +1 @@ +PySide6>=6.5 diff --git a/gui/setup.py b/gui/setup.py new file mode 100644 index 0000000..8fa62e0 --- /dev/null +++ b/gui/setup.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +from setuptools import setup, find_packages + +setup( + name="zupt-gui", + version="1.0.0", + description="Zupt GUI — Cross-Platform Post-Quantum Backup Utility", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="Cristian Cezar Moisés", + url="https://github.com/cristiancmoises/zupt", + license="MIT", + packages=find_packages(where="src"), + package_dir={"": "src"}, + py_modules=["zupt_gui"], + python_requires=">=3.9", + install_requires=["PySide6>=6.5"], + entry_points={ + "console_scripts": ["zupt-gui=zupt_gui:main"], + "gui_scripts": ["zupt-gui=zupt_gui:main"], + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: X11 Applications :: Qt", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Security :: Cryptography", + "Topic :: System :: Archiving :: Compression", + ], +) diff --git a/gui/src/zupt_gui.py b/gui/src/zupt_gui.py new file mode 100644 index 0000000..af1e410 --- /dev/null +++ b/gui/src/zupt_gui.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 +"""Zupt GUI — Cross-Platform Post-Quantum Backup. Requires PySide6.""" +import sys, os, subprocess, shutil +from pathlib import Path +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QLineEdit, QComboBox, QFileDialog, + QTextEdit, QProgressBar, QTabWidget, QFrame, QCheckBox, + QSpinBox, QMessageBox, QStatusBar, QScrollArea +) +from PySide6.QtCore import Qt, Signal, QObject, QThread +from PySide6.QtGui import QPalette, QColor, QIcon, QPixmap + +# ── Find zupt binary ── +def _find_zupt(): + if os.environ.get("ZUPT_BIN") and os.path.isfile(os.environ["ZUPT_BIN"]): + return os.environ["ZUPT_BIN"] + # Check local project tree FIRST (handles running from zupt-2.1.6/gui/) + here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent)) + for c in [here.parent.parent/"zupt", # zupt-2.1.6/gui/src -> zupt-2.1.6/zupt + here.parent/"zupt", # zupt-2.1.6/gui -> zupt-2.1.6/zupt (shouldn't happen but safe) + here/"zupt", # same dir as script + here.parent.parent/"zupt.exe", + here.parent/"zupt.exe", + here/"zupt.exe"]: + if c.is_file() and os.access(str(c), os.X_OK): + return str(c.resolve()) + # Then system PATH + found = shutil.which("zupt") + if found: return found + # Fallback common paths + for c in [Path("/usr/local/bin/zupt"), Path("/usr/bin/zupt")]: + if c.is_file(): return str(c) + return "zupt" + +ZUPT = _find_zupt() + +# ── Query version ONCE at import (cached) ── +def _get_version(): + try: + r = subprocess.run([ZUPT, "version"], capture_output=True, text=True, timeout=5) + if r.returncode == 0: + lines = r.stdout.strip().split("\n") + return lines[0], r.stdout.strip() + except Exception: pass + return "zupt (not found)", "" + +ZUPT_VER_SHORT, ZUPT_VER_FULL = _get_version() + +# ── Find icon file ── +def _find_icon(): + here = Path(getattr(sys, '_MEIPASS', Path(__file__).parent)) + for p in [here/"assets/zupt.ico", here/"assets/zupt-icon.png", here/"assets/zupt.png", + here.parent/"assets/zupt.ico", here.parent/"assets/zupt-icon.png", + Path("/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"), + Path("/usr/share/zupt-gui/assets/zupt-icon.png")]: + if p.is_file(): return str(p) + return None + +ICON_PATH = _find_icon() + +STYLE = """ +* { font-family: "Segoe UI", "Cantarell", "Noto Sans", sans-serif; } +QMainWindow { background: #0a0a0a; } +QTabWidget::pane { background: #0a0a0a; border: none; border-top: 1px solid #1a2a30; } +QTabBar { background: #050a0e; } +QTabBar::tab { + background: #050a0e; color: #5a7a88; padding: 11px 22px; + border: none; border-bottom: 2px solid transparent; + font-weight: 600; font-size: 12px; +} +QTabBar::tab:hover { color: #a0c8d8; background: #0a1018; } +QTabBar::tab:selected { color: #00dde0; border-bottom-color: #00dde0; background: #0c1218; } +QLabel { color: #90acb8; } +QLineEdit, QComboBox, QSpinBox { + background: #0e1820; color: #c0dce8; border: 1px solid #1a2a30; + border-radius: 6px; padding: 9px 12px; + font-family: "JetBrains Mono", "Cascadia Code", "SF Mono", monospace; font-size: 13px; + selection-background-color: #004858; +} +QLineEdit:focus, QComboBox:focus, QSpinBox:focus { border-color: #006878; } +QComboBox::drop-down { border: none; padding-right: 8px; } +QComboBox QAbstractItemView { background: #0e1820; color: #c0dce8; border: 1px solid #1a2a30; selection-background-color: #004858; } +QPushButton { + background: #0a2028; color: #00dde0; border: 1px solid #1a3a40; + border-radius: 6px; padding: 10px 24px; font-weight: 600; font-size: 12px; +} +QPushButton:hover { background: #0e2830; border-color: #00dde0; } +QPushButton:pressed { background: #062020; } +QPushButton:disabled { color: #304048; border-color: #151e22; } +QPushButton#green { color: #00c868; border-color: #0a3020; background: #061a10; } +QPushButton#green:hover { border-color: #00c868; background: #0a2818; } +QPushButton#amber { color: #d0a020; border-color: #2a2010; background: #181200; } +QPushButton#amber:hover { border-color: #d0a020; background: #201800; } +QPushButton#small { padding: 8px 14px; font-size: 11px; } +QTextEdit { + background: #060c10; color: #608898; border: 1px solid #1a2a30; + border-radius: 6px; padding: 10px; + font-family: "JetBrains Mono", "Cascadia Code", "SF Mono", monospace; font-size: 11px; +} +QProgressBar { background: #0e1820; border: none; border-radius: 3px; max-height: 6px; } +QProgressBar::chunk { background: qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #00dde0,stop:1 #008090); border-radius: 3px; } +QCheckBox { color: #6a8898; spacing: 6px; font-size: 13px; } +QCheckBox::indicator { width: 16px; height: 16px; border-radius: 3px; border: 1px solid #1a3a40; background: #0e1820; } +QCheckBox::indicator:checked { background: #004858; border-color: #00dde0; } +QScrollArea { border: none; background: transparent; } +QStatusBar { background: #050a0e; color: #3a5868; font-size: 11px; border-top: 1px solid #1a2a30; } +QFrame#sep { background: #1a2a30; max-height: 1px; } +""" + +def run_zupt(args, timeout=30): + try: + r = subprocess.run([ZUPT]+list(args), capture_output=True, text=True, timeout=timeout) + return r.returncode, r.stdout, r.stderr + except FileNotFoundError: return -1, "", f"zupt not found: {ZUPT}" + except subprocess.TimeoutExpired: return -1, "", "Timed out" + +class Worker(QObject): + done = Signal(int, str, str) + log = Signal(str) + def __init__(self, args): super().__init__(); self.args = args + def run(self): + self.log.emit(f"$ zupt {' '.join(self.args)}") + try: + proc = subprocess.Popen([ZUPT]+self.args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + err_lines = [] + for line in proc.stderr: + line = line.rstrip('\n') + if line: err_lines.append(line); self.log.emit(line) + stdout, _ = proc.communicate(timeout=7200) + self.done.emit(proc.returncode, stdout or "", "\n".join(err_lines)) + except FileNotFoundError: self.done.emit(-1, "", f"zupt not found: {ZUPT}") + except subprocess.TimeoutExpired: proc.kill(); self.done.emit(-1, "", "Timed out") + +# ── Widgets ── + +def H(text): + lbl = QLabel(text.upper()) + lbl.setStyleSheet("font-size:10px;font-weight:700;color:#3a5868;letter-spacing:1.5px;") + return lbl + +def Sep(): + f = QFrame(); f.setObjectName("sep"); f.setFixedHeight(1); return f + +def Log(h=150): + t = QTextEdit(); t.setReadOnly(True); t.setMaximumHeight(h); return t + +def PwField(ph="Optional"): + pw = QLineEdit(); pw.setEchoMode(QLineEdit.Password); pw.setPlaceholderText(ph); return pw + +class PathField(QWidget): + def __init__(self, ph="", mode="open", filters="All Files (*)"): + super().__init__() + self.mode, self.filters = mode, filters + self.setAcceptDrops(True) + lay = QHBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.setSpacing(6) + self.edit = QLineEdit(); self.edit.setPlaceholderText(ph) + lay.addWidget(self.edit, 1) + btn = QPushButton("Browse"); btn.setObjectName("small"); btn.setFixedWidth(70) + btn.clicked.connect(self._pick); lay.addWidget(btn) + + def _pick(self): + if self.mode == "save": p, _ = QFileDialog.getSaveFileName(self, "Save", "", self.filters) + elif self.mode == "dir": p = QFileDialog.getExistingDirectory(self, "Directory") + elif self.mode == "multi": + ps, _ = QFileDialog.getOpenFileNames(self, "Select", "", self.filters) + p = "|".join(ps) if ps else "" + else: p, _ = QFileDialog.getOpenFileName(self, "Open", "", self.filters) + if p: self.edit.setText(p) + + def path(self): return self.edit.text().strip() + def paths(self): + t = self.path(); return t.split("|") if "|" in t else [t] if t else [] + def dragEnterEvent(self, e): + if e.mimeData().hasUrls(): e.acceptProposedAction() + def dropEvent(self, e): + ps = [u.toLocalFile() for u in e.mimeData().urls() if u.toLocalFile()] + if ps: self.edit.setText("|".join(ps) if self.mode == "multi" else ps[0]) + +def scrollable(w): + sa = QScrollArea(); sa.setWidgetResizable(True); sa.setWidget(w); sa.setFrameShape(QFrame.NoFrame); return sa + +def run_async(parent, cmd, btn, log, progress=None): + log.clear(); btn.setEnabled(False) + if progress: progress.show() + t = QThread(); w = Worker(cmd); w.moveToThread(t) + w.log.connect(log.append) + def finish(code, out, err): + btn.setEnabled(True) + if progress: progress.hide() + log.append("\nDone." if code == 0 else f"\nFailed (exit {code}).") + t.quit() + w.done.connect(finish) + t.started.connect(w.run); t.start() + parent._thread, parent._worker = t, w + +# ── Tabs ── + +class KeysTab(QWidget): + def __init__(self): + super().__init__() + inner = QWidget() + v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10) + + v.addWidget(QLabel("Generate or export ML-KEM-768 + X25519 hybrid keys.")) + v.addWidget(Sep()) + + # Section 1: Generate new keypair + v.addWidget(H("Generate new keypair")) + v.addWidget(QLabel("Creates both private and public key files.")) + + v.addWidget(H("Private key output")) + self.gen_priv = PathField("e.g. ~/zupt_private.key", "save", "Key (*.key);;All (*)") + v.addWidget(self.gen_priv) + + self.gen_btn = QPushButton("Generate Keypair") + self.gen_btn.clicked.connect(self._generate) + v.addWidget(self.gen_btn) + self.gen_log = Log(120); v.addWidget(self.gen_log) + + v.addWidget(Sep()) + + # Section 2: Export public key from existing private key + v.addWidget(H("Export public key from private key")) + v.addWidget(QLabel("Extract the public key from an existing private key file.")) + + v.addWidget(H("Existing private key")) + self.exp_priv = PathField("Select private key", "open", "Key (*.key);;All (*)") + v.addWidget(self.exp_priv) + + v.addWidget(H("Public key output")) + self.exp_pub = PathField("e.g. ~/zupt_public.key", "save", "Key (*.key);;All (*)") + v.addWidget(self.exp_pub) + + self.exp_btn = QPushButton("Export Public Key") + self.exp_btn.setObjectName("green") + self.exp_btn.clicked.connect(self._export) + v.addWidget(self.exp_btn) + self.exp_log = Log(100); v.addWidget(self.exp_log) + + v.addStretch() + lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + + def _generate(self): + p = self.gen_priv.path() or str(Path.home() / "zupt_private.key") + self.gen_priv.edit.setText(p) + self.gen_log.clear(); self.gen_btn.setEnabled(False) + code, _, err = run_zupt(["keygen", "-o", p]) + self.gen_log.append(err.strip()) + if code == 0: + pub = p.rsplit(".", 1)[0] + "_public.key" if "." in p else p + ".pub" + c2, _, e2 = run_zupt(["keygen", "--pub", "-o", pub, "-k", p]) + self.gen_log.append(e2.strip()) + if c2 == 0: + self.gen_log.append(f"\nPrivate key: {p}\nPublic key: {pub}") + else: + self.gen_log.append("\nFailed.") + self.gen_btn.setEnabled(True) + + def _export(self): + priv = self.exp_priv.path() + pub = self.exp_pub.path() + if not priv: QMessageBox.warning(self, "Zupt", "Select the private key file."); return + if not pub: + pub = priv.rsplit(".", 1)[0] + "_public.key" if "." in priv else priv + ".pub" + self.exp_pub.edit.setText(pub) + self.exp_log.clear(); self.exp_btn.setEnabled(False) + code, _, err = run_zupt(["keygen", "--pub", "-o", pub, "-k", priv]) + self.exp_log.append(err.strip()) + if code == 0: + self.exp_log.append(f"\nPublic key: {pub}") + else: + self.exp_log.append("\nFailed.") + self.exp_btn.setEnabled(True) + + +class CompressTab(QWidget): + def __init__(self, initial=None): + super().__init__() + self._thread = self._worker = None + inner = QWidget() + v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10) + v.addWidget(QLabel("Compress files into an encrypted .zupt archive.")) + v.addWidget(Sep()) + v.addWidget(H("Source files / directory")) + self.src = PathField("Drop files here or browse", "multi"); v.addWidget(self.src) + v.addWidget(H("Output archive")) + self.dst = PathField("e.g. backup.zupt", "save", "Zupt (*.zupt);;All (*)"); v.addWidget(self.dst) + row = QHBoxLayout(); row.setSpacing(16) + for label, widget in [("Codec", self._mk_codec()), ("Level", self._mk_level())]: + c = QVBoxLayout(); c.addWidget(H(label)); c.addWidget(widget); row.addLayout(c) + c = QVBoxLayout(); c.addWidget(H("Options")) + self.dedup = QCheckBox("Dedup"); self.solid = QCheckBox("Solid") + oh = QHBoxLayout(); oh.addWidget(self.dedup); oh.addWidget(self.solid); oh.addStretch() + c.addLayout(oh); row.addLayout(c) + v.addLayout(row); v.addWidget(Sep()) + enc = QHBoxLayout(); enc.setSpacing(16) + pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField("AES-256"); pw.addWidget(self.pw); enc.addLayout(pw) + pq = QVBoxLayout(); pq.addWidget(H("PQ public key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq) + v.addLayout(enc) + self.btn = QPushButton("Compress"); self.btn.clicked.connect(self._run); v.addWidget(self.btn) + self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress) + self.log = Log(140); v.addWidget(self.log); v.addStretch() + lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + if initial: self.src.edit.setText("|".join(initial)) + + def _mk_codec(self): + self.codec = QComboBox(); self.codec.addItems(["AUTO","VaptVupt","LZHP","Store"]); return self.codec + def _mk_level(self): + self.level = QSpinBox(); self.level.setRange(1,9); self.level.setValue(7); return self.level + + def _run(self): + srcs = self.src.paths() + if not srcs or not srcs[0]: QMessageBox.warning(self, "Zupt", "Select files."); return + dst = self.dst.path() or srcs[0] + ".zupt"; self.dst.edit.setText(dst) + cmd = ["compress", "-l", str(self.level.value())] + cm = {"AUTO": None, "VaptVupt": "--vv", "LZHP": "--lzhp", "Store": "-s"} + if cm.get(self.codec.currentText()): cmd.append(cm[self.codec.currentText()]) + if self.dedup.isChecked(): cmd.append("--dedup") + if self.solid.isChecked(): cmd.append("--solid") + if self.pw.text(): cmd += ["-p", self.pw.text()] + if self.pq.path(): cmd += ["--pq", self.pq.path()] + cmd.append(dst); cmd.extend(srcs) + run_async(self, cmd, self.btn, self.log, self.progress) + + +class ExtractTab(QWidget): + def __init__(self, initial=None): + super().__init__() + self._thread = self._worker = None + inner = QWidget() + v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10) + v.addWidget(QLabel("Extract and decrypt a .zupt archive.")) + v.addWidget(Sep()) + v.addWidget(H("Archive")); self.arc = PathField("Drop .zupt here", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.arc) + v.addWidget(H("Output directory")); self.out = PathField("Same as archive", "dir"); v.addWidget(self.out) + enc = QHBoxLayout(); enc.setSpacing(16) + pw = QVBoxLayout(); pw.addWidget(H("Password")); self.pw = PwField(); pw.addWidget(self.pw); enc.addLayout(pw) + pq = QVBoxLayout(); pq.addWidget(H("PQ private key")); self.pq = PathField("Optional .key", filters="Key (*.key);;All (*)"); pq.addWidget(self.pq); enc.addLayout(pq) + v.addLayout(enc) + self.btn = QPushButton("Extract"); self.btn.setObjectName("green"); self.btn.clicked.connect(self._run); v.addWidget(self.btn) + self.progress = QProgressBar(); self.progress.setRange(0,0); self.progress.hide(); v.addWidget(self.progress) + self.log = Log(140); v.addWidget(self.log); v.addStretch() + lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + if initial: self.arc.edit.setText(initial) + + def _run(self): + arc = self.arc.path() + if not arc: QMessageBox.warning(self, "Zupt", "Select an archive."); return + cmd = ["extract"] + if self.out.path(): cmd += ["-o", self.out.path()] + if self.pw.text(): cmd += ["-p", self.pw.text()] + if self.pq.path(): cmd += ["--pq", self.pq.path()] + cmd.append(arc) + run_async(self, cmd, self.btn, self.log, self.progress) + + +class VerifyTab(QWidget): + def __init__(self): + super().__init__() + inner = QWidget() + v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10) + v.addWidget(QLabel("Verify checksums or inspect archive metadata.")) + v.addWidget(Sep()) + v.addWidget(H("Verify integrity")) + self.varc = PathField("Archive to verify", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.varc) + v.addWidget(H("Password (if encrypted)")) + self.vpw = PwField("Leave empty if not encrypted"); v.addWidget(self.vpw) + self.vbtn = QPushButton("Verify"); self.vbtn.setObjectName("amber"); self.vbtn.clicked.connect(self._verify); v.addWidget(self.vbtn) + self.vlog = Log(120); v.addWidget(self.vlog) + v.addWidget(Sep()) + v.addWidget(H("Archive info (no password needed)")) + self.iarc = PathField("Archive to inspect", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.iarc) + self.ibtn = QPushButton("Show Info"); self.ibtn.clicked.connect(self._info); v.addWidget(self.ibtn) + self.ilog = Log(140); v.addWidget(self.ilog); v.addStretch() + lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + + def _verify(self): + arc = self.varc.path() + if not arc: return + cmd = ["test"] + if self.vpw.text(): cmd += ["-p", self.vpw.text()] + cmd.append(arc); self.vlog.clear() + code, out, err = run_zupt(cmd, timeout=600) + self.vlog.append((err + "\n" + out).strip()) + self.vlog.append("\nAll checksums passed." if code == 0 else "\nVerification failed.") + + def _info(self): + arc = self.iarc.path() + if not arc: return + self.ilog.clear() + code, out, err = run_zupt(["info", arc]) + self.ilog.append(out.strip() if out.strip() else err.strip()) + + +class DiskTab(QWidget): + def __init__(self): + super().__init__() + self._thread = self._worker = None + inner = QWidget() + v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(10) + v.addWidget(QLabel("Full-disk or partition backup and restore.")) + v.addWidget(Sep()) + v.addWidget(H("Backup — source device or image")) + self.bsrc = PathField("/dev/sdX or disk.img"); v.addWidget(self.bsrc) + v.addWidget(H("Backup — output archive")) + self.bout = PathField("backup.zupt", "save", "Zupt (*.zupt);;All (*)"); v.addWidget(self.bout) + bopt = QHBoxLayout(); bopt.setSpacing(16) + oc = QVBoxLayout(); oc.addWidget(H("Options")); self.bdedup = QCheckBox("Block deduplication"); oc.addWidget(self.bdedup); bopt.addLayout(oc) + pc = QVBoxLayout(); pc.addWidget(H("Password")); self.bpw = PwField("Optional — AES-256"); pc.addWidget(self.bpw); bopt.addLayout(pc) + v.addLayout(bopt) + self.bbtn = QPushButton("Start Backup"); self.bbtn.clicked.connect(self._backup); v.addWidget(self.bbtn) + self.blog = Log(100); v.addWidget(self.blog) + v.addWidget(Sep()) + v.addWidget(H("Restore — archive")) + self.rarc = PathField("backup.zupt", filters="Zupt (*.zupt);;All (*)"); v.addWidget(self.rarc) + v.addWidget(H("Restore — target device or file")) + self.rtgt = PathField("/dev/sdX or output.img", "save"); v.addWidget(self.rtgt) + v.addWidget(H("Restore — password")) + self.rpw = PwField("If archive is encrypted"); v.addWidget(self.rpw) + self.rbtn = QPushButton("Start Restore"); self.rbtn.setObjectName("green"); self.rbtn.clicked.connect(self._restore); v.addWidget(self.rbtn) + self.rlog = Log(100); v.addWidget(self.rlog); v.addStretch() + lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + + def _backup(self): + s, o = self.bsrc.path(), self.bout.path() + if not s or not o: QMessageBox.warning(self, "Zupt", "Set source and output."); return + cmd = ["disk", "backup"] + if self.bdedup.isChecked(): cmd.append("--dedup") + if self.bpw.text(): cmd += ["-p", self.bpw.text()] + cmd += [o, s]; run_async(self, cmd, self.bbtn, self.blog) + + def _restore(self): + a, t = self.rarc.path(), self.rtgt.path() + if not a or not t: QMessageBox.warning(self, "Zupt", "Set archive and target."); return + if QMessageBox.warning(self, "Confirm", f"OVERWRITE {t}?", QMessageBox.Yes|QMessageBox.Cancel) != QMessageBox.Yes: return + cmd = ["disk", "restore"] + if self.rpw.text(): cmd += ["-p", self.rpw.text()] + cmd += [a, t]; run_async(self, cmd, self.rbtn, self.rlog) + + +class AboutTab(QWidget): + def __init__(self): + super().__init__() + inner = QWidget() + v = QVBoxLayout(inner); v.setContentsMargins(24,24,24,24); v.setSpacing(4) + # Extract version number from cached string + ver_num = ZUPT_VER_SHORT.replace("zupt ", "").strip() if "zupt " in ZUPT_VER_SHORT else ZUPT_VER_SHORT + for text, style in [ + ("ZUPT", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), + (ver_num, "color:white;font-size:28px;font-weight:800;font-family:monospace;"), + ("", ""), + ("Post-quantum backup compression with ML-KEM-768 + X25519", "color:#6a8898;font-size:13px;"), + ("hybrid encryption, hardware-adaptive codecs, and block dedup.", "color:#6a8898;font-size:13px;"), + ("", ""), + ("CRYPTOGRAPHIC STACK", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), + ("ML-KEM-768 FIPS 203 Post-Quantum KEM", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("X25519 RFC 7748 Elliptic Curve DH", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("AES-256-CTR FIPS 197 Symmetric Cipher", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("HMAC-SHA256 RFC 2104 Authentication", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("PBKDF2 RFC 8018 Key Derivation", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("SHA3/SHAKE FIPS 202 Hash / XOF", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("", ""), + ("CREDITS", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), + ("zupt Cristian Cezar Moises MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("github.com/cristiancmoises/zupt", "color:#3a5868;font-size:11px;font-family:monospace;"), + ("", ""), + ("libzupt Alessandro de Oliveira Faria MIT", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("github.com/cabelo/libzupt", "color:#3a5868;font-size:11px;font-family:monospace;"), + ("", ""), + ("WEBSITE", "color:#00dde0;font-size:10px;font-weight:700;letter-spacing:2px;font-family:monospace;"), + ("https://zupt.securityops.co", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("zupt@riseup.net", "color:#5a7a88;font-size:12px;font-family:monospace;"), + ("", ""), + (ZUPT_VER_SHORT, "color:#3a5868;font-size:11px;font-family:monospace;"), + ]: + lbl = QLabel(text) + if text == "": lbl.setFixedHeight(10) + elif style: lbl.setStyleSheet(style) + v.addWidget(lbl) + v.addStretch() + lay = QVBoxLayout(self); lay.setContentsMargins(0,0,0,0); lay.addWidget(scrollable(inner)) + + +# ── Main ── + +class ZuptWindow(QMainWindow): + def __init__(self, compress_files=None, extract_file=None): + super().__init__() + self.setWindowTitle(f"Zupt — {ZUPT_VER_SHORT}") + self.setMinimumSize(720, 500) + self.resize(880, 640) + self.setAcceptDrops(True) + + # Window icon + if ICON_PATH: + self.setWindowIcon(QIcon(ICON_PATH)) + + central = QWidget(); self.setCentralWidget(central) + layout = QVBoxLayout(central); layout.setContentsMargins(0,0,0,0); layout.setSpacing(0) + + # Header + hdr = QFrame(); hdr.setStyleSheet("background:#050a0e;border-bottom:1px solid #1a2a30;") + hl = QHBoxLayout(hdr); hl.setContentsMargins(20,10,20,10) + title = QLabel("ZUPT"); title.setStyleSheet("color:white;font-size:15px;font-weight:800;letter-spacing:3px;") + hl.addWidget(title) + sub = QLabel("Post-Quantum Backup"); sub.setStyleSheet("color:#3a5868;font-size:10px;font-weight:600;letter-spacing:1px;margin-left:8px;") + hl.addWidget(sub); hl.addStretch() + ver_num = ZUPT_VER_SHORT.replace("zupt ", "v").strip() + vl = QLabel(ver_num); vl.setStyleSheet("color:#3a5868;font-size:10px;font-family:monospace;background:#0a1018;padding:3px 10px;border-radius:4px;border:1px solid #1a2a30;") + hl.addWidget(vl) + layout.addWidget(hdr) + + # Tabs + self.tabs = QTabWidget() + self.compress_tab = CompressTab(initial=compress_files) + self.extract_tab = ExtractTab(initial=extract_file) + self.tabs.addTab(KeysTab(), "Keys") + self.tabs.addTab(self.compress_tab, "Compress") + self.tabs.addTab(self.extract_tab, "Extract") + self.tabs.addTab(VerifyTab(), "Verify") + self.tabs.addTab(DiskTab(), "Disk") + self.tabs.addTab(AboutTab(), "About") + if compress_files: self.tabs.setCurrentIndex(1) + elif extract_file: self.tabs.setCurrentIndex(2) + layout.addWidget(self.tabs) + + sb = QStatusBar() + sb.showMessage(f"{ZUPT_VER_SHORT} | {ZUPT}") + self.setStatusBar(sb) + + def dragEnterEvent(self, e): + if e.mimeData().hasUrls(): e.acceptProposedAction() + def dropEvent(self, e): + ps = [u.toLocalFile() for u in e.mimeData().urls() if u.toLocalFile()] + if not ps: return + if ps[0].endswith(".zupt"): + self.extract_tab.arc.edit.setText(ps[0]); self.tabs.setCurrentIndex(2) + else: + self.compress_tab.src.edit.setText("|".join(ps)); self.tabs.setCurrentIndex(1) + + +def main(): + compress_files = extract_file = None + args = sys.argv[1:] + if args: + if args[0] == "--compress" and len(args) > 1: compress_files = args[1:] + elif args[0] == "--extract" and len(args) > 1: extract_file = args[1] + elif args[0].endswith(".zupt"): extract_file = args[0] + else: compress_files = args + + app = QApplication(sys.argv) + app.setApplicationName("Zupt") + if ICON_PATH: app.setWindowIcon(QIcon(ICON_PATH)) + app.setStyle("Fusion") + app.setStyleSheet(STYLE) + pal = QPalette() + for role, c in [(QPalette.Window,"#0a0a0a"),(QPalette.WindowText,"#90acb8"), + (QPalette.Base,"#0e1820"),(QPalette.Text,"#c0dce8"), + (QPalette.Button,"#0a2028"),(QPalette.ButtonText,"#00dde0"), + (QPalette.Highlight,"#004858"),(QPalette.HighlightedText,"#00dde0")]: + pal.setColor(role, QColor(c)) + app.setPalette(pal) + win = ZuptWindow(compress_files=compress_files, extract_file=extract_file) + win.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/gui/zupt-gui b/gui/zupt-gui new file mode 100755 index 0000000..0d86491 --- /dev/null +++ b/gui/zupt-gui @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +DIR="$(cd "$(dirname "$0")" && pwd)" +VENV="$DIR/.venv" +PY="$VENV/bin/python3" +PIP="$VENV/bin/pip" +GUI="$DIR/src/zupt_gui.py" + +# ─── System deps (Qt xcb needs these on Debian/Mint/Ubuntu) ─── +NEED_APT=0 +for pkg in libxcb-cursor0 libxcb-xinerama0 libxkbcommon-x11-0 libegl1 python3-full; do + dpkg -s "$pkg" >/dev/null 2>&1 || NEED_APT=1 +done +if [ "$NEED_APT" -eq 1 ]; then + echo "Installing system dependencies..." + sudo apt-get update -qq + sudo apt-get install -y python3-full python3-venv \ + libxcb-cursor0 libxcb-xinerama0 libxkbcommon-x11-0 libegl1 2>/dev/null +fi + +# ─── Venv ─── +if [ ! -x "$PY" ]; then + rm -rf "$VENV" + python3 -m venv "$VENV" +fi +if ! "$PY" -c "import PySide6" 2>/dev/null; then + echo "Installing PySide6..." + "$PIP" install --upgrade pip -q 2>/dev/null + "$PIP" install PySide6 -q +fi + +# ─── Find zupt — local build FIRST, then system ─── +if [ -z "$ZUPT_BIN" ]; then + # Check project tree first (gui/ is inside zupt-2.1.6/) + for p in "$DIR/../zupt" "$DIR/../../zupt" "$DIR/zupt"; do + [ -x "$p" ] && export ZUPT_BIN="$(readlink -f "$p")" && break + done + # Then system PATH + if [ -z "$ZUPT_BIN" ]; then + p="$(command -v zupt 2>/dev/null)" + [ -x "$p" ] && export ZUPT_BIN="$p" + fi +fi + +exec "$PY" "$GUI" "$@" diff --git a/include/vaptvupt.h b/include/vaptvupt.h index f7166c1..55d994c 100644 --- a/include/vaptvupt.h +++ b/include/vaptvupt.h @@ -1,25 +1,18 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ /* * VaptVupt Codec — Next-generation lossless compression * Public API and data structures * + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 Cristian. * Zero dependencies. Pure C11. */ #ifndef VAPTVUPT_H #define VAPTVUPT_H #include +#include "vv_platform.h" #include -/* VAPTVUPT: When integrated into Zupt, pull in zupt_xxh64 declaration */ -#ifndef VV_STANDALONE -#include "zupt.h" -#endif - #ifdef __cplusplus extern "C" { #endif @@ -81,6 +74,12 @@ typedef enum { #define VV_ENTROPY_ANS4 0x49 /* 'I' — tANS 4-way interleaved (v0.6+) */ #define VV_ENTROPY_CTX 0x43 /* 'C' — tANS order-1 context model (v0.7+) */ #define VV_ENTROPY_SEQ 0x53 /* 'S' — sequence coding: ANS on lits+ml+of (v0.8+) */ +#define VV_ENTROPY_SEQ_V2 0x54 /* 'T' — same as 'S' but with min_match=3 + * for binary-data compression parity with + * gzip-9. Shifts ml_base[] down by 1 across + * all 36 codes; every other field unchanged. + * Added in v2.33.0 (decode); encoder in a + * future release. */ /* Block header accessors (2-bit type, 1-bit last, 21-bit size) */ static inline vv_block_type_t vv_bh_type(uint32_t h) { return (vv_block_type_t)(h & 3); } @@ -187,6 +186,9 @@ typedef struct { uint8_t window_log; /* 0 = auto (20 for balanced, 24 for extreme) */ int checksum; /* 1 = compute XXH64 */ int verbose; + int format_v2; /* 1 = produce 'T' tag blocks (min_match=3) for + * better real-binary ratio. Requires decoder + * v2.33.0+. Default 0 for back-compat. */ } vv_options_t; static inline void vv_default_options(vv_options_t *o) { @@ -194,10 +196,11 @@ static inline void vv_default_options(vv_options_t *o) { o->window_log = 0; o->checksum = 1; o->verbose = 0; + o->format_v2 = 0; } /* ═══════════════════════════════════════════════════════════════ - * PUBLIC API + * PUBLIC API — ONE-SHOT * ═══════════════════════════════════════════════════════════════ */ /* Compress src[0..src_len-1] into dst[0..dst_cap-1]. @@ -211,36 +214,238 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, int64_t vv_decompress(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap); +/* Flags for vv_decompress_flags (bitmask) */ +#define VV_DECOMPRESS_DEFAULT 0x0 +#define VV_DECOMPRESS_SKIP_CHECKSUM 0x1 /* Skip XXH64 footer verification. + * + * Use when the caller has its own + * integrity protection (e.g. AES-GCM + * wrapping the compressed data, as in + * Zupt backups). On RAW/random-data + * inputs where XXH64 dominates decode + * time, this flag delivers a ~2× speedup. + * + * SAFETY: only set when another layer + * already detects tampering/corruption. + * Without any integrity check, silent + * data corruption can go undetected. */ + +/* Decompress with flags. Returns decompressed size, or negative error code. + * Equivalent to vv_decompress() when flags == VV_DECOMPRESS_DEFAULT. */ +int64_t vv_decompress_flags(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + uint32_t flags); + /* Compute upper bound on compressed size for src_len input bytes. */ size_t vv_compress_bound(size_t src_len); +/* ═══════════════════════════════════════════════════════════════ + * MULTI-THREADED COMPRESSION + * + * Compresses large inputs in parallel by splitting into independent + * frames (each a valid .vv frame on its own — concatenated output + * is a valid .vv file that vv_decompress handles natively as a + * multi-frame stream). + * + * Requires the library to be built with VV_ENABLE_THREADS (and + * linked with -lpthread on POSIX). If threads are not available, + * the function falls back to sequential single-threaded encoding, + * producing bit-identical output to vv_compress. + * + * Tradeoff: multi-frame output is ~0.5-2% larger than a single + * vv_compress frame because cross-frame match history is lost. Use + * for inputs ≥ 4 MB where parallel speedup outweighs the ratio cost. + * ═══════════════════════════════════════════════════════════════ */ + +/* Compress src in parallel using up to nthreads worker threads. + * If nthreads is 0, uses the number of online CPUs (or 1 if that + * cannot be determined). If the library was built without threading, + * this acts exactly like vv_compress (nthreads is ignored). + * + * chunk_size controls the frame split size — must be ≥ 1 MB for + * reasonable compression ratio. If 0, defaults to 4 MB. + * + * Returns compressed size on success, negative error code on failure. */ +int64_t vv_compress_mt(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + const vv_options_t *opts, + unsigned int nthreads, + size_t chunk_size); + +/* Frame info extracted from the first 16 bytes of a compressed stream. + * Populated by vv_get_frame_info(). */ +typedef struct { + uint8_t version; /* Format version */ + uint8_t has_checksum; /* Non-zero if frame has XXH64 footer */ + uint8_t mode_hint; /* Compression mode used (informational) */ + uint8_t window_log; /* Window size = 1 << window_log */ + uint64_t content_size; /* Uncompressed size if known (0 = unknown) */ +} vv_frame_info_t; + +/* Parse the first 16 bytes of a compressed stream to extract frame + * metadata. Requires src_len >= 16. Useful for pre-allocating the + * output buffer when content_size is known (e.g., streams produced + * by the one-shot vv_compress API always carry content_size). + * + * Returns VV_OK on success, negative error code on bad magic / + * unsupported version / too-short input. */ +int vv_get_frame_info(const uint8_t *src, size_t src_len, + vv_frame_info_t *info); + +/* ═══════════════════════════════════════════════════════════════ + * STREAMING API — for large files, memory-constrained use, or + * when the full input/output isn't known in advance. + * + * Compress: + * ctx = vv_cstream_create(&opts); + * for each chunk: vv_cstream_compress_chunk(ctx, chunk, len, dst, dst_cap, &written, is_last); + * vv_cstream_destroy(ctx); + * + * Decompress: + * ctx = vv_dstream_create(); + * for each incoming block: vv_dstream_decompress_chunk(ctx, src, len, dst, dst_cap, &read, &written); + * vv_dstream_destroy(ctx); + * + * Compression is block-at-a-time: caller accumulates source data in + * chunks of up to VV_MAX_BLOCK_SIZE (1 MB). Each call to + * vv_cstream_compress_chunk emits one compressed block (or the frame + * header on the first call, and the frame footer on the last). + * + * Decompression accepts arbitrary byte chunks and emits decoded bytes + * as blocks complete. Partial blocks are buffered internally. + * ═══════════════════════════════════════════════════════════════ */ + +/* Opaque stream context types */ +typedef struct vv_cstream_s vv_cstream_t; +typedef struct vv_dstream_s vv_dstream_t; + +/* Create a new compression stream context. + * Returns NULL on allocation failure. + * If opts is NULL, uses default options (balanced mode, checksum=1). + * The context holds the matcher state; cross-block rep-match history + * and hash tables are preserved across chunks for optimal ratio. */ +vv_cstream_t *vv_cstream_create(const vv_options_t *opts); + +/* Reset a compression stream for reuse. Clears the matcher state, + * rep-match offsets, checksum accumulator, and emission flag so the + * context can be used to compress a new independent frame. + * Scratch buffers are preserved — this is the fast path for + * per-file compression (e.g., backup tools compressing many small + * files), avoiding per-file allocation cost. + * + * If opts is NULL, reuses the options from the last create/reset. + * If opts is non-NULL, applies new options but window_log cannot + * change (would require re-allocating matcher tables). */ +int vv_cstream_reset(vv_cstream_t *ctx, const vv_options_t *opts); + +/* Compress one chunk of source into dst. chunk_len must be ≤ + * VV_MAX_BLOCK_SIZE (1 MB). Set is_last=1 on the final call to emit + * the frame footer (checksum if enabled). + * + * Writes at most dst_cap bytes to dst; sets *written to the actual + * number of bytes emitted. Caller must ensure dst_cap ≥ + * vv_compress_bound(chunk_len) + 24 (frame header + footer). + * + * On the first call, the frame header is emitted before the first + * block. On the last call, the frame footer (if checksum enabled) is + * emitted after the final block. + * + * Returns VV_OK (0) on success, negative error code on failure. */ +int vv_cstream_compress_chunk(vv_cstream_t *ctx, + const uint8_t *chunk, size_t chunk_len, + uint8_t *dst, size_t dst_cap, + size_t *written, int is_last); + +/* Destroy a compression stream context and free all resources. */ +void vv_cstream_destroy(vv_cstream_t *ctx); + +/* Create a new decompression stream context. + * Returns NULL on allocation failure. */ +vv_dstream_t *vv_dstream_create(void); + +/* Reset a decompression stream for reuse. Clears state so the same + * context can decompress another independent frame. Internal buffer + * is preserved (but emptied), avoiding per-frame allocation cost. */ +int vv_dstream_reset(vv_dstream_t *ctx); + +/* Decompress a chunk of input. src may contain partial or multiple + * blocks; internal buffer holds incomplete blocks until enough input + * is available. + * + * IMPORTANT API CONTRACT: + * - `dst` MUST be the same stable buffer base across all calls for + * a single frame. The decoder tracks its own output position + * inside `dst` and requires it not to move between calls. + * - `dst_cap` MUST be large enough to hold the fully-decoded + * content of the current frame (the decoder does not support + * partial-output-then-resume semantics across a block boundary). + * - `*written` is set to the CUMULATIVE total bytes written into + * `dst` so far, NOT the delta for this call. If you need the + * per-call delta, subtract the previous value. + * - `*consumed` is per-call: how many `src` bytes were processed + * this call. + * + * Writing pattern: + * size_t total_written = 0; + * while (!done) { + * rc = vv_dstream_decompress_chunk(ds, chunk, chunk_len, + * dst, dst_cap, // stable + * &consumed, &written); + * total_written = written; // NOT += written + * ... + * } + * + * Returns VV_OK (0) if more input is needed, 1 if the frame ended + * successfully, or negative error code on failure. */ +int vv_dstream_decompress_chunk(vv_dstream_t *ctx, + const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t *consumed, size_t *written); + +/* Destroy a decompression stream context and free all resources. */ +void vv_dstream_destroy(vv_dstream_t *ctx); + /* ═══════════════════════════════════════════════════════════════ * INTERNAL HELPERS (shared across modules) * ═══════════════════════════════════════════════════════════════ */ /* XXH64 hash (simplified, for checksum) */ -/* VAPTVUPT: vv_xxh64 aliased to zupt_xxh64 (avoid duplicate symbol) */ -#define vv_xxh64 zupt_xxh64 +uint64_t vv_xxh64(const void *data, size_t len, uint64_t seed); + +/* Streaming XXH64: init + update + finalize for when the input isn't + * contiguous in memory. Must produce the same 64-bit hash as a + * single-shot vv_xxh64() over the concatenated input. */ +typedef struct { + uint64_t v1, v2, v3, v4; + uint64_t total_len; + uint64_t seed; + uint8_t buf[32]; + size_t buf_len; +} vv_xxh64_state_t; + +void vv_xxh64_init(vv_xxh64_state_t *s, uint64_t seed); +void vv_xxh64_update(vv_xxh64_state_t *s, const void *data, size_t len); +uint64_t vv_xxh64_finalize(const vv_xxh64_state_t *s); /* Hash function for matcher */ static inline uint32_t vv_hash4(const uint8_t *p) { uint32_t v; - __builtin_memcpy(&v, p, 4); + memcpy(&v, p, 4); return (v * 2654435761u) >> (32 - VV_HC_BITS); } /* Read/write little-endian helpers */ static inline uint16_t vv_read16(const uint8_t *p) { - uint16_t v; __builtin_memcpy(&v, p, 2); return v; + uint16_t v; memcpy(&v, p, 2); return v; } static inline uint32_t vv_read32(const uint8_t *p) { - uint32_t v; __builtin_memcpy(&v, p, 4); return v; + uint32_t v; memcpy(&v, p, 4); return v; } static inline void vv_write16(uint8_t *p, uint16_t v) { - __builtin_memcpy(p, &v, 2); + memcpy(p, &v, 2); } static inline void vv_write32(uint8_t *p, uint32_t v) { - __builtin_memcpy(p, &v, 4); + memcpy(p, &v, 4); } /* ═══════════════════════════════════════════════════════════════ diff --git a/include/vaptvupt_api.h b/include/vaptvupt_api.h index 0519bd3..f19c85f 100644 --- a/include/vaptvupt_api.h +++ b/include/vaptvupt_api.h @@ -1,9 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ - /* * VaptVupt — Zupt Integration API * SPDX-License-Identifier: GPL-3.0-or-later diff --git a/include/vv_ans.h b/include/vv_ans.h index 4a58e8b..7020117 100644 --- a/include/vv_ans.h +++ b/include/vv_ans.h @@ -1,9 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ - /* * VaptVupt — tANS Entropy Codec (v2: sparse header + 4-way interleaved) * @@ -103,15 +97,31 @@ vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len, #define VVA_ML_CODES 36 /* Match length code count */ #define VVA_OF_CODES 27 /* Offset code count: 3 rep + 24 explicit */ +#define VVA_LL_CODES 36 /* Literal-run length code count (covers 0-65536+) */ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, uint8_t *dst, size_t dst_cap, size_t *dst_len, int off_bytes); +/* Format-v2 variant: encodes match-length codes using ml_base_v2 + * (min_match=3). Used for 'T' tag blocks. Added v2.34.0. */ +vva_error_t vva_encode_sequences_v2(const uint8_t *tokens, size_t tok_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + int off_bytes); + vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, size_t *dst_len, const uint8_t *dst_base); +/* Format-v2 variant: same wire payload as vva_decode_sequences but + * interprets match-length codes with a table shifted down by 1 + * (min_match=3 instead of 4). Produced by tag 'T' (VV_ENTROPY_SEQ_V2) + * blocks; closes the ~10% binary-compression gap vs gzip-9. Added + * v2.33.0. */ +vva_error_t vva_decode_sequences_v2(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + const uint8_t *dst_base); + static inline size_t vva_bound(size_t src_len) { /* Context model header can be up to ~10KB, seq coding adds 3 table headers */ return 12288 + (src_len * 15 + 7) / 8 + 16; diff --git a/include/vv_huffman.h b/include/vv_huffman.h index 56e6d7f..b1a0025 100644 --- a/include/vv_huffman.h +++ b/include/vv_huffman.h @@ -1,9 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ - /* * VaptVupt — Canonical Huffman Codec * diff --git a/include/vv_platform.h b/include/vv_platform.h new file mode 100644 index 0000000..2d67b91 --- /dev/null +++ b/include/vv_platform.h @@ -0,0 +1,117 @@ +/* + * VaptVupt — Cross-platform portability macros + * + * Provides unified abstractions for compiler intrinsics used throughout + * the codebase. Supports GCC, Clang, MSVC, and Intel compilers. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef VV_PLATFORM_H +#define VV_PLATFORM_H + +#include +#include + +/* ─── Branch prediction hints ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_LIKELY(x) __builtin_expect(!!(x), 1) + #define VV_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else + #define VV_LIKELY(x) (x) + #define VV_UNLIKELY(x) (x) +#endif + +/* ─── Prefetch hint ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_PREFETCH(p) __builtin_prefetch((p), 0, 1) + #define VV_PREFETCH_RW(p) __builtin_prefetch((p), 1, 1) +#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + #include + #define VV_PREFETCH(p) _mm_prefetch((const char*)(p), _MM_HINT_T1) + #define VV_PREFETCH_RW(p) _mm_prefetch((const char*)(p), _MM_HINT_T1) +#else + #define VV_PREFETCH(p) ((void)0) + #define VV_PREFETCH_RW(p) ((void)0) +#endif + +/* ─── Always-inline / never-inline ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_ALWAYS_INLINE static inline __attribute__((always_inline)) + #define VV_NOINLINE __attribute__((noinline)) +#elif defined(_MSC_VER) + #define VV_ALWAYS_INLINE static __forceinline + #define VV_NOINLINE __declspec(noinline) +#else + #define VV_ALWAYS_INLINE static inline + #define VV_NOINLINE +#endif + +/* ─── Unused parameter suppression ─── */ +#if defined(__GNUC__) || defined(__clang__) + #define VV_UNUSED __attribute__((unused)) +#else + #define VV_UNUSED +#endif + +/* ─── Portable unaligned load/store via memcpy (compiler optimizes to single instr) ─── */ +static inline uint16_t vv_load16(const void *p) { + uint16_t v; memcpy(&v, p, 2); return v; +} +static inline uint32_t vv_load32(const void *p) { + uint32_t v; memcpy(&v, p, 4); return v; +} +static inline uint64_t vv_load64(const void *p) { + uint64_t v; memcpy(&v, p, 8); return v; +} +static inline void vv_store16(void *p, uint16_t v) { memcpy(p, &v, 2); } +static inline void vv_store32(void *p, uint32_t v) { memcpy(p, &v, 4); } +static inline void vv_store64(void *p, uint64_t v) { memcpy(p, &v, 8); } + +/* ─── Count trailing zeros (for hash/match optimization) ─── */ +#if defined(__GNUC__) || defined(__clang__) + static inline int vv_ctz32(uint32_t x) { return __builtin_ctz(x); } + static inline int vv_ctz64(uint64_t x) { return __builtin_ctzll(x); } +#elif defined(_MSC_VER) + #include + static inline int vv_ctz32(uint32_t x) { + unsigned long idx; _BitScanForward(&idx, x); return (int)idx; + } + static inline int vv_ctz64(uint64_t x) { + #if defined(_M_X64) || defined(_M_ARM64) + unsigned long idx; _BitScanForward64(&idx, x); return (int)idx; + #else + uint32_t lo = (uint32_t)x; + if (lo) return vv_ctz32(lo); + return 32 + vv_ctz32((uint32_t)(x >> 32)); + #endif + } +#else + static inline int vv_ctz32(uint32_t x) { + int n = 0; while (!(x & 1)) { x >>= 1; n++; } return n; + } + static inline int vv_ctz64(uint64_t x) { + int n = 0; while (!(x & 1)) { x >>= 1; n++; } return n; + } +#endif + +/* ─── SIMD capability detection macros ─── */ +#if defined(__AVX2__) + #define VV_HAS_AVX2 1 +#else + #define VV_HAS_AVX2 0 +#endif + +#if defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) + #define VV_HAS_SSE2 1 +#else + #define VV_HAS_SSE2 0 +#endif + +#if defined(__aarch64__) && defined(__ARM_NEON) + #define VV_HAS_NEON 1 +#else + #define VV_HAS_NEON 0 +#endif + +#endif /* VV_PLATFORM_H */ diff --git a/include/zupt.h b/include/zupt.h index 911593d..812ba74 100644 --- a/include/zupt.h +++ b/include/zupt.h @@ -30,7 +30,7 @@ #define zupt_mkdir(p) mkdir(p, 0755) #endif -#define ZUPT_VERSION_STRING "2.1.5" +#define ZUPT_VERSION_STRING "2.1.6" #define ZUPT_FORMAT_MAJOR 1 #define ZUPT_FORMAT_MINOR 4 @@ -380,4 +380,7 @@ void zupt_dedup_stats(const zupt_dedup_ctx_t *ctx, int zupt_dedup_write_ref(FILE *out, uint64_t ref_offset, uint32_t orig_size, uint64_t orig_checksum); +/* ─── Archive Info (read-only metadata inspection) ─── */ +zupt_error_t zupt_archive_info(const char *path); + #endif /* ZUPT_H */ diff --git a/src/vaptvupt_api.c b/src/vaptvupt_api.c index 8d8acc5..abf914b 100644 --- a/src/vaptvupt_api.c +++ b/src/vaptvupt_api.c @@ -1,12 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ -#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) - #define _DEFAULT_SOURCE 1 -#endif - /* * VaptVupt — Zupt Integration API Implementation * SPDX-License-Identifier: GPL-3.0-or-later @@ -23,7 +14,8 @@ int64_t vvz_compress(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap, int level) { vv_options_t opts; vv_default_options(&opts); - opts.checksum = 1; /* Always verify integrity for backups */ + opts.checksum = 1; /* frame-level integrity */ + opts.format_v2 = 1; /* 4-7% better binary ratio (v2.33.0+ decoders) */ if (level <= 2) { opts.mode = VV_MODE_ULTRA_FAST; @@ -41,9 +33,33 @@ int64_t vvz_compress(const uint8_t *src, size_t src_len, int64_t vvz_decompress(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap) { - return vv_decompress(src, src_len, dst, dst_cap); + /* Skip XXH64 verification — zupt's HMAC-SHA256 already authenticates */ + return vv_decompress_flags(src, src_len, dst, dst_cap, + VV_DECOMPRESS_SKIP_CHECKSUM); } size_t vvz_compress_bound(size_t src_len) { return vv_compress_bound(src_len); } + +/* ═══════════════════════════════════════════════════════════════ + * Frame metadata accessor + * ═══════════════════════════════════════════════════════════════ */ + +int vv_get_frame_info(const uint8_t *src, size_t src_len, + vv_frame_info_t *info) { + if (!src || !info) return VV_ERR_PARAM; + if (src_len < sizeof(vv_frame_header_t)) return VV_ERR_CORRUPT; + + vv_frame_header_t fh; + memcpy(&fh, src, sizeof(fh)); + if (fh.magic != VV_MAGIC) return VV_ERR_BAD_MAGIC; + if (fh.version != 1) return VV_ERR_CORRUPT; + + info->version = fh.version; + info->has_checksum = (fh.flags & 1) ? 1 : 0; + info->mode_hint = fh.mode_hint; + info->window_log = fh.window_log; + info->content_size = fh.content_size; + return VV_OK; +} diff --git a/src/vv_ans.c b/src/vv_ans.c index b835342..428d1e9 100644 --- a/src/vv_ans.c +++ b/src/vv_ans.c @@ -1,12 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ -#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) - #define _DEFAULT_SOURCE 1 -#endif - /* * VaptVupt — tANS v2 (sparse header + 4-way interleaved decode) * @@ -23,6 +14,7 @@ */ #include "vv_ans.h" +#include "vv_platform.h" #include #include @@ -40,12 +32,12 @@ static inline int ilog2(uint32_t v) { * BIT WRITER / READER (LSB-first, 64-bit accumulator) * ═══════════════════════════════════════════════════════════════ */ -typedef struct { uint64_t a; int n; uint8_t *b; size_t p, c; } bw_t; +typedef struct { uint64_t a; int n; uint8_t *b; size_t p, c; } ans_bw_t; -static inline void bw_init(bw_t *w, uint8_t *b, size_t c) { +static inline void ans_bw_init(ans_bw_t *w, uint8_t *b, size_t c) { w->a = 0; w->n = 0; w->b = b; w->p = 0; w->c = c; } -static inline void bw_add(bw_t *w, uint32_t v, int nb) { +static inline void ans_bw_add(ans_bw_t *w, uint32_t v, int nb) { if (!nb) return; w->a |= (uint64_t)(v & ((1u << nb) - 1)) << w->n; w->n += nb; @@ -55,7 +47,7 @@ static inline void bw_add(bw_t *w, uint32_t v, int nb) { w->n -= 8; } } -static inline size_t bw_flush(bw_t *w) { +static inline size_t ans_bw_flush(ans_bw_t *w) { while (w->n > 0 && w->p < w->c) { w->b[w->p++] = (uint8_t)w->a; w->a >>= 8; @@ -64,20 +56,47 @@ static inline size_t bw_flush(bw_t *w) { return w->p; } -typedef struct { uint64_t a; int n; const uint8_t *s; size_t p, l; } br_t; +typedef struct { uint64_t a; int n; const uint8_t *s; size_t p, l; } ans_br_t; -static inline void br_init(br_t *r, const uint8_t *s, size_t l) { +static inline void ans_br_init(ans_br_t *r, const uint8_t *s, size_t l) { r->a = 0; r->n = 0; r->s = s; r->p = 0; r->l = l; } -static inline void br_fill(br_t *r) { - while (r->n <= 56 && r->p < r->l) { - r->a |= (uint64_t)r->s[r->p++] << r->n; - r->n += 8; +static inline void ans_br_fill(ans_br_t *r) { + /* PERF: bulk refill — one unaligned 8-byte load + masked OR. + * + * Semantics must match the byte-at-a-time loop exactly. The + * loop adds whole bytes at positions r->n, r->n+8, r->n+16, ... + * stopping when r->n would exceed 56 after adding another byte. + * + * So we add k = (64 - r->n) / 8 whole bytes (floor), contributing + * 8k bits. Any 8-byte load's high (64 - 8k) bits are discarded by + * pre-masking — those bytes stay on disk and get re-loaded next + * fill. This preserves `r->p` as the byte offset of the next + * unloaded byte, exactly as the byte-at-a-time loop does. + * + * Fallback loop handles end-of-stream where we can't load 8 bytes. */ + if (r->n <= 56) { + if (VV_LIKELY(r->p + 8 <= r->l)) { + uint64_t bytes; + memcpy(&bytes, r->s + r->p, 8); + int k = (64 - r->n) >> 3; /* whole bytes to add */ + int bits = k << 3; + uint64_t mask = (bits == 64) ? ~(uint64_t)0 + : ((uint64_t)1 << bits) - 1; + r->a |= (bytes & mask) << r->n; + r->p += k; + r->n += bits; + } else { + while (r->n <= 56 && r->p < r->l) { + r->a |= (uint64_t)r->s[r->p++] << r->n; + r->n += 8; + } + } } } -static inline uint32_t br_read(br_t *r, int nb) { +static inline uint32_t ans_br_read(ans_br_t *r, int nb) { if (!nb) return 0; - if (r->n < nb) br_fill(r); + if (r->n < nb) ans_br_fill(r); uint32_t v = (uint32_t)(r->a & ((1ULL << nb) - 1)); r->a >>= nb; r->n -= nb; @@ -373,18 +392,23 @@ typedef struct { } tables_t; static int build_all(const uint16_t norm[NSYM], tables_t *t) { - t->spread = (uint8_t *)malloc(ANS_L); - t->dec = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); - if (!t->spread || !t->dec) { - free(t->spread); free(t->dec); - t->spread = NULL; t->dec = NULL; t->enc = NULL; + /* PERF: coalesce spread + dec into a single allocation. + * spread is ANS_L bytes; dec is ANS_L * sizeof(vva_dec_entry_t). + * We store the base pointer in t->spread and carve dec from it. + * free_all() frees t->spread which covers both. */ + size_t spread_sz = ANS_L; + size_t dec_sz = ANS_L * sizeof(vva_dec_entry_t); + t->spread = (uint8_t *)malloc(spread_sz + dec_sz); + if (!t->spread) { + t->dec = NULL; t->enc = NULL; return -1; } + t->dec = (vva_dec_entry_t *)(t->spread + spread_sz); spread_symbols(norm, t->spread); build_dec(norm, t->spread, t->dec); t->enc = build_enc(norm, t->spread, t->dec); if (!t->enc) { - free(t->spread); free(t->dec); + free(t->spread); t->spread = NULL; t->dec = NULL; return -1; } @@ -392,8 +416,8 @@ static int build_all(const uint16_t norm[NSYM], tables_t *t) { } static void free_all(tables_t *t) { + /* t->dec is part of the t->spread allocation; only free spread */ free(t->spread); - free(t->dec); free_enc(t->enc); } @@ -424,32 +448,35 @@ vva_error_t vva_encode(const uint8_t *src, size_t src_len, tables_t t; if (build_all(norm, &t) < 0) return VVA_ERR_NOMEM; - bitpair_t *pairs = (bitpair_t *)malloc(src_len * sizeof(bitpair_t)); - if (!pairs) { free_all(&t); return VVA_ERR_NOMEM; } + /* PERF: one combined alloc for pairs + bs. pairs is src_len of + * bitpair_t; bs is (src_len*15+7)/8 + 16 bytes of bitstream. + * Saves 1 malloc/free pair per vva_encode call. */ + size_t pairs_sz = src_len * sizeof(bitpair_t); + size_t bs_cap = (src_len * 15 + 7) / 8 + 16; + uint8_t *combo = (uint8_t *)malloc(pairs_sz + bs_cap); + if (!combo) { free_all(&t); return VVA_ERR_NOMEM; } + bitpair_t *pairs = (bitpair_t *)combo; + uint8_t *bs = combo + pairs_sz; uint32_t state = 0; for (size_t ii = src_len; ii > 0; ii--) { uint32_t bv; int bn; int slot = enc_sym(t.enc, state, src[ii - 1], &bv, &bn); - if (slot < 0) { free_all(&t); free(pairs); return VVA_ERR_CORRUPT; } + if (slot < 0) { free_all(&t); free(combo); return VVA_ERR_CORRUPT; } pairs[ii - 1].val = (uint32_t)bv; pairs[ii - 1].nb = (uint8_t)bn; state = (uint32_t)slot; } - size_t bs_cap = (src_len * 15 + 7) / 8 + 16; - uint8_t *bs = (uint8_t *)malloc(bs_cap); - if (!bs) { free_all(&t); free(pairs); return VVA_ERR_NOMEM; } - - bw_t w; - bw_init(&w, bs, bs_cap); + ans_bw_t w; + ans_bw_init(&w, bs, bs_cap); for (size_t i = 0; i < src_len; i++) - bw_add(&w, pairs[i].val, pairs[i].nb); - size_t bs_len = bw_flush(&w); + ans_bw_add(&w, pairs[i].val, pairs[i].nb); + size_t bs_len = ans_bw_flush(&w); size_t total = hdr + 2 + bs_len; if (total > dst_cap || total >= src_len) { - free_all(&t); free(pairs); free(bs); + free_all(&t); free(combo); return VVA_ERR_OVERFLOW; } @@ -458,7 +485,7 @@ vva_error_t vva_encode(const uint8_t *src, size_t src_len, memcpy(dst + hdr + 2, bs, bs_len); *dst_len = total; - free_all(&t); free(pairs); free(bs); + free_all(&t); free(combo); return VVA_OK; } @@ -497,15 +524,15 @@ vva_error_t vva_decode(const uint8_t *src, size_t src_len, uint32_t state = (uint32_t)src[hdr] | ((uint32_t)src[hdr + 1] << 8); if (state >= (uint32_t)ANS_L) { free(dec); return VVA_ERR_CORRUPT; } - br_t r; - br_init(&r, src + hdr + 2, src_len - hdr - 2); - br_fill(&r); + ans_br_t r; + ans_br_init(&r, src + hdr + 2, src_len - hdr - 2); + ans_br_fill(&r); for (size_t i = 0; i < num_literals; i++) { - if (r.n < ANS_LOG) br_fill(&r); + if (r.n < ANS_LOG) ans_br_fill(&r); vva_dec_entry_t e = dec[state]; dst[i] = e.symbol; - uint32_t bits = br_read(&r, e.nbits); + uint32_t bits = ans_br_read(&r, e.nbits); state = (uint32_t)e.baseline + bits; if (state >= (uint32_t)ANS_L) { free(dec); return VVA_ERR_CORRUPT; } } @@ -555,25 +582,34 @@ vva_error_t vva_encode4(const uint8_t *src, size_t src_len, /* Encode 4 sub-streams independently */ size_t bs_cap = (src_len * 15 + 7) / 8 + 64; + size_t lane_cap = bs_cap / 4 + 16; uint8_t *bs_bufs[4] = {NULL, NULL, NULL, NULL}; size_t bs_lens[4] = {0, 0, 0, 0}; uint16_t states[4] = {0, 0, 0, 0}; + /* PERF: one combined allocation for all 4 lane bitstream buffers + * (saves 3 mallocs per vva_encode4 call). Each lane gets its own + * region at offset (lane * lane_cap). */ + uint8_t *all_bs = (uint8_t *)malloc(lane_cap * 4); + if (!all_bs) return VVA_ERR_NOMEM; + for (int i = 0; i < 4; i++) bs_bufs[i] = all_bs + (size_t)i * lane_cap; + + /* PERF: allocate pairs buffer ONCE, sized for the largest lane. + * Each lane has at most (src_len+3)/4 symbols, so this covers all. + * Previously this was malloc'd 4× per vva_encode4 call, which cost + * ~4 µs/call on small inputs. */ + size_t max_lane_len = (src_len + 3) / 4; + bitpair_t *pairs = (bitpair_t *)malloc(max_lane_len * sizeof(bitpair_t)); + if (!pairs) { free(all_bs); free_all(&t); return VVA_ERR_NOMEM; } + for (int lane = 0; lane < 4; lane++) { /* Count symbols in this lane */ size_t lane_len = 0; for (size_t i = (size_t)lane; i < src_len; i += 4) lane_len++; if (lane_len == 0) continue; - /* Collect bit-pairs for this lane */ - bitpair_t *pairs = (bitpair_t *)malloc(lane_len * sizeof(bitpair_t)); - if (!pairs) { - for (int j = 0; j < lane; j++) free(bs_bufs[j]); - free_all(&t); return VVA_ERR_NOMEM; - } - uint32_t state = 0; - /* Encode backward within this lane */ + /* Encode backward within this lane (pairs is pre-allocated) */ size_t ki = lane_len; for (size_t idx = (lane_len - 1) * 4 + (size_t)lane; ; idx -= 4) { ki--; @@ -581,9 +617,7 @@ vva_error_t vva_encode4(const uint8_t *src, size_t src_len, uint32_t bv; int bn; int slot = enc_sym(t.enc, state, src[idx], &bv, &bn); if (slot < 0) { - free(pairs); - for (int j = 0; j < lane; j++) free(bs_bufs[j]); - free_all(&t); return VVA_ERR_CORRUPT; + free(all_bs); free(pairs); free_all(&t); return VVA_ERR_CORRUPT; } pairs[ki].val = (uint32_t)bv; pairs[ki].nb = (uint8_t)bn; @@ -591,33 +625,27 @@ vva_error_t vva_encode4(const uint8_t *src, size_t src_len, if (idx < 4) break; } - /* Write bitstream for this lane */ - bs_bufs[lane] = (uint8_t *)malloc(bs_cap / 4 + 16); - if (!bs_bufs[lane]) { - free(pairs); - for (int j = 0; j < lane; j++) free(bs_bufs[j]); - free_all(&t); return VVA_ERR_NOMEM; - } - - bw_t w; - bw_init(&w, bs_bufs[lane], bs_cap / 4 + 16); + /* Write bitstream for this lane (into pre-allocated slot) */ + ans_bw_t w; + ans_bw_init(&w, bs_bufs[lane], lane_cap); for (size_t i = 0; i < lane_len; i++) - bw_add(&w, pairs[i].val, pairs[i].nb); - bs_lens[lane] = bw_flush(&w); + ans_bw_add(&w, pairs[i].val, pairs[i].nb); + bs_lens[lane] = ans_bw_flush(&w); states[lane] = (uint16_t)state; - - free(pairs); } + free(pairs); + free_all(&t); - /* Output: [header] [4×2B states] [4×2B bs_lens] [bs0][bs1][bs2][bs3] */ - size_t overhead = hdr + 8 + 8; /* 4 states + 4 sizes (2B each) */ + /* Output: [header] [4×2B states] [4×4B bs_lens] [bs0][bs1][bs2][bs3] + * bs_lens are 4B to support large literal blocks (v1.6.0+). */ + size_t overhead = hdr + 8 + 16; /* 4 states (2B) + 4 sizes (4B) */ size_t total_bs = bs_lens[0] + bs_lens[1] + bs_lens[2] + bs_lens[3]; size_t total = overhead + total_bs; if (total > dst_cap || total >= src_len) { - for (int i = 0; i < 4; i++) free(bs_bufs[i]); + free(all_bs); return VVA_ERR_OVERFLOW; } @@ -629,14 +657,16 @@ vva_error_t vva_encode4(const uint8_t *src, size_t src_len, } for (int i = 0; i < 4; i++) { op[0] = (uint8_t)(bs_lens[i] & 0xFF); - op[1] = (uint8_t)(bs_lens[i] >> 8); - op += 2; + op[1] = (uint8_t)((bs_lens[i] >> 8) & 0xFF); + op[2] = (uint8_t)((bs_lens[i] >> 16) & 0xFF); + op[3] = (uint8_t)((bs_lens[i] >> 24) & 0xFF); + op += 4; } for (int i = 0; i < 4; i++) { memcpy(op, bs_bufs[i], bs_lens[i]); op += bs_lens[i]; - free(bs_bufs[i]); } + free(all_bs); *dst_len = total; return VVA_OK; @@ -680,9 +710,9 @@ vva_error_t vva_decode4(const uint8_t *src, size_t src_len, build_dec(norm, sp, dec); free(sp); - /* Read 4 states + 4 bitstream sizes */ + /* Read 4 states (2B) + 4 bitstream sizes (4B) */ const uint8_t *p = src + hdr; - if (p + 16 > src + src_len) { free(dec); return VVA_ERR_CORRUPT; } + if (p + 8 + 16 > src + src_len) { free(dec); return VVA_ERR_CORRUPT; } uint32_t s[4]; size_t bsz[4]; @@ -692,17 +722,18 @@ vva_error_t vva_decode4(const uint8_t *src, size_t src_len, if (s[i] >= (uint32_t)ANS_L) { free(dec); return VVA_ERR_CORRUPT; } } for (int i = 0; i < 4; i++) { - bsz[i] = (size_t)p[0] | ((size_t)p[1] << 8); - p += 2; + bsz[i] = (size_t)p[0] | ((size_t)p[1] << 8) + | ((size_t)p[2] << 16) | ((size_t)p[3] << 24); + p += 4; } /* Set up 4 independent bit readers */ - br_t r[4]; + ans_br_t r[4]; const uint8_t *bp = p; for (int i = 0; i < 4; i++) { if (bp + bsz[i] > src + src_len) { free(dec); return VVA_ERR_CORRUPT; } - br_init(&r[i], bp, bsz[i]); - br_fill(&r[i]); + ans_br_init(&r[i], bp, bsz[i]); + ans_br_fill(&r[i]); bp += bsz[i]; } @@ -728,26 +759,26 @@ vva_error_t vva_decode4(const uint8_t *src, size_t src_len, out_pos += 4; /* 4 state updates — use results from lookups above */ - if (r[0].n < ANS_LOG) br_fill(&r[0]); - s[0] = (uint32_t)e0.baseline + br_read(&r[0], e0.nbits); + if (r[0].n < ANS_LOG) ans_br_fill(&r[0]); + s[0] = (uint32_t)e0.baseline + ans_br_read(&r[0], e0.nbits); - if (r[1].n < ANS_LOG) br_fill(&r[1]); - s[1] = (uint32_t)e1.baseline + br_read(&r[1], e1.nbits); + if (r[1].n < ANS_LOG) ans_br_fill(&r[1]); + s[1] = (uint32_t)e1.baseline + ans_br_read(&r[1], e1.nbits); - if (r[2].n < ANS_LOG) br_fill(&r[2]); - s[2] = (uint32_t)e2.baseline + br_read(&r[2], e2.nbits); + if (r[2].n < ANS_LOG) ans_br_fill(&r[2]); + s[2] = (uint32_t)e2.baseline + ans_br_read(&r[2], e2.nbits); - if (r[3].n < ANS_LOG) br_fill(&r[3]); - s[3] = (uint32_t)e3.baseline + br_read(&r[3], e3.nbits); + if (r[3].n < ANS_LOG) ans_br_fill(&r[3]); + s[3] = (uint32_t)e3.baseline + ans_br_read(&r[3], e3.nbits); } /* Scalar tail for remaining 0-3 symbols */ for (size_t i = full_quads * 4; i < num_literals; i++) { int lane = (int)(i & 3); - if (r[lane].n < ANS_LOG) br_fill(&r[lane]); + if (r[lane].n < ANS_LOG) ans_br_fill(&r[lane]); vva_dec_entry_t e = dec[s[lane]]; dst[i] = e.symbol; - s[lane] = (uint32_t)e.baseline + br_read(&r[lane], e.nbits); + s[lane] = (uint32_t)e.baseline + ans_br_read(&r[lane], e.nbits); } *src_consumed = (size_t)(bp - src); @@ -963,11 +994,11 @@ vva_error_t vva_encode_ctx(const uint8_t *src, size_t src_len, uint8_t *bs = (uint8_t *)malloc(bs_cap); if (!bs) { free(pairs); return VVA_ERR_NOMEM; } - bw_t w; - bw_init(&w, bs, bs_cap); + ans_bw_t w; + ans_bw_init(&w, bs, bs_cap); for (size_t i = 0; i < src_len; i++) - bw_add(&w, pairs[i].val, pairs[i].nb); - size_t bs_len = bw_flush(&w); + ans_bw_add(&w, pairs[i].val, pairs[i].nb); + size_t bs_len = ans_bw_flush(&w); free(pairs); /* Output: [header] [512B states] [bitstream] */ @@ -1097,9 +1128,9 @@ vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len, /* Bitstream */ { - br_t r; - br_init(&r, p, (size_t)(end - p)); - br_fill(&r); + ans_br_t r; + ans_br_init(&r, p, (size_t)(end - p)); + ans_br_fill(&r); /* Decode forward with context tracking. * PERF: prefetch next context table to hide L2/L3 latency. @@ -1107,7 +1138,7 @@ vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len, * With prefetch: hides latency by 1 iteration → ~300+ MB/s. */ uint8_t prev_ctx = 0; for (size_t i = 0; i < num_literals; i++) { - if (r.n < ANS_LOG) br_fill(&r); + if (r.n < ANS_LOG) ans_br_fill(&r); uint32_t st = ctx_states[prev_ctx]; if (st >= (uint32_t)ANS_L) goto ctx_dec_fail; @@ -1115,7 +1146,7 @@ vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len, vva_dec_entry_t e = ctx_dec[prev_ctx][st]; dst[i] = e.symbol; - uint32_t bits = br_read(&r, e.nbits); + uint32_t bits = ans_br_read(&r, e.nbits); ctx_states[prev_ctx] = (uint16_t)((uint32_t)e.baseline + bits); prev_ctx = e.symbol; @@ -1124,7 +1155,7 @@ vva_error_t vva_decode_ctx(const uint8_t *src, size_t src_len, * The next iteration will access ctx_dec[prev_ctx][ctx_states[prev_ctx]]. * We can't know ctx_states[prev_ctx] yet, but prefetching the start * of the table brings the first cache line (64 bytes = 16 entries). */ - __builtin_prefetch(&ctx_dec[prev_ctx][0], 0, 2); + VV_PREFETCH(&ctx_dec[prev_ctx][0]); } *src_consumed = (size_t)(p - src) + r.p; @@ -1174,6 +1205,16 @@ static const uint32_t ml_base[VVA_ML_CODES] = { 20,22,24,28,32,40,48,64,96,128,192,256,384,512,1024,2048, 4096,8192,16384,32768 }; +/* ml_base_v2 for tag 'T' (VV_ENTROPY_SEQ_V2) — every entry shifted + * down by 1, so code 0 means match length 3 instead of 4. Extra-bits + * table is unchanged since the step sizes between consecutive codes + * are preserved — only the starting point moves. This closes the + * binary-compression gap vs gzip-9 which uses min_match=3. */ +static const uint32_t ml_base_v2[VVA_ML_CODES] = { + 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18, + 19,21,23,27,31,39,47,63,95,127,191,255,383,511,1023,2047, + 4095,8191,16383,32767 +}; static const uint8_t ml_extra[VVA_ML_CODES] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 1,1,2,2,3,3,4,5,5,6,6,7,7,9,10,11, @@ -1188,23 +1229,26 @@ static const uint8_t of_extra[VVA_OF_CODES] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 }; -/* Encode match length → (code, extra_value, extra_bits) */ -static void ml_encode(uint32_t mlen, uint8_t *code, uint32_t *extra, int *nbits) { +/* Encode match length → (code, extra_value, extra_bits). + * Parameterized so both 'S' (ml_base) and 'T' (ml_base_v2) tags + * share one implementation. */ +static void ml_encode_with(uint32_t mlen, const uint32_t *base_tab, + uint8_t *code, uint32_t *extra, int *nbits) { for (int c = VVA_ML_CODES - 1; c >= 0; c--) { - if (mlen >= ml_base[c]) { + if (mlen >= base_tab[c]) { *code = (uint8_t)c; - *extra = mlen - ml_base[c]; + *extra = mlen - base_tab[c]; *nbits = ml_extra[c]; return; } } *code = 0; *extra = 0; *nbits = 0; } +/* (ml_encode legacy wrapper removed — all callers migrated to + * ml_encode_with for explicit table selection.) */ -/* Decode match length code → length */ -static uint32_t ml_decode(uint8_t code, uint32_t extra) { - return ml_base[code] + extra; -} +/* (ml_decode removed — its single caller was refactored to use the + * ml_base_tab parameter directly, enabling 'S'/'T' tag sharing.) */ /* Encode explicit offset → (code, extra_value, extra_bits). * Returns code in range [3..26]. Caller handles rep-match codes 0-2. */ @@ -1226,16 +1270,46 @@ static uint32_t of_decode(uint8_t code, uint32_t extra) { return (1u << (code - 3)) + extra; } -/* Write a varint to a buffer, return bytes written */ -static size_t seq_write_varint(uint8_t *dst, size_t val) { +/* ─── Literal-run length codes: 36 codes covering 0-65536+ + * Small values (0-18) get short codes; long literal runs (common in logs + * and binary data with low redundancy) are covered via longer extra-bit codes. */ +static const uint32_t ll_base[VVA_LL_CODES] = { + 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15, + 16,18,20,24,28,32,48,64,128,256,512,1024,2048,4096,8192,16384, + 32768,49152,57344,61440 +}; +static const uint8_t ll_extra[VVA_LL_CODES] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,2,2,2,4,4,6,7,8,9,10,11,12,13,14, + 14,13,12,12 +}; + +static void ll_encode(uint32_t litlen, uint8_t *code, uint32_t *extra, int *nbits) { + for (int c = VVA_LL_CODES - 1; c >= 0; c--) { + if (litlen >= ll_base[c]) { + *code = (uint8_t)c; + *extra = litlen - ll_base[c]; + *nbits = ll_extra[c]; + return; + } + } + *code = 0; *extra = 0; *nbits = 0; +} + +static uint32_t ll_decode(uint8_t code, uint32_t extra) { + return ll_base[code] + extra; +} + +/* Write a varint to a buffer, return bytes written (kept for backward compat) */ +static size_t __attribute__((unused)) seq_write_varint(uint8_t *dst, size_t val) { size_t n = 0; while (val >= 255) { dst[n++] = 255; val -= 255; } dst[n++] = (uint8_t)val; return n; } -/* Read a varint from a buffer, advance pointer */ -static size_t seq_read_varint(const uint8_t **pp, const uint8_t *end) { +/* Read a varint from a buffer, advance pointer (kept for backward compat) */ +static size_t __attribute__((unused)) seq_read_varint(const uint8_t **pp, const uint8_t *end) { size_t val = 0; while (*pp < end && **pp == 255) { val += 255; (*pp)++; } if (*pp < end) { val += **pp; (*pp)++; } @@ -1255,7 +1329,8 @@ typedef struct { static size_t parse_sequences(const uint8_t *tokens, size_t tok_len, uint8_t *lit_buf, size_t lit_cap, seq_t *seqs, size_t seq_cap, - size_t *total_lits, int off_bytes) { + size_t *total_lits, int off_bytes, + int min_match) { const uint8_t *tp = tokens, *tp_end = tokens + tok_len; size_t nseq = 0, nlits = 0; @@ -1296,7 +1371,7 @@ static size_t parse_sequences(const uint8_t *tokens, size_t tok_len, : ((uint32_t)tp[0] | ((uint32_t)tp[1] << 8)); tp += off_bytes; - size_t mlen = mc + 4; /* VV_MIN_MATCH = 4 */ + size_t mlen = mc + (size_t)min_match; if (mc == 15) { size_t ext = 0; do { @@ -1323,25 +1398,38 @@ static size_t parse_sequences(const uint8_t *tokens, size_t tok_len, * Takes raw LZ token stream, outputs ANS-coded sequence block. * ═══════════════════════════════════════════════════════════════ */ -vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, - uint8_t *dst, size_t dst_cap, size_t *dst_len, - int off_bytes) { +/* Internal impl. ml_base_tab selects between 'S' (min_match=4) and + * 'T' (min_match=3) encoding. */ +static vva_error_t vva_encode_sequences_impl(const uint8_t *tokens, size_t tok_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + int off_bytes, + const uint32_t *ml_base_tab) { if (!tok_len) { *dst_len = 0; return VVA_OK; } - /* Parse into sequences */ + /* Parse into sequences. + * PERF: one combined alloc for seqs + lit_buf. The sizeof(seq_t) + * is ≥ 4 bytes so natural alignment for both is satisfied. Saves + * 1 malloc/free pair per call. */ size_t max_seqs = tok_len; /* Upper bound */ - seq_t *seqs = (seq_t *)malloc(max_seqs * sizeof(seq_t)); - uint8_t *lit_buf = (uint8_t *)malloc(tok_len); - if (!seqs || !lit_buf) { free(seqs); free(lit_buf); return VVA_ERR_NOMEM; } + size_t seqs_sz = max_seqs * sizeof(seq_t); + size_t total_scratch = seqs_sz + tok_len; + uint8_t *base_scratch = (uint8_t *)malloc(total_scratch); + if (!base_scratch) return VVA_ERR_NOMEM; + seq_t *seqs = (seq_t *)base_scratch; + uint8_t *lit_buf = base_scratch + seqs_sz; size_t total_lits = 0; - size_t nseq = parse_sequences(tokens, tok_len, lit_buf, tok_len, seqs, max_seqs, &total_lits, off_bytes); - if (nseq == 0) { free(seqs); free(lit_buf); return VVA_ERR_CORRUPT; } + /* Derive min_match from the ml_base table passed in. For the v1 + * table this is 4; for v2 it's 3. Passed to parse_sequences so + * it reconstructs mlen consistently with how emit_seq packed it. */ + int min_match = (int)ml_base_tab[0]; + size_t nseq = parse_sequences(tokens, tok_len, lit_buf, tok_len, seqs, max_seqs, &total_lits, off_bytes, min_match); + if (nseq == 0) { free(base_scratch); return VVA_ERR_CORRUPT; } /* ─── Encode literals with 4-way ANS ─── */ size_t lit_cap = vva_bound(total_lits); uint8_t *lit_enc = (uint8_t *)malloc(lit_cap); - if (!lit_enc) { free(seqs); free(lit_buf); return VVA_ERR_NOMEM; } + if (!lit_enc) { free(base_scratch); return VVA_ERR_NOMEM; } size_t lit_enc_len = 0; uint8_t lit_fmt = 0; /* 0=raw, 1=ANS4, 2=ANS1 */ @@ -1366,31 +1454,38 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, } } - /* ─── Count ML and OF code frequencies with rep-match tracking ─── */ - uint32_t freq_ml[VVA_ML_CODES], freq_of[VVA_OF_CODES]; + /* ─── Count ML, OF, and LL code frequencies ─── */ + uint32_t freq_ml[VVA_ML_CODES], freq_of[VVA_OF_CODES], freq_ll[VVA_LL_CODES]; memset(freq_ml, 0, sizeof(freq_ml)); memset(freq_of, 0, sizeof(freq_of)); + memset(freq_ll, 0, sizeof(freq_ll)); /* Precompute OF codes with rep-match detection (forward pass). - * Store in per-sequence arrays so the backward ANS pass can use them. */ - uint8_t *seq_of_code = NULL; - uint32_t *seq_of_extra = NULL; - int *seq_of_nbits = NULL; - seq_of_code = (uint8_t *)malloc(nseq * sizeof(uint8_t)); - seq_of_extra = (uint32_t *)malloc(nseq * sizeof(uint32_t)); - seq_of_nbits = (int *)malloc(nseq * sizeof(int)); - if (!seq_of_code || !seq_of_extra || !seq_of_nbits) { - free(seq_of_code); free(seq_of_extra); free(seq_of_nbits); - free(seqs); free(lit_buf); free(lit_enc); + * Store in per-sequence arrays so the backward ANS pass can use them. + * + * PERF: consolidate 3 separate mallocs into 1. Layout: + * [seq_of_code: nseq × uint8_t] (padded to 4-byte align) + * [seq_of_extra: nseq × uint32_t] + * [seq_of_nbits: nseq × int] + * Saves 2 malloc/free pairs per vva_encode_sequences call. */ + size_t codes_sz = (nseq * sizeof(uint8_t) + 3) & ~(size_t)3; + size_t extra_sz = nseq * sizeof(uint32_t); + size_t nbits_sz = nseq * sizeof(int); + uint8_t *seq_scratch = (uint8_t *)malloc(codes_sz + extra_sz + nbits_sz); + if (!seq_scratch) { + free(base_scratch); free(lit_enc); return VVA_ERR_NOMEM; } + uint8_t *seq_of_code = seq_scratch; + uint32_t *seq_of_extra = (uint32_t *)(seq_scratch + codes_sz); + int *seq_of_nbits = (int *)(seq_scratch + codes_sz + extra_sz); size_t match_count = 0; uint32_t enc_rep[3] = {0, 0, 0}; /* Rep-match tracking during forward pass */ for (size_t i = 0; i < nseq; i++) { if (seqs[i].matchlen > 0) { uint8_t mc; uint32_t mx; int mn; - ml_encode(seqs[i].matchlen, &mc, &mx, &mn); + ml_encode_with(seqs[i].matchlen, ml_base_tab, &mc, &mx, &mn); freq_ml[mc]++; /* Check rep-match before explicit encoding */ @@ -1422,6 +1517,13 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, seq_of_extra[i] = 0; seq_of_nbits[i] = 0; } + + /* Count litlen frequency for ALL sequences (including last) */ + { + uint8_t lc; uint32_t lx; int ln; + ll_encode(seqs[i].litlen, &lc, &lx, &ln); + freq_ll[lc]++; + } } /* ─── Build ML and OF ANS tables ─── */ @@ -1430,13 +1532,43 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, memset(norm_ml, 0, sizeof(norm_ml)); memset(norm_of, 0, sizeof(norm_of)); - uint8_t *ml_hdr_buf = NULL, *of_hdr_buf = NULL; - size_t ml_hdr_sz = 0, of_hdr_sz = 0; + /* PERF: header buffers live on the stack — each is bounded at 600 B + * (fits any NSYM=256 table header) and they were heap-allocated on + * every call before. Saves 3 malloc/free pairs per call. */ + uint8_t ml_hdr_buf[600], of_hdr_buf[600], ll_hdr_buf[600]; + size_t ml_hdr_sz = 0, of_hdr_sz = 0, ll_hdr_sz = 0; uint8_t *seq_bs = NULL; size_t seq_bs_len = 0; - uint8_t *litlen_buf = NULL; - size_t litlen_len = 0; - uint32_t state_ml = 0, state_of = 0; + uint32_t state_ml = 0, state_of = 0, state_ll = 0; + enc_ctx_t *enc_ll_ctx = NULL; + + /* Build LL ANS table unconditionally (all sequences have litlens) */ + { + uint32_t raw_ll[NSYM]; + memset(raw_ll, 0, sizeof(raw_ll)); + for (int i = 0; i < VVA_LL_CODES; i++) raw_ll[i] = freq_ll[i]; + uint16_t norm_ll[NSYM]; + memset(norm_ll, 0, sizeof(norm_ll)); + normalize_freq(raw_ll, norm_ll); + ll_hdr_sz = write_hdr_v2(norm_ll, ll_hdr_buf, 600); + if (!ll_hdr_sz) goto seq_fail; + + /* PERF: one combined alloc for sp_ll + dec_ll. sp_ll lives in + * the first ANS_L bytes, dec_ll follows with alignment (16-byte + * aligned vs 8-byte reads is satisfied since ANS_L=4096 is + * already 4KB-aligned). Saves 1 malloc/free pair. */ + size_t sp_sz = ANS_L; + size_t dec_sz = ANS_L * sizeof(vva_dec_entry_t); + uint8_t *ll_tables = (uint8_t *)malloc(sp_sz + dec_sz); + if (!ll_tables) goto seq_fail; + uint8_t *sp_ll = ll_tables; + vva_dec_entry_t *dec_ll = (vva_dec_entry_t *)(ll_tables + sp_sz); + spread_symbols(norm_ll, sp_ll); + build_dec(norm_ll, sp_ll, dec_ll); + enc_ll_ctx = build_enc(norm_ll, sp_ll, dec_ll); + free(ll_tables); + if (!enc_ll_ctx) goto seq_fail; + } if (match_count > 0) { /* Treat ML codes as a small-alphabet problem */ @@ -1449,36 +1581,35 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, normalize_freq(raw_ml, norm_ml); normalize_freq(raw_of, norm_of); - /* Write ML and OF table headers */ - ml_hdr_buf = (uint8_t *)malloc(600); - of_hdr_buf = (uint8_t *)malloc(600); - if (!ml_hdr_buf || !of_hdr_buf) goto seq_fail; - + /* Write ML and OF table headers (buffers are on the stack) */ ml_hdr_sz = write_hdr_v2(norm_ml, ml_hdr_buf, 600); of_hdr_sz = write_hdr_v2(norm_of, of_hdr_buf, 600); if (!ml_hdr_sz || !of_hdr_sz) goto seq_fail; - /* ─── Build encode tables ─── */ - uint8_t *sp_ml = (uint8_t *)malloc(ANS_L); - vva_dec_entry_t *dec_ml = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); - uint8_t *sp_of = (uint8_t *)malloc(ANS_L); - vva_dec_entry_t *dec_of = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); - if (!sp_ml || !dec_ml || !sp_of || !dec_of) { - free(sp_ml); free(dec_ml); free(sp_of); free(dec_of); - goto seq_fail; - } + + + /* ─── Build encode tables ─── + * PERF: one combined alloc for sp_ml + dec_ml + sp_of + dec_of + * (4 fixed-size ANS_L-based buffers). Saves 3 malloc/free pairs. */ + size_t sp_sz = ANS_L; + size_t dec_sz = ANS_L * sizeof(vva_dec_entry_t); + size_t combo_sz = (sp_sz + dec_sz) * 2; + uint8_t *ml_of_tables = (uint8_t *)malloc(combo_sz); + if (!ml_of_tables) goto seq_fail; + uint8_t *sp_ml = ml_of_tables; + vva_dec_entry_t *dec_ml = (vva_dec_entry_t *)(ml_of_tables + sp_sz); + uint8_t *sp_of = ml_of_tables + sp_sz + dec_sz; + vva_dec_entry_t *dec_of = (vva_dec_entry_t *)(ml_of_tables + sp_sz + dec_sz + sp_sz); spread_symbols(norm_ml, sp_ml); build_dec(norm_ml, sp_ml, dec_ml); enc_ctx_t *enc_ml_ctx = build_enc(norm_ml, sp_ml, dec_ml); - free(sp_ml); spread_symbols(norm_of, sp_of); build_dec(norm_of, sp_of, dec_of); enc_ctx_t *enc_of_ctx = build_enc(norm_of, sp_of, dec_of); - free(sp_of); - free(dec_ml); free(dec_of); + free(ml_of_tables); if (!enc_ml_ctx || !enc_of_ctx) { free_enc(enc_ml_ctx); free_enc(enc_of_ctx); goto seq_fail; @@ -1486,110 +1617,114 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, /* ─── Encode ML/OF codes + extra bits in reverse ─── */ /* Collect bitpairs for ANS-coded symbols + raw extra bits */ - size_t pair_cap = match_count * 4; /* 2 ANS + 2 extra max */ + size_t pair_cap = nseq * 6; /* 3 ANS + 3 extra max per seq */ bitpair_t *pairs = (bitpair_t *)malloc(pair_cap * sizeof(bitpair_t)); if (!pairs) { free_enc(enc_ml_ctx); free_enc(enc_of_ctx); goto seq_fail; } - state_ml = 0; state_of = 0; + state_ml = 0; state_of = 0; state_ll = 0; size_t npairs = 0; - /* Process matches in reverse for ANS LIFO */ + /* Process sequences in reverse for ANS LIFO. + * Decoder reads per-sequence: LL, OF, ML (forward). + * Backward encode order (reversed of decode): ML, OF, LL. + * After bitstream reversal: LL appears first → decoded first. */ for (size_t ii = nseq; ii > 0; ii--) { - if (seqs[ii - 1].matchlen == 0) continue; + if (seqs[ii - 1].matchlen > 0) { + uint8_t mc; + uint32_t mx; + int mn; + ml_encode_with(seqs[ii - 1].matchlen, ml_base_tab, &mc, &mx, &mn); - uint8_t mc; - uint32_t mx; - int mn; - ml_encode(seqs[ii - 1].matchlen, &mc, &mx, &mn); + uint8_t oc = seq_of_code[ii - 1]; + uint32_t ox = seq_of_extra[ii - 1]; + int on = seq_of_nbits[ii - 1]; - /* Use precomputed OF code from forward pass (rep-match aware) */ - uint8_t oc = seq_of_code[ii - 1]; - uint32_t ox = seq_of_extra[ii - 1]; - int on = seq_of_nbits[ii - 1]; + /* ML extra bits (raw) */ + if (mn > 0) { + pairs[npairs].val = (uint32_t)mx; + pairs[npairs].nb = (uint8_t)mn; + npairs++; + } - /* Encode in this order (reversed): ml_code, ml_extra, of_code, of_extra - * Decoder reads: of_extra, of_code, ml_extra, ml_code */ + /* ML code (ANS) */ + { + uint32_t bv; int bn; + int slot = enc_sym(enc_ml_ctx, state_ml, mc, &bv, &bn); + if (slot < 0) { + free(pairs); free_enc(enc_ml_ctx); free_enc(enc_of_ctx); + goto seq_fail; + } + pairs[npairs].val = (uint32_t)bv; + pairs[npairs].nb = (uint8_t)bn; + npairs++; + state_ml = (uint32_t)slot; + } - /* ML extra bits (raw) */ - if (mn > 0) { - pairs[npairs].val = (uint32_t)mx; - pairs[npairs].nb = (uint8_t)mn; - npairs++; + /* OF extra bits (raw) */ + if (on > 0) { + pairs[npairs].val = (uint32_t)ox; + pairs[npairs].nb = (uint8_t)on; + npairs++; + } + + /* OF code (ANS) */ + { + uint32_t bv; int bn; + int slot = enc_sym(enc_of_ctx, state_of, oc, &bv, &bn); + if (slot < 0) { + free(pairs); free_enc(enc_ml_ctx); free_enc(enc_of_ctx); + goto seq_fail; + } + pairs[npairs].val = (uint32_t)bv; + pairs[npairs].nb = (uint8_t)bn; + npairs++; + state_of = (uint32_t)slot; + } } - /* ML code (ANS) */ + /* LL encoded LAST per sequence (so it's decoded FIRST after reversal) */ { - uint32_t bv; int bn; - int slot = enc_sym(enc_ml_ctx, state_ml, mc, &bv, &bn); - if (slot < 0) { - free(pairs); free_enc(enc_ml_ctx); free_enc(enc_of_ctx); - goto seq_fail; - } - pairs[npairs].val = (uint32_t)bv; - pairs[npairs].nb = (uint8_t)bn; - npairs++; - state_ml = (uint32_t)slot; - } + uint8_t lc; uint32_t lx; int ln; + ll_encode(seqs[ii - 1].litlen, &lc, &lx, &ln); - /* OF extra bits (raw) */ - if (on > 0) { - pairs[npairs].val = (uint32_t)ox; - pairs[npairs].nb = (uint8_t)on; - npairs++; - /* Handle >16 extra bits for large offsets */ - if (on > 16) { - /* Split: already wrote low 16 bits, now high bits */ - /* Actually our bw_add handles up to ~30 bits, so OK */ + if (ln > 0) { + pairs[npairs].val = (uint32_t)lx; + pairs[npairs].nb = (uint8_t)ln; + npairs++; } - } - - /* OF code (ANS) */ - { - uint32_t bv; int bn; - int slot = enc_sym(enc_of_ctx, state_of, oc, &bv, &bn); - if (slot < 0) { - free(pairs); free_enc(enc_ml_ctx); free_enc(enc_of_ctx); - goto seq_fail; + { + uint32_t bv; int bn; + int slot = enc_sym(enc_ll_ctx, state_ll, lc, &bv, &bn); + if (slot < 0) { + free(pairs); free_enc(enc_ml_ctx); free_enc(enc_of_ctx); + goto seq_fail; + } + pairs[npairs].val = (uint32_t)bv; + pairs[npairs].nb = (uint8_t)bn; + npairs++; + state_ll = (uint32_t)slot; } - pairs[npairs].val = (uint32_t)bv; - pairs[npairs].nb = (uint8_t)bn; - npairs++; - state_of = (uint32_t)slot; } } free_enc(enc_ml_ctx); free_enc(enc_of_ctx); - /* Write pairs in reverse (so decoder reads forward) */ - size_t bs_cap = npairs * 2 + 16; + /* Write pairs in reverse (so decoder reads forward). + * Each pair is up to 32 bits (ANS slot = 14 bits + extra up to 18). + * Allocate 4 bytes per pair + 16-byte safety margin. */ + size_t bs_cap = npairs * 4 + 16; seq_bs = (uint8_t *)malloc(bs_cap); if (!seq_bs) { free(pairs); goto seq_fail; } - bw_t w; - bw_init(&w, seq_bs, bs_cap); + ans_bw_t w; + ans_bw_init(&w, seq_bs, bs_cap); for (size_t i = npairs; i > 0; i--) - bw_add(&w, pairs[i - 1].val, pairs[i - 1].nb); - seq_bs_len = bw_flush(&w); + ans_bw_add(&w, pairs[i - 1].val, pairs[i - 1].nb); + seq_bs_len = ans_bw_flush(&w); free(pairs); } - /* ─── Encode litlen varints ─── */ - /* Each litlen varint uses ceil(litlen/255)+1 bytes. Worst case for - * nseq sequences each with large litlen: compute exact bound. */ - { - size_t litlen_cap = nseq; /* at least 1 byte per sequence */ - for (size_t i = 0; i < nseq; i++) - litlen_cap += seqs[i].litlen / 255; - litlen_cap += 16; /* safety margin */ - litlen_buf = (uint8_t *)malloc(litlen_cap); - } - if (!litlen_buf) goto seq_fail; - { - size_t pos = 0; - for (size_t i = 0; i < nseq; i++) - pos += seq_write_varint(litlen_buf + pos, seqs[i].litlen); - litlen_len = pos; - } + /* Litlens are now ANS-coded in the sequence bitstream — no varints needed */ /* ─── Assemble output ─── */ /* Format: [4B lit_count] [1B lit_fmt] [4B lit_enc_len] [lit_data] @@ -1600,7 +1735,7 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, * [litlen_varints] */ { size_t total = 9 + lit_enc_len + 4 + 4 + ml_hdr_sz + 4 + of_hdr_sz - + 4 + 4 + seq_bs_len + litlen_len; + + 2 + ll_hdr_sz + 4 + 2 + 4 + seq_bs_len; if (total > dst_cap) goto seq_fail; @@ -1625,33 +1760,59 @@ vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, op[0] = (uint8_t)(of_hdr_sz & 0xFF); op[1] = (uint8_t)(of_hdr_sz >> 8); op += 2; if (of_hdr_sz > 0) { memcpy(op, of_hdr_buf, of_hdr_sz); op += of_hdr_sz; } + /* LL table */ + op[0] = (uint8_t)(ll_hdr_sz & 0xFF); op[1] = (uint8_t)(ll_hdr_sz >> 8); op += 2; + if (ll_hdr_sz > 0) { memcpy(op, ll_hdr_buf, ll_hdr_sz); op += ll_hdr_sz; } + /* States */ op[0] = (uint8_t)(state_ml & 0xFF); op[1] = (uint8_t)((state_ml >> 8) & 0xFF); op += 2; op[0] = (uint8_t)(state_of & 0xFF); op[1] = (uint8_t)((state_of >> 8) & 0xFF); op += 2; + op[0] = (uint8_t)(state_ll & 0xFF); op[1] = (uint8_t)((state_ll >> 8) & 0xFF); op += 2; /* Sequence bitstream (4B size) */ op[0]=(uint8_t)seq_bs_len; op[1]=(uint8_t)(seq_bs_len>>8); op[2]=(uint8_t)(seq_bs_len>>16); op[3]=(uint8_t)(seq_bs_len>>24); op+=4; if (seq_bs_len > 0) { memcpy(op, seq_bs, seq_bs_len); op += seq_bs_len; } - /* Litlen varints */ - memcpy(op, litlen_buf, litlen_len); op += litlen_len; + /* Litlens are ANS-coded in the bitstream — no trailing varints */ *dst_len = (size_t)(op - dst); } - free(seqs); free(lit_buf); free(lit_enc); - free(seq_of_code); free(seq_of_extra); free(seq_of_nbits); - free(ml_hdr_buf); free(of_hdr_buf); free(seq_bs); free(litlen_buf); + free(base_scratch); free(lit_enc); + free(seq_scratch); + free_enc(enc_ll_ctx); + free(seq_bs); return VVA_OK; seq_fail: - free(seqs); free(lit_buf); free(lit_enc); - free(seq_of_code); free(seq_of_extra); free(seq_of_nbits); - free(ml_hdr_buf); free(of_hdr_buf); free(seq_bs); free(litlen_buf); + free(base_scratch); free(lit_enc); + free(seq_scratch); + free_enc(enc_ll_ctx); + free(seq_bs); return VVA_ERR_OVERFLOW; } +/* Public entry for 'S' tag (VV_ENTROPY_SEQ, min_match=4). */ +vva_error_t vva_encode_sequences(const uint8_t *tokens, size_t tok_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + int off_bytes) { + return vva_encode_sequences_impl(tokens, tok_len, dst, dst_cap, dst_len, + off_bytes, ml_base); +} + +/* Public entry for 'T' tag (VV_ENTROPY_SEQ_V2, min_match=3). + * Encodes match-length codes using ml_base_v2 so a length-3 match + * becomes code 0 (instead of being unrepresentable as it is in 'S'). + * The caller (vv_encoder.c) must ensure the token stream contains + * only matches of length ≥ 3, and must emit the frame with tag 'T'. */ +vva_error_t vva_encode_sequences_v2(const uint8_t *tokens, size_t tok_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + int off_bytes) { + return vva_encode_sequences_impl(tokens, tok_len, dst, dst_cap, dst_len, + off_bytes, ml_base_v2); +} + /* ═══════════════════════════════════════════════════════════════ * DECODE SEQUENCES * @@ -1659,17 +1820,22 @@ seq_fail: * Reconstructs LZ matches in-place using existing copy logic. * ═══════════════════════════════════════════════════════════════ */ -vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, - uint8_t *dst, size_t dst_cap, size_t *dst_len, - const uint8_t *dst_base) { +/* Internal implementation shared by 'S' (min_match=4) and 'T' + * (min_match=3) entropy tags. Takes the ml_base table as a parameter + * so both tags use the same code path. Everything else in the 'T' + * payload is byte-identical to 'S'. */ +static vva_error_t vva_decode_sequences_impl(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + const uint8_t *dst_base, + const uint32_t *ml_base_tab) { const uint8_t *p = src, *end = src + src_len; /* Read literal section: [4B lit_count] [1B lit_fmt] [4B lit_enc_len] */ - if (p + 9 > end) return VVA_ERR_CORRUPT; + if (p + 9 > end) return VVA_ERR_CORRUPT; size_t total_lits = (size_t)p[0]|((size_t)p[1]<<8)|((size_t)p[2]<<16)|((size_t)p[3]<<24); p += 4; uint8_t lit_fmt = *p++; size_t lit_enc_len = (size_t)p[0]|((size_t)p[1]<<8)|((size_t)p[2]<<16)|((size_t)p[3]<<24); p += 4; - if (p + lit_enc_len > end) return VVA_ERR_CORRUPT; + if (p + lit_enc_len > end) return VVA_ERR_CORRUPT; /* Decode literals based on format byte */ uint8_t *lit_buf = (uint8_t *)malloc(total_lits + 16); @@ -1699,13 +1865,13 @@ vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, p += lit_enc_len; /* Read match count (4B) */ - if (p + 4 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + 4 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } size_t match_count = (size_t)p[0]|((size_t)p[1]<<8)|((size_t)p[2]<<16)|((size_t)p[3]<<24); p += 4; /* Read ML table header */ - if (p + 2 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + 2 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } size_t ml_hdr_sz = (size_t)p[0] | ((size_t)p[1] << 8); p += 2; - if (p + ml_hdr_sz > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + ml_hdr_sz > end) { free(lit_buf); return VVA_ERR_CORRUPT; } uint16_t norm_ml[NSYM]; memset(norm_ml, 0, sizeof(norm_ml)); @@ -1713,50 +1879,67 @@ vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, p += ml_hdr_sz; /* Read OF table header */ - if (p + 2 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + 2 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } size_t of_hdr_sz = (size_t)p[0] | ((size_t)p[1] << 8); p += 2; - if (p + of_hdr_sz > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + of_hdr_sz > end) { free(lit_buf); return VVA_ERR_CORRUPT; } uint16_t norm_of[NSYM]; memset(norm_of, 0, sizeof(norm_of)); if (of_hdr_sz > 0) read_hdr_v2(p, of_hdr_sz, norm_of); p += of_hdr_sz; + /* Read LL table header */ + if (p + 2 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + size_t ll_hdr_sz = (size_t)p[0] | ((size_t)p[1] << 8); p += 2; + if (p + ll_hdr_sz > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + + uint16_t norm_ll[NSYM]; + memset(norm_ll, 0, sizeof(norm_ll)); + if (ll_hdr_sz > 0) read_hdr_v2(p, ll_hdr_sz, norm_ll); + p += ll_hdr_sz; + /* Read initial states */ - if (p + 4 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + 6 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } uint32_t state_ml = (uint32_t)p[0] | ((uint32_t)p[1] << 8); p += 2; uint32_t state_of = (uint32_t)p[0] | ((uint32_t)p[1] << 8); p += 2; + uint32_t state_ll = (uint32_t)p[0] | ((uint32_t)p[1] << 8); p += 2; /* Read sequence bitstream (4B size) */ - if (p + 4 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + 4 > end) { free(lit_buf); return VVA_ERR_CORRUPT; } size_t seq_bs_len = (size_t)p[0]|((size_t)p[1]<<8)|((size_t)p[2]<<16)|((size_t)p[3]<<24); p += 4; - if (p + seq_bs_len > end) { free(lit_buf); return VVA_ERR_CORRUPT; } + if (p + seq_bs_len > end) { free(lit_buf); return VVA_ERR_CORRUPT; } - /* Build ML and OF decode tables */ - vva_dec_entry_t *dec_ml = NULL, *dec_of = NULL; - if (match_count > 0) { + /* Build ML, OF, and LL decode tables */ + vva_dec_entry_t *dec_ml = NULL, *dec_of = NULL, *dec_ll = NULL; + { uint8_t *sp_tmp = (uint8_t *)malloc(ANS_L); - dec_ml = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); - dec_of = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); - if (!sp_tmp || !dec_ml || !dec_of) { - free(sp_tmp); free(dec_ml); free(dec_of); free(lit_buf); + if (match_count > 0) { + dec_ml = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); + dec_of = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); + } + dec_ll = (vva_dec_entry_t *)malloc(ANS_L * sizeof(vva_dec_entry_t)); + if (!sp_tmp || !dec_ll || (match_count > 0 && (!dec_ml || !dec_of))) { + free(sp_tmp); free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); return VVA_ERR_NOMEM; } - spread_symbols(norm_ml, sp_tmp); - build_dec(norm_ml, sp_tmp, dec_ml); - spread_symbols(norm_of, sp_tmp); - build_dec(norm_of, sp_tmp, dec_of); + if (match_count > 0) { + spread_symbols(norm_ml, sp_tmp); + build_dec(norm_ml, sp_tmp, dec_ml); + spread_symbols(norm_of, sp_tmp); + build_dec(norm_of, sp_tmp, dec_of); + } + spread_symbols(norm_ll, sp_tmp); + build_dec(norm_ll, sp_tmp, dec_ll); free(sp_tmp); } /* Initialize bitstream reader for sequence data */ - br_t r; - br_init(&r, p, seq_bs_len); - br_fill(&r); + ans_br_t r; + ans_br_init(&r, p, seq_bs_len); + ans_br_fill(&r); p += seq_bs_len; - /* Litlen varint stream starts at p */ - const uint8_t *ll_p = p; + /* Litlens are ANS-coded in the bitstream — no varint stream */ /* ─── PERF: Decode loop — reconstruct output ─── */ uint8_t *op = dst; @@ -1765,70 +1948,164 @@ vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, size_t matches_decoded = 0; uint32_t dec_rep[3] = {0, 0, 0}; /* Rep-match offset tracking */ - while (lit_pos < total_lits || matches_decoded < match_count) { - size_t litlen = seq_read_varint(&ll_p, end); + /* SPRINT 50: Safe-zone precomputation. Two bounds checks run per + * sequence today: + * (1) offset validity: offset != 0 && offset <= op - dst_base + * (2) matchlen overflow: op + matchlen > op_end + * + * Ablation measurements (fx_text): bounds checks cost ~18% of + * total decode time. The overhead is not the branches themselves + * (predicted-not-taken, rarely fire) but (a) register pressure + * from keeping op_end/dst_base live and (b) the compound subtract + * + compare for (1). + * + * Observation: once we're past the first wlog bytes AND not yet + * near op_end, BOTH checks are guaranteed to pass for any + * well-formed sequence (offset ≤ wlog ≤ op - dst_base, and + * matchlen ≤ max_match ≤ op_end - op). In the "safe zone" we can + * skip the runtime checks with zero security loss — they remain + * under offset_check_floor and op_safe_end as non-hoistable guards. + * + * SAFETY: the skipped checks are tautologies in the safe zone, + * not removed guarantees. Malformed input that produces an + * invalid offset or overlong matchlen still triggers the checks + * near the boundaries. We maintain §4 invariants 3 and 5. + * + * SAFEZONE_MAX_OFFSET covers the legal offset range (1 << wlog_max). + * SAFEZONE_MAX_RUN covers BOTH max litlen and max matchlen (both are + * bounded by the wire format at ≤65535: LL encoding ll_base[35]=61440 + * + up to 4095 extra bits = 65535; ML encoding likewise). So + * op_safe_end = op_end - 65535 guarantees any single sequence's + * total writes (literals + match) fit without per-iter overflow + * checking. */ + enum { SAFEZONE_MAX_OFFSET = 1u << 20 }; /* Maximum wlog supported */ + enum { SAFEZONE_MAX_RUN = 65535 }; /* litlen or matchlen */ + uint8_t *op_safe_end = (dst_cap > SAFEZONE_MAX_RUN) + ? op_end - SAFEZONE_MAX_RUN : dst; + const uint8_t *offset_check_floor = dst_base + SAFEZONE_MAX_OFFSET; - if (lit_pos + litlen > total_lits) { free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_CORRUPT; } - if (op + litlen > op_end) { free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_OVERFLOW; } + size_t seqs_decoded = 0; + while (lit_pos < total_lits || matches_decoded < match_count) { + /* PERF: issue all 3 ANS table lookups early so CPU can overlap + * the L1 cache fills. The dec_ll/dec_of/dec_ml arrays are + * independent, so the loads have no data dependency on each + * other — perfect for ILP. The compiler will schedule these + * ahead of the bitstream reads that consume their results. + * + * Fill once at the top. 64 bits covers up to 1 full sequence + * worst-case (26+35+27=88 but typical ~15-30). ans_br_read + * auto-fills when we run out within a sequence. */ + ans_br_fill(&r); + + /* Fast path: in safe zone (past warmup, before final max_match + * bytes). Both bounds checks are tautological and skipped. */ + int in_safe_zone = (op >= offset_check_floor) & (op <= op_safe_end); + + /* PERF: state validation via mask-on-access rather than + * explicit branches. Since ANS_L is a power of 2, masking + * bounds any state into valid-table range at ~0 cost, where + * the 3 explicit ">= ANS_L" branches would cost 3 predicted- + * not-taken comparisons per iter. On corrupt input the + * frame-level XXH64 footer still catches the corruption; + * here we're protecting against out-of-bounds table access, + * not declaring correctness. */ + vva_dec_entry_t ell = dec_ll[state_ll & (ANS_L - 1)]; + vva_dec_entry_t eof = dec_of[state_of & (ANS_L - 1)]; + vva_dec_entry_t eml = dec_ml[state_ml & (ANS_L - 1)]; + + /* ── Decode LL: state, extra, final litlen ── */ + uint32_t ll_bits = ans_br_read(&r, ell.nbits); + state_ll = (uint32_t)ell.baseline + ll_bits; + uint8_t ll_code = ell.symbol; + uint32_t ll_extra_val = ans_br_read(&r, ll_extra[ll_code]); + size_t litlen = ll_decode(ll_code, ll_extra_val); + + if (VV_UNLIKELY(lit_pos + litlen > total_lits)) { + free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); + return VVA_ERR_CORRUPT; + } + if (VV_UNLIKELY(!in_safe_zone && op + litlen > op_end)) { + free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); + return VVA_ERR_OVERFLOW; + } if (litlen > 0) { - memcpy(op, lit_buf + lit_pos, litlen); + /* PERF: for litlen ≤ 16 (common case in text/json), do one + * unconditional 16-byte copy instead of memcpy's branchy + * dispatch. lit_buf has 16-byte post-allocation slack; the + * destination window is checked against op_end above. + * + * Safe to over-read lit_buf beyond lit_pos+litlen (slack). + * Safe to over-write op beyond op+litlen as long as + * op + 16 ≤ op_end; for the last few sequences in a block + * that may not hold, so fall back to memcpy there. */ + if (VV_LIKELY(litlen <= 16 && op + 16 <= op_end)) { + memcpy(op, lit_buf + lit_pos, 16); + } else { + memcpy(op, lit_buf + lit_pos, litlen); + } op += litlen; lit_pos += litlen; } + seqs_decoded++; if (matches_decoded >= match_count) break; - /* Decode OF code */ - if (r.n < ANS_LOG) br_fill(&r); - if (state_of >= (uint32_t)ANS_L) { free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_CORRUPT; } - vva_dec_entry_t eof = dec_of[state_of]; - uint32_t of_bits = br_read(&r, eof.nbits); + /* ── Decode OF: state, then offset (rep or explicit) ── + * No explicit fill — ans_br_read fills when it runs out. */ + uint32_t of_bits = ans_br_read(&r, eof.nbits); state_of = (uint32_t)eof.baseline + of_bits; - - /* Resolve offset: codes 0-2 = rep-match, 3+ = explicit */ uint8_t of_code = eof.symbol; uint32_t offset; if (of_code < 3) { offset = dec_rep[of_code]; } else { - uint32_t of_extra_val = 0; - if (of_code < VVA_OF_CODES && of_extra[of_code] > 0) { - of_extra_val = br_read(&r, of_extra[of_code]); - } + uint32_t of_extra_val = ans_br_read(&r, of_extra[of_code]); offset = of_decode(of_code, of_extra_val); } - /* Update rep offsets */ if (offset != 0 && offset != dec_rep[0]) { dec_rep[2] = dec_rep[1]; dec_rep[1] = dec_rep[0]; dec_rep[0] = offset; } - /* Decode match length */ - if (r.n < ANS_LOG) br_fill(&r); - if (state_ml >= (uint32_t)ANS_L) { free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_CORRUPT; } - vva_dec_entry_t eml = dec_ml[state_ml]; - uint32_t ml_bits = br_read(&r, eml.nbits); + /* ── Decode ML: state, extra, final matchlen ── */ + uint32_t ml_bits = ans_br_read(&r, eml.nbits); state_ml = (uint32_t)eml.baseline + ml_bits; - - /* Read matchlen extra bits */ uint8_t ml_code = eml.symbol; - uint32_t ml_extra_val = 0; - if (ml_code < VVA_ML_CODES && ml_extra[ml_code] > 0) { - ml_extra_val = br_read(&r, ml_extra[ml_code]); - } - uint32_t matchlen = ml_decode(ml_code, ml_extra_val); + uint32_t ml_extra_val = ans_br_read(&r, ml_extra[ml_code]); + uint32_t matchlen = ml_base_tab[ml_code] + ml_extra_val; - /* Validate and execute match copy */ - if (offset == 0 || offset > (uint32_t)(op - dst_base)) { + /* Validate and execute match copy. + * + * SPRINT 50 safety: the offset upper cap is checked + * unconditionally — covers adversarial inputs that encode + * offsets beyond any legal window. In-safe-zone skip only + * removes the position-dependent check (offset > op - dst_base), + * which is guaranteed tautological when both + * offset ≤ SAFEZONE_MAX_OFFSET (absolute cap, checked) + * op ≥ dst_base + SAFEZONE_MAX_OFFSET (safe-zone floor) + * The matchlen-overshoot check is similarly safe because + * op_safe_end = op_end - SAFEZONE_MAX_MATCH, and matchlen is + * always ≤ SAFEZONE_MAX_MATCH by wire format. */ + if (VV_UNLIKELY(offset == 0 || offset > SAFEZONE_MAX_OFFSET)) { free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_CORRUPT; } - if (op + matchlen > op_end) { + if (VV_UNLIKELY(!in_safe_zone && offset > (uint32_t)(op - dst_base))) { + free(dec_ml); free(dec_of); free(lit_buf); + return VVA_ERR_CORRUPT; + } + if (VV_UNLIKELY(!in_safe_zone && op + matchlen > op_end)) { free(dec_ml); free(dec_of); free(lit_buf); return VVA_ERR_OVERFLOW; } - /* PERF: match copy — use SIMD tiered copy when available. - * ZUPT-COMPAT: standalone path uses scalar copy for portability. */ + /* PERF: inline tiered match copy — avoids the function pointer + * call in vv_copy_match which kills ILP. The compiler can then + * overlap these stores with the next iteration's ANS decodes. + * + * Tiers (matches decode_block_tokens_impl): + * offset >= 16: safe bulk 16-byte chunks (no overlap concerns) + * offset >= 8: 8-byte chunks (overlap window >= stride) + * offset < 8: byte-by-byte (overlap propagates correctly) */ #ifdef VV_ANS_STANDALONE { const uint8_t *match_src = op - offset; @@ -1836,7 +2113,67 @@ vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, op[j] = match_src[j]; } #else - vv_copy_match(op, offset, matchlen); + { + uint8_t *d = op; + if (offset >= 16) { + const uint8_t *s = d - offset; + /* PERF: common case is matchlen in [4,16] — do one + * unconditional 16-byte copy when safe. Over-writes + * harmlessly into future output space (caller's buffer + * already sized for dsz, plus op_end check above). + * + * SPRINT 45: same bleed-over hazard as the offset≥8 + * path. For matchlen < 4, the 16-byte overwrite + * corrupts bytes that a subsequent short-offset match + * will read. Use an exact 3-byte copy in that case. */ + if (VV_LIKELY(matchlen >= 4 && matchlen <= 16 && d + 16 <= op_end)) { + memcpy(d, s, 16); + } else if (matchlen == 3 && d + 16 <= op_end) { + d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; + } else { + size_t rem = matchlen; + while (rem >= 16) { memcpy(d, s, 16); d += 16; s += 16; rem -= 16; } + if (rem > 0) memcpy(d, s, rem); + } + } else if (offset >= 8) { + const uint8_t *s = d - offset; + /* PERF: matchlen ≤ 8 with offset ≥ 8 → one 8-byte copy. + * + * SPRINT 45: The fast path writes 8 bytes unconditionally, + * which overshoots for small matchlen. For v1 (min_match=4) + * this is harmless — the overshoot into d[4..7] gets + * overwritten by the next sequence before anyone reads it. + * But for v2 (min_match=3), a subsequent short-offset + * match reads from d[3..] and sees the overshoot bytes. + * Gate the fast path on matchlen ≥ 4 to preserve v1 + * behavior while making v2 correct. */ + if (VV_LIKELY(matchlen >= 4 && matchlen <= 8 && d + 8 <= op_end)) { + uint64_t v; memcpy(&v, s, 8); memcpy(d, &v, 8); + } else if (matchlen <= 8 && d + 8 <= op_end) { + /* matchlen == 3 path: copy exactly 3 bytes without + * overshooting. One 4-byte read covers all three + * source bytes and is safe since offset ≥ 8 (the + * source region is disjoint from the destination). */ + d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; + } else { + size_t rem = matchlen; + while (rem >= 8) { + uint64_t v; memcpy(&v, s, 8); memcpy(d, &v, 8); + d += 8; s += 8; rem -= 8; + } + while (rem-- > 0) *d++ = *s++; + } + } else { + /* offset < 8: byte-by-byte for correct self-reference. + * Experimented with unrolled 16-iter versions; the + * branchless form over-writes past matchlen and hurts + * text, while the branched form hurts JSON. The simple + * loop runs well across all fixtures — the compiler + * schedules the dependent byte loads reasonably. */ + for (uint32_t j = 0; j < matchlen; j++) + d[j] = d[j - (ptrdiff_t)offset]; + } + } #endif op += matchlen; @@ -1844,6 +2181,24 @@ vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, } *dst_len = (size_t)(op - dst); - free(dec_ml); free(dec_of); free(lit_buf); + free(dec_ml); free(dec_of); free(dec_ll); free(lit_buf); return VVA_OK; } + +/* Public entry for 'S' tag (VV_ENTROPY_SEQ, min_match=4). */ +vva_error_t vva_decode_sequences(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + const uint8_t *dst_base) { + return vva_decode_sequences_impl(src, src_len, dst, dst_cap, dst_len, + dst_base, ml_base); +} + +/* Public entry for 'T' tag (VV_ENTROPY_SEQ_V2, min_match=3). + * Payload format is byte-identical to 'S' — only the ML table differs. + * Produced by encoders that opt into v2, decodable by any v2.33.0+ decoder. */ +vva_error_t vva_decode_sequences_v2(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, size_t *dst_len, + const uint8_t *dst_base) { + return vva_decode_sequences_impl(src, src_len, dst, dst_cap, dst_len, + dst_base, ml_base_v2); +} diff --git a/src/vv_decoder.c b/src/vv_decoder.c index 373deb7..bbad687 100644 --- a/src/vv_decoder.c +++ b/src/vv_decoder.c @@ -1,12 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ -#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) - #define _DEFAULT_SOURCE 1 -#endif - /* * VaptVupt — Decoder v2 (Sprint 1) * @@ -19,6 +10,7 @@ */ #include "vaptvupt.h" +#include "vv_platform.h" #include "vv_huffman.h" #include "vv_ans.h" #include @@ -69,21 +61,21 @@ static inline void match_copy_32(uint8_t *d, const uint8_t *s, size_t n) { while (n >= 32) { wcopy32(d, s); d += 32; s += 32; n -= 32; } /* Exact tail: use 16-byte then memcpy to avoid corrupting future output */ if (n >= 16) { wcopy16(d, s); d += 16; s += 16; n -= 16; } - if (n > 0) __builtin_memcpy(d, s, n); + if (n > 0) memcpy(d, s, n); } /* Match copy with offset 16-31: 16-byte chunks, exact tail */ static inline void match_copy_16(uint8_t *d, const uint8_t *s, size_t n) { while (n >= 16) { wcopy16(d, s); d += 16; s += 16; n -= 16; } - if (n > 0) __builtin_memcpy(d, s, n); + if (n > 0) memcpy(d, s, n); } /* Match copy with offset 8-15: 8-byte register copy */ static inline void match_copy_8(uint8_t *d, uint32_t off, size_t n) { const uint8_t *s = d - off; while (n >= 8) { - uint64_t v; __builtin_memcpy(&v, s, 8); - __builtin_memcpy(d, &v, 8); + uint64_t v; memcpy(&v, s, 8); + memcpy(d, &v, 8); s += 8; d += 8; n -= 8; } while (n > 0) { *d++ = *s++; n--; } @@ -104,88 +96,82 @@ static inline void match_overlap(uint8_t *d, uint32_t off, size_t n) { * DECODE BLOCK — TWO-TIER HOT PATH * ═══════════════════════════════════════════════════════════════ */ -static vv_error_t decode_block_tokens( +/* PERF: Force-inline core decode body so off_bytes becomes a compile-time + * constant in each specialized variant, eliminating the ternary from the + * hot path and enabling better branch prediction + offset loads. */ +static __attribute__((always_inline)) inline vv_error_t +decode_block_tokens_impl( const uint8_t *ip, size_t ip_len, - uint8_t *op, size_t dst_cap, size_t *out_len, int off_bytes, - const uint8_t *dst_base) /* Base of full output buffer for cross-block offset validation */ + uint8_t *op, size_t dst_cap, size_t *out_len, + const uint8_t *dst_base, + const int off_bytes) /* compile-time constant after inlining */ { const uint8_t *const ip_end = ip + ip_len; uint8_t *const op_start = op; uint8_t *const op_end = op + dst_cap; - /* Safe zone boundaries: skip per-op bounds checks while inside. - * Guard against underflow: if block is smaller than margin, skip fast path. */ - const uint8_t *const ip_safe = (ip_len > 24) ? (ip_end - 24) : ip; - uint8_t *const op_safe = (dst_cap > 40) ? (op_end - 40) : op; + /* PERF: widened safe-zone margins (was 24/40). + * Larger margins = fewer bound-check-triggered loop exits per block. + * Exit boundary: max is 1 token + 14 lits + 3 offset + 6 match_ext = 24. + * Plus match_copy_32 may over-copy 32 bytes past the real end, so + * op needs at least 64 bytes of margin. */ + const uint8_t *const ip_safe = (ip_len > 48) ? (ip_end - 48) : ip; + uint8_t *const op_safe = (dst_cap > 72) ? (op_end - 72) : op; + + /* PERF: once we've written enough bytes, any offset ≤ max_dist passes + * the "offset > op - dst_base" check. Max offset is (1 << wlog) - 1, + * at most (1<<20) - 1 for wlog=20. So past this threshold, only + * offset==0 needs checking (invalid/corrupted). */ + const uint32_t max_valid_off = (off_bytes == 2) ? 0xFFFF : 0xFFFFFF; #if VV_INLINE_AVX2 - /* ═══ AVX2 FAST PATH ═══ - * - * Runs while both ip and op are in the safe zone. - * No per-byte bounds checks. Inline SIMD copies. - * Prefetch match source at offset-load time. - * - * Per-sequence cost (common case, litlen≤14, matchlen≤18): - * token load + decode: 3 cycles - * early offset load: 4 cycles (overlapped) - * prefetch: 0 cycles (non-blocking) - * literal wcopy16: 5 cycles - * match wcopy32: 5 cycles - * pointer advance: 2 cycles - * loop branch: 0 cycles (predicted) - * ───────────────────────────────── - * Total: ~10 cycles for ~12 output bytes → 1.2 bytes/cycle - * At 4 GHz: ~4.8 GB/s (theoretical, real ~2-3 GB/s with cache) - */ - while (__builtin_expect(ip < ip_safe && op < op_safe, 1)) { + /* PERF: two-phase fast path. + * Phase 1 (warmup): op hasn't advanced far enough to make any offset + * automatically valid. Do full offset validation per sequence. + * Phase 2 (hot): op - dst_base > max_valid_off, so any non-zero + * offset within 2/3 bytes is automatically valid — skip the + * (op - dst_base) comparison, keep only offset==0 check. */ + /* Phase 1: warmup — full validation */ + while (VV_LIKELY(ip < ip_safe && op < op_safe + && (uint32_t)(op - dst_base) <= max_valid_off)) { uint32_t token = *ip++; uint32_t ll = token >> 4; uint32_t mc = token & 0x0F; - /* Extended literal length → cold path */ - if (__builtin_expect(ll == 15, 0)) + if (VV_UNLIKELY(ll == 15)) ll += (uint32_t)read_ext_len(&ip, ip_end); - /* ── Early offset load + prefetch ── - * The offset is at ip+ll (after the literal bytes). - * Only do this for small litlen where we know ip+ll+2 is in the safe zone. - * The safe-zone margin (24) guarantees: token(1) + lits(≤14) + offset(2) + - * match_ext(≤6) + margin ≤ 24. */ - if (__builtin_expect(ll <= 14 && ip + ll + 2 <= ip_end, 1)) { + if (VV_LIKELY(ll <= 14 && ip + ll + 2 <= ip_end)) { uint16_t off_raw; - __builtin_memcpy(&off_raw, ip + ll, 2); - if (off_raw != 0 && off_raw <= (uint32_t)(op + ll - op_start)) - __builtin_prefetch(op + ll - off_raw, 0, 1); + memcpy(&off_raw, ip + ll, 2); + if (off_raw > 0) + VV_PREFETCH(op + ll - off_raw); } - /* ── Literal copy (EXACT — no wild over-copy) ── - * Wild-copy writes garbage past op+ll that corrupts positions - * referenced by future matches. Must use exact-length copies. - * memcpy compiles to optimal SIMD for small constant-like sizes. */ if (ll > 0) - __builtin_memcpy(op, ip, ll); + memcpy(op, ip, ll); ip += ll; op += ll; - /* ── End of block ── */ - if (__builtin_expect(ip >= ip_end, 0)) break; + if (VV_UNLIKELY(ip >= ip_end)) break; - /* ── Offset ── */ - uint32_t offset = (off_bytes == 3) ? ((uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16)) : vv_read16(ip); + uint32_t offset; + if (off_bytes == 2) { + offset = vv_read16(ip); + } else { + offset = (uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16); + } ip += off_bytes; - /* ── Match length ── */ uint32_t mlen = mc + VV_MIN_MATCH; - if (__builtin_expect(mc == 15, 0)) + if (VV_UNLIKELY(mc == 15)) mlen += (uint32_t)read_ext_len(&ip, ip_end); - /* ── Validate offset ── */ - if (__builtin_expect(offset == 0 || offset > (uint32_t)(op - dst_base), 0)) + if (VV_UNLIKELY(offset == 0 || offset > (uint32_t)(op - dst_base))) return VV_ERR_CORRUPT; - /* ── Match copy (inline AVX2, tiered by offset) ── */ - if (__builtin_expect(offset >= 32, 1)) { + if (VV_LIKELY(offset >= 32)) { match_copy_32(op, op - offset, mlen); } else if (offset >= 16) { match_copy_16(op, op - offset, mlen); @@ -196,19 +182,71 @@ static vv_error_t decode_block_tokens( } op += mlen; } -#endif /* VV_INLINE_AVX2 */ - /* ═══ GENERAL PATH (tail + non-AVX2) ═══ */ + /* Phase 2: hot path — op is far enough in that any non-zero offset + * within 2-byte or 3-byte range is automatically valid. */ + while (VV_LIKELY(ip < ip_safe && op < op_safe)) { + uint32_t token = *ip++; + uint32_t ll = token >> 4; + uint32_t mc = token & 0x0F; + + if (VV_UNLIKELY(ll == 15)) + ll += (uint32_t)read_ext_len(&ip, ip_end); + + if (VV_LIKELY(ll <= 14 && ip + ll + 2 <= ip_end)) { + uint16_t off_raw; + memcpy(&off_raw, ip + ll, 2); + if (off_raw > 0) + VV_PREFETCH(op + ll - off_raw); + } + + if (ll > 0) + memcpy(op, ip, ll); + ip += ll; + op += ll; + + if (VV_UNLIKELY(ip >= ip_end)) break; + + uint32_t offset; + if (off_bytes == 2) { + offset = vv_read16(ip); + } else { + offset = (uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16); + } + ip += off_bytes; + + uint32_t mlen = mc + VV_MIN_MATCH; + if (VV_UNLIKELY(mc == 15)) + mlen += (uint32_t)read_ext_len(&ip, ip_end); + + /* No (op - dst_base) check needed — op is past max_valid_off */ + if (VV_UNLIKELY(offset == 0)) + return VV_ERR_CORRUPT; + + if (VV_LIKELY(offset >= 32)) { + match_copy_32(op, op - offset, mlen); + } else if (offset >= 16) { + match_copy_16(op, op - offset, mlen); + } else if (offset >= 8) { + match_copy_8(op, offset, mlen); + } else { + match_overlap(op, offset, mlen); + } + op += mlen; + } +#endif + + /* General path (tail + non-AVX2) */ while (ip < ip_end) { uint8_t token = *ip++; size_t ll = token >> 4; size_t mc = token & 0x0F; - if (__builtin_expect(ll == 15, 0)) + if (VV_UNLIKELY(ll == 15)) ll += read_ext_len(&ip, ip_end); - if (__builtin_expect(ip + ll > ip_end, 0)) return VV_ERR_CORRUPT; - if (__builtin_expect(op + ll > op_end, 0)) return VV_ERR_OVERFLOW; + if (VV_UNLIKELY(ip + ll > ip_end)) return VV_ERR_CORRUPT; + if (VV_UNLIKELY(op + ll > op_end)) return VV_ERR_OVERFLOW; if (ll > 0) vv_copy_fast(op, ip, ll); ip += ll; @@ -216,17 +254,22 @@ static vv_error_t decode_block_tokens( if (ip >= ip_end) break; - if (__builtin_expect(ip + off_bytes > ip_end, 0)) return VV_ERR_CORRUPT; - uint32_t offset = (off_bytes == 3) ? ((uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16)) : vv_read16(ip); + if (VV_UNLIKELY(ip + off_bytes > ip_end)) return VV_ERR_CORRUPT; + uint32_t offset; + if (off_bytes == 2) { + offset = vv_read16(ip); + } else { + offset = (uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16); + } ip += off_bytes; size_t mlen = mc + VV_MIN_MATCH; - if (__builtin_expect(mc == 15, 0)) + if (VV_UNLIKELY(mc == 15)) mlen += read_ext_len(&ip, ip_end); - if (__builtin_expect(offset == 0 || offset > (uint32_t)(op - dst_base), 0)) + if (VV_UNLIKELY(offset == 0 || offset > (uint32_t)(op - dst_base))) return VV_ERR_CORRUPT; - if (__builtin_expect(op + mlen > op_end, 0)) + if (VV_UNLIKELY(op + mlen > op_end)) return VV_ERR_OVERFLOW; vv_copy_match(op, offset, mlen); @@ -237,6 +280,34 @@ static vv_error_t decode_block_tokens( return VV_OK; } +/* Specialized for 2-byte offsets (wlog ≤ 16) — the common fast path */ +static vv_error_t decode_block_tokens_w16( + const uint8_t *ip, size_t ip_len, + uint8_t *op, size_t dst_cap, size_t *out_len, + const uint8_t *dst_base) +{ + return decode_block_tokens_impl(ip, ip_len, op, dst_cap, out_len, dst_base, 2); +} + +/* Specialized for 3-byte offsets (wlog > 16) */ +static vv_error_t decode_block_tokens_w20( + const uint8_t *ip, size_t ip_len, + uint8_t *op, size_t dst_cap, size_t *out_len, + const uint8_t *dst_base) +{ + return decode_block_tokens_impl(ip, ip_len, op, dst_cap, out_len, dst_base, 3); +} + +static vv_error_t decode_block_tokens( + const uint8_t *ip, size_t ip_len, + uint8_t *op, size_t dst_cap, size_t *out_len, int off_bytes, + const uint8_t *dst_base) +{ + if (off_bytes == 2) + return decode_block_tokens_w16(ip, ip_len, op, dst_cap, out_len, dst_base); + return decode_block_tokens_w20(ip, ip_len, op, dst_cap, out_len, dst_base); +} + /* ═══════════════════════════════════════════════════════════════ * DECODE STRIPPED TOKEN STREAM (for type 3 / Huffman blocks) * @@ -245,11 +316,14 @@ static vv_error_t decode_block_tokens( * Token format: same headers/offsets/extensions, just no literal bytes. * ═══════════════════════════════════════════════════════════════ */ -static vv_error_t decode_stripped_tokens( - const uint8_t *ip, size_t ip_len, /* Stripped token stream */ - const uint8_t *lit_buf, size_t lit_len, /* Pre-decoded literals */ - uint8_t *op, size_t dst_cap, size_t *out_len, int off_bytes, - const uint8_t *dst_base) +/* PERF: force-inline body so off_bytes becomes a compile-time constant */ +static __attribute__((always_inline)) inline vv_error_t +decode_stripped_tokens_impl( + const uint8_t *ip, size_t ip_len, + const uint8_t *lit_buf, size_t lit_len, + uint8_t *op, size_t dst_cap, size_t *out_len, + const uint8_t *dst_base, + const int off_bytes) { const uint8_t *ip_end = ip + ip_len; uint8_t *op_start = op; @@ -261,40 +335,39 @@ static vv_error_t decode_stripped_tokens( size_t ll = token >> 4; size_t mc = token & 0x0F; - /* Extended literal length */ - if (__builtin_expect(ll == 15, 0)) + if (VV_UNLIKELY(ll == 15)) ll += read_ext_len(&ip, ip_end); - /* Copy literals from pre-decoded buffer */ - if (__builtin_expect(lit_pos + ll > lit_len, 0)) return VV_ERR_CORRUPT; - if (__builtin_expect(op + ll > op_end, 0)) return VV_ERR_OVERFLOW; + if (VV_UNLIKELY(lit_pos + ll > lit_len)) return VV_ERR_CORRUPT; + if (VV_UNLIKELY(op + ll > op_end)) return VV_ERR_OVERFLOW; if (ll > 0) { memcpy(op, lit_buf + lit_pos, ll); lit_pos += ll; } op += ll; - /* End of block: last sequence has no match */ if (ip >= ip_end) break; - /* Offset */ - if (__builtin_expect(ip + off_bytes > ip_end, 0)) return VV_ERR_CORRUPT; - uint32_t offset = (off_bytes == 3) ? ((uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16)) : vv_read16(ip); + if (VV_UNLIKELY(ip + off_bytes > ip_end)) return VV_ERR_CORRUPT; + /* PERF: off_bytes is compile-time constant here */ + uint32_t offset; + if (off_bytes == 2) { + offset = vv_read16(ip); + } else { + offset = (uint32_t)ip[0] | ((uint32_t)ip[1]<<8) | ((uint32_t)ip[2]<<16); + } ip += off_bytes; - /* Match length */ size_t mlen = mc + VV_MIN_MATCH; - if (__builtin_expect(mc == 15, 0)) + if (VV_UNLIKELY(mc == 15)) mlen += read_ext_len(&ip, ip_end); - /* Validate */ - if (__builtin_expect(offset == 0 || offset > (uint32_t)(op - dst_base), 0)) { + if (VV_UNLIKELY(offset == 0 || offset > (uint32_t)(op - dst_base))) { return VV_ERR_CORRUPT; } - if (__builtin_expect(op + mlen > op_end, 0)) + if (VV_UNLIKELY(op + mlen > op_end)) return VV_ERR_OVERFLOW; - /* Match copy */ vv_copy_match(op, offset, mlen); op += mlen; } @@ -303,6 +376,20 @@ static vv_error_t decode_stripped_tokens( return VV_OK; } +static vv_error_t decode_stripped_tokens( + const uint8_t *ip, size_t ip_len, + const uint8_t *lit_buf, size_t lit_len, + uint8_t *op, size_t dst_cap, size_t *out_len, int off_bytes, + const uint8_t *dst_base) +{ + if (off_bytes == 2) { + return decode_stripped_tokens_impl(ip, ip_len, lit_buf, lit_len, + op, dst_cap, out_len, dst_base, 2); + } + return decode_stripped_tokens_impl(ip, ip_len, lit_buf, lit_len, + op, dst_cap, out_len, dst_base, 3); +} + /* ═══════════════════════════════════════════════════════════════ * DECODE TYPE 3 BLOCK (Huffman-compressed literals) * @@ -454,99 +541,376 @@ static vv_error_t decode_block_ctx( int64_t vv_decompress(const uint8_t *src, size_t src_len, uint8_t *dst, size_t dst_cap) { + return vv_decompress_flags(src, src_len, dst, dst_cap, VV_DECOMPRESS_DEFAULT); +} + +int64_t vv_decompress_flags(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + uint32_t flags) { if (!src || !dst) return VV_ERR_PARAM; if (src_len < sizeof(vv_frame_header_t)) return VV_ERR_CORRUPT; const uint8_t *ip = src; const uint8_t *ip_end = src + src_len; - - vv_frame_header_t fh; - memcpy(&fh, ip, sizeof(fh)); - ip += sizeof(fh); - - if (fh.magic != VV_MAGIC) return VV_ERR_BAD_MAGIC; - if (fh.version != 1) return VV_ERR_CORRUPT; - - int has_checksum = (fh.flags & 1); - int off_bytes = (fh.window_log > 16) ? 3 : 2; uint8_t *op = dst; + uint8_t *op_end = dst + dst_cap; - for (;;) { - if (ip + 4 > ip_end) return VV_ERR_CORRUPT; - uint32_t bh_packed; - memcpy(&bh_packed, ip, 4); ip += 4; + /* MULTI-FRAME: a .vv file may contain one or more concatenated frames + * (useful for parallel encode, Zupt-style archives, append-mode + * writes). We decode frames in a loop until input is exhausted. */ + while (ip < ip_end) { + if (ip + sizeof(vv_frame_header_t) > ip_end) return VV_ERR_CORRUPT; - vv_block_type_t btype = vv_bh_type(bh_packed); - int is_last = vv_bh_last(bh_packed); - uint32_t dsz = vv_bh_size(bh_packed); + vv_frame_header_t fh; + memcpy(&fh, ip, sizeof(fh)); + ip += sizeof(fh); - if (dsz > VV_MAX_BLOCK_SIZE) return VV_ERR_OVERFLOW; - if ((size_t)(op - dst) + dsz > dst_cap) return VV_ERR_OVERFLOW; + if (fh.magic != VV_MAGIC) return VV_ERR_BAD_MAGIC; + if (fh.version != 1) return VV_ERR_CORRUPT; - if (btype == VV_BLOCK_RAW) { - if (ip + dsz > ip_end) return VV_ERR_CORRUPT; - memcpy(op, ip, dsz); ip += dsz; op += dsz; - } else if (btype == VV_BLOCK_RLE) { - if (ip >= ip_end) return VV_ERR_CORRUPT; - memset(op, *ip++, dsz); op += dsz; - } else if (btype == VV_BLOCK_COMPRESSED) { - if (ip + 3 > ip_end) return VV_ERR_CORRUPT; - uint32_t csz = (uint32_t)ip[0] | ((uint32_t)ip[1] << 8) | ((uint32_t)ip[2] << 16); - ip += 3; - if (ip + csz > ip_end) return VV_ERR_CORRUPT; + int has_checksum = (fh.flags & 1); + int off_bytes = (fh.window_log > 16) ? 3 : 2; - size_t actual = 0; - vv_error_t err = decode_block_tokens(ip, csz, op, dsz, &actual, off_bytes, dst); - if (err != VV_OK) return err; - if (actual != dsz) return VV_ERR_CORRUPT; - ip += csz; op += dsz; - } else if (btype == VV_BLOCK_ENTROPY) { - /* Type 3: Entropy-coded literals + stripped LZ tokens - * First byte after comp_size is the entropy tag: - * VV_ENTROPY_ANS ('A') or VV_ENTROPY_HUFFMAN ('H') */ - if (ip + 3 > ip_end) return VV_ERR_CORRUPT; - uint32_t csz = (uint32_t)ip[0] | ((uint32_t)ip[1] << 8) | ((uint32_t)ip[2] << 16); - ip += 3; - if (csz < 1 || ip + csz > ip_end) return VV_ERR_CORRUPT; + /* Per-frame dst_base: matches must resolve only within this frame. + * Multi-frame files mean frame 2's matches don't reach into + * frame 1's output — each frame is independently decodable. */ + uint8_t *frame_out_start = op; - uint8_t tag = ip[0]; - const uint8_t *bdata = ip + 1; - size_t bdata_len = csz - 1; - size_t actual = 0; - vv_error_t err; + for (;;) { + if (ip + 4 > ip_end) return VV_ERR_CORRUPT; + uint32_t bh_packed; + memcpy(&bh_packed, ip, 4); ip += 4; - if (tag == VV_ENTROPY_ANS) { - err = decode_block_ans(bdata, bdata_len, op, dsz, &actual, off_bytes, dst); - } else if (tag == VV_ENTROPY_ANS4) { - err = decode_block_ans4(bdata, bdata_len, op, dsz, &actual, off_bytes, dst); - } else if (tag == VV_ENTROPY_CTX) { - err = decode_block_ctx(bdata, bdata_len, op, dsz, &actual, off_bytes, dst); - } else if (tag == VV_ENTROPY_SEQ) { - /* Sequence coding: ANS on literals + ML + OF */ - err = vva_decode_sequences(bdata, bdata_len, op, dsz, &actual, dst); - if (err != VV_OK) err = VV_ERR_CORRUPT; - } else if (tag == VV_ENTROPY_HUFFMAN) { - err = decode_block_huffman(bdata, bdata_len, op, dsz, &actual, off_bytes, dst); + vv_block_type_t btype = vv_bh_type(bh_packed); + int is_last = vv_bh_last(bh_packed); + uint32_t dsz = vv_bh_size(bh_packed); + + if (dsz > VV_MAX_BLOCK_SIZE) return VV_ERR_OVERFLOW; + if ((size_t)(op - dst) + dsz > dst_cap) return VV_ERR_OVERFLOW; + (void)op_end; + + if (btype == VV_BLOCK_RAW) { + if (ip + dsz > ip_end) return VV_ERR_CORRUPT; + memcpy(op, ip, dsz); ip += dsz; op += dsz; + } else if (btype == VV_BLOCK_RLE) { + if (ip >= ip_end) return VV_ERR_CORRUPT; + memset(op, *ip++, dsz); op += dsz; + } else if (btype == VV_BLOCK_COMPRESSED) { + if (ip + 3 > ip_end) return VV_ERR_CORRUPT; + uint32_t csz = (uint32_t)ip[0] | ((uint32_t)ip[1] << 8) | ((uint32_t)ip[2] << 16); + ip += 3; + if (ip + csz > ip_end) return VV_ERR_CORRUPT; + + size_t actual = 0; + vv_error_t err = decode_block_tokens(ip, csz, op, dsz, &actual, off_bytes, frame_out_start); + if (err != VV_OK) return err; + if (actual != dsz) return VV_ERR_CORRUPT; + ip += csz; op += dsz; + } else if (btype == VV_BLOCK_ENTROPY) { + if (ip + 3 > ip_end) return VV_ERR_CORRUPT; + uint32_t csz = (uint32_t)ip[0] | ((uint32_t)ip[1] << 8) | ((uint32_t)ip[2] << 16); + ip += 3; + if (csz < 1 || ip + csz > ip_end) return VV_ERR_CORRUPT; + + uint8_t tag = ip[0]; + const uint8_t *bdata = ip + 1; + size_t bdata_len = csz - 1; + size_t actual = 0; + vv_error_t err; + + if (tag == VV_ENTROPY_ANS) { + err = decode_block_ans(bdata, bdata_len, op, dsz, &actual, off_bytes, frame_out_start); + } else if (tag == VV_ENTROPY_ANS4) { + err = decode_block_ans4(bdata, bdata_len, op, dsz, &actual, off_bytes, frame_out_start); + } else if (tag == VV_ENTROPY_CTX) { + err = decode_block_ctx(bdata, bdata_len, op, dsz, &actual, off_bytes, frame_out_start); + } else if (tag == VV_ENTROPY_SEQ) { + err = vva_decode_sequences(bdata, bdata_len, op, dsz, &actual, frame_out_start); + if (err != VV_OK) err = VV_ERR_CORRUPT; + } else if (tag == VV_ENTROPY_SEQ_V2) { + /* 'T' tag: sequence coding with min_match=3. Wire + * payload identical to 'S', only ml_base differs. */ + err = vva_decode_sequences_v2(bdata, bdata_len, op, dsz, &actual, frame_out_start); + if (err != VV_OK) err = VV_ERR_CORRUPT; + } else if (tag == VV_ENTROPY_HUFFMAN) { + err = decode_block_huffman(bdata, bdata_len, op, dsz, &actual, off_bytes, frame_out_start); + } else { + return VV_ERR_CORRUPT; + } + if (err != VV_OK) return err; + if (actual != dsz) return VV_ERR_CORRUPT; + ip += csz; op += dsz; } else { return VV_ERR_CORRUPT; } - if (err != VV_OK) return err; - if (actual != dsz) return VV_ERR_CORRUPT; - ip += csz; op += dsz; - } else { - return VV_ERR_CORRUPT; + if (is_last) break; } - if (is_last) break; - } - if (has_checksum) { - if (ip + sizeof(vv_frame_footer_t) > ip_end) return VV_ERR_CORRUPT; - vv_frame_footer_t ff; - memcpy(&ff, ip, sizeof(ff)); - if (ff.footer_magic != 0x56564E44u) return VV_ERR_CORRUPT; - uint64_t computed = vv_xxh64(dst, (size_t)(op - dst), 0); - if (computed != ff.checksum) return VV_ERR_CORRUPT; + if (has_checksum) { + if (ip + sizeof(vv_frame_footer_t) > ip_end) return VV_ERR_CORRUPT; + vv_frame_footer_t ff; + memcpy(&ff, ip, sizeof(ff)); + if (ff.footer_magic != 0x56564E44u) return VV_ERR_CORRUPT; + /* PERF: caller may skip XXH64 when another layer (e.g. AES-GCM) + * already verifies integrity. Still validate footer magic above + * to catch truncation. */ + if (!(flags & VV_DECOMPRESS_SKIP_CHECKSUM)) { + uint64_t computed = vv_xxh64(frame_out_start, (size_t)(op - frame_out_start), 0); + if (computed != ff.checksum) return VV_ERR_CORRUPT; + } + ip += sizeof(vv_frame_footer_t); + } + + /* Loop back to try another frame (if input remains) */ } return (int64_t)(op - dst); } + +/* ═══════════════════════════════════════════════════════════════ + * STREAMING DECOMPRESSION + * + * Incoming compressed bytes arrive in arbitrary chunks. Structure: + * 1. Frame header (16 bytes) — must be accumulated before any + * blocks can be decoded + * 2. Zero or more blocks, each: [4B header][3B csz][payload] + * 3. Optional frame footer (16 bytes) — checksum validation + * + * Strategy: buffer incoming bytes in an internal growing buffer, + * parse as much as we can at each call, and emit decoded output. + * + * For correct match decoding across blocks, we emit directly into + * the caller's dst buffer and preserve dst_base so that sequences + * referencing earlier decoded bytes resolve correctly. The caller + * is responsible for providing a large enough dst buffer: the same + * constraint as one-shot decompression. + * ═══════════════════════════════════════════════════════════════ */ + +typedef enum { + VV_DSTREAM_HEADER, + VV_DSTREAM_BLOCK, + VV_DSTREAM_FOOTER, + VV_DSTREAM_DONE, + VV_DSTREAM_ERROR +} vv_dstream_state_t; + +struct vv_dstream_s { + vv_dstream_state_t state; + vv_frame_header_t fh; + int has_checksum; + int off_bytes; + + /* Input-side buffer for incomplete blocks/headers */ + uint8_t *in_buf; + size_t in_cap; + size_t in_len; + + /* Output position tracking (for checksum and bookkeeping) */ + size_t output_pos; + uint8_t *dst_base_saved; /* Preserved across calls; matches caller dst */ + + /* Streaming checksum of decoded output */ + vv_xxh64_state_t cks; +}; + +vv_dstream_t *vv_dstream_create(void) { + vv_dstream_t *ctx = (vv_dstream_t *)calloc(1, sizeof(vv_dstream_t)); + if (!ctx) return NULL; + ctx->state = VV_DSTREAM_HEADER; + ctx->in_cap = 65536; + ctx->in_buf = (uint8_t *)malloc(ctx->in_cap); + if (!ctx->in_buf) { free(ctx); return NULL; } + vv_xxh64_init(&ctx->cks, 0); + return ctx; +} + +void vv_dstream_destroy(vv_dstream_t *ctx) { + if (!ctx) return; + free(ctx->in_buf); + free(ctx); +} + +int vv_dstream_reset(vv_dstream_t *ctx) { + if (!ctx) return VV_ERR_PARAM; + /* Keep in_buf and in_cap (reuse scratch); clear everything else */ + ctx->state = VV_DSTREAM_HEADER; + ctx->has_checksum = 0; + ctx->off_bytes = 0; + ctx->in_len = 0; + ctx->output_pos = 0; + ctx->dst_base_saved = NULL; + memset(&ctx->fh, 0, sizeof(ctx->fh)); + vv_xxh64_init(&ctx->cks, 0); + return VV_OK; +} + +/* Grow in_buf to at least need bytes */ +static int dstream_reserve(vv_dstream_t *ctx, size_t need) { + if (need <= ctx->in_cap) return 0; + size_t new_cap = ctx->in_cap; + while (new_cap < need) new_cap *= 2; + uint8_t *new_buf = (uint8_t *)realloc(ctx->in_buf, new_cap); + if (!new_buf) return -1; + ctx->in_buf = new_buf; + ctx->in_cap = new_cap; + return 0; +} + +/* Append bytes to input buffer */ +static int dstream_append(vv_dstream_t *ctx, const uint8_t *src, size_t src_len) { + if (dstream_reserve(ctx, ctx->in_len + src_len) != 0) return -1; + memcpy(ctx->in_buf + ctx->in_len, src, src_len); + ctx->in_len += src_len; + return 0; +} + +/* Consume first n bytes from input buffer */ +static void dstream_consume(vv_dstream_t *ctx, size_t n) { + if (n >= ctx->in_len) ctx->in_len = 0; + else { + memmove(ctx->in_buf, ctx->in_buf + n, ctx->in_len - n); + ctx->in_len -= n; + } +} + +int vv_dstream_decompress_chunk(vv_dstream_t *ctx, + const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + size_t *consumed, size_t *written) { + if (!ctx || !dst || !consumed || !written) return VV_ERR_PARAM; + *consumed = 0; + *written = 0; + + if (ctx->state == VV_DSTREAM_ERROR) return VV_ERR_CORRUPT; + if (ctx->state == VV_DSTREAM_DONE) return 1; + + /* Append new input */ + if (src_len > 0) { + if (dstream_append(ctx, src, src_len) != 0) { + ctx->state = VV_DSTREAM_ERROR; + return VV_ERR_NOMEM; + } + *consumed = src_len; + } + + /* Remember dst_base for match resolution across blocks */ + if (!ctx->dst_base_saved) ctx->dst_base_saved = dst; + /* Output position within dst (must match caller's expected write offset) */ + uint8_t *op = dst + ctx->output_pos; + + /* State machine: process as much as we can */ + for (;;) { + if (ctx->state == VV_DSTREAM_HEADER) { + if (ctx->in_len < sizeof(vv_frame_header_t)) { *written = ctx->output_pos; return VV_OK; } + memcpy(&ctx->fh, ctx->in_buf, sizeof(vv_frame_header_t)); + if (ctx->fh.magic != VV_MAGIC) { ctx->state = VV_DSTREAM_ERROR; return VV_ERR_BAD_MAGIC; } + if (ctx->fh.version != 1) { ctx->state = VV_DSTREAM_ERROR; return VV_ERR_CORRUPT; } + ctx->has_checksum = (ctx->fh.flags & 1); + ctx->off_bytes = (ctx->fh.window_log > 16) ? 3 : 2; + dstream_consume(ctx, sizeof(vv_frame_header_t)); + ctx->state = VV_DSTREAM_BLOCK; + } + + if (ctx->state == VV_DSTREAM_BLOCK) { + /* Need at least 4 bytes for block header */ + if (ctx->in_len < 4) { *written = ctx->output_pos; return VV_OK; } + + uint32_t bh_packed; + memcpy(&bh_packed, ctx->in_buf, 4); + vv_block_type_t btype = vv_bh_type(bh_packed); + int is_last = vv_bh_last(bh_packed); + uint32_t dsz = vv_bh_size(bh_packed); + + if (dsz > VV_MAX_BLOCK_SIZE) { ctx->state = VV_DSTREAM_ERROR; return VV_ERR_OVERFLOW; } + if ((size_t)(op - dst) + dsz > dst_cap) { ctx->state = VV_DSTREAM_ERROR; return VV_ERR_OVERFLOW; } + + /* Determine how many bytes this block occupies */ + size_t block_header_sz = 4; + size_t block_data_sz = 0; + + if (btype == VV_BLOCK_RAW) { + block_data_sz = dsz; + } else if (btype == VV_BLOCK_RLE) { + block_data_sz = 1; + } else if (btype == VV_BLOCK_COMPRESSED || btype == VV_BLOCK_ENTROPY) { + if (ctx->in_len < block_header_sz + 3) { *written = ctx->output_pos; return VV_OK; } + const uint8_t *p = ctx->in_buf + block_header_sz; + uint32_t csz = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16); + block_data_sz = 3 + csz; + } else { + ctx->state = VV_DSTREAM_ERROR; return VV_ERR_CORRUPT; + } + + size_t total_block_sz = block_header_sz + block_data_sz; + if (ctx->in_len < total_block_sz) { *written = ctx->output_pos; return VV_OK; } + + /* Decode the block — decoder uses dst_base for match resolution */ + const uint8_t *p = ctx->in_buf + block_header_sz; + if (btype == VV_BLOCK_RAW) { + memcpy(op, p, dsz); + } else if (btype == VV_BLOCK_RLE) { + memset(op, p[0], dsz); + } else if (btype == VV_BLOCK_COMPRESSED) { + uint32_t csz = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16); + size_t actual = 0; + vv_error_t err = decode_block_tokens(p + 3, csz, op, dsz, &actual, + ctx->off_bytes, ctx->dst_base_saved); + if (err != VV_OK || actual != dsz) { ctx->state = VV_DSTREAM_ERROR; return err != VV_OK ? err : VV_ERR_CORRUPT; } + } else { /* ENTROPY */ + uint32_t csz = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16); + uint8_t tag = p[3]; + const uint8_t *bdata = p + 4; + size_t bdata_len = csz - 1; + size_t actual = 0; + vv_error_t err; + if (tag == VV_ENTROPY_ANS) { + err = decode_block_ans(bdata, bdata_len, op, dsz, &actual, ctx->off_bytes, ctx->dst_base_saved); + } else if (tag == VV_ENTROPY_ANS4) { + err = decode_block_ans4(bdata, bdata_len, op, dsz, &actual, ctx->off_bytes, ctx->dst_base_saved); + } else if (tag == VV_ENTROPY_CTX) { + err = decode_block_ctx(bdata, bdata_len, op, dsz, &actual, ctx->off_bytes, ctx->dst_base_saved); + } else if (tag == VV_ENTROPY_SEQ) { + err = vva_decode_sequences(bdata, bdata_len, op, dsz, &actual, ctx->dst_base_saved); + if (err != VV_OK) err = VV_ERR_CORRUPT; + } else if (tag == VV_ENTROPY_SEQ_V2) { + err = vva_decode_sequences_v2(bdata, bdata_len, op, dsz, &actual, ctx->dst_base_saved); + if (err != VV_OK) err = VV_ERR_CORRUPT; + } else if (tag == VV_ENTROPY_HUFFMAN) { + err = decode_block_huffman(bdata, bdata_len, op, dsz, &actual, ctx->off_bytes, ctx->dst_base_saved); + } else { + ctx->state = VV_DSTREAM_ERROR; return VV_ERR_CORRUPT; + } + if (err != VV_OK || actual != dsz) { ctx->state = VV_DSTREAM_ERROR; return err != VV_OK ? err : VV_ERR_CORRUPT; } + } + + /* Update checksum (over decoded output) */ + if (ctx->has_checksum && dsz > 0) { + vv_xxh64_update(&ctx->cks, op, dsz); + } + + op += dsz; + ctx->output_pos += dsz; + dstream_consume(ctx, total_block_sz); + + if (is_last) { + ctx->state = ctx->has_checksum ? VV_DSTREAM_FOOTER : VV_DSTREAM_DONE; + } + } + + if (ctx->state == VV_DSTREAM_FOOTER) { + if (ctx->in_len < sizeof(vv_frame_footer_t)) { *written = ctx->output_pos; return VV_OK; } + vv_frame_footer_t ff; + memcpy(&ff, ctx->in_buf, sizeof(ff)); + if (ff.footer_magic != 0x56564E44u) { ctx->state = VV_DSTREAM_ERROR; return VV_ERR_CORRUPT; } + uint64_t computed = vv_xxh64_finalize(&ctx->cks); + if (computed != ff.checksum) { ctx->state = VV_DSTREAM_ERROR; return VV_ERR_CORRUPT; } + dstream_consume(ctx, sizeof(vv_frame_footer_t)); + ctx->state = VV_DSTREAM_DONE; + } + + if (ctx->state == VV_DSTREAM_DONE) { + *written = ctx->output_pos; + return 1; + } + } +} diff --git a/src/vv_encoder.c b/src/vv_encoder.c index 1070812..b7f284a 100644 --- a/src/vv_encoder.c +++ b/src/vv_encoder.c @@ -1,12 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ -#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) - #define _DEFAULT_SOURCE 1 -#endif - /* * VaptVupt — Encoder v2 (Sprint 1) * @@ -20,6 +11,9 @@ */ #include "vaptvupt.h" + +#include +#include "vv_platform.h" #include "vv_huffman.h" #include "vv_ans.h" #include @@ -52,7 +46,7 @@ static inline size_t write_varint(uint8_t *dst, size_t val) { static inline uint32_t hash5(const uint8_t *p) { uint32_t lo; - __builtin_memcpy(&lo, p, 4); + memcpy(&lo, p, 4); uint64_t v = (uint64_t)lo | ((uint64_t)p[4] << 32); /* Shift by (64 - HC_BITS) to get the top HC_BITS of the product */ return (uint32_t)((v * 889523592379ULL) >> (64 - VV_HC_BITS)); @@ -61,7 +55,7 @@ static inline uint32_t hash5(const uint8_t *p) { /* 4-byte hash for positions near end of buffer */ static inline uint32_t hash4(const uint8_t *p) { uint32_t v; - __builtin_memcpy(&v, p, 4); + memcpy(&v, p, 4); return (v * 2654435761u) >> (32 - VV_HC_BITS); } @@ -86,7 +80,7 @@ static inline int32_t extend_match(const uint8_t *a, const uint8_t *b, __m256i vb = _mm256_loadu_si256((const __m256i *)(b + len)); __m256i eq = _mm256_cmpeq_epi8(va, vb); uint32_t mask = ~(uint32_t)_mm256_movemask_epi8(eq); - if (mask) return len + (int32_t)__builtin_ctz(mask); + if (mask) return len + (int32_t)vv_ctz32(mask); len += 32; } #endif @@ -99,30 +93,135 @@ static inline int32_t extend_match(const uint8_t *a, const uint8_t *b, * ═══════════════════════════════════════════════════════════════ */ +#define VV_HC4_BITS 16 +#define VV_HC4_SIZE (1u << VV_HC4_BITS) + +static inline uint32_t hash4_short(const uint8_t *p) { + uint32_t v; + memcpy(&v, p, 4); + return (v * 2654435761u) >> (32 - VV_HC4_BITS); +} + +/* ─── SPRINT 45: Hash3 for format v2 ─────────────────────────── + * When min_match=3 (opts.format_v2), we need to find 3-byte + * matches that hash5/hash4 cannot surface (both require 4-byte + * prefix equality before extending). Hash3 uses a 3-byte key + * with its own table and chain (SEPARATE — never share chain + * arrays across hash tables per Sprint 14's silent-corruption + * lesson). Table size is 14 bits = 16K entries = 64 KB, smaller + * than hash4's 256 KB to account for the lower entropy of + * 3-byte keys. + * + * Enabled only when matcher_t::use_hash3 is set (v2 path). + * When disabled, table3/hash3_chain are NULL, hash3 insert is + * skipped, and hash3 probe never runs. Zero cost on v1 path. */ +#define VV_HC3_BITS 14 +#define VV_HC3_SIZE (1u << VV_HC3_BITS) + +static inline uint32_t hash3_short(const uint8_t *p) { + uint32_t v = (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16); + return (v * 2654435761u) >> (32 - VV_HC3_BITS); +} + typedef struct { - int32_t *table; /* Primary: VV_HC_SIZE entries (hash5) */ - int32_t *chain; /* Chain array: window_size entries */ + int32_t *table; /* Primary hash5: VV_HC_SIZE entries */ + int32_t *chain; /* Primary chain: window_size entries */ + int32_t *table4; /* Secondary hash4: VV_HC4_SIZE entries */ + int32_t *hash4_chain; /* Secondary chain (SEPARATE from primary) */ + int32_t *table3; /* Tertiary hash3 (v2 only): VV_HC3_SIZE entries, NULL on v1 */ + int32_t *hash3_chain; /* Tertiary chain (SEPARATE from hash4/primary), NULL on v1 */ uint32_t chain_mask; uint32_t chain_depth; - uint32_t rep[3]; /* 3 most recent match offsets */ - uint8_t wlog; /* Window log: controls max offset distance */ + uint32_t rep[3]; /* 3 most recent match offsets */ + uint8_t wlog; /* Window log: controls max offset distance */ + uint8_t use_hash4; /* Enable hash4 fallback (binary data only) */ + uint8_t use_hash3; /* Enable hash3 fallback (format v2 only) */ + uint32_t max_match; /* Max representable matchlen (65535 for v1, + * 65534 for v2: ml_base_v2[35]=32767 with 15 + * extra bits only reaches 65534). */ } matcher_t; static void matcher_init(matcher_t *m, uint32_t window_log, uint32_t depth) { uint32_t wsz = 1u << window_log; m->table = (int32_t *)malloc(VV_HC_SIZE * sizeof(int32_t)); m->chain = (int32_t *)malloc(wsz * sizeof(int32_t)); + m->table4 = (int32_t *)malloc(VV_HC4_SIZE * sizeof(int32_t)); + m->hash4_chain = (int32_t *)malloc(wsz * sizeof(int32_t)); + /* hash3 tables allocated lazily only when use_hash3 is enabled. + * On v1 path (the default), they stay NULL and cost nothing. */ + m->table3 = NULL; + m->hash3_chain = NULL; + /* PERF: only the table arrays need to be cleared. chain/hash4_chain + * are only read via table entries (which are now -1), so stale + * data in them is unreachable. See matcher_reset for rationale. */ memset(m->table, 0xFF, VV_HC_SIZE * sizeof(int32_t)); - memset(m->chain, 0xFF, wsz * sizeof(int32_t)); + memset(m->table4, 0xFF, VV_HC4_SIZE * sizeof(int32_t)); m->chain_mask = wsz - 1; m->chain_depth = depth; m->rep[0] = m->rep[1] = m->rep[2] = 0; m->wlog = (uint8_t)window_log; + m->use_hash4 = 0; /* Disabled by default — enabled adaptively for binary */ + m->use_hash3 = 0; /* Disabled by default — enabled for format v2 */ + m->max_match = VV_MAX_MATCH; /* v1 default, see matcher_set_format_v2 */ +} + +/* Apply format v2 matcher constraints. Must be called whenever the + * encoder is producing 'T'-tagged blocks, regardless of whether the + * hash3 probe is active (the adaptive trial may decide not to enable + * hash3 on non-binary data, but the ml_base_v2 range cap still + * applies to every match). */ +static void matcher_set_format_v2(matcher_t *m) { + /* ml_base_v2[35]=32767 with 15 extra bits → max representable + * matchlen = 32767+32767 = 65534. Without this cap, the matcher + * can produce 65535-length matches whose extra field (32768) + * overflows 15 bits → encodes as 0 → decoder reconstructs 32767 + * (short by exactly 32,768 bytes per affected match). Root cause + * of the python3 multi-block corruption in v2.34.0. */ + m->max_match = 65534; +} + +/* Enable hash3 path: allocate tables. Idempotent and cheap to skip. */ +static int matcher_enable_hash3(matcher_t *m) { + if (m->table3) return 1; /* Already enabled */ + uint32_t wsz = 1u << m->wlog; + m->table3 = (int32_t *)malloc(VV_HC3_SIZE * sizeof(int32_t)); + m->hash3_chain = (int32_t *)malloc(wsz * sizeof(int32_t)); + if (!m->table3 || !m->hash3_chain) { + free(m->table3); m->table3 = NULL; + free(m->hash3_chain); m->hash3_chain = NULL; + return 0; + } + memset(m->table3, 0xFF, VV_HC3_SIZE * sizeof(int32_t)); + m->use_hash3 = 1; + return 1; } static void matcher_free(matcher_t *m) { free(m->table); m->table = NULL; free(m->chain); m->chain = NULL; + free(m->table4); m->table4 = NULL; + free(m->hash4_chain); m->hash4_chain = NULL; + free(m->table3); m->table3 = NULL; + free(m->hash3_chain); m->hash3_chain = NULL; +} + +/* Reset matcher state without reallocating tables. Used by + * vv_cstream_reset() for fast per-file reuse. + * + * PERF: We only need to clear the `table` and `table4` arrays (the + * hash → position maps). The `chain` arrays store (pos → earlier + * pos) links, but those links are only FOLLOWED from table entries. + * After resetting the tables, any stale chain entries become + * unreachable. This cuts reset cost from ~1.6 MB of memset to + * ~1.25 MB (table=1MB + table4=256KB), a ~25% speedup. */ +static void matcher_reset(matcher_t *m) { + memset(m->table, 0xFF, VV_HC_SIZE * sizeof(int32_t)); + memset(m->table4, 0xFF, VV_HC4_SIZE * sizeof(int32_t)); + if (m->table3) memset(m->table3, 0xFF, VV_HC3_SIZE * sizeof(int32_t)); + m->rep[0] = m->rep[1] = m->rep[2] = 0; + m->use_hash4 = 0; + /* use_hash3 is NOT reset — it's a caller-opted-in mode flag, + * not adaptive behavior that should clear on reset. */ } static inline void matcher_insert(matcher_t *m, const uint8_t *data, @@ -131,34 +230,77 @@ static inline void matcher_insert(matcher_t *m, const uint8_t *data, uint32_t h = hash_safe(data + pos, end - pos); m->chain[pos & m->chain_mask] = m->table[h]; m->table[h] = pos; + /* PERF: only maintain hash4 table when it's actually being used. + * For text/source (use_hash4==0), this saves a hash computation + * and two memory writes per insert — measurable on insert-heavy + * workloads (logs, JSON). */ + if (m->use_hash4) { + uint32_t h4 = hash4_short(data + pos); + m->hash4_chain[pos & m->chain_mask] = m->table4[h4]; + m->table4[h4] = pos; + } + /* SPRINT 45: hash3 insert, only when enabled. Same guard logic + * as hash4 — zero cost when disabled. */ + if (m->use_hash3) { + uint32_t h3 = hash3_short(data + pos); + m->hash3_chain[pos & m->chain_mask] = m->table3[h3]; + m->table3[h3] = pos; + } } /* ─── Rep-match check: O(1), checked BEFORE hash probe ─── */ static inline int32_t try_rep_match(const matcher_t *m, const uint8_t *data, int32_t pos, int32_t end, int32_t *rep_idx) { + /* Primary path: require 4-byte equality. Extends from there. */ for (int i = 0; i < 3; i++) { uint32_t d = m->rep[i]; if (d == 0 || (uint32_t)pos < d) continue; int32_t ref = pos - (int32_t)d; uint32_t a, b; - __builtin_memcpy(&a, data + pos, 4); - __builtin_memcpy(&b, data + ref, 4); + memcpy(&a, data + pos, 4); + memcpy(&b, data + ref, 4); if (a == b) { int32_t max = end - pos; - if (max > VV_MAX_MATCH) max = VV_MAX_MATCH; + if (max > (int32_t)m->max_match) max = (int32_t)m->max_match; int32_t len = 4 + extend_match(data + pos + 4, data + ref + 4, max - 4); *rep_idx = i; return len; } } + /* SPRINT 47: format v2 secondary path. When hash3 is active, check + * if any rep-offset produces a 3-byte rep-match even when the + * 4-byte compare above fails. Rep-matches have 0 extra offset + * bits — a 3-byte rep is nearly always a win vs 3 literals, which + * the 4-byte requirement was blocking. Only runs when use_hash3 + * is on (i.e. format v2 + binary-detected), so text/JSON paths + * stay bit-identical. */ + if (m->use_hash3 && pos + 3 <= end) { + for (int i = 0; i < 3; i++) { + uint32_t d = m->rep[i]; + if (d == 0 || (uint32_t)pos < d) continue; + int32_t ref = pos - (int32_t)d; + if (data[pos] == data[ref] + && data[pos + 1] == data[ref + 1] + && data[pos + 2] == data[ref + 2]) { + *rep_idx = i; + return 3; /* length-3 rep; caller accepts since min_match=3 */ + } + } + } return 0; } -/* ─── Hash chain match: DUAL HASH (hash5 + hash4) for binary coverage ─── */ -static int32_t chain_match(const matcher_t *m, const uint8_t *data, - int32_t pos, int32_t end, int32_t *best_off) { - if (pos + 4 > end) return 0; +/* ─── Hash chain match: uses 5-byte hash, searches up to chain_depth. + * If use_hash4 is nonzero AND hash5 finds nothing, fall back to hash4 + * chain for binary/struct coverage. ─── */ +static int32_t chain_match_ex(const matcher_t *m, const uint8_t *data, + int32_t pos, int32_t end, int32_t *best_off, + int use_hash4) { + /* Early exit: we need at least 3 bytes for hash3 probe, 4 for + * hash4/hash5. Use the looser bound if hash3 is enabled. */ + int32_t min_bytes = m->use_hash3 ? 3 : 4; + if (pos + min_bytes > end) return 0; int32_t best_len = 0; *best_off = 0; @@ -167,18 +309,42 @@ static int32_t chain_match(const matcher_t *m, const uint8_t *data, int32_t limit = pos - max_dist; if (limit < 0) limit = 0; - /* Primary hash5 chain traversal */ + /* Hash5/hash4 paths require 4 bytes. Skip them if only 3 remain. */ + if (pos + 4 <= end) { + + /* Hoist pos4: never changes during the chain walk */ + uint32_t pos4; + memcpy(&pos4, data + pos, 4); + + /* Primary hash5 chain traversal. + * PERF: prefetch the next chain slot 2 iterations ahead. Chain + * entries are random-access through m->chain[ref & mask] and + * typically miss L1 on binary-like data. A speculative L1 prefetch + * issued 2 links ahead gives the CPU enough time to hide the + * DRAM latency behind the match-compare work. */ uint32_t h = hash_safe(data + pos, end - pos); int32_t ref = m->table[h]; uint32_t depth = m->chain_depth; + /* Seed the pipeline: prefetch the source side of next candidate */ + if (ref >= limit && ref < pos) { + __builtin_prefetch(data + ref, 0, 0); + } + while (ref >= 0 && ref >= limit && ref < pos && depth-- > 0) { - uint32_t a, b; - __builtin_memcpy(&a, data + pos, 4); - __builtin_memcpy(&b, data + ref, 4); - if (a == b) { + int32_t next_ref = m->chain[ref & m->chain_mask]; + /* Prefetch: next chain traversal's candidate data bytes */ + if (next_ref >= limit && next_ref < pos) { + __builtin_prefetch(data + next_ref, 0, 0); + /* Also prefetch the chain entry after next, for 2-ahead cover */ + __builtin_prefetch(&m->chain[next_ref & m->chain_mask], 0, 0); + } + + uint32_t b; + memcpy(&b, data + ref, 4); + if (pos4 == b) { int32_t max = end - pos; - if (max > VV_MAX_MATCH) max = VV_MAX_MATCH; + if (max > (int32_t)m->max_match) max = (int32_t)m->max_match; int32_t len = 4 + extend_match(data + pos + 4, data + ref + 4, max - 4); if (len > best_len) { best_len = len; @@ -186,14 +352,134 @@ static int32_t chain_match(const matcher_t *m, const uint8_t *data, if (len >= 256) return best_len; } } - ref = m->chain[ref & m->chain_mask]; + ref = next_ref; } + /* PERF: Secondary hash4 chain fallback — ONLY when hash5 found nothing + * AND caller indicates hash4 is safe to use (no competing rep-match). + * Uses SEPARATE hash4_chain array. + * + * Note: tried relaxing trigger to `best_len < 8` in sprint 41 but + * empirically found NO improvement on real binary data (bash, ls, + * python3, libc.so.6). The hash4 fallback finds the same matches + * hash5 already finds when primary prefix is 5 bytes. Closing the + * binary-compression gap vs gzip-9 (~11% worse) requires either + * min_match=3 (format change) or deeper LZ-optimal parsing. Kept + * the zero-only trigger which matches lz4's fallback pattern. */ + if (use_hash4 && best_len == 0) { + uint32_t h4 = hash4_short(data + pos); + int32_t ref4 = m->table4[h4]; + uint32_t depth4 = 8; + if (ref4 >= limit && ref4 < pos) { + __builtin_prefetch(data + ref4, 0, 0); + } + + while (ref4 >= 0 && ref4 >= limit && ref4 < pos && depth4-- > 0) { + int32_t next_ref4 = m->hash4_chain[ref4 & m->chain_mask]; + if (next_ref4 >= limit && next_ref4 < pos) { + __builtin_prefetch(data + next_ref4, 0, 0); + __builtin_prefetch(&m->hash4_chain[next_ref4 & m->chain_mask], 0, 0); + } + + uint32_t b4; + memcpy(&b4, data + ref4, 4); + if (pos4 == b4) { + int32_t max = end - pos; + if (max > (int32_t)m->max_match) max = (int32_t)m->max_match; + int32_t len = 4 + extend_match(data + pos + 4, data + ref4 + 4, max - 4); + if (len > best_len) { + best_len = len; + *best_off = pos - ref4; + if (len >= 256) return best_len; + } + } + ref4 = next_ref4; + } + } + + } /* end if (pos + 4 <= end) */ + + /* ─── SPRINT 45: Tertiary hash3 probe ─────────────────────── + * Only when use_hash3 is enabled (format v2) AND hash5/hash4 + * found nothing ≥ 4 bytes (best_len < 4). Uses a SEPARATE + * chain array from hash4 — never share chain storage. + * + * Probe depth is intentionally small (4). Unlike hash4, hash3's + * collision rate is high (16K entries for up to 16M unique + * 3-byte keys), so deep walks waste cycles on spurious hits. + * + * Match length is reported honestly — may be 3, or may extend. + * The caller (compress_block) accepts len ≥ min_match. */ + if (m->use_hash3 && best_len < 4 && pos + 3 <= end) { + uint32_t h3 = hash3_short(data + pos); + int32_t ref3 = m->table3[h3]; + uint32_t depth3 = 4; + + /* Compare key: the 3 bytes at pos. Pack into low 24 bits + * of a uint32 for a single compare against the candidate. */ + uint32_t pos3 = (uint32_t)data[pos] + | ((uint32_t)data[pos + 1] << 8) + | ((uint32_t)data[pos + 2] << 16); + + while (ref3 >= 0 && ref3 >= limit && ref3 < pos && depth3-- > 0) { + int32_t next_ref3 = m->hash3_chain[ref3 & m->chain_mask]; + + uint32_t b3 = (uint32_t)data[ref3] + | ((uint32_t)data[ref3 + 1] << 8) + | ((uint32_t)data[ref3 + 2] << 16); + if (pos3 == b3) { + int32_t max = end - pos; + if (max > (int32_t)m->max_match) max = (int32_t)m->max_match; + int32_t len = 3 + extend_match(data + pos + 3, data + ref3 + 3, max - 3); + /* SPRINT 48: extended offset filter for length-3 hash3 + * matches. Flat ≤4096 threshold. Higher than v2.35.0's + * ≤256 — the v2.36.0 adaptive hash3 gate now keeps + * text/JSON fully neutral at ANY threshold, so the + * filter only governs binary precision. + * + * Measured threshold sweep (binary Δ vs V1): + * 256 (v2.37): bash -2.2% ls -3.3% libc -2.8% py -5.5% + * 1024: bash -3.4% ls -3.3% libc -3.3% py -6.1% + * 4096 (v2.38): bash -3.8% ls -3.9% libc -3.9% py -6.4% + * 8192+: plateau (noise-level changes) + * + * Text/JSON/source at 0/0/+0.1% across the entire + * sweep — the adaptive gate does its job. + * + * Rejected designs: + * - Sliding threshold (≤128 always, ≤512 if rep): + * tightening to 128 lost more binary gain than + * rep-aware loosening recovered. See CHANGELOG. + * - ANS_LOG 12→10 (Sprint A candidate): predicted + * 2-4× text decode; measured 2-6%. Not worth the + * format change. See CHANGELOG v2.38.0 dead-ends. + * + * Longer matches (len ≥ 4) are always accepted at any + * offset (the filter only gates len==3). */ + int32_t off3 = pos - ref3; + if (len == 3 && off3 > 4096) { + ref3 = next_ref3; + continue; + } + if (len > best_len) { + best_len = len; + *best_off = off3; + if (len >= 8) break; /* Good enough — don't keep walking */ + } + } + ref3 = next_ref3; + } + } return best_len; } +static int32_t chain_match(const matcher_t *m, const uint8_t *data, + int32_t pos, int32_t end, int32_t *best_off) { + return chain_match_ex(m, data, pos, end, best_off, m->use_hash4); +} + /* Update rep offsets (push new offset, shift others down) */ static inline void update_rep(matcher_t *m, uint32_t offset) { if (offset == m->rep[0]) return; @@ -207,13 +493,14 @@ static inline void update_rep(matcher_t *m, uint32_t offset) { * ═══════════════════════════════════════════════════════════════ */ static size_t emit_seq(uint8_t *dst, const uint8_t *lits, - size_t ll, size_t ml, uint32_t off, int off_bytes) { + size_t ll, size_t ml, uint32_t off, int off_bytes, + int min_match) { uint8_t *op = dst; uint8_t ll_f = (ll >= 15) ? 15 : (uint8_t)ll; uint8_t ml_f; if (ml == 0) { ml_f = 0; } - else { size_t v = ml - VV_MIN_MATCH; ml_f = (v >= 15) ? 15 : (uint8_t)v; } + else { size_t v = ml - (size_t)min_match; ml_f = (v >= 15) ? 15 : (uint8_t)v; } *op++ = (ll_f << 4) | ml_f; @@ -230,8 +517,8 @@ static size_t emit_seq(uint8_t *dst, const uint8_t *lits, } else { vv_write16(op, (uint16_t)off); op += 2; } - if (ml - VV_MIN_MATCH >= 15) - op += write_varint(op, ml - VV_MIN_MATCH - 15); + if (ml - (size_t)min_match >= 15) + op += write_varint(op, ml - (size_t)min_match - 15); } return (size_t)(op - dst); } @@ -247,21 +534,21 @@ static size_t emit_seq(uint8_t *dst, const uint8_t *lits, static size_t compress_block(const uint8_t *src, size_t start_pos, size_t block_len, uint8_t *dst, size_t dst_cap, - matcher_t *m, vv_mode_t mode) { + matcher_t *m, vv_mode_t mode, int min_match) { uint8_t *op = dst; int32_t pos = (int32_t)start_pos; int32_t end = (int32_t)(start_pos + block_len); const uint8_t *lit_start = src + start_pos; int off_bytes = (m->wlog > 16) ? 3 : 2; - while (pos < end - (int32_t)VV_MIN_MATCH) { + while (pos < end - min_match) { int32_t mlen = 0, moff = 0; /* ─── Step 1: Try rep-match (free, no hash lookup) ─── */ int32_t rep_idx = -1; int32_t rep_len = try_rep_match(m, src, pos, end, &rep_idx); - if (rep_len >= (int32_t)VV_MIN_MATCH) { + if (rep_len >= min_match) { mlen = rep_len; moff = (int32_t)m->rep[rep_idx]; } @@ -278,8 +565,8 @@ static size_t compress_block(const uint8_t *src, size_t start_pos, size_t block_ } /* ─── Step 3: Lazy evaluation (balanced + extreme) ─── */ - if (mode >= VV_MODE_BALANCED && mlen >= (int32_t)VV_MIN_MATCH && - pos + 1 < end - (int32_t)VV_MIN_MATCH) { + if (mode >= VV_MODE_BALANCED && mlen >= min_match && + pos + 1 < end - min_match) { /* Check pos+1 */ matcher_insert(m, src, pos, end); int32_t noff = 0; @@ -290,20 +577,49 @@ static size_t compress_block(const uint8_t *src, size_t start_pos, size_t block_ int32_t nrl = try_rep_match(m, src, pos + 1, end, &nri); if (nrl > nlen) { nlen = nrl; noff = (int32_t)m->rep[nri]; } - if (nlen > mlen + 1) { + /* standard */int32_t lazy_gain = 2; + if (nlen > mlen + lazy_gain) { /* pos+1 is significantly better: emit literal, shift */ pos++; mlen = nlen; moff = noff; - /* Lazy-2: also check pos+2 (extreme mode) */ - if (mode >= VV_MODE_EXTREME && pos + 1 < end - (int32_t)VV_MIN_MATCH) { + /* Lazy-2 disabled in v2.24.0. + * + * Previously, after a lazy-1 shift to pos+1, this block + * would try another shift to pos+2 if n2len > mlen + 1. + * That was a classic "greedy past the peak" bug: + * + * Empirical results on 50KB English-text corpus: + * balanced: 11,279 bytes + * extreme + lazy-2 (+1): 12,157 bytes (+8% vs balanced) + * extreme + lazy-2 (+2): 12,157 bytes (threshold didn't matter) + * extreme + lazy-2 (+4): 11,860 bytes (still worse) + * extreme, lazy-2 off: 10,718 bytes (5% better!) + * + * On 4 text corpora tested, lazy-2 cost an aggregate + * 2,463 bytes vs disabled. On 2 JSON corpora, it saved + * 213 bytes. Net-bytes-across-inputs: disabled wins by + * ~10×. Disabling produces the strictly correct + * "extreme >= balanced ratio" contract on all tested + * inputs except JSON, where the regression is tiny + * (<5%) and offset-encoding-cost-aware parsing would + * be the proper fix (future work). + * + * Root cause: lazy-2's break-even model doesn't + * account for the offset-extra-bits encoding cost of + * the shifted-to match. On text, deeper chain search + * finds long matches at far offsets whose extra-bits + * cost exceeds the gain from the extra match length. */ + (void)lazy_gain; /* still used in lazy-1 above */ + if (0) { + /* dead code — reference for future cost-aware work */ matcher_insert(m, src, pos, end); int32_t n2off = 0; int32_t n2len = chain_match(m, src, pos + 1, end, &n2off); int32_t n2ri = -1; int32_t n2rl = try_rep_match(m, src, pos + 1, end, &n2ri); if (n2rl > n2len) { n2len = n2rl; n2off = (int32_t)m->rep[n2ri]; } - if (n2len > mlen + 1) { + if (n2len > mlen + lazy_gain) { pos++; mlen = n2len; moff = n2off; } @@ -312,13 +628,13 @@ static size_t compress_block(const uint8_t *src, size_t start_pos, size_t block_ } /* ─── Step 4: Emit sequence or literal ─── */ - if (mlen >= (int32_t)VV_MIN_MATCH) { + if (mlen >= min_match) { size_t ll = (size_t)(src + pos - lit_start); size_t needed = 1 + (ll >= 15 ? ll / 255 + 2 : 0) + ll + 2 + ((size_t)mlen / 255 + 2); if ((size_t)(op - dst) + needed > dst_cap) return 0; - op += emit_seq(op, lit_start, ll, (size_t)mlen, (uint32_t)moff, off_bytes); + op += emit_seq(op, lit_start, ll, (size_t)mlen, (uint32_t)moff, off_bytes, min_match); /* ─── Hash insertion with skip heuristic ─── */ if (mlen >= 16) { @@ -347,7 +663,7 @@ static size_t compress_block(const uint8_t *src, size_t start_pos, size_t block_ size_t ll = (size_t)(src + end - lit_start); size_t needed = 1 + (ll >= 15 ? ll / 255 + 2 : 0) + ll; if ((size_t)(op - dst) + needed > dst_cap) return 0; - op += emit_seq(op, lit_start, ll, 0, 0, off_bytes); + op += emit_seq(op, lit_start, ll, 0, 0, off_bytes, min_match); } return (size_t)(op - dst); @@ -425,6 +741,185 @@ static size_t extract_literals( return total_lits; } +/* ═══════════════════════════════════════════════════════════════ + * BLOCK EMISSION HELPER + * + * Encodes a single block of up to VV_MAX_BLOCK_SIZE bytes from + * src[block_start..block_start+braw) and emits the compressed block + * to dst. Picks the best path (raw / LZ-raw / 'S' seq / 'I'/'C' lit) + * via winner-takes-all in balanced+extreme modes. + * + * Used by both the one-shot vv_compress() and the streaming + * vv_cstream_compress_chunk(). Expects: + * - src: source buffer (full source for vv_compress; the + * persistent stream buffer for streaming) + * - block_start: offset in src where this block begins + * - braw: block raw length (≤ VV_MAX_BLOCK_SIZE) + * - last: 1 if this is the last block in the frame + * - m: matcher state (persists across blocks) + * - mode: compression mode (affects path choice) + * - wlog: window log + * - tmp/tcap: scratch buffer for LZ-compressed tokens + * - lit_buf/lit_cap, stripped, ent_buf/ent_cap: entropy scratch + * - dst/dst_cap: output buffer + * + * Returns bytes written to dst on success, or 0 on overflow. */ +static size_t emit_block(const uint8_t *src, size_t block_start, size_t braw, + int last, matcher_t *m, vv_mode_t mode, uint8_t wlog, + uint8_t *tmp, size_t tcap, + uint8_t *lit_buf, size_t lit_cap, + uint8_t *stripped, uint8_t *ent_buf, size_t ent_cap, + uint8_t *dst, size_t dst_cap, int min_match) { + uint8_t *op = dst; + + size_t csz = compress_block(src, block_start, braw, tmp, tcap, m, mode, min_match); + + if (csz == 0 || csz >= braw) { + /* Incompressible: store raw */ + if ((size_t)(op - dst) + 4 + braw > dst_cap) return 0; + uint32_t bh = vv_bh_pack(VV_BLOCK_RAW, last, (uint32_t)braw); + memcpy(op, &bh, 4); op += 4; + memcpy(op, src + block_start, braw); op += braw; + return (size_t)(op - dst); + } + + if (mode >= VV_MODE_BALANCED) { + /* Path A: sequence coding ('S') */ + size_t seq_len = 0; + int seq_valid = 0; + size_t seq_block_sz = (size_t)-1; + int off_bytes = (wlog > 16) ? 3 : 2; + /* Format v2: when min_match < 4 (i.e. 3), encode with the v2 + * table so length-3 matches are representable as code 0. The + * token stream produced by compress_block(min_match=3) may + * contain 3-byte matches that v1 encode_sequences cannot + * represent correctly. */ + int use_v2 = (min_match < (int)VV_MIN_MATCH); + vva_error_t serr = use_v2 + ? vva_encode_sequences_v2(tmp, csz, ent_buf, ent_cap, &seq_len, off_bytes) + : vva_encode_sequences(tmp, csz, ent_buf, ent_cap, &seq_len, off_bytes); + if (serr == VVA_OK) { + seq_block_sz = 4 + 3 + 1 + seq_len; + seq_valid = 1; + } + + /* Path B: literal-only entropy ('I' or 'C') */ + size_t stripped_len = 0; + size_t lit_count = 0; + uint8_t *ent_buf2 = ent_buf + ent_cap / 2; + size_t ent_cap2 = ent_cap / 2; + size_t ent_len = 0; + uint8_t ent_tag = 0; + size_t ent_block_sz = (size_t)-1; + + int try_path_b = 1; + if (mode == VV_MODE_BALANCED && seq_valid && seq_block_sz < (braw / 3)) { + /* SPRINT 29 (revised in v2.15): always try Path B in BALANCED + * mode, comparing both costs and picking the smaller. The + * earlier "skip Path B if seq compressed >3:1" heuristic + * (added in Sprint 28 for speed) saved ~30% encode time but + * hurt ratio on text-heavy data — Silesia dickens/reymont + * showed Path B's 'C' tag would have produced 5-10% smaller + * output but never got the chance. + * + * v2.15 trade-off: encoder is ~25% slower in BALANCED mode + * but ratio improves measurably on text. Decode speed is + * unaffected (decoder doesn't care which tag was chosen). + * + * In ULTRA_FAST/FAST modes the original skip remains in + * effect because those modes are throughput-priority. */ + (void)try_path_b; + } + + if (try_path_b) { + lit_count = extract_literals(tmp, csz, lit_buf, lit_cap, + stripped, &stripped_len, off_bytes); + if (lit_count > 0) { + if (mode >= VV_MODE_BALANCED && lit_count >= 4096) { + vva_error_t aerr = vva_encode_ctx(lit_buf, lit_count, + ent_buf2, ent_cap2, &ent_len); + if (aerr == VVA_OK) ent_tag = VV_ENTROPY_CTX; + } + if (!ent_tag) { + vva_error_t aerr = vva_encode4(lit_buf, lit_count, + ent_buf2, ent_cap2, &ent_len); + if (aerr == VVA_OK) ent_tag = VV_ENTROPY_ANS4; + } + if (!ent_tag) { + vva_error_t aerr = vva_encode(lit_buf, lit_count, + ent_buf2, ent_cap2, &ent_len); + if (aerr == VVA_OK) ent_tag = VV_ENTROPY_ANS; + } + if (ent_tag) { + ent_block_sz = 4 + 3 + 1 + 2 + 2 + ent_len + stripped_len; + } + } + } + + size_t raw_block_sz = 4 + 3 + csz; + + if (seq_valid && seq_block_sz <= ent_block_sz && seq_block_sz < raw_block_sz) { + if ((size_t)(op - dst) + seq_block_sz > dst_cap) return 0; + uint32_t bh = vv_bh_pack(VV_BLOCK_ENTROPY, last, (uint32_t)braw); + memcpy(op, &bh, 4); op += 4; + uint32_t total_comp = (uint32_t)(1 + seq_len); + op[0] = (uint8_t)(total_comp); + op[1] = (uint8_t)(total_comp >> 8); + op[2] = (uint8_t)(total_comp >> 16); + op += 3; + *op++ = use_v2 ? VV_ENTROPY_SEQ_V2 : VV_ENTROPY_SEQ; + memcpy(op, ent_buf, seq_len); op += seq_len; + } else if (!use_v2 && ent_tag && ent_block_sz < raw_block_sz) { + /* Path B (H/I/C entropy) uses `stripped` tokens which still + * contain v1-format matchlen bytes. Only safe for v1. For + * v2, we must skip this fallback to avoid emitting v1 tokens + * that a v2-aware decoder wouldn't reconstruct correctly. */ + if ((size_t)(op - dst) + ent_block_sz > dst_cap) return 0; + uint32_t bh = vv_bh_pack(VV_BLOCK_ENTROPY, last, (uint32_t)braw); + memcpy(op, &bh, 4); op += 4; + uint32_t total_comp = (uint32_t)(5 + ent_len + stripped_len); + op[0] = (uint8_t)(total_comp); + op[1] = (uint8_t)(total_comp >> 8); + op[2] = (uint8_t)(total_comp >> 16); + op += 3; + *op++ = ent_tag; + op[0] = (uint8_t)(lit_count); op[1] = (uint8_t)(lit_count >> 8); op += 2; + op[0] = (uint8_t)(ent_len); op[1] = (uint8_t)(ent_len >> 8); op += 2; + memcpy(op, ent_buf2, ent_len); op += ent_len; + memcpy(op, stripped, stripped_len); op += stripped_len; + } else if (!use_v2) { + /* Plain VV_BLOCK_COMPRESSED carries raw v1-format tokens. + * For v2, we must not emit these — the decoder would + * reconstruct matchlen with +4 instead of +3. Fall to RAW + * block instead (handled below via "else" when raw_block_sz + * is smaller). We reach this branch only when the previous + * conditions all failed AND we're NOT v2. */ + if ((size_t)(op - dst) + raw_block_sz > dst_cap) return 0; + uint32_t bh = vv_bh_pack(VV_BLOCK_COMPRESSED, last, (uint32_t)braw); + memcpy(op, &bh, 4); op += 4; + op[0] = (uint8_t)(csz); op[1] = (uint8_t)(csz >> 8); op[2] = (uint8_t)(csz >> 16); + op += 3; + memcpy(op, tmp, csz); op += csz; + } else { + /* v2 path, sequence coding didn't fit/help: emit RAW. */ + if ((size_t)(op - dst) + 4 + braw > dst_cap) return 0; + uint32_t bh = vv_bh_pack(VV_BLOCK_RAW, last, (uint32_t)braw); + memcpy(op, &bh, 4); op += 4; + memcpy(op, src + block_start, braw); op += braw; + } + } else { + /* Ultra-fast mode */ + if ((size_t)(op - dst) + 4 + 3 + csz > dst_cap) return 0; + uint32_t bh = vv_bh_pack(VV_BLOCK_COMPRESSED, last, (uint32_t)braw); + memcpy(op, &bh, 4); op += 4; + op[0] = (uint8_t)(csz); op[1] = (uint8_t)(csz >> 8); op[2] = (uint8_t)(csz >> 16); + op += 3; + memcpy(op, tmp, csz); op += csz; + } + + return (size_t)(op - dst); +} + /* ═══════════════════════════════════════════════════════════════ * PUBLIC API: COMPRESS * ═══════════════════════════════════════════════════════════════ */ @@ -452,32 +947,37 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, } switch (opts->mode) { case VV_MODE_ULTRA_FAST: depth = 4; break; - case VV_MODE_BALANCED: depth = 48; break; + case VV_MODE_BALANCED: depth = 24; break; /* was 48 — halving barely affects ratio, doubles speed */ case VV_MODE_EXTREME: depth = 256; break; - default: depth = 48; + default: depth = 24; } - /* ─── ADAPTIVE WINDOW: sample first 64KB at wlog=16 vs wlog=20. - * PERF: only samples 64KB (not full 1MB block) — 16× faster trial. - * If wlog=20 saves ≥3%, use wider window for the whole frame. ─── */ + /* ─── ADAPTIVE WINDOW + HASH4 detection in a single trial. + * PERF: previously this was two separate 128K+64K=192K trials, run + * sequentially. We can make BOTH decisions from the SAME trial: + * - window: wlog=20 wins if it saves ≥3% vs wlog=16 + * - hash4: enable if ratio < 2:1 (indicates binary-like data) */ + int enable_hash4 = 0; if (opts->window_log == 0 && opts->mode >= VV_MODE_BALANCED && src_len > 65536) { - size_t trial_len = 262144; /* Sample 256KB — catches patterns up to 200KB apart */ + size_t trial_len = 131072; if (trial_len > src_len) trial_len = src_len; size_t trial_cap = trial_len + trial_len / 255 + 1024; uint8_t *trial_buf = (uint8_t *)malloc(trial_cap); if (trial_buf) { - /* PERF: use greedy depth=4 for trials — 10× faster than lazy-48 */ matcher_t m16; matcher_init(&m16, 16, 4); - size_t sz16 = compress_block(src, 0, trial_len, trial_buf, trial_cap, &m16, VV_MODE_ULTRA_FAST); + size_t sz16 = compress_block(src, 0, trial_len, trial_buf, trial_cap, &m16, VV_MODE_ULTRA_FAST, VV_MIN_MATCH); matcher_free(&m16); matcher_t m20; matcher_init(&m20, 20, 4); - size_t sz20 = compress_block(src, 0, trial_len, trial_buf, trial_cap, &m20, VV_MODE_ULTRA_FAST); + size_t sz20 = compress_block(src, 0, trial_len, trial_buf, trial_cap, &m20, VV_MODE_ULTRA_FAST, VV_MIN_MATCH); matcher_free(&m20); free(trial_buf); if (sz20 > 0 && sz16 > 0 && sz20 < (sz16 * 97 / 100)) wlog = 20; + /* Binary-like detection: best trial ratio < 2:1 */ + size_t best_sz = (sz20 > 0 && sz20 < sz16) ? sz20 : sz16; + if (best_sz > 0 && best_sz * 2 > trial_len) enable_hash4 = 1; } } @@ -496,9 +996,29 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, /* Matcher */ matcher_t m; matcher_init(&m, wlog, depth); + m.use_hash4 = (uint8_t)enable_hash4; /* From fused adaptive-window trial */ + /* Format v2 cap applies to EVERY match emitted from this matcher, + * not just those produced via hash3. Set unconditionally when + * opts.format_v2 is active. */ + if (opts->format_v2) { + matcher_set_format_v2(&m); + } + /* Hash3 enablement is a separate, adaptive decision. Only fires + * on binary-like data (enable_hash4) where length-3 matches + * actually help. On text/JSON it stays off to avoid regressions. */ + if (opts->format_v2 && enable_hash4) { + if (!matcher_enable_hash3(&m)) { + matcher_free(&m); + return VV_ERR_NOMEM; + } + } - /* Temp buffer */ - size_t tcap = VV_MAX_BLOCK_SIZE + VV_MAX_BLOCK_SIZE / 255 + 1024; + /* Temp buffer. + * PERF: size to the actual input (not always 1MB). For a 4KB input, + * tcap was ~1.03MB — a wasteful allocation. Now allocate just enough + * to hold the LZ-tokenized output, bounded by VV_MAX_BLOCK_SIZE. */ + size_t block_bound = src_len < VV_MAX_BLOCK_SIZE ? src_len : VV_MAX_BLOCK_SIZE; + size_t tcap = block_bound + block_bound / 255 + 1024; uint8_t *tmp = (uint8_t *)malloc(tcap); if (!tmp) { matcher_free(&m); return VV_ERR_NOMEM; } @@ -506,8 +1026,12 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, uint8_t *lit_buf = NULL, *stripped = NULL, *ent_buf = NULL; size_t lit_cap = 0, ent_cap = 0; if (opts->mode >= VV_MODE_BALANCED) { - lit_cap = VV_MAX_BLOCK_SIZE; - ent_cap = vva_bound(VV_MAX_BLOCK_SIZE); + /* PERF: size these to the actual input too — they only need to + * cover the single in-flight block's worth of literals/entropy + * output. For small one-shot calls this avoids ~3 MB of wasted + * allocation and page-faulting every call. */ + lit_cap = block_bound; + ent_cap = vva_bound(block_bound); lit_buf = (uint8_t *)malloc(lit_cap); stripped = (uint8_t *)malloc(tcap); ent_buf = (uint8_t *)malloc(ent_cap); @@ -526,115 +1050,26 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, memcpy(op, &bh, 4); op += 4; } + /* Format v2: when opts->format_v2 is set, encode with min_match=3. + * Produces 'T'-tagged ENTROPY blocks which only v2.33.0+ decoders + * can read. Closes the real-binary compression gap vs gzip-9. */ + int min_match = opts->format_v2 ? 3 : (int)VV_MIN_MATCH; + while (remaining > 0) { size_t braw = remaining > VV_MAX_BLOCK_SIZE ? VV_MAX_BLOCK_SIZE : remaining; int last = (remaining <= VV_MAX_BLOCK_SIZE); size_t block_start = (size_t)(ip - src); - size_t csz = compress_block(src, block_start, braw, tmp, tcap, &m, opts->mode); - - if (csz == 0 || csz >= braw) { - /* Incompressible: store raw */ - uint32_t bh = vv_bh_pack(VV_BLOCK_RAW, last, (uint32_t)braw); - memcpy(op, &bh, 4); op += 4; - memcpy(op, ip, braw); op += braw; - } else if (opts->mode >= VV_MODE_BALANCED) { - /* ═══ WINNER-TAKES-ALL block selection ═══ - * TRADEOFF: we encode the block twice (once 'S', once 'I'/'C') - * and pick the smaller. This costs ~2× encode time but ensures - * we NEVER regress ratio vs any previous codec version. - * Encode speed is not the bottleneck (decode is). */ - - /* ── Path A: sequence coding ('S') ── */ - size_t seq_len = 0; - int seq_valid = 0; - size_t seq_block_sz = (size_t)-1; /* Total bytes if we emit 'S' */ - int off_bytes = (wlog > 16) ? 3 : 2; - vva_error_t serr = vva_encode_sequences(tmp, csz, - ent_buf, ent_cap, &seq_len, off_bytes); - if (serr == VVA_OK) { - seq_block_sz = 4 + 3 + 1 + seq_len; /* block_hdr + comp_sz + tag + data */ - seq_valid = 1; - } - - /* ── Path B: literal-only entropy ('I' or 'C') ── */ - size_t stripped_len = 0; - size_t lit_count = extract_literals(tmp, csz, lit_buf, lit_cap, - stripped, &stripped_len, off_bytes); - - /* Use second half of ent_buf for path B to avoid overwriting path A */ - uint8_t *ent_buf2 = ent_buf + ent_cap / 2; - size_t ent_cap2 = ent_cap / 2; - size_t ent_len = 0; - uint8_t ent_tag = 0; - size_t ent_block_sz = (size_t)-1; - - if (lit_count > 0) { - if (opts->mode >= VV_MODE_EXTREME && lit_count >= 64) { - vva_error_t aerr = vva_encode_ctx(lit_buf, lit_count, - ent_buf2, ent_cap2, &ent_len); - if (aerr == VVA_OK) ent_tag = VV_ENTROPY_CTX; - } - if (!ent_tag) { - vva_error_t aerr = vva_encode4(lit_buf, lit_count, - ent_buf2, ent_cap2, &ent_len); - if (aerr == VVA_OK) ent_tag = VV_ENTROPY_ANS4; - } - if (!ent_tag) { - vva_error_t aerr = vva_encode(lit_buf, lit_count, - ent_buf2, ent_cap2, &ent_len); - if (aerr == VVA_OK) ent_tag = VV_ENTROPY_ANS; - } - if (ent_tag) { - ent_block_sz = 4 + 3 + 1 + 2 + 2 + ent_len + stripped_len; - } - } - - /* ── Path C: raw type-1 block ── */ - size_t raw_block_sz = 4 + 3 + csz; - - /* ── Pick winner ── */ - if (seq_valid && seq_block_sz <= ent_block_sz && seq_block_sz < raw_block_sz) { - /* 'S' wins — emit sequence-coded block */ - uint32_t bh = vv_bh_pack(VV_BLOCK_ENTROPY, last, (uint32_t)braw); - memcpy(op, &bh, 4); op += 4; - uint32_t total_comp = (uint32_t)(1 + seq_len); - op[0] = (uint8_t)(total_comp); - op[1] = (uint8_t)(total_comp >> 8); - op[2] = (uint8_t)(total_comp >> 16); - op += 3; - *op++ = VV_ENTROPY_SEQ; - memcpy(op, ent_buf, seq_len); op += seq_len; - } else if (ent_tag && ent_block_sz < raw_block_sz) { - /* 'I'/'C' wins — emit literal-entropy block */ - uint32_t bh = vv_bh_pack(VV_BLOCK_ENTROPY, last, (uint32_t)braw); - memcpy(op, &bh, 4); op += 4; - uint32_t total_comp = (uint32_t)(5 + ent_len + stripped_len); - op[0] = (uint8_t)(total_comp); - op[1] = (uint8_t)(total_comp >> 8); - op[2] = (uint8_t)(total_comp >> 16); - op += 3; - *op++ = ent_tag; - op[0] = (uint8_t)(lit_count); op[1] = (uint8_t)(lit_count >> 8); op += 2; - op[0] = (uint8_t)(ent_len); op[1] = (uint8_t)(ent_len >> 8); op += 2; - memcpy(op, ent_buf2, ent_len); op += ent_len; - memcpy(op, stripped, stripped_len); op += stripped_len; - } else { - /* Raw type-1 wins (or nothing compresses) */ - uint32_t bh = vv_bh_pack(VV_BLOCK_COMPRESSED, last, (uint32_t)braw); - memcpy(op, &bh, 4); op += 4; - op[0] = (uint8_t)(csz); op[1] = (uint8_t)(csz >> 8); op[2] = (uint8_t)(csz >> 16); - op += 3; - memcpy(op, tmp, csz); op += csz; - } - } else { - /* Ultra-fast mode: emit type 1 block directly */ - uint32_t bh = vv_bh_pack(VV_BLOCK_COMPRESSED, last, (uint32_t)braw); - memcpy(op, &bh, 4); op += 4; - op[0] = (uint8_t)(csz); op[1] = (uint8_t)(csz >> 8); op[2] = (uint8_t)(csz >> 16); - op += 3; - memcpy(op, tmp, csz); op += csz; + size_t written = emit_block(src, block_start, braw, last, &m, opts->mode, wlog, + tmp, tcap, lit_buf, lit_cap, + stripped, ent_buf, ent_cap, + op, dst_cap - (size_t)(op - dst), min_match); + if (written == 0) { + free(lit_buf); free(stripped); free(ent_buf); + free(tmp); matcher_free(&m); + return VV_ERR_OVERFLOW; } + op += written; ip += braw; remaining -= braw; } @@ -651,3 +1086,450 @@ int64_t vv_compress(const uint8_t *src, size_t src_len, matcher_free(&m); return (int64_t)(op - dst); } + +/* ═══════════════════════════════════════════════════════════════ + * STREAMING COMPRESSION + * + * A compression stream buffers persistent state across calls: + * - The matcher (hash tables, chains, rep-match offsets) + * - Scratch buffers (tmp/lit_buf/stripped/ent_buf) + * - Streaming xxh64 state for the frame checksum + * - The full input so far in sliding-window form (needed because + * LZ matches can reference up to 2^wlog bytes back) + * + * Each call to vv_cstream_compress_chunk() appends chunk bytes to + * the internal source buffer, emits one block covering those bytes, + * and optionally emits the frame header (first call) and footer + * (when is_last is set). + * + * Memory cost: 2 × window_size + ~10 MB scratch (ent_buf, etc.). + * For wlog=16 that's ~131 KB + scratch; wlog=20 is ~2 MB + scratch. + * ═══════════════════════════════════════════════════════════════ */ + +struct vv_cstream_s { + vv_options_t opts; + uint8_t wlog; + matcher_t m; + + /* Scratch buffers — allocated once, reused across chunks */ + uint8_t *tmp; size_t tcap; + uint8_t *lit_buf; size_t lit_cap; + uint8_t *stripped; + uint8_t *ent_buf; size_t ent_cap; + + /* Sliding-window source buffer. We accumulate input so offset-based + * match references resolve correctly. Old bytes beyond the window + * are dropped in periodic compaction. */ + uint8_t *src_buf; /* Capacity = 2 × window_size */ + size_t src_cap; + size_t src_head; /* First valid byte index in src_buf */ + size_t src_len; /* Number of valid bytes in src_buf */ + size_t global_offset; /* src_buf[i] corresponds to stream offset (global_offset - src_len + i) */ + + /* Streaming checksum */ + vv_xxh64_state_t cks; + + int header_emitted; +}; + +vv_cstream_t *vv_cstream_create(const vv_options_t *opts) { + vv_cstream_t *ctx = (vv_cstream_t *)calloc(1, sizeof(vv_cstream_t)); + if (!ctx) return NULL; + + if (opts) ctx->opts = *opts; + else vv_default_options(&ctx->opts); + + /* Resolve window log (fixed for streams — no adaptive probe) */ + uint8_t wlog = ctx->opts.window_log; + if (wlog == 0) wlog = 16; + ctx->wlog = wlog; + + uint32_t depth; + switch (ctx->opts.mode) { + case VV_MODE_ULTRA_FAST: depth = 4; break; + case VV_MODE_BALANCED: depth = 24; break; + case VV_MODE_EXTREME: depth = 256; break; + default: depth = 24; + } + + matcher_init(&ctx->m, wlog, depth); + /* Format v2 matchlen cap applies to every match — set whenever + * streaming opts has format_v2 on, not just when hash3 fires. */ + if (opts->format_v2) { + matcher_set_format_v2(&ctx->m); + } + /* SPRINT 45: enable hash3 for format v2 streaming. Must free + * ctx before returning NULL — callers use NULL-check semantics + * here, not error codes. */ + if (opts->format_v2) { + if (!matcher_enable_hash3(&ctx->m)) { + matcher_free(&ctx->m); + free(ctx); + return NULL; + } + } + + /* Scratch buffers sized for VV_MAX_BLOCK_SIZE */ + ctx->tcap = VV_MAX_BLOCK_SIZE + VV_MAX_BLOCK_SIZE / 255 + 1024; + ctx->tmp = (uint8_t *)malloc(ctx->tcap); + ctx->lit_cap = VV_MAX_BLOCK_SIZE; + ctx->lit_buf = (uint8_t *)malloc(ctx->lit_cap); + ctx->stripped = (uint8_t *)malloc(ctx->lit_cap); + ctx->ent_cap = vva_bound(VV_MAX_BLOCK_SIZE); + ctx->ent_buf = (uint8_t *)malloc(ctx->ent_cap); + + /* Source window = 2 × window_size so a full block of input can + * land before we compact. */ + size_t window = (size_t)1u << wlog; + ctx->src_cap = window * 2 + VV_MAX_BLOCK_SIZE; + ctx->src_buf = (uint8_t *)malloc(ctx->src_cap); + + if (!ctx->tmp || !ctx->lit_buf || !ctx->stripped || !ctx->ent_buf || !ctx->src_buf) { + vv_cstream_destroy(ctx); + return NULL; + } + + if (ctx->opts.checksum) vv_xxh64_init(&ctx->cks, 0); + ctx->header_emitted = 0; + return ctx; +} + +void vv_cstream_destroy(vv_cstream_t *ctx) { + if (!ctx) return; + free(ctx->tmp); free(ctx->lit_buf); free(ctx->stripped); free(ctx->ent_buf); + free(ctx->src_buf); + matcher_free(&ctx->m); + free(ctx); +} + +int vv_cstream_reset(vv_cstream_t *ctx, const vv_options_t *opts) { + if (!ctx) return VV_ERR_PARAM; + + /* Apply new options if provided. window_log cannot change without + * reallocating the matcher tables — reject the change. */ + if (opts) { + uint8_t new_wlog = opts->window_log; + if (new_wlog == 0) new_wlog = 16; + if (new_wlog != ctx->wlog) return VV_ERR_PARAM; + ctx->opts = *opts; + } + + /* Update chain_depth in case the mode changed */ + uint32_t depth; + switch (ctx->opts.mode) { + case VV_MODE_ULTRA_FAST: depth = 4; break; + case VV_MODE_BALANCED: depth = 24; break; + case VV_MODE_EXTREME: depth = 256; break; + default: depth = 24; + } + ctx->m.chain_depth = depth; + + matcher_reset(&ctx->m); + + /* Reset sliding-window source buffer */ + ctx->src_head = 0; + ctx->src_len = 0; + ctx->global_offset = 0; + + /* Reset checksum */ + if (ctx->opts.checksum) vv_xxh64_init(&ctx->cks, 0); + + ctx->header_emitted = 0; + return VV_OK; +} + +int vv_cstream_compress_chunk(vv_cstream_t *ctx, + const uint8_t *chunk, size_t chunk_len, + uint8_t *dst, size_t dst_cap, + size_t *written, int is_last) { + if (!ctx || !dst || !written) return VV_ERR_PARAM; + if (chunk_len > VV_MAX_BLOCK_SIZE) return VV_ERR_PARAM; + *written = 0; + + uint8_t *op = dst; + size_t cap_left = dst_cap; + + /* Emit frame header on first call */ + if (!ctx->header_emitted) { + if (cap_left < sizeof(vv_frame_header_t)) return VV_ERR_OVERFLOW; + vv_frame_header_t fh; + memset(&fh, 0, sizeof(fh)); + fh.magic = VV_MAGIC; + fh.version = 1; + fh.flags = ctx->opts.checksum ? 1 : 0; + fh.mode_hint = (uint8_t)ctx->opts.mode; + fh.window_log = ctx->wlog; + /* content_size unknown in streaming mode → 0 */ + fh.content_size = 0; + memcpy(op, &fh, sizeof(fh)); + op += sizeof(fh); cap_left -= sizeof(fh); + ctx->header_emitted = 1; + } + + /* Append chunk to sliding-window source buffer. + * Compact the buffer if needed to stay under src_cap. We keep + * the last (window_size) bytes as match-lookback history. */ + if (chunk_len > 0) { + size_t window = (size_t)1u << ctx->wlog; + size_t needed = ctx->src_len + chunk_len; + if (needed > ctx->src_cap) { + /* Compact: drop everything older than (window) bytes before end */ + size_t keep = ctx->src_len > window ? window : ctx->src_len; + size_t drop = ctx->src_len - keep; + if (drop > 0) { + memmove(ctx->src_buf, ctx->src_buf + drop, keep); + ctx->src_len = keep; + /* Adjust matcher table/chain entries: positions were + * relative to src_buf[0] and are now shifted by -drop. + * Easiest correct approach: invalidate chains — they + * reference positions < limit automatically and are + * bounded-distance walked. The hash table's `table[h]` + * entries would now point at shifted positions, but + * we can shift them en masse. */ + /* Shift matcher table entries (positions get re-based) */ + for (uint32_t i = 0; i < VV_HC_SIZE; i++) { + if (ctx->m.table[i] >= (int32_t)drop) + ctx->m.table[i] -= (int32_t)drop; + else ctx->m.table[i] = -1; + } + for (uint32_t i = 0; i < VV_HC4_SIZE; i++) { + if (ctx->m.table4[i] >= (int32_t)drop) + ctx->m.table4[i] -= (int32_t)drop; + else ctx->m.table4[i] = -1; + } + /* Chain arrays are also indexed by position — shift those + * too, BUT the array is indexed by (pos & chain_mask) so + * we need to shift values (the successor position) while + * keeping the circular layout. For simplicity and safety, + * we rebuild conservatively: clear chain entries whose + * references would now be negative. */ + for (uint32_t i = 0; i < (1u << ctx->wlog); i++) { + if (ctx->m.chain[i] >= (int32_t)drop) + ctx->m.chain[i] -= (int32_t)drop; + else ctx->m.chain[i] = -1; + if (ctx->m.hash4_chain[i] >= (int32_t)drop) + ctx->m.hash4_chain[i] -= (int32_t)drop; + else ctx->m.hash4_chain[i] = -1; + } + } + } + memcpy(ctx->src_buf + ctx->src_len, chunk, chunk_len); + ctx->src_len += chunk_len; + ctx->global_offset += chunk_len; + + if (ctx->opts.checksum) vv_xxh64_update(&ctx->cks, chunk, chunk_len); + } + + /* Emit block(s) for the newly added chunk_len bytes. + * block_start in the src_buf = ctx->src_len - chunk_len. */ + if (chunk_len == 0 && is_last) { + /* Empty final chunk: emit empty raw-last block */ + if (cap_left < 4) return VV_ERR_OVERFLOW; + uint32_t bh = vv_bh_pack(VV_BLOCK_RAW, 1, 0); + memcpy(op, &bh, 4); op += 4; cap_left -= 4; + } else if (chunk_len > 0) { + size_t block_start = ctx->src_len - chunk_len; + int stream_min_match = ctx->opts.format_v2 ? 3 : (int)VV_MIN_MATCH; + size_t block_sz = emit_block(ctx->src_buf, block_start, chunk_len, is_last, + &ctx->m, ctx->opts.mode, ctx->wlog, + ctx->tmp, ctx->tcap, + ctx->lit_buf, ctx->lit_cap, + ctx->stripped, ctx->ent_buf, ctx->ent_cap, + op, cap_left, stream_min_match); + if (block_sz == 0) return VV_ERR_OVERFLOW; + op += block_sz; cap_left -= block_sz; + } + + /* Emit frame footer on last chunk */ + if (is_last && ctx->opts.checksum) { + if (cap_left < sizeof(vv_frame_footer_t)) return VV_ERR_OVERFLOW; + vv_frame_footer_t ff; + ff.checksum = vv_xxh64_finalize(&ctx->cks); + ff.footer_magic = 0x56564E44u; + memcpy(op, &ff, sizeof(ff)); + op += sizeof(ff); cap_left -= sizeof(ff); + } + + *written = (size_t)(op - dst); + return VV_OK; +} + +/* ═══════════════════════════════════════════════════════════════ + * MULTI-THREADED COMPRESSION + * + * Strategy: split input into chunks of chunk_size bytes. Each chunk + * is encoded independently via vv_compress() into its own .vv frame. + * Output frames are concatenated into dst. vv_decompress handles + * multi-frame input natively. + * + * When VV_ENABLE_THREADS is defined, use pthread to run N worker + * threads in parallel. Otherwise, run sequentially. + * + * Ratio cost: frames are independent — cross-frame match history + * is lost at chunk boundaries. For chunk_size ≥ 4 MB on + * compressible data, the ratio hit is typically < 2%. + * ═══════════════════════════════════════════════════════════════ */ + +#ifdef VV_ENABLE_THREADS +#include +#include + +typedef struct { + const uint8_t *src; + size_t src_len; + uint8_t *dst; + size_t dst_cap; + const vv_options_t *opts; + int64_t result; /* compressed size, or error code */ +} mt_task_t; + +typedef struct { + mt_task_t *tasks; + size_t ntasks; + volatile size_t next_task; + pthread_mutex_t mutex; +} mt_pool_t; + +static void *mt_worker(void *arg) { + mt_pool_t *pool = (mt_pool_t *)arg; + for (;;) { + pthread_mutex_lock(&pool->mutex); + size_t idx = pool->next_task++; + pthread_mutex_unlock(&pool->mutex); + if (idx >= pool->ntasks) break; + mt_task_t *t = &pool->tasks[idx]; + t->result = vv_compress(t->src, t->src_len, t->dst, t->dst_cap, t->opts); + } + return NULL; +} +#endif + +int64_t vv_compress_mt(const uint8_t *src, size_t src_len, + uint8_t *dst, size_t dst_cap, + const vv_options_t *opts, + unsigned int nthreads, + size_t chunk_size) { + if (!src || !dst || !opts) return VV_ERR_PARAM; + if (chunk_size == 0) chunk_size = 4 * 1024 * 1024; /* 4 MB default */ + if (chunk_size < VV_MAX_BLOCK_SIZE) chunk_size = VV_MAX_BLOCK_SIZE; + + /* For small inputs, just use vv_compress directly — no speedup + * available and avoids the per-frame fixed overhead. */ + if (src_len <= chunk_size) { + return vv_compress(src, src_len, dst, dst_cap, opts); + } + + /* Split into N chunks */ + size_t n_chunks = (src_len + chunk_size - 1) / chunk_size; + + /* Allocate per-chunk temporary output buffers. Each could be up to + * vv_compress_bound(chunk_size), which can be ~4 MB * 1.01 for a + * 4 MB chunk. Total scratch = n_chunks * ~4 MB. */ + uint8_t **chunk_dst = (uint8_t **)calloc(n_chunks, sizeof(uint8_t *)); + int64_t *chunk_sz = (int64_t *)calloc(n_chunks, sizeof(int64_t)); + if (!chunk_dst || !chunk_sz) { + free(chunk_dst); free(chunk_sz); + return VV_ERR_NOMEM; + } + + size_t chunk_cap = vv_compress_bound(chunk_size); + int alloc_failed = 0; + for (size_t i = 0; i < n_chunks; i++) { + chunk_dst[i] = (uint8_t *)malloc(chunk_cap); + if (!chunk_dst[i]) { alloc_failed = 1; break; } + } + if (alloc_failed) { + for (size_t i = 0; i < n_chunks; i++) free(chunk_dst[i]); + free(chunk_dst); free(chunk_sz); + return VV_ERR_NOMEM; + } + +#ifdef VV_ENABLE_THREADS + /* Determine thread count */ + if (nthreads == 0) { + long n = sysconf(_SC_NPROCESSORS_ONLN); + nthreads = (n > 0) ? (unsigned int)n : 1; + } + if (nthreads > n_chunks) nthreads = (unsigned int)n_chunks; + if (nthreads == 0) nthreads = 1; + + /* Build task list */ + mt_task_t *tasks = (mt_task_t *)malloc(n_chunks * sizeof(mt_task_t)); + if (!tasks) { + for (size_t i = 0; i < n_chunks; i++) free(chunk_dst[i]); + free(chunk_dst); free(chunk_sz); + return VV_ERR_NOMEM; + } + for (size_t i = 0; i < n_chunks; i++) { + size_t off = i * chunk_size; + size_t len = (off + chunk_size <= src_len) ? chunk_size : (src_len - off); + tasks[i].src = src + off; + tasks[i].src_len = len; + tasks[i].dst = chunk_dst[i]; + tasks[i].dst_cap = chunk_cap; + tasks[i].opts = opts; + tasks[i].result = 0; + } + + mt_pool_t pool; + pool.tasks = tasks; + pool.ntasks = n_chunks; + pool.next_task = 0; + pthread_mutex_init(&pool.mutex, NULL); + + pthread_t *threads = (pthread_t *)malloc(nthreads * sizeof(pthread_t)); + if (!threads) { + pthread_mutex_destroy(&pool.mutex); + free(tasks); + for (size_t i = 0; i < n_chunks; i++) free(chunk_dst[i]); + free(chunk_dst); free(chunk_sz); + return VV_ERR_NOMEM; + } + for (unsigned int t = 0; t < nthreads; t++) + pthread_create(&threads[t], NULL, mt_worker, &pool); + for (unsigned int t = 0; t < nthreads; t++) + pthread_join(threads[t], NULL); + free(threads); + pthread_mutex_destroy(&pool.mutex); + + for (size_t i = 0; i < n_chunks; i++) chunk_sz[i] = tasks[i].result; + free(tasks); +#else + /* Sequential fallback: encode each chunk in turn. */ + (void)nthreads; + for (size_t i = 0; i < n_chunks; i++) { + size_t off = i * chunk_size; + size_t len = (off + chunk_size <= src_len) ? chunk_size : (src_len - off); + chunk_sz[i] = vv_compress(src + off, len, chunk_dst[i], chunk_cap, opts); + } +#endif + + /* Check for errors and total up sizes */ + int64_t total = 0; + for (size_t i = 0; i < n_chunks; i++) { + if (chunk_sz[i] < 0) { + int64_t err = chunk_sz[i]; + for (size_t j = 0; j < n_chunks; j++) free(chunk_dst[j]); + free(chunk_dst); free(chunk_sz); + return err; + } + total += chunk_sz[i]; + } + + if ((size_t)total > dst_cap) { + for (size_t i = 0; i < n_chunks; i++) free(chunk_dst[i]); + free(chunk_dst); free(chunk_sz); + return VV_ERR_OVERFLOW; + } + + /* Concatenate frames into dst */ + uint8_t *op = dst; + for (size_t i = 0; i < n_chunks; i++) { + memcpy(op, chunk_dst[i], (size_t)chunk_sz[i]); + op += chunk_sz[i]; + free(chunk_dst[i]); + } + free(chunk_dst); free(chunk_sz); + + return total; +} diff --git a/src/vv_huffman.c b/src/vv_huffman.c index b7885b9..ad897e1 100644 --- a/src/vv_huffman.c +++ b/src/vv_huffman.c @@ -1,12 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ -#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) - #define _DEFAULT_SOURCE 1 -#endif - /* * VaptVupt — Canonical Huffman Codec Implementation * @@ -526,7 +517,7 @@ vvh_error_t vvh_decode(const uint8_t *src, size_t src_len, int sym = (int)(entry & 0xFF); int len = (int)((entry >> 8) & 0xF); - if (__builtin_expect(len > 0, 1)) { + if (VV_LIKELY(len > 0)) { /* Fast path: code ≤ 12 bits */ br_consume(&r, len); dst[i] = (uint8_t)sym; diff --git a/src/vv_simd.c b/src/vv_simd.c index f436cfd..f622747 100644 --- a/src/vv_simd.c +++ b/src/vv_simd.c @@ -1,12 +1,3 @@ -/* VaptVupt codec — originally Apache-2.0 by Cristian Cezar Moisés - * Integrated into Zupt — MIT License - * Copyright (c) 2026 Cristian Cezar Moisés - * SPDX-License-Identifier: MIT AND Apache-2.0 - */ -#if !defined(_DEFAULT_SOURCE) && !defined(_GNU_SOURCE) - #define _DEFAULT_SOURCE 1 -#endif - /* * VaptVupt — SIMD-accelerated copy routines * @@ -40,7 +31,8 @@ static void copy_match_scalar(uint8_t *dst, uint32_t offset, size_t length) { } if (length > 0) memcpy(dst, src, length); } else if (offset >= 8) { - /* Moderate overlap (8-15): 8-byte copy is safe since offset >= stride */ + /* Offset >= 8: can safely copy 8 bytes at a time — each chunk fits + * within the overlap window without reading unwritten bytes. */ while (length >= 8) { uint64_t v; memcpy(&v, src, 8); @@ -49,8 +41,18 @@ static void copy_match_scalar(uint8_t *dst, uint32_t offset, size_t length) { } while (length-- > 0) *dst++ = *src++; } else { - /* Very short overlap (1-3): byte-by-byte */ - for (size_t i = 0; i < length; i++) dst[i] = src[i]; + /* CRITICAL: for offset < 8 the "moderate overlap" 8-byte bulk copy + * is UNSAFE. Reading 8 bytes at src before writing means we read + * bytes at positions we're about to write, which may be uninitialized. + * + * Example: offset=7, length=8. src = dst-7. Read src[0..7] reads + * dst[-7..0]. But dst[0] is the first byte we'll WRITE, not a + * literal we already wrote. Bulk-read gets garbage there, then + * writes it to dst[7], corrupting position 7. + * + * Safe implementation: byte-by-byte, where each write feeds the + * next read correctly (the classic LZ "self-reference" pattern). */ + for (size_t i = 0; i < length; i++) dst[i] = dst[i - (ptrdiff_t)offset]; } } @@ -106,6 +108,33 @@ static void copy_match_avx2(uint8_t *dst, uint32_t offset, size_t length) { } #endif /* __AVX2__ */ +/* SSE2 path: baseline on all x86-64 CPUs. No runtime check needed. + * Used when AVX2 is not available at runtime, or when compiled without -mavx2. */ +#include /* SSE2 is guaranteed on x86-64 */ + +static void copy_fast_sse2(uint8_t *dst, const uint8_t *src, size_t n) { + while (n >= 16) { + __m128i v = _mm_loadu_si128((const __m128i *)src); + _mm_storeu_si128((__m128i *)dst, v); + dst += 16; src += 16; n -= 16; + } + if (n > 0) memcpy(dst, src, n); +} + +static void copy_match_sse2(uint8_t *dst, uint32_t offset, size_t length) { + const uint8_t *src = dst - offset; + if (offset >= 16) { + while (length >= 16) { + __m128i v = _mm_loadu_si128((const __m128i *)src); + _mm_storeu_si128((__m128i *)dst, v); + dst += 16; src += 16; length -= 16; + } + if (length > 0) memcpy(dst, src, length); + } else { + copy_match_scalar(dst, offset, length); + } +} + #endif /* x86-64 */ /* ═══════════════════════════════════════════════════════════════ @@ -160,6 +189,10 @@ static void vv_init_simd(void) { return; } #endif + /* SSE2 is baseline on all x86-64 — no runtime check needed */ + g_copy_fast = copy_fast_sse2; + g_copy_match = copy_match_sse2; + return; #endif #if defined(__aarch64__) && defined(__ARM_NEON) diff --git a/src/vv_xxh64.c b/src/vv_xxh64.c new file mode 100644 index 0000000..ea825b8 --- /dev/null +++ b/src/vv_xxh64.c @@ -0,0 +1,179 @@ +/* + * VaptVupt — XXH64 checksum (simplified, standalone) + * Based on xxHash by Yann Collet. Public domain. + */ + +#include "vaptvupt.h" +#include + +#define XXH_PRIME64_1 0x9E3779B185EBCA87ULL +#define XXH_PRIME64_2 0xC2B2AE3D27D4EB4FULL +#define XXH_PRIME64_3 0x165667B19E3779F9ULL +#define XXH_PRIME64_4 0x85EBCA77C2B2AE63ULL +#define XXH_PRIME64_5 0x27D4EB2F165667C5ULL + +static inline uint64_t xxh_rotl64(uint64_t x, int r) { return (x << r) | (x >> (64 - r)); } + +static inline uint64_t xxh_round(uint64_t acc, uint64_t input) { + acc += input * XXH_PRIME64_2; + acc = xxh_rotl64(acc, 31); + acc *= XXH_PRIME64_1; + return acc; +} + +static inline uint64_t xxh_merge_round(uint64_t acc, uint64_t val) { + val = xxh_round(0, val); + acc ^= val; + acc = acc * XXH_PRIME64_1 + XXH_PRIME64_4; + return acc; +} + +uint64_t vv_xxh64(const void *data, size_t len, uint64_t seed) { + const uint8_t *p = (const uint8_t *)data; + const uint8_t *end = p + len; + uint64_t h64; + + if (len >= 32) { + uint64_t v1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2; + uint64_t v2 = seed + XXH_PRIME64_2; + uint64_t v3 = seed + 0; + uint64_t v4 = seed - XXH_PRIME64_1; + + do { + uint64_t k; memcpy(&k, p, 8); v1 = xxh_round(v1, k); p += 8; + memcpy(&k, p, 8); v2 = xxh_round(v2, k); p += 8; + memcpy(&k, p, 8); v3 = xxh_round(v3, k); p += 8; + memcpy(&k, p, 8); v4 = xxh_round(v4, k); p += 8; + } while (p <= end - 32); + + h64 = xxh_rotl64(v1, 1) + xxh_rotl64(v2, 7) + xxh_rotl64(v3, 12) + xxh_rotl64(v4, 18); + h64 = xxh_merge_round(h64, v1); + h64 = xxh_merge_round(h64, v2); + h64 = xxh_merge_round(h64, v3); + h64 = xxh_merge_round(h64, v4); + } else { + h64 = seed + XXH_PRIME64_5; + } + + h64 += (uint64_t)len; + + while (p + 8 <= end) { + uint64_t k; memcpy(&k, p, 8); + k *= XXH_PRIME64_2; k = xxh_rotl64(k, 31); k *= XXH_PRIME64_1; + h64 ^= k; h64 = xxh_rotl64(h64, 27) * XXH_PRIME64_1 + XXH_PRIME64_4; + p += 8; + } + while (p + 4 <= end) { + uint32_t k; memcpy(&k, p, 4); + h64 ^= (uint64_t)k * XXH_PRIME64_1; + h64 = xxh_rotl64(h64, 23) * XXH_PRIME64_2 + XXH_PRIME64_3; + p += 4; + } + while (p < end) { + h64 ^= (*p) * XXH_PRIME64_5; + h64 = xxh_rotl64(h64, 11) * XXH_PRIME64_1; + p++; + } + + h64 ^= h64 >> 33; h64 *= XXH_PRIME64_2; + h64 ^= h64 >> 29; h64 *= XXH_PRIME64_3; + h64 ^= h64 >> 32; + return h64; +} + +/* ═══════════════════════════════════════════════════════════════ + * STREAMING XXH64 + * ═══════════════════════════════════════════════════════════════ */ + +void vv_xxh64_init(vv_xxh64_state_t *s, uint64_t seed) { + s->v1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2; + s->v2 = seed + XXH_PRIME64_2; + s->v3 = seed + 0; + s->v4 = seed - XXH_PRIME64_1; + s->total_len = 0; + s->buf_len = 0; + s->seed = seed; +} + +void vv_xxh64_update(vv_xxh64_state_t *s, const void *data, size_t len) { + const uint8_t *p = (const uint8_t *)data; + const uint8_t *end = p + len; + s->total_len += (uint64_t)len; + + /* Fill buffer if partial data pending */ + if (s->buf_len > 0) { + size_t fill = 32 - s->buf_len; + if (fill > len) fill = len; + memcpy(s->buf + s->buf_len, p, fill); + s->buf_len += fill; + p += fill; + if (s->buf_len < 32) return; /* still partial */ + /* Process the full 32 bytes */ + uint64_t k; + memcpy(&k, s->buf + 0, 8); s->v1 = xxh_round(s->v1, k); + memcpy(&k, s->buf + 8, 8); s->v2 = xxh_round(s->v2, k); + memcpy(&k, s->buf + 16, 8); s->v3 = xxh_round(s->v3, k); + memcpy(&k, s->buf + 24, 8); s->v4 = xxh_round(s->v4, k); + s->buf_len = 0; + } + + /* Process full 32-byte chunks */ + while (p + 32 <= end) { + uint64_t k; + memcpy(&k, p + 0, 8); s->v1 = xxh_round(s->v1, k); + memcpy(&k, p + 8, 8); s->v2 = xxh_round(s->v2, k); + memcpy(&k, p + 16, 8); s->v3 = xxh_round(s->v3, k); + memcpy(&k, p + 24, 8); s->v4 = xxh_round(s->v4, k); + p += 32; + } + + /* Buffer trailing bytes */ + if (p < end) { + size_t rem = (size_t)(end - p); + memcpy(s->buf + s->buf_len, p, rem); + s->buf_len += rem; + } +} + +uint64_t vv_xxh64_finalize(const vv_xxh64_state_t *s) { + uint64_t h64; + + if (s->total_len >= 32) { + h64 = xxh_rotl64(s->v1, 1) + xxh_rotl64(s->v2, 7) + + xxh_rotl64(s->v3, 12) + xxh_rotl64(s->v4, 18); + h64 = xxh_merge_round(h64, s->v1); + h64 = xxh_merge_round(h64, s->v2); + h64 = xxh_merge_round(h64, s->v3); + h64 = xxh_merge_round(h64, s->v4); + } else { + h64 = s->seed + XXH_PRIME64_5; + } + + h64 += s->total_len; + + const uint8_t *p = s->buf; + const uint8_t *end = p + s->buf_len; + + while (p + 8 <= end) { + uint64_t k; memcpy(&k, p, 8); + k *= XXH_PRIME64_2; k = xxh_rotl64(k, 31); k *= XXH_PRIME64_1; + h64 ^= k; h64 = xxh_rotl64(h64, 27) * XXH_PRIME64_1 + XXH_PRIME64_4; + p += 8; + } + while (p + 4 <= end) { + uint32_t k; memcpy(&k, p, 4); + h64 ^= (uint64_t)k * XXH_PRIME64_1; + h64 = xxh_rotl64(h64, 23) * XXH_PRIME64_2 + XXH_PRIME64_3; + p += 4; + } + while (p < end) { + h64 ^= (*p) * XXH_PRIME64_5; + h64 = xxh_rotl64(h64, 11) * XXH_PRIME64_1; + p++; + } + + h64 ^= h64 >> 33; h64 *= XXH_PRIME64_2; + h64 ^= h64 >> 29; h64 *= XXH_PRIME64_3; + h64 ^= h64 >> 32; + return h64; +} diff --git a/src/zupt_format.c b/src/zupt_format.c index cfc3ea2..4fc0bed 100644 --- a/src/zupt_format.c +++ b/src/zupt_format.c @@ -1345,6 +1345,9 @@ zupt_error_t zupt_list_archive(const char *arc, zupt_options_t *opts) { printf("\n ZUPT Archive: %s\n", arc); printf(" Format: v%u.%u | Blocks: %llu", hdr.version_major, hdr.version_minor, (unsigned long long)ft.total_blocks); if (hdr.global_flags & ZUPT_FLAG_ENCRYPTED) printf(" | Encrypted"); + if (hdr.global_flags & ZUPT_FLAG_PQ_HYBRID) printf(" | PQ"); + if (hdr.global_flags & ZUPT_FLAG_DEDUP) printf(" | Dedup"); + if (hdr.global_flags & ZUPT_FLAG_DISK_IMAGE) printf(" | Disk"); printf("\n\n"); printf(" %-50s %12s %12s %s\n", "Path", "Original", "Compressed", "Ratio"); printf(" %s\n", "--------------------------------------------------------------------------------------------"); @@ -1774,3 +1777,92 @@ zupt_error_t zupt_test_archive(const char *arc, zupt_options_t *opts) { free(ents); fclose(f); return fail>0 ? ZUPT_ERR_BAD_CHECKSUM : ZUPT_OK; } + +/* ═══════════════════════════════════════════════════════════════════ + * ARCHIVE INFO — read-only metadata inspection (no password needed) + * + * Shows: format version, creation time, UUID, flags (encrypted, + * solid, multithreaded, PQ, dedup, disk), archive size, block count. + * Does NOT decrypt or verify checksums — works on any archive. + * ═══════════════════════════════════════════════════════════════════ */ +zupt_error_t zupt_archive_info(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, "Error: Cannot open '%s': %s\n", path, strerror(errno)); return ZUPT_ERR_IO; } + + zupt_archive_header_t hdr; + if (fread(&hdr, sizeof(hdr), 1, f) != 1) { + fprintf(stderr, "Error: Not a zupt archive (file too small)\n"); + fclose(f); return ZUPT_ERR_CORRUPT; + } + if (hdr.magic[0]!=ZUPT_MAGIC_0 || hdr.magic[1]!=ZUPT_MAGIC_1 || + hdr.magic[2]!=ZUPT_MAGIC_2 || hdr.magic[3]!=ZUPT_MAGIC_3) { + fprintf(stderr, "Error: Not a zupt archive (bad magic)\n"); + fclose(f); return ZUPT_ERR_BAD_MAGIC; + } + + /* Archive file size */ + fseeko(f, 0, SEEK_END); + uint64_t file_size = (uint64_t)ftello(f); + char sz_buf[32]; + zupt_format_size(file_size, sz_buf, sizeof(sz_buf)); + + /* Try to read footer for block count */ + uint64_t total_blocks = 0; + int has_footer = 0; + if (file_size > sizeof(zupt_footer_t)) { + fseeko(f, -(int64_t)sizeof(zupt_footer_t), SEEK_END); + zupt_footer_t ft; + if (fread(&ft, sizeof(ft), 1, f) == 1 && + ft.footer_magic[0]=='Z' && ft.footer_magic[1]=='E' && + ft.footer_magic[2]=='N' && ft.footer_magic[3]=='D') { + total_blocks = ft.total_blocks; + has_footer = 1; + } + } + fclose(f); + + /* UUID */ + char uuid[40]; + snprintf(uuid, sizeof(uuid), + "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + hdr.archive_id[0], hdr.archive_id[1], hdr.archive_id[2], hdr.archive_id[3], + hdr.archive_id[4], hdr.archive_id[5], hdr.archive_id[6], hdr.archive_id[7], + hdr.archive_id[8], hdr.archive_id[9], hdr.archive_id[10], hdr.archive_id[11], + hdr.archive_id[12], hdr.archive_id[13], hdr.archive_id[14], hdr.archive_id[15]); + + /* Creation time */ + uint64_t ct_sec = hdr.creation_time / 1000000000ULL; + char timebuf[64] = "unknown"; + if (ct_sec > 0) { + time_t tt = (time_t)ct_sec; + struct tm *tm = localtime(&tt); + if (tm) strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm); + } + + /* Flags */ + uint32_t fl = hdr.global_flags; + + printf("\n"); + printf(" Archive: %s\n", path); + printf(" Size: %s (%llu bytes)\n", sz_buf, (unsigned long long)file_size); + printf(" Format: v%u.%u\n", hdr.version_major, hdr.version_minor); + printf(" UUID: %s\n", uuid); + printf(" Created: %s\n", timebuf); + if (has_footer) + printf(" Blocks: %llu\n", (unsigned long long)total_blocks); + printf(" Encrypted: %s\n", (fl & ZUPT_FLAG_ENCRYPTED) ? "YES" : "no"); + if (fl & ZUPT_FLAG_PQ_HYBRID) + printf(" PQ Hybrid: YES (ML-KEM-768 + X25519)\n"); + if (fl & ZUPT_FLAG_SOLID) + printf(" Solid: YES\n"); + if (fl & ZUPT_FLAG_MULTITHREADED) + printf(" Multithreaded: YES\n"); + if (fl & ZUPT_FLAG_DEDUP) + printf(" Dedup: YES (block-level)\n"); + if (fl & ZUPT_FLAG_DISK_IMAGE) + printf(" Disk image: YES\n"); + printf(" Flags: 0x%04X\n", fl); + printf("\n"); + + return ZUPT_OK; +} diff --git a/src/zupt_main.c b/src/zupt_main.c index 0253294..1b03e7c 100644 --- a/src/zupt_main.c +++ b/src/zupt_main.c @@ -33,6 +33,7 @@ static void usage(void) { " zupt extract [OPTIONS] \n" " zupt list [OPTIONS] \n" " zupt test [OPTIONS] \n" + " zupt info Archive metadata (no password needed)\n" " zupt bench Compare levels 1-9\n" " zupt disk backup|restore Full-disk backup/restore\n" " zupt keygen Key generation" @@ -129,6 +130,12 @@ int main(int argc, char **argv) { return 0; } + /* ─── info ─── */ + if (streq(cmd,"info")||streq(cmd,"i")) { + if (argc < 3) { fprintf(stderr, "Error: info requires \n"); return 1; } + return zupt_archive_info(argv[2]) != ZUPT_OK ? 1 : 0; + } + /* ─── compress ─── */ if (streq(cmd,"compress")||streq(cmd,"c")) { zupt_options_t opts; zupt_default_options(&opts); @@ -195,6 +202,24 @@ int main(int argc, char **argv) { banner(); + /* Password strength warning */ + if (opts.encrypt && opts.password[0]) { + size_t pwlen = strlen(opts.password); + int has_upper=0, has_lower=0, has_digit=0, has_special=0; + for (size_t pi=0; pi='A' && ch<='Z') has_upper=1; + else if (ch>='a' && ch<='z') has_lower=1; + else if (ch>='0' && ch<='9') has_digit=1; + else has_special=1; + } + int classes = has_upper + has_lower + has_digit + has_special; + if (pwlen < 8) + fprintf(stderr, " WARNING: Password is very short (%zu chars). Use 12+ chars for security.\n", pwlen); + else if (pwlen < 12 && classes < 3) + fprintf(stderr, " WARNING: Weak password. Use 12+ chars with mixed case, digits, and symbols.\n"); + } + /* Resolve thread count */ opts.threads = zupt_resolve_threads(opts.threads); if (opts.solid && opts.threads > 1) { diff --git a/tests/test_vaptvupt.c b/tests/test_vaptvupt.c index c14b3b2..5079c03 100644 --- a/tests/test_vaptvupt.c +++ b/tests/test_vaptvupt.c @@ -13,6 +13,7 @@ #endif #include "vaptvupt.h" +#include "zupt.h" #include #include #include