Release: v2.1.6 - Added VaptVupt 2.40
This commit is contained in:
parent
8e0eb1ee41
commit
572031910c
41 changed files with 4309 additions and 741 deletions
35
CHANGELOG.md
35
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 <archive>` 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 |
|
||||
|
|
|
|||
7
Makefile
7
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
17
ROADMAP.md
17
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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Security Policy — Zupt
|
||||
# Security Policy — Zupt
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
|
|
|
|||
21
gui/LICENSE-GUI
Normal file
21
gui/LICENSE-GUI
Normal file
|
|
@ -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.
|
||||
127
gui/README.md
Normal file
127
gui/README.md
Normal file
|
|
@ -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
|
||||
BIN
gui/assets/zupt-128.png
Normal file
BIN
gui/assets/zupt-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
gui/assets/zupt-icon.png
Normal file
BIN
gui/assets/zupt-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
gui/assets/zupt.ico
Normal file
BIN
gui/assets/zupt.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 802 B |
BIN
gui/assets/zupt.png
Normal file
BIN
gui/assets/zupt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
88
gui/install.sh
Executable file
88
gui/install.sh
Executable file
|
|
@ -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'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/x-zupt">
|
||||
<comment>Zupt Archive</comment>
|
||||
<glob pattern="*.zupt"/>
|
||||
<icon name="package-x-generic"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
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."
|
||||
49
gui/packaging/appimage/build-appimage.sh
Executable file
49
gui/packaging/appimage/build-appimage.sh
Executable file
|
|
@ -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
|
||||
13
gui/packaging/deb/control
Normal file
13
gui/packaging/deb/control
Normal file
|
|
@ -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 <cristian@zupt.dev>
|
||||
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.
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
37
gui/packaging/flatpak/dev.zupt.gui.yml
Normal file
37
gui/packaging/flatpak/dev.zupt.gui.yml
Normal file
|
|
@ -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: .
|
||||
43
gui/packaging/rpm/zupt-gui.spec
Normal file
43
gui/packaging/rpm/zupt-gui.spec
Normal file
|
|
@ -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
|
||||
92
gui/packaging/windows/build-windows.bat
Normal file
92
gui/packaging/windows/build-windows.bat
Normal file
|
|
@ -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
|
||||
132
gui/packaging/windows/zupt-installer.nsi
Normal file
132
gui/packaging/windows/zupt-installer.nsi
Normal file
|
|
@ -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
|
||||
12
gui/packaging/zupt-gui.desktop
Normal file
12
gui/packaging/zupt-gui.desktop
Normal file
|
|
@ -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;
|
||||
1
gui/requirements.txt
Normal file
1
gui/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
PySide6>=6.5
|
||||
32
gui/setup.py
Normal file
32
gui/setup.py
Normal file
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
569
gui/src/zupt_gui.py
Normal file
569
gui/src/zupt_gui.py
Normal file
|
|
@ -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()
|
||||
46
gui/zupt-gui
Executable file
46
gui/zupt-gui
Executable file
|
|
@ -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" "$@"
|
||||
|
|
@ -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 <stdint.h>
|
||||
#include "vv_platform.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
117
include/vv_platform.h
Normal file
117
include/vv_platform.h
Normal file
|
|
@ -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 <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ─── 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 <emmintrin.h>
|
||||
#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 <intrin.h>
|
||||
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 */
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
981
src/vv_ans.c
981
src/vv_ans.c
File diff suppressed because it is too large
Load diff
702
src/vv_decoder.c
702
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 <string.h>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1208
src/vv_encoder.c
1208
src/vv_encoder.c
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <emmintrin.h> /* 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)
|
||||
|
|
|
|||
179
src/vv_xxh64.c
Normal file
179
src/vv_xxh64.c
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* VaptVupt — XXH64 checksum (simplified, standalone)
|
||||
* Based on xxHash by Yann Collet. Public domain.
|
||||
*/
|
||||
|
||||
#include "vaptvupt.h"
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ static void usage(void) {
|
|||
" zupt extract [OPTIONS] <archive.zupt>\n"
|
||||
" zupt list [OPTIONS] <archive.zupt>\n"
|
||||
" zupt test [OPTIONS] <archive.zupt>\n"
|
||||
" zupt info <archive.zupt> Archive metadata (no password needed)\n"
|
||||
" zupt bench <files/dirs...> 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 <archive.zupt>\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<pwlen; pi++) {
|
||||
unsigned char ch = (unsigned char)opts.password[pi];
|
||||
if (ch>='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) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#endif
|
||||
|
||||
#include "vaptvupt.h"
|
||||
#include "zupt.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
|
|||
Loading…
Reference in a new issue