Fixes the "GUI functions don't work on GNU Guix" report and a batch of
adversarial-audit findings across the CLI, decoder, crypto and packaging.
GUI (gui/src/zupt_gui.py)
- Root cause of the broken GUI: every encryption path defaulted to the
libzuptsdk "SDK v2" modes, which are absent from the source-only build and
exit 1. Reworked Keys/Compress/Extract around the native modes with a
build-aware PQ-mode selector: Hybrid (--pq, default), Full PQ (--pq-only),
and SDK v2 only when the binary reports WITH_SDK support. Capability is
detected from `version` ("Build:"/"KDF:" lines) with a `help` fallback.
- Extract/Verify gain a PQ private-key input with auto-detect (reads the
archive via `info` to pick --pq vs --pq-only). Verify could not verify any
PQ archive before (password field only).
- run_async now holds a LIST of in-flight (thread, worker) refs; DiskTab's two
buttons previously shared one slot, so a second op GC'd the first QThread
mid-run. About tab corrected (codec 2.60.4, PBKDF2 default, --pq-only, URL).
CLI (src/zupt_main.c)
- compress -p <archive> <files> swallowed the archive name as the password and
truncated the first input file (silent data loss, exit 0). Added a
data-loss guard: refuse to overwrite an existing non-.zupt file as the output
archive unless -y/--force; plus a self-overwrite guard.
- compress <archive> <src> -p <pw> wrote an UNENCRYPTED archive (exit 0)
because options after the first positional were treated as files. Now errors
on a misplaced option (with a `--` escape for real dashed filenames).
- Removed duplicated dead --pq-box/--pq-sdk branches. version/banner/usage now
state the build's real KDF (PBKDF2 on source-only) and repo URL; examples
lead with native --pq / --pq-only.
Security (audit findings)
- vv_decoder.c: the two AVX2 fast-path token decoders read a 2-/3-byte match
offset after only checking 1 byte remained -> heap over-read on a crafted
archive. Added the `ip + off_bytes > ip_end` guard the tail path already had.
- zupt_crypto.c: wipe ML-KEM/X25519 secret-key buffers when hybrid decrypt
init fails on key read (matches the pq-only path).
- zupt_format.c: bound attacker-controlled encryption_header_off before the
(off_t)+7 arithmetic in the info enc_type reader (avoid signed-overflow UB).
Packaging (would fail source-only)
- debian/rules (staged into debian/zupt, installed vendored .so), aur/PKGBUILD,
nix/flake.nix, homebrew (vendored .so + nonexistent AUDIT.md, stale /zupt
URLs) now build source-only. opensuse spec %files ships the shell completions
make install writes (rpmbuild no longer fails on unpackaged files);
_service + debian/control point at the vaptvupt repo; KDF claim corrected.
Cross-platform GUI packaging (new)
- packaging/portable/: OS-agnostic GUI package (zupt_gui.py + .bat/.command/.sh
launchers + README) that runs on Windows/macOS/Linux/BSD with Python+PySide6.
- .github/workflows/cross-platform.yml: builds real native binaries on
windows-latest + macos-latest runners (CLI, PyInstaller GUI, Inno Setup
installer, .dmg) and the portable zip, attaching them to the release on tag.
- packaging/windows/vaptvupt-gui.iss: Inno Setup installer script.
Validation: make check 16/16 (all distro-safe checks), GUI imports + command
contracts verified against the fixed CLI. test_help_consistency updated to
assert the truthful (build-aware) default KDF.
1003 lines
51 KiB
C
1003 lines
51 KiB
C
/*
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
* Copyright (c) 2025-2026 Cristian Cezar Moisés
|
|
* ZUPT - CLI v1.5.0
|
|
* Multi-threaded compression, AES-256 encryption, progress bars
|
|
*/
|
|
#include "zupt.h"
|
|
#include "zupt_thread.h"
|
|
#include "zupt_cpuid.h"
|
|
#include "vaptvupt.h" /* VAPTVUPT: codec ID */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h> /* stat()/S_ISREG for the compress output-overwrite guard */
|
|
|
|
/* MSVC's <sys/stat.h> defines _S_IFREG/S_IFREG but not the S_ISREG macro. */
|
|
#ifndef S_ISREG
|
|
# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#include <conio.h>
|
|
#else
|
|
#include <termios.h>
|
|
#endif
|
|
|
|
static void banner(void) {
|
|
fprintf(stderr,
|
|
"%s %s - %s\n"
|
|
"Format v%d.%d | Codec: VaptVupt + Zupt-LZ | Checksum: XXH64\n"
|
|
"Encryption: AES-256-CTR + HMAC-SHA256 | KDF: "
|
|
#ifdef ZUPT_WITH_SDK
|
|
"Argon2id (default) / PBKDF2 (--kdf pbkdf2)\n\n",
|
|
#else
|
|
"PBKDF2-SHA256 (Argon2id needs a WITH_SDK=1 build)\n\n",
|
|
#endif
|
|
ZUPT_PRODUCT_NAME, ZUPT_VERSION_STRING, ZUPT_PRODUCT_TAGLINE,
|
|
ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR);
|
|
}
|
|
|
|
static void usage(void) {
|
|
banner();
|
|
/* usage() text exceeds C99's 4095-char string-literal limit, so we
|
|
* split it into logical sections, one fprintf call per section.
|
|
* Don't merge these back into a single literal — see F-13 in
|
|
* AUDIT.md for the regression test (tests/test_help_consistency.sh)
|
|
* that asserts this. */
|
|
|
|
/* ── Section 1: synopsis ── */
|
|
fprintf(stderr,
|
|
"Usage:\n"
|
|
" vaptvupt compress [OPTIONS] <output.zupt> <files/dirs...>\n"
|
|
" vaptvupt extract [OPTIONS] <archive.zupt>\n"
|
|
" vaptvupt list [OPTIONS] <archive.zupt>\n"
|
|
" vaptvupt test [OPTIONS] <archive.zupt>\n"
|
|
" vaptvupt info <archive.zupt> Archive metadata (no password needed)\n"
|
|
" vaptvupt bench <files/dirs...> Compare levels 1-9\n"
|
|
" vaptvupt disk backup|restore Full-disk backup/restore\n"
|
|
" vaptvupt keygen Key generation\n"
|
|
" vaptvupt version\n"
|
|
" vaptvupt help\n"
|
|
"\n"
|
|
"Note: archive extension stays .zupt for format continuity.\n"
|
|
" The `zupt` command is preserved as a legacy alias.\n"
|
|
"\n");
|
|
|
|
/* ── Section 2: compress options ── */
|
|
fprintf(stderr,
|
|
"Compress Options:\n"
|
|
" -l, --level <1-9> Compression level (default: 7)\n"
|
|
" 1-2: fast, small window\n"
|
|
" 3-5: balanced\n"
|
|
" 6-7: high compression (default)\n"
|
|
" 8-9: maximum, 1MB window, deep search\n"
|
|
" -b, --block <SIZE> Block size in bytes (default: 128KB)\n"
|
|
" -s, --store Store without compression\n"
|
|
" -f, --fast Use fast LZ codec (less compression)\n"
|
|
" --vv, --vaptvupt Use VaptVupt codec (LZ + ANS entropy, default)\n"
|
|
" --lzhp Use Zupt-LZHP codec (LZ77+Huffman, no SIMD needed)\n"
|
|
" -p, --password <PW> Encrypt with AES-256 (bare -p prompts). Options must\n"
|
|
" precede <output.zupt>; a value ending in .zupt is\n"
|
|
" taken as the password, so put -p before the archive.\n"
|
|
#ifdef ZUPT_WITH_SDK
|
|
" --kdf <argon2id|pbkdf2> KDF for password mode. Default: argon2id.\n"
|
|
" Use 'pbkdf2' for v2.4.0-and-older reader compatibility.\n"
|
|
#else
|
|
" --kdf <pbkdf2> KDF for password mode. Default (and only, this build):\n"
|
|
" PBKDF2-SHA256 600k. Argon2id needs a WITH_SDK=1 build.\n"
|
|
#endif
|
|
" -c, --comment <TEXT> Embed a free-form archive comment (v2.4.3+).\n"
|
|
" --comment-file <FILE> Read comment from file (max 4096 bytes).\n"
|
|
" --pq <pubkey> Post-quantum HYBRID encryption (ML-KEM-768 + X25519) [recommended]\n"
|
|
" --pq-only <pubkey> FULL post-quantum encryption (ML-KEM-768 only, no classical layer)\n"
|
|
" --pq-sdk <pubkey> Post-quantum encryption via libzuptsdk (WITH_SDK=1 builds only)\n"
|
|
" --pq-box <pubkey> Post-quantum sealed box via libpqvaptvupt (WITH_SDK=1 builds only)\n"
|
|
" --dedup, -D Block-level deduplication\n"
|
|
" --solid Solid mode (single stream)\n"
|
|
" -y, --force Overwrite an existing non-.zupt file as the output archive\n"
|
|
" -v, --verbose Verbose per-file output\n"
|
|
" -t, --threads <N> Thread count (0=auto, 1=single, 2-64=explicit)\n"
|
|
"\n");
|
|
|
|
/* ── Section 3: extract/list/test options ── */
|
|
fprintf(stderr,
|
|
"Extract/List/Test Options:\n"
|
|
" -o, --output <DIR> Output directory (extract only)\n"
|
|
" -p, --password <PW> Decryption password\n"
|
|
" --pq <privkey> Post-quantum HYBRID decryption (ML-KEM-768 + X25519)\n"
|
|
" --pq-only <privkey> FULL post-quantum decryption (ML-KEM-768 only)\n"
|
|
" --pq-sdk <privkey> Post-quantum decryption via libzuptsdk (WITH_SDK=1 builds only)\n"
|
|
" --pq-box <privkey> Post-quantum sealed-box decryption (libpqvaptvupt)\n"
|
|
" -v, --verbose Verbose output\n"
|
|
" -t, --threads <N> Thread count for decompression\n"
|
|
"\n"
|
|
"Keygen Options:\n"
|
|
" -o <file> Output keyfile path (required)\n"
|
|
" --pub Export public key from existing private key (-k)\n"
|
|
" -k <privkey> Source private keyfile (with --pub)\n"
|
|
" (default) Generate HYBRID keypair (ML-KEM-768 + X25519) for --pq\n"
|
|
" --pq-only Generate FULL post-quantum keypair (ML-KEM-768 only) for --pq-only\n"
|
|
" --sdk, --pq-sdk Generate SDK v2 keypair (libzuptsdk; WITH_SDK=1 builds only)\n"
|
|
" --box, --pq-box Generate pq-box keypair (libpqvaptvupt; WITH_SDK=1 builds only)\n"
|
|
" Use each key with its matching mode.\n"
|
|
"\n"
|
|
"Directories are traversed recursively.\n"
|
|
"\n");
|
|
|
|
/* ── Section 4: examples ── */
|
|
fprintf(stderr,
|
|
"Examples:\n"
|
|
" # Post-quantum HYBRID workflow (--pq, recommended)\n"
|
|
" vaptvupt keygen -o mykey.key # Generate hybrid private key\n"
|
|
" vaptvupt keygen --pub -o pub.key -k mykey.key # Export public key\n"
|
|
" vaptvupt compress --pq pub.key backup.zupt ~/Documents/ # Encrypt\n"
|
|
" vaptvupt extract --pq mykey.key backup.zupt -o ~/restored/ # Decrypt\n"
|
|
"\n"
|
|
" # Full (pure) post-quantum workflow (--pq-only, ML-KEM-768 only)\n"
|
|
" vaptvupt keygen --pq-only -o pqkey # Generate pq-only private key\n"
|
|
" vaptvupt keygen --pub --pq-only -o pqkey.pub -k pqkey # Export public key\n"
|
|
" vaptvupt compress --pq-only pqkey.pub backup.zupt files/ # Encrypt (no classical layer)\n"
|
|
" vaptvupt extract --pq-only pqkey backup.zupt -o out/ # Decrypt\n"
|
|
"\n"
|
|
" # Conventional / password (PBKDF2-SHA256)\n"
|
|
" vaptvupt compress backup.zupt ~/Documents/ # No encryption\n"
|
|
" vaptvupt compress -l 9 -p mysecret secure.zupt data/ # Password + max compression\n"
|
|
" vaptvupt list secure.zupt -p mysecret # List with password\n"
|
|
" vaptvupt extract -o restored/ -p mysecret secure.zupt # Extract with password\n"
|
|
" vaptvupt bench ~/Documents/ # Benchmark\n"
|
|
"\n"
|
|
" # SDK v2 / sealed-box modes require an upstream 'make WITH_SDK=1' build:\n"
|
|
" # keygen --sdk / --box, compress/extract --pq-sdk / --pq-box\n"
|
|
"\n");
|
|
|
|
/* ── Section 5: footer ── */
|
|
fprintf(stderr,
|
|
"Default codec: VaptVupt LZ + ANS " ZUPT_CODEC_RELEASE " (AVX2/NEON SIMD)\n"
|
|
"Encryption: AES-256-CTR + HMAC-SHA256 (Encrypt-then-MAC)\n"
|
|
#ifdef ZUPT_WITH_SDK
|
|
"KDF: Argon2id (default); PBKDF2-SHA256 600k iter via --kdf pbkdf2\n"
|
|
#else
|
|
"KDF: PBKDF2-SHA256 600k iter (default; Argon2id needs WITH_SDK=1)\n"
|
|
#endif
|
|
"Post-quantum: --pq (hybrid ML-KEM-768 + X25519, recommended); --pq-only (ML-KEM-768 only)\n"
|
|
"Format: v1.6 (since v2.3.1); archives byte-compatible with v2.3.1+\n"
|
|
"\n"
|
|
"License: AGPL-3.0-or-later (VaptVupt) + GPL-3.0-or-later (VaptVupt codec)\n"
|
|
" Dual-licensed: commercial license available: sac@securityops.co\n"
|
|
"Project: https://git.securityops.co/cristiancmoises/vaptvupt\n"
|
|
);
|
|
}
|
|
|
|
/* Securely prompt for password (hide input) */
|
|
static void prompt_password(const char *prompt, char *buf, size_t cap) {
|
|
fprintf(stderr, "%s", prompt);
|
|
#ifdef _WIN32
|
|
size_t i = 0;
|
|
while (i < cap - 1) {
|
|
int c = _getch();
|
|
if (c == '\r' || c == '\n') break;
|
|
if (c == '\b' && i > 0) { i--; continue; }
|
|
buf[i++] = (char)c;
|
|
}
|
|
buf[i] = '\0';
|
|
fprintf(stderr, "\n");
|
|
#else
|
|
struct termios old, new_t;
|
|
tcgetattr(0, &old);
|
|
new_t = old;
|
|
/* Clear the ECHO bit. ~ECHO is `int` (negative); c_lflag is
|
|
* tcflag_t (unsigned int). The cast makes the conversion
|
|
* explicit and silences -Wsign-conversion. */
|
|
new_t.c_lflag &= (tcflag_t)~ECHO;
|
|
tcsetattr(0, TCSANOW, &new_t);
|
|
if (fgets(buf, (int)cap, stdin)) {
|
|
size_t len = strlen(buf);
|
|
if (len > 0 && buf[len-1] == '\n') buf[len-1] = '\0';
|
|
}
|
|
tcsetattr(0, TCSANOW, &old);
|
|
fprintf(stderr, "\n");
|
|
#endif
|
|
}
|
|
|
|
static int streq(const char *a, const char *b) { return strcmp(a,b)==0; }
|
|
static int isopt(const char *a) { return a[0]=='-'; }
|
|
|
|
int main(int argc, char **argv) {
|
|
/* Detect CPU features (AES-NI, AVX2) at startup */
|
|
zupt_detect_cpu(&zupt_cpu);
|
|
|
|
if (argc < 2) { usage(); return 1; }
|
|
const char *cmd = argv[1];
|
|
|
|
if (streq(cmd,"help")||streq(cmd,"--help")||streq(cmd,"-h")) { usage(); return 0; }
|
|
if (streq(cmd,"version")||streq(cmd,"--version")||streq(cmd,"-V")) {
|
|
printf("vaptvupt %s (formerly zupt; renamed in v3.0.0 — INPI Brasil trademark)\n"
|
|
"Format: v%d.%d | Archive extension: .zupt (unchanged)\n"
|
|
"Codec: VaptVupt " ZUPT_CODEC_RELEASE " (0x%04X) — LZ + ANS, optimal parser + large-window extreme\n"
|
|
"Encryption: AES-256-CTR + HMAC-SHA256\n"
|
|
#ifdef ZUPT_WITH_SDK
|
|
"KDF: Argon2id (default) / PBKDF2-SHA256 %d iter (--kdf pbkdf2)\n"
|
|
"Post-quantum: --pq hybrid (ML-KEM-768 + X25519), --pq-only (ML-KEM-768), --pq-sdk/--pq-box (libzuptsdk)\n"
|
|
"Build: full (libzuptsdk: Argon2id, --pq-sdk, --pq-box available)\n"
|
|
#else
|
|
"KDF: PBKDF2-SHA256 %d iter (default; Argon2id needs WITH_SDK=1)\n"
|
|
"Post-quantum: --pq hybrid (ML-KEM-768 + X25519), --pq-only (ML-KEM-768 only) — FIPS 203 + RFC 7748\n"
|
|
"Build: source-only (native crypto; --pq-sdk/--pq-box/Argon2id need WITH_SDK=1)\n"
|
|
#endif
|
|
"License: AGPL-3.0-or-later (VaptVupt) + GPL-3.0-or-later (codec)\n"
|
|
" Dual-licensed: commercial license available\n"
|
|
"Project: https://git.securityops.co/cristiancmoises/vaptvupt\n"
|
|
"Commercial: sac@securityops.co\n",
|
|
ZUPT_VERSION_STRING, ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR,
|
|
ZUPT_CODEC_VAPTVUPT, ZUPT_KDF_ITERATIONS);
|
|
/* Runtime crypto hardware acceleration (reflects this CPU). */
|
|
printf("HW accel (this CPU):");
|
|
int any = 0;
|
|
if (zupt_cpu.has_aesni && zupt_cpu.has_avx) { printf(" AES-NI"); any = 1; }
|
|
if (zupt_cpu.has_shani) { printf(" SHA-NI"); any = 1; }
|
|
if (zupt_cpu.has_avx2) { printf(" AVX2(codec)"); any = 1; }
|
|
printf("%s\n", any ? "" : " none (portable C fallback)");
|
|
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);
|
|
int ai = 2;
|
|
int force = 0; /* -y/--force: allow overwriting a non-.zupt output */
|
|
while (ai<argc && isopt(argv[ai])) {
|
|
if ((streq(argv[ai],"-l")||streq(argv[ai],"--level"))&&ai+1<argc) {
|
|
opts.level=atoi(argv[++ai]); if(opts.level<1)opts.level=1; if(opts.level>9)opts.level=9;
|
|
} else if ((streq(argv[ai],"-b")||streq(argv[ai],"--block"))&&ai+1<argc) {
|
|
opts.block_size=(uint32_t)atol(argv[++ai]);
|
|
if(opts.block_size<ZUPT_MIN_BLOCK_SZ)opts.block_size=ZUPT_MIN_BLOCK_SZ;
|
|
if(opts.block_size>ZUPT_MAX_BLOCK_SZ)opts.block_size=ZUPT_MAX_BLOCK_SZ;
|
|
} else if (streq(argv[ai],"-s")||streq(argv[ai],"--store")) {
|
|
opts.codec_id=ZUPT_CODEC_STORE;
|
|
} else if (streq(argv[ai],"-f")||streq(argv[ai],"--fast")) {
|
|
opts.codec_id=ZUPT_CODEC_ZUPT_LZ;
|
|
} else if (streq(argv[ai],"--vv")||streq(argv[ai],"--vaptvupt")) {
|
|
opts.codec_id=ZUPT_CODEC_VAPTVUPT; /* VAPTVUPT */
|
|
} else if (streq(argv[ai],"--lzhp")) {
|
|
opts.codec_id=ZUPT_CODEC_ZUPT_LZHP;
|
|
} else if (streq(argv[ai],"-p")||streq(argv[ai],"--password")) {
|
|
opts.encrypt=1;
|
|
if (ai+1<argc && !isopt(argv[ai+1])) {
|
|
strncpy(opts.password, argv[++ai], sizeof(opts.password)-1);
|
|
} else {
|
|
prompt_password("Password: ", opts.password, sizeof(opts.password));
|
|
char confirm[256];
|
|
prompt_password("Confirm: ", confirm, sizeof(confirm));
|
|
if (strcmp(opts.password, confirm)!=0) {
|
|
fprintf(stderr, "Error: Passwords do not match.\n"); return 1;
|
|
}
|
|
}
|
|
} else if (streq(argv[ai],"-v")||streq(argv[ai],"--verbose")) {
|
|
opts.verbose=1;
|
|
} else if (streq(argv[ai],"--solid")||streq(argv[ai],"-S")) {
|
|
opts.solid=1;
|
|
} else if ((streq(argv[ai],"-t")||streq(argv[ai],"--threads"))&&ai+1<argc) {
|
|
opts.threads=atoi(argv[++ai]);
|
|
if(opts.threads<0)opts.threads=0;
|
|
if(opts.threads>ZUPT_MAX_THREADS)opts.threads=ZUPT_MAX_THREADS;
|
|
} else if (streq(argv[ai],"--pq-box")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.box_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
|
opts.pqonly_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--dedup")||streq(argv[ai],"-D")) {
|
|
opts.dedup=1;
|
|
} else if ((streq(argv[ai],"-c")||streq(argv[ai],"--comment"))&&ai+1<argc) {
|
|
/* v2.4.3: free-form archive comment. Encrypted along with
|
|
* data blocks when -p/--pq is also set. */
|
|
ai++;
|
|
strncpy(opts.comment, argv[ai], ZUPT_MAX_COMMENT_LEN - 1);
|
|
opts.comment[ZUPT_MAX_COMMENT_LEN - 1] = '\0';
|
|
opts.has_comment = 1;
|
|
} else if (streq(argv[ai],"--comment-file")&&ai+1<argc) {
|
|
ai++;
|
|
FILE *cf = fopen(argv[ai], "rb");
|
|
if (!cf) {
|
|
fprintf(stderr, "Error: --comment-file: cannot open '%s'\n", argv[ai]);
|
|
return 1;
|
|
}
|
|
size_t n = fread(opts.comment, 1, ZUPT_MAX_COMMENT_LEN - 1, cf);
|
|
opts.comment[n] = '\0';
|
|
while (n > 0 && (opts.comment[n-1] == '\n' || opts.comment[n-1] == '\r')) {
|
|
opts.comment[--n] = '\0';
|
|
}
|
|
opts.has_comment = (n > 0);
|
|
fclose(cf);
|
|
} else if (streq(argv[ai],"--kdf")&&ai+1<argc) {
|
|
/* v2.4.1: explicit KDF selection for password mode.
|
|
* Default (without --kdf): Argon2id. Use --kdf pbkdf2
|
|
* for compatibility with v2.4.0 and older readers. */
|
|
ai++;
|
|
if (streq(argv[ai],"pbkdf2")) {
|
|
opts.kdf_legacy_pbkdf2 = 1;
|
|
} else if (streq(argv[ai],"argon2id") || streq(argv[ai],"argon2")) {
|
|
opts.kdf_legacy_pbkdf2 = 0;
|
|
} else {
|
|
fprintf(stderr, "Error: --kdf must be 'argon2id' or 'pbkdf2', got '%s'\n", argv[ai]);
|
|
return 1;
|
|
}
|
|
} else if (streq(argv[ai],"-y")||streq(argv[ai],"--force")) {
|
|
force = 1;
|
|
} else {
|
|
fprintf(stderr,"Error: Unknown option '%s'\n",argv[ai]); return 1;
|
|
}
|
|
ai++;
|
|
}
|
|
if (argc-ai<2) {
|
|
fprintf(stderr,"Error: compress requires <output.zupt> <files/dirs...>\n"); return 1;
|
|
}
|
|
const char *output = argv[ai++];
|
|
|
|
/* Reject a misplaced option among the file positionals. Without this,
|
|
* `compress out.zupt dir -p secret` silently treats "-p"/"secret" as
|
|
* (skipped) input files and writes an UNENCRYPTED archive with exit 0
|
|
* — the user believes it is encrypted. Options must precede the
|
|
* output archive (use `--` before a real filename that starts with '-'). */
|
|
int seen_dashdash = 0;
|
|
for (int i=ai; i<argc; i++) {
|
|
if (!seen_dashdash && streq(argv[i],"--")) { seen_dashdash = 1; continue; }
|
|
if (!seen_dashdash && isopt(argv[i])) {
|
|
fprintf(stderr,
|
|
"Error: option '%s' appears after the output archive.\n"
|
|
" In compress, all options (including -p/--pq) must come BEFORE\n"
|
|
" the output archive name. Example:\n"
|
|
" vaptvupt compress -p PASSWORD %s %s ...\n",
|
|
argv[i], output, (i>ai ? argv[ai] : "<files>"));
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Data-loss guard. `compress -p out.zupt a.txt b.txt` makes -p swallow
|
|
* "out.zupt" as the PASSWORD, shifts positionals so the output archive
|
|
* becomes "a.txt", and truncates a.txt (a user data file) with archive
|
|
* bytes — silently, exit 0. Refuse to overwrite an existing regular file
|
|
* that is not a .zupt archive unless -y/--force is given. Archives the
|
|
* tool writes end in .zupt, so this never blocks normal use. */
|
|
{
|
|
struct stat ost;
|
|
size_t olen = strlen(output);
|
|
int is_zupt = (olen >= 5 && strcmp(output + olen - 5, ".zupt") == 0);
|
|
if (!force && !is_zupt && stat(output, &ost) == 0 && S_ISREG(ost.st_mode)) {
|
|
fprintf(stderr,
|
|
"Error: refusing to overwrite existing file '%s' as the output archive\n"
|
|
" (it does not end in .zupt). If you meant to set a password, use\n"
|
|
" '-p<password>' or put '-p PASSWORD' BEFORE the archive name.\n"
|
|
" Pass -y/--force to overwrite '%s' anyway.\n",
|
|
output, output);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Skip a leading `--` separator before the file list. */
|
|
if (ai < argc && streq(argv[ai], "--")) ai++;
|
|
|
|
/* Collect files (expand directories recursively). Guard against the
|
|
* output archive also being one of the inputs (self-overwrite). */
|
|
zupt_filelist_t fl; zupt_filelist_init(&fl);
|
|
for (int i=ai; i<argc; i++) {
|
|
if (streq(argv[i], output)) {
|
|
fprintf(stderr, "Error: input '%s' is the same as the output archive.\n", argv[i]);
|
|
zupt_filelist_free(&fl); return 1;
|
|
}
|
|
zupt_collect_files(&fl, argv[i], argv[i]);
|
|
}
|
|
|
|
if (fl.count == 0) {
|
|
fprintf(stderr, "Error: No files found.\n");
|
|
zupt_filelist_free(&fl); return 1;
|
|
}
|
|
|
|
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) {
|
|
fprintf(stderr, " Note: solid mode is single-threaded (cross-file LZ context)\n");
|
|
opts.threads = 1;
|
|
}
|
|
|
|
/* Resolve AUTO codec based on hardware detection */
|
|
if (opts.codec_id == ZUPT_CODEC_AUTO)
|
|
opts.codec_id = zupt_resolve_auto_codec();
|
|
|
|
fprintf(stderr, " Collected %d file(s) for compression%s\n", fl.count,
|
|
opts.solid ? " (SOLID MODE)" : "");
|
|
if (opts.threads > 1)
|
|
fprintf(stderr, " Threads: %d\n", opts.threads);
|
|
if (opts.encrypt) fprintf(stderr, " Encryption: ENABLED\n");
|
|
fprintf(stderr, "\n");
|
|
|
|
zupt_error_t err;
|
|
if (opts.solid) {
|
|
err = zupt_compress_solid(output,
|
|
(const char**)fl.arc_paths, (const char**)fl.paths, fl.count, &opts);
|
|
} else {
|
|
err = zupt_compress_files(output,
|
|
(const char**)fl.arc_paths, (const char**)fl.paths, fl.count, &opts);
|
|
}
|
|
zupt_filelist_free(&fl);
|
|
zupt_secure_wipe(opts.password, sizeof(opts.password));
|
|
return err==ZUPT_OK ? 0 : 1;
|
|
}
|
|
|
|
/* ─── extract ─── */
|
|
if (streq(cmd,"extract")||streq(cmd,"x")) {
|
|
zupt_options_t opts; zupt_default_options(&opts);
|
|
const char *outdir = NULL;
|
|
const char *archive = NULL;
|
|
/* POSIX-friendly: accept options before OR after the positional
|
|
* archive argument. Scan all argv from index 2; pick first
|
|
* non-option as archive, parse all options regardless of order. */
|
|
int ai = 2;
|
|
while (ai < argc) {
|
|
if (!isopt(argv[ai])) {
|
|
if (!archive) { archive = argv[ai]; ai++; continue; }
|
|
fprintf(stderr, "Error: unexpected extra argument '%s'\n", argv[ai]); return 1;
|
|
}
|
|
if ((streq(argv[ai],"-o")||streq(argv[ai],"--output"))&&ai+1<argc)
|
|
outdir = argv[++ai];
|
|
else if (streq(argv[ai],"-p")||streq(argv[ai],"--password")) {
|
|
opts.encrypt=1;
|
|
if (ai+1<argc && !isopt(argv[ai+1])) strncpy(opts.password,argv[++ai],sizeof(opts.password)-1);
|
|
else prompt_password("Password: ", opts.password, sizeof(opts.password));
|
|
} else if (streq(argv[ai],"-v")||streq(argv[ai],"--verbose")) opts.verbose=1;
|
|
else if ((streq(argv[ai],"-t")||streq(argv[ai],"--threads"))&&ai+1<argc) {
|
|
opts.threads=atoi(argv[++ai]);
|
|
if(opts.threads<0)opts.threads=0;
|
|
if(opts.threads>ZUPT_MAX_THREADS)opts.threads=ZUPT_MAX_THREADS;
|
|
}
|
|
else if (streq(argv[ai],"--pq-box")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.box_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
|
opts.pqonly_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
}
|
|
else { fprintf(stderr,"Unknown option '%s'\n",argv[ai]); return 1; }
|
|
ai++;
|
|
}
|
|
if (!archive) { fprintf(stderr,"Error: extract requires <archive.zupt>\n"); return 1; }
|
|
banner();
|
|
zupt_error_t err = zupt_extract_archive(archive, outdir, &opts);
|
|
zupt_secure_wipe(opts.password, sizeof(opts.password));
|
|
return err==ZUPT_OK ? 0 : 1;
|
|
}
|
|
|
|
/* ─── list ─── */
|
|
if (streq(cmd,"list")||streq(cmd,"l")) {
|
|
zupt_options_t opts; zupt_default_options(&opts);
|
|
const char *archive = NULL;
|
|
int ai = 2;
|
|
while (ai < argc) {
|
|
if (!isopt(argv[ai])) {
|
|
if (!archive) { archive = argv[ai]; ai++; continue; }
|
|
fprintf(stderr, "Error: unexpected extra argument '%s'\n", argv[ai]); return 1;
|
|
}
|
|
if (streq(argv[ai],"-v")||streq(argv[ai],"--verbose")) opts.verbose=1;
|
|
else if (streq(argv[ai],"-p")||streq(argv[ai],"--password")) {
|
|
opts.encrypt=1;
|
|
if (ai+1<argc && !isopt(argv[ai+1])) strncpy(opts.password,argv[++ai],sizeof(opts.password)-1);
|
|
else prompt_password("Password: ", opts.password, sizeof(opts.password));
|
|
}
|
|
else if (streq(argv[ai],"--pq-box")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.box_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
|
opts.pqonly_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
}
|
|
else { fprintf(stderr,"Unknown option '%s'\n",argv[ai]); return 1; }
|
|
ai++;
|
|
}
|
|
if (!archive) { fprintf(stderr,"Error: list requires <archive.zupt>\n"); return 1; }
|
|
zupt_error_t err = zupt_list_archive(archive, &opts);
|
|
zupt_secure_wipe(opts.password, sizeof(opts.password));
|
|
return err==ZUPT_OK ? 0 : 1;
|
|
}
|
|
|
|
/* ─── test ─── */
|
|
if (streq(cmd,"test")||streq(cmd,"t")) {
|
|
zupt_options_t opts; zupt_default_options(&opts);
|
|
const char *archive = NULL;
|
|
int ai = 2;
|
|
while (ai < argc) {
|
|
if (!isopt(argv[ai])) {
|
|
if (!archive) { archive = argv[ai]; ai++; continue; }
|
|
fprintf(stderr, "Error: unexpected extra argument '%s'\n", argv[ai]); return 1;
|
|
}
|
|
if (streq(argv[ai],"-v")||streq(argv[ai],"--verbose")) opts.verbose=1;
|
|
else if (streq(argv[ai],"-p")||streq(argv[ai],"--password")) {
|
|
opts.encrypt=1;
|
|
if (ai+1<argc && !isopt(argv[ai+1])) strncpy(opts.password,argv[++ai],sizeof(opts.password)-1);
|
|
else prompt_password("Password: ", opts.password, sizeof(opts.password));
|
|
}
|
|
else if (streq(argv[ai],"--pq-box")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.box_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-sdk")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.sdk_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
|
opts.pqonly_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
}
|
|
else { fprintf(stderr,"Unknown option '%s'\n",argv[ai]); return 1; }
|
|
ai++;
|
|
}
|
|
if (!archive) { fprintf(stderr,"Error: test requires <archive.zupt>\n"); return 1; }
|
|
banner();
|
|
zupt_error_t err = zupt_test_archive(archive, &opts);
|
|
zupt_secure_wipe(opts.password, sizeof(opts.password));
|
|
return err==ZUPT_OK ? 0 : 1;
|
|
}
|
|
|
|
/* ─── bench ─── */
|
|
if (streq(cmd,"bench")||streq(cmd,"b")) {
|
|
int ai = 2;
|
|
int compare_mode = 0;
|
|
if (ai < argc && streq(argv[ai], "--compare")) { compare_mode = 1; ai++; }
|
|
|
|
if (!compare_mode && ai >= argc) { fprintf(stderr, "Error: bench requires <files/dirs...> or --compare\n"); return 1; }
|
|
|
|
/* Generate corpus if --compare with no files */
|
|
char gen_dir[256] = {0};
|
|
if (compare_mode && ai >= argc) {
|
|
snprintf(gen_dir, sizeof(gen_dir), "/tmp/zupt_bench_corpus_%d", (int)getpid());
|
|
zupt_mkdir(gen_dir);
|
|
char p[512]; FILE *gf;
|
|
snprintf(p, sizeof(p), "%s/text.txt", gen_dir);
|
|
gf = fopen(p, "wb");
|
|
if (gf) { for (int i=0;i<15000;i++) fprintf(gf, "The quick brown fox jumps over the lazy dog. Line %d value %d.\n", i, i*17%997); fclose(gf); }
|
|
snprintf(p, sizeof(p), "%s/data.json", gen_dir);
|
|
gf = fopen(p, "wb");
|
|
if (gf) { for (int i=0;i<12000;i++) fprintf(gf, "{\"id\":%d,\"name\":\"user_%d\",\"score\":%d}\n", i, i, i*31%1000); fclose(gf); }
|
|
snprintf(p, sizeof(p), "%s/records.csv", gen_dir);
|
|
gf = fopen(p, "wb");
|
|
if (gf) { fprintf(gf,"id,name,score\n"); for (int i=0;i<14000;i++) fprintf(gf,"%d,user_%d,%d\n", i, i, i*17%100); fclose(gf); }
|
|
snprintf(p, sizeof(p), "%s/random.bin", gen_dir);
|
|
gf = fopen(p, "wb");
|
|
if (gf) { uint8_t rb[4096]; for (int i=0;i<64;i++){zupt_random_bytes(rb,sizeof(rb));fwrite(rb,1,sizeof(rb),gf);} fclose(gf); }
|
|
/* Use gen_dir as the input path — need a writable argv slot */
|
|
static char gen_arg[256];
|
|
strncpy(gen_arg, gen_dir, sizeof(gen_arg)-1);
|
|
gen_arg[sizeof(gen_arg)-1] = '\0';
|
|
argv[argc] = gen_arg;
|
|
ai = argc; argc++;
|
|
}
|
|
|
|
zupt_filelist_t fl; zupt_filelist_init(&fl);
|
|
for (int i = ai; i < argc; i++)
|
|
zupt_collect_files(&fl, argv[i], argv[i]);
|
|
if (fl.count == 0) { fprintf(stderr, "No files found.\n"); zupt_filelist_free(&fl); return 1; }
|
|
|
|
uint64_t total_in = 0;
|
|
for (int i = 0; i < fl.count; i++) {
|
|
FILE *tf = fopen(fl.paths[i], "rb");
|
|
if (tf) { fseek(tf, 0, SEEK_END); total_in += (uint64_t)ftell(tf); fclose(tf); }
|
|
}
|
|
char isz[32]; zupt_format_size(total_in, isz, sizeof(isz));
|
|
banner();
|
|
|
|
if (compare_mode) {
|
|
fprintf(stderr, " Codec Comparison — %d file(s), %s\n\n", fl.count, isz);
|
|
fprintf(stderr, " %-20s %12s %12s %10s\n", "Codec", "Compress", "Decompress", "Ratio");
|
|
fprintf(stderr, " ────────────────────────────────────────────────────────────\n");
|
|
|
|
char tmp_path[256], tmp_out[256];
|
|
snprintf(tmp_path, sizeof(tmp_path), "/tmp/zupt_cmp_%d.zupt", (int)getpid());
|
|
snprintf(tmp_out, sizeof(tmp_out), "/tmp/zupt_cmp_out_%d", (int)getpid());
|
|
|
|
struct { const char *name; uint16_t codec; int level; } codecs[] = {
|
|
{"VaptVupt UF", ZUPT_CODEC_VAPTVUPT, 1},
|
|
{"VaptVupt BAL", ZUPT_CODEC_VAPTVUPT, 5},
|
|
{"VaptVupt EXT", ZUPT_CODEC_VAPTVUPT, 9},
|
|
{"Zupt-LZHP", ZUPT_CODEC_ZUPT_LZHP,7},
|
|
{"Zupt-LZ", ZUPT_CODEC_ZUPT_LZ, 5},
|
|
};
|
|
int ncodecs = (int)(sizeof(codecs)/sizeof(codecs[0]));
|
|
|
|
for (int ci = 0; ci < ncodecs; ci++) {
|
|
zupt_options_t opts; zupt_default_options(&opts);
|
|
opts.codec_id = codecs[ci].codec; opts.level = codecs[ci].level; opts.quiet = 1;
|
|
|
|
struct timespec t0, t1;
|
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
zupt_error_t cerr = zupt_compress_files(tmp_path,
|
|
(const char**)fl.arc_paths, (const char**)fl.paths, fl.count, &opts);
|
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
double csec = (double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_nsec-t0.tv_nsec)/1e9;
|
|
if (csec < 0.001) csec = 0.001;
|
|
|
|
if (cerr != ZUPT_OK) { fprintf(stderr, " %-20s FAILED\n", codecs[ci].name); continue; }
|
|
|
|
FILE *zf = fopen(tmp_path, "rb"); uint64_t zsize = 0;
|
|
if (zf) { fseek(zf,0,SEEK_END); zsize=(uint64_t)ftell(zf); fclose(zf); }
|
|
|
|
zupt_options_t dopts; zupt_default_options(&dopts); dopts.quiet = 1;
|
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
zupt_extract_archive(tmp_path, tmp_out, &dopts);
|
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
double dsec = (double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_nsec-t0.tv_nsec)/1e9;
|
|
if (dsec < 0.001) dsec = 0.001;
|
|
|
|
fprintf(stderr, " %-20s %9.1f MB/s %9.1f MB/s %8.2f:1\n",
|
|
codecs[ci].name, (double)total_in/csec/1048576.0,
|
|
(double)total_in/dsec/1048576.0,
|
|
total_in>0&&zsize>0?(double)total_in/(double)zsize:1.0);
|
|
|
|
char rm[512]; snprintf(rm,sizeof(rm),"rm -rf '%s'",tmp_out); if (system(rm)) { /* ignore */ }
|
|
remove(tmp_path);
|
|
}
|
|
|
|
/* External tools */
|
|
fprintf(stderr, " ────────────────────────────────────────────────────────────\n");
|
|
char concat[256];
|
|
snprintf(concat, sizeof(concat), "/tmp/zupt_cmp_cat_%d", (int)getpid());
|
|
FILE *cf = fopen(concat, "wb");
|
|
if (cf) {
|
|
for (int i=0;i<fl.count;i++){FILE*inf=fopen(fl.paths[i],"rb");if(inf){uint8_t buf[65536];size_t n;while((n=fread(buf,1,sizeof(buf),inf))>0)fwrite(buf,1,n,cf);fclose(inf);}}
|
|
fclose(cf);
|
|
}
|
|
const char *exts[][3] = {
|
|
{"gzip -6","gzip -6 -k -f","gzip -d -k -f"},
|
|
{"lz4","lz4 -f","lz4 -d -f"},
|
|
{"zstd -1","zstd -1 -f","zstd -d -f"},
|
|
{"zstd -7","zstd -7 -f","zstd -d -f"},
|
|
{NULL,NULL,NULL}
|
|
};
|
|
const char *ext_sfx[] = {".gz",".lz4",".zst",".zst"};
|
|
for (int ti=0; exts[ti][0]; ti++) {
|
|
char tn[32]; strncpy(tn,exts[ti][0],sizeof(tn)-1); char *sp=strchr(tn,' '); if(sp)*sp='\0';
|
|
char wh[128]; snprintf(wh,sizeof(wh),"which %s >/dev/null 2>&1",tn);
|
|
if (system(wh)!=0) continue;
|
|
|
|
char co[256]; snprintf(co,sizeof(co),"%s%s",concat,ext_sfx[ti]);
|
|
remove(co);
|
|
char ccmd[512]; snprintf(ccmd,sizeof(ccmd),"%s %s >/dev/null 2>&1",exts[ti][1],concat);
|
|
struct timespec t0,t1;
|
|
clock_gettime(CLOCK_MONOTONIC,&t0); if (system(ccmd)) { /* ignore */ } clock_gettime(CLOCK_MONOTONIC,&t1);
|
|
double csec=(double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_nsec-t0.tv_nsec)/1e9; if(csec<0.001)csec=0.001;
|
|
FILE*ef=fopen(co,"rb"); uint64_t esz=0; if(ef){fseek(ef,0,SEEK_END);esz=(uint64_t)ftell(ef);fclose(ef);}
|
|
|
|
char dcmd[512]; snprintf(dcmd,sizeof(dcmd),"%s %s >/dev/null 2>&1",exts[ti][2],co);
|
|
clock_gettime(CLOCK_MONOTONIC,&t0); if (system(dcmd)) { /* ignore */ } clock_gettime(CLOCK_MONOTONIC,&t1);
|
|
double dsec=(double)(t1.tv_sec-t0.tv_sec)+(double)(t1.tv_nsec-t0.tv_nsec)/1e9; if(dsec<0.001)dsec=0.001;
|
|
|
|
fprintf(stderr, " %-20s %9.1f MB/s %9.1f MB/s %8.2f:1\n",
|
|
exts[ti][0], (double)total_in/csec/1048576.0, (double)total_in/dsec/1048576.0,
|
|
total_in>0&&esz>0?(double)total_in/(double)esz:1.0);
|
|
remove(co); char dec[512]; snprintf(dec,sizeof(dec),"%s.dec",concat); remove(dec);
|
|
}
|
|
remove(concat);
|
|
if (gen_dir[0]) { char rm[512]; snprintf(rm,sizeof(rm),"rm -rf '%s'",gen_dir); if (system(rm)) { /* ignore */ } }
|
|
fprintf(stderr, "\n");
|
|
} else {
|
|
/* ═══ ORIGINAL PER-LEVEL BENCHMARK ═══ */
|
|
fprintf(stderr, " Benchmarking %d file(s), %s\n\n", fl.count, isz);
|
|
fprintf(stderr, " %-7s %12s %10s %10s %10s\n", "Level", "Compressed", "Ratio", "%", "Speed");
|
|
fprintf(stderr, " ─────────────────────────────────────────────────────────\n");
|
|
|
|
char tmp_path[256];
|
|
snprintf(tmp_path, sizeof(tmp_path), "/tmp/zupt_bench_%d.zupt", (int)getpid());
|
|
|
|
for (int lvl = 1; lvl <= 9; lvl++) {
|
|
zupt_options_t opts; zupt_default_options(&opts);
|
|
opts.level = lvl;
|
|
opts.verbose = 0;
|
|
opts.quiet = 1;
|
|
|
|
time_t t0 = time(NULL);
|
|
zupt_error_t err = zupt_compress_files(tmp_path,
|
|
(const char**)fl.arc_paths, (const char**)fl.paths, fl.count, &opts);
|
|
time_t elapsed = time(NULL) - t0;
|
|
if (elapsed < 1) elapsed = 1;
|
|
|
|
if (err == ZUPT_OK) {
|
|
FILE *zf = fopen(tmp_path, "rb");
|
|
uint64_t zsize = 0;
|
|
if (zf) { fseek(zf, 0, SEEK_END); zsize = (uint64_t)ftell(zf); fclose(zf); }
|
|
|
|
char csz[32]; zupt_format_size(zsize, csz, sizeof(csz));
|
|
double ratio = total_in > 0 ? (double)total_in / (double)zsize : 1.0;
|
|
double pct = total_in > 0 ? (double)zsize / (double)total_in * 100.0 : 100.0;
|
|
double speed = (double)total_in / (double)elapsed / 1048576.0;
|
|
|
|
fprintf(stderr, " %-7d %12s %9.2f:1 %9.1f%% %8.1f MB/s\n",
|
|
lvl, csz, ratio, pct, speed);
|
|
} else {
|
|
fprintf(stderr, " %-7d %12s\n", lvl, "FAILED");
|
|
}
|
|
remove(tmp_path);
|
|
}
|
|
fprintf(stderr, "\n");
|
|
}
|
|
|
|
zupt_filelist_free(&fl);
|
|
return 0;
|
|
}
|
|
|
|
/* ─── disk (backup/restore) ─── */
|
|
if (streq(cmd,"disk")) {
|
|
if (argc < 3) {
|
|
fprintf(stderr, "Usage:\n");
|
|
fprintf(stderr, " zupt disk backup [OPTIONS] <output.zupt> <device_or_file>\n");
|
|
fprintf(stderr, " zupt disk restore [OPTIONS] <archive.zupt> <target_device_or_file>\n");
|
|
fprintf(stderr, "\nOptions:\n");
|
|
fprintf(stderr, " -l <1-9> Compression level (default: 7)\n");
|
|
fprintf(stderr, " -b <SIZE> Block size (default: 4MB for disks)\n");
|
|
fprintf(stderr, " -p [PW] Password encryption\n");
|
|
fprintf(stderr, " --pq <keyfile> Post-quantum encryption\n");
|
|
fprintf(stderr, " --vv Force VaptVupt codec\n");
|
|
fprintf(stderr, " --lzhp Force Zupt-LZHP codec\n");
|
|
fprintf(stderr, " -t <N> Thread count\n");
|
|
fprintf(stderr, " -v Verbose\n");
|
|
fprintf(stderr, "\nExamples:\n");
|
|
fprintf(stderr, " zupt disk backup backup.zupt /dev/sda1\n");
|
|
fprintf(stderr, " zupt disk backup -p secret encrypted.zupt /dev/nvme0n1p2\n");
|
|
fprintf(stderr, " zupt disk backup --pq pub.key pq_backup.zupt disk.img\n");
|
|
fprintf(stderr, " zupt disk restore backup.zupt /dev/sda1\n");
|
|
fprintf(stderr, " zupt disk restore -p secret encrypted.zupt /dev/sda1\n");
|
|
return 1;
|
|
}
|
|
|
|
const char *subcmd = argv[2];
|
|
if (!streq(subcmd,"backup") && !streq(subcmd,"restore")) {
|
|
fprintf(stderr, "Error: disk subcommand must be 'backup' or 'restore'\n");
|
|
return 1;
|
|
}
|
|
|
|
zupt_options_t opts; zupt_default_options(&opts);
|
|
int ai = 3;
|
|
while (ai<argc && isopt(argv[ai])) {
|
|
if ((streq(argv[ai],"-l")||streq(argv[ai],"--level"))&&ai+1<argc) {
|
|
opts.level=atoi(argv[++ai]); if(opts.level<1)opts.level=1; if(opts.level>9)opts.level=9;
|
|
} else if ((streq(argv[ai],"-b")||streq(argv[ai],"--block"))&&ai+1<argc) {
|
|
opts.block_size=(uint32_t)atol(argv[++ai]);
|
|
if(opts.block_size<ZUPT_MIN_BLOCK_SZ)opts.block_size=ZUPT_MIN_BLOCK_SZ;
|
|
if(opts.block_size>ZUPT_MAX_BLOCK_SZ)opts.block_size=ZUPT_MAX_BLOCK_SZ;
|
|
} else if (streq(argv[ai],"--vv")||streq(argv[ai],"--vaptvupt")) {
|
|
opts.codec_id=ZUPT_CODEC_VAPTVUPT;
|
|
} else if (streq(argv[ai],"--lzhp")) {
|
|
opts.codec_id=ZUPT_CODEC_ZUPT_LZHP;
|
|
} else if (streq(argv[ai],"-s")||streq(argv[ai],"--store")) {
|
|
opts.codec_id=ZUPT_CODEC_STORE;
|
|
} else if (streq(argv[ai],"-p")||streq(argv[ai],"--password")) {
|
|
opts.encrypt=1;
|
|
if (ai+1<argc && !isopt(argv[ai+1])) {
|
|
strncpy(opts.password, argv[++ai], sizeof(opts.password)-1);
|
|
} else {
|
|
prompt_password("Password: ", opts.password, sizeof(opts.password));
|
|
if (streq(subcmd,"backup")) {
|
|
char confirm[256];
|
|
prompt_password("Confirm: ", confirm, sizeof(confirm));
|
|
if (strcmp(opts.password, confirm)!=0) {
|
|
fprintf(stderr, "Error: Passwords do not match.\n"); return 1;
|
|
}
|
|
}
|
|
}
|
|
} else if (streq(argv[ai],"-v")||streq(argv[ai],"--verbose")) {
|
|
opts.verbose=1;
|
|
} else if ((streq(argv[ai],"-t")||streq(argv[ai],"--threads"))&&ai+1<argc) {
|
|
opts.threads=atoi(argv[++ai]);
|
|
} else if (streq(argv[ai],"--pq-only")&&ai+1<argc) {
|
|
opts.pqonly_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--pq")&&ai+1<argc) {
|
|
opts.pq_mode=1; opts.encrypt=1;
|
|
strncpy(opts.keyfile, argv[++ai], sizeof(opts.keyfile)-1);
|
|
} else if (streq(argv[ai],"--dedup")||streq(argv[ai],"-D")) {
|
|
opts.dedup=1;
|
|
} else if ((streq(argv[ai],"-c")||streq(argv[ai],"--comment"))&&ai+1<argc) {
|
|
/* v2.4.3: free-form archive comment. Encrypted along with
|
|
* data blocks when -p/--pq is also set. */
|
|
ai++;
|
|
strncpy(opts.comment, argv[ai], ZUPT_MAX_COMMENT_LEN - 1);
|
|
opts.comment[ZUPT_MAX_COMMENT_LEN - 1] = '\0';
|
|
opts.has_comment = 1;
|
|
} else if (streq(argv[ai],"--comment-file")&&ai+1<argc) {
|
|
ai++;
|
|
FILE *cf = fopen(argv[ai], "rb");
|
|
if (!cf) {
|
|
fprintf(stderr, "Error: --comment-file: cannot open '%s'\n", argv[ai]);
|
|
return 1;
|
|
}
|
|
size_t n = fread(opts.comment, 1, ZUPT_MAX_COMMENT_LEN - 1, cf);
|
|
opts.comment[n] = '\0';
|
|
while (n > 0 && (opts.comment[n-1] == '\n' || opts.comment[n-1] == '\r')) {
|
|
opts.comment[--n] = '\0';
|
|
}
|
|
opts.has_comment = (n > 0);
|
|
fclose(cf);
|
|
} else if (streq(argv[ai],"--kdf")&&ai+1<argc) {
|
|
/* v2.4.1: explicit KDF selection for password mode.
|
|
* Default (without --kdf): Argon2id. Use --kdf pbkdf2
|
|
* for compatibility with v2.4.0 and older readers. */
|
|
ai++;
|
|
if (streq(argv[ai],"pbkdf2")) {
|
|
opts.kdf_legacy_pbkdf2 = 1;
|
|
} else if (streq(argv[ai],"argon2id") || streq(argv[ai],"argon2")) {
|
|
opts.kdf_legacy_pbkdf2 = 0;
|
|
} else {
|
|
fprintf(stderr, "Error: --kdf must be 'argon2id' or 'pbkdf2', got '%s'\n", argv[ai]);
|
|
return 1;
|
|
}
|
|
} else {
|
|
fprintf(stderr,"Error: Unknown option '%s'\n",argv[ai]); return 1;
|
|
}
|
|
ai++;
|
|
}
|
|
|
|
if (argc - ai < 2) {
|
|
fprintf(stderr, "Error: disk %s requires <archive> <device/file>\n", subcmd);
|
|
return 1;
|
|
}
|
|
|
|
banner();
|
|
|
|
if (streq(subcmd,"backup")) {
|
|
const char *output = argv[ai];
|
|
const char *source = argv[ai+1];
|
|
fprintf(stderr, " Full-Disk Backup\n");
|
|
fprintf(stderr, " ═══════════════════════════════════════\n\n");
|
|
zupt_error_t err = zupt_disk_backup(output, source, &opts);
|
|
zupt_secure_wipe(opts.password, sizeof(opts.password));
|
|
return err == ZUPT_OK ? 0 : 1;
|
|
} else {
|
|
const char *archive = argv[ai];
|
|
const char *target = argv[ai+1];
|
|
fprintf(stderr, " Full-Disk Restore\n");
|
|
fprintf(stderr, " ═══════════════════════════════════════\n\n");
|
|
zupt_error_t err = zupt_disk_restore(archive, target, &opts);
|
|
zupt_secure_wipe(opts.password, sizeof(opts.password));
|
|
return err == ZUPT_OK ? 0 : 1;
|
|
}
|
|
}
|
|
|
|
/* ─── keygen ─── */
|
|
if (streq(cmd,"keygen")) {
|
|
const char *outfile = NULL;
|
|
const char *privfile = NULL;
|
|
int export_pub = 0;
|
|
int sdk_mode = 0;
|
|
int box_mode = 0;
|
|
int pqonly_mode = 0;
|
|
int ai = 2;
|
|
while (ai < argc && isopt(argv[ai])) {
|
|
if ((streq(argv[ai],"-o")||streq(argv[ai],"--output")) && ai+1 < argc)
|
|
outfile = argv[++ai];
|
|
else if ((streq(argv[ai],"-k")||streq(argv[ai],"--key")) && ai+1 < argc)
|
|
privfile = argv[++ai];
|
|
else if (streq(argv[ai],"--pub"))
|
|
export_pub = 1;
|
|
else if (streq(argv[ai],"--sdk")||streq(argv[ai],"--pq-sdk"))
|
|
sdk_mode = 1;
|
|
else if (streq(argv[ai],"--box")||streq(argv[ai],"--pq-box"))
|
|
box_mode = 1;
|
|
else if (streq(argv[ai],"--pq-only")||streq(argv[ai],"--pqonly"))
|
|
pqonly_mode = 1;
|
|
else { fprintf(stderr, "Unknown option '%s'\n", argv[ai]); return 1; }
|
|
ai++;
|
|
}
|
|
|
|
if (!outfile) {
|
|
fprintf(stderr, "Error: keygen requires -o <output_file>\n");
|
|
fprintf(stderr, " zupt keygen -o keyfile.key # Generate keypair\n");
|
|
fprintf(stderr, " zupt keygen --pub -o pub.key -k priv.key # Export public key\n");
|
|
return 1;
|
|
}
|
|
|
|
banner();
|
|
if (export_pub) {
|
|
if (!privfile) { fprintf(stderr, "Error: --pub requires -k <private_keyfile>\n"); return 1; }
|
|
fprintf(stderr, " Exporting public key from: %s\n", privfile);
|
|
int erc = pqonly_mode ? zupt_pq_export_pubkey(privfile, outfile)
|
|
: zupt_hybrid_export_pubkey(privfile, outfile);
|
|
if (erc != 0) {
|
|
fprintf(stderr, "Error: Failed to export public key%s.\n",
|
|
pqonly_mode ? "" : " (for full-PQ keys use: keygen --pub --pq-only)");
|
|
return 1;
|
|
}
|
|
fprintf(stderr, " Public key written to: %s\n", outfile);
|
|
} else if (pqonly_mode) {
|
|
fprintf(stderr, " Generating ML-KEM-768 keypair (full post-quantum, no X25519)...\n");
|
|
if (zupt_pq_keygen(outfile) != 0) {
|
|
fprintf(stderr, "Error: full-PQ key generation failed.\n"); return 1;
|
|
}
|
|
fprintf(stderr, " Private key written to: %s\n", outfile);
|
|
fprintf(stderr, " SECURITY: Keep this file secret. Back it up securely.\n");
|
|
fprintf(stderr, " To export public key: vaptvupt keygen --pub --pq-only -o pub.key -k %s\n", outfile);
|
|
} else if (box_mode) {
|
|
fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair (pq-box format)...\n");
|
|
char pubfile[512];
|
|
snprintf(pubfile, sizeof(pubfile), "%s.pub", outfile);
|
|
if (zupt_pqbox_keygen(outfile, pubfile) != 0) {
|
|
fprintf(stderr, "Error: pq-box key generation failed.\n"); return 1;
|
|
}
|
|
fprintf(stderr, " Private key: %s\n", outfile);
|
|
fprintf(stderr, " Public key: %s\n", pubfile);
|
|
fprintf(stderr, " SECURITY: Keep the private key file secret.\n");
|
|
} else if (sdk_mode) {
|
|
fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair (SDK-v2 format)...\n");
|
|
char pubfile[512];
|
|
snprintf(pubfile, sizeof(pubfile), "%s.pub", outfile);
|
|
if (zupt_sdk_hybrid_keygen(outfile, pubfile) != 0) {
|
|
fprintf(stderr,
|
|
"Error: SDK-v2 key generation is unavailable in this build.\n"
|
|
" --pq-sdk needs libzuptsdk, which is not part of the source-only\n"
|
|
" build. For post-quantum keys use one of the native modes:\n"
|
|
" vaptvupt keygen -o key # hybrid ML-KEM-768 + X25519 (--pq)\n"
|
|
" vaptvupt keygen --pq-only -o key # full PQ, ML-KEM-768 only (--pq-only)\n"
|
|
" (Rebuild upstream with 'make WITH_SDK=1' to enable --pq-sdk.)\n");
|
|
return 1;
|
|
}
|
|
fprintf(stderr, " Private key: %s\n", outfile);
|
|
fprintf(stderr, " Public key: %s\n", pubfile);
|
|
fprintf(stderr, " SECURITY: Keep the private key file secret.\n");
|
|
} else {
|
|
fprintf(stderr, " Generating ML-KEM-768 + X25519 keypair...\n");
|
|
if (zupt_hybrid_keygen(outfile) != 0) {
|
|
fprintf(stderr, "Error: Key generation failed.\n"); return 1;
|
|
}
|
|
fprintf(stderr, " Private key written to: %s\n", outfile);
|
|
fprintf(stderr, " SECURITY: Keep this file secret. Back it up securely.\n");
|
|
fprintf(stderr, " To export public key: zupt keygen --pub -o pub.key -k %s\n", outfile);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
fprintf(stderr, "Unknown command '%s'. Run 'zupt help'.\n", cmd);
|
|
return 1;
|
|
}
|