zupt/src/zupt_main.c
Cristian Cezar Moisés 51fa068f50 sdk: integrate libvuptsdk (renamed libzuptsdk); decouple --pq-box
libvuptsdk (git.securityops.co/cristiancmoises/libvuptsdk) is the renamed
libzuptsdk: only the .so filename/SONAME changed (libzuptsdk.so.2 ->
libvuptsdk.so.2); the C API (zuptsdk_* symbols, zuptsdk.h) is unchanged.

- Rename vendor/zuptsdk -> vendor/vuptsdk with libvuptsdk's headers.
- Makefile WITH_SDK=1 now links -lvuptsdk (+ its transitive libcrypto/libargon2
  deps via SDK_DEPLIBS) instead of -lzuptsdk, and installs libvuptsdk.so.*.
- DECOUPLE --pq-box: it needs the SEPARATE libpqvaptvupt, which libvuptsdk does
  NOT provide, so gate it behind a new WITH_PQBOX=1 (was folded into WITH_SDK).
  zupt_crypto_pqbox.c now keys on ZUPT_WITH_PQBOX; WITH_SDK=1 alone builds and
  links cleanly with just libvuptsdk and enables --pq-sdk + Argon2id.
- Banner/help renamed libzuptsdk -> libvuptsdk; the machine-readable 'Build:'
  line lists --pq-box only under WITH_PQBOX. GUI _get_caps matches on 'vuptsdk'.

Validated on Guix: WITH_SDK=1 links libvuptsdk + libcrypto + libargon2, runs,
banner 'Build: full (libvuptsdk: Argon2id, --pq-sdk available)', keygen --sdk +
--pq-sdk encrypt/decrypt byte-exact roundtrip. Default source-only build
unchanged (make check 16/16).
2026-07-12 14:47:40 -03:00

1011 lines
52 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 libvuptsdk (WITH_SDK=1 builds only)\n"
" --pq-box <pubkey> Post-quantum sealed box via libpqvaptvupt (WITH_PQBOX=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 libvuptsdk (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 (libvuptsdk; WITH_SDK=1 builds only)\n"
" --box, --pq-box Generate pq-box keypair (libpqvaptvupt; WITH_PQBOX=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 (libvuptsdk)"
#ifdef ZUPT_WITH_PQBOX
", --pq-box (libpqvaptvupt)"
#endif
"\n"
"Build: full (libvuptsdk: Argon2id, --pq-sdk"
#ifdef ZUPT_WITH_PQBOX
", --pq-box"
#endif
" 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 libvuptsdk, 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;
}