zupt/src/zupt_format.c
Cristian Cezar Moisés 28b2744c67 Release 2.1.3: Fix disk backup corruption, solid PQ encryption, block I/O, and format mismatches
- Corrected LZHP prediction encoding in disk backups to prevent data corruption
- Disabled spurious SOLID flag for per-block disk archives
- Shared write_enc_header() across all encryption paths to eliminate format mismatches
- Enabled solid compression with PQ encryption support
- Updated block device restore to use O_SYNC + fsync/sync
- Improved Termux/Android host detection for safer builds
- Made zupt_w8(), zupt_w16le(), zupt_w64le() non-static for shared use
2026-04-11 15:59:59 -03:00

1671 lines
72 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* ZUPT - Archive Format I/O v0.6.0
*
* v0.6.0 changes:
* - Multi-threaded compression and decompression via zupt_parallel.h
* - Format version bump v1.2 → v1.3 (backward compatible)
* - ZUPT_FLAG_MULTITHREADED informational flag
* - N=1 path is bit-for-bit identical to v0.5.1
*/
#define _GNU_SOURCE
#include "zupt.h"
#include "zupt_cpuid.h" /* zupt_cpu for AUTO codec detection */
#include "zupt_parallel.h"
#include "vaptvupt.h" /* VAPTVUPT: VaptVupt codec integration */
#include "vaptvupt_api.h" /* VAPTVUPT: simplified Zupt integration API */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#ifdef _WIN32
#include <io.h>
#define fseeko _fseeki64
#define ftello _ftelli64
#endif
/* ═══════════════════════════════════════════════════════════════════
* UTILITY
* ═══════════════════════════════════════════════════════════════════ */
const char *zupt_strerror(zupt_error_t e) {
switch (e) {
case ZUPT_OK: return "Success";
case ZUPT_ERR_IO: return "I/O error";
case ZUPT_ERR_CORRUPT: return "Archive is corrupt";
case ZUPT_ERR_BAD_MAGIC: return "Not a .zupt archive";
case ZUPT_ERR_BAD_VERSION: return "Unsupported version";
case ZUPT_ERR_BAD_CHECKSUM: return "Checksum mismatch";
case ZUPT_ERR_NOMEM: return "Out of memory";
case ZUPT_ERR_OVERFLOW: return "Overflow";
case ZUPT_ERR_INVALID: return "Invalid argument";
case ZUPT_ERR_NOT_FOUND: return "Not found";
case ZUPT_ERR_UNSUPPORTED: return "Unsupported";
case ZUPT_ERR_AUTH_FAIL: return "Authentication failed (wrong password?)";
default: return "Unknown error";
}
}
const char *zupt_codec_name(uint16_t id) {
switch (id) {
case ZUPT_CODEC_STORE: return "Store";
case ZUPT_CODEC_ZUPT_LZ: return "Zupt-LZ";
case ZUPT_CODEC_ZUPT_LZH: return "Zupt-LZH";
case ZUPT_CODEC_ZUPT_LZHP: return "Zupt-LZHP";
case ZUPT_CODEC_VAPTVUPT: return "VaptVupt"; /* VAPTVUPT */
case ZUPT_CODEC_AUTO: return "Auto";
default: return "Unknown";
}
}
void zupt_default_options(zupt_options_t *o) {
memset(o, 0, sizeof(*o));
o->level = 7;
o->block_size = 0;
o->codec_id = ZUPT_CODEC_AUTO; /* Auto-detect: VaptVupt if AVX2, else LZHP */
/* Init keyring canaries */
o->keyring.canary_head = ZUPT_CANARY;
o->keyring.canary_tail = ZUPT_CANARY;
}
/* Resolve ZUPT_CODEC_AUTO to a concrete codec.
* VaptVupt decode works on ALL architectures (scalar fallback), but the
* AVX2 SIMD decode path gives ~3× throughput. On non-AVX2 hardware,
* Zupt-LZHP is a better default since its simpler decoder doesn't
* benefit from SIMD as much.
*
* Detection order:
* 1. Compile-time: __x86_64__ + __AVX2__ → VaptVupt (compiled with -mavx2)
* 2. Runtime: zupt_cpu.has_avx2 → VaptVupt (for x86_64 without -mavx2)
* 3. Compile-time: __aarch64__ + __ARM_NEON → VaptVupt (NEON decode)
* 4. Fallback: Zupt-LZHP (works everywhere)
*/
uint16_t zupt_resolve_auto_codec(void) {
#if defined(__x86_64__) || defined(_M_X64)
/* x86_64: check AVX2 at compile time (via -mavx2) or runtime (cpuid) */
#if defined(__AVX2__)
return ZUPT_CODEC_VAPTVUPT; /* Compiled with -mavx2: inline SIMD decode */
#else
if (zupt_cpu.has_avx2)
return ZUPT_CODEC_VAPTVUPT; /* Runtime AVX2: vv_simd.c dispatch */
return ZUPT_CODEC_ZUPT_LZHP; /* No AVX2: use LZHP */
#endif
#elif defined(__aarch64__) && defined(__ARM_NEON)
return ZUPT_CODEC_VAPTVUPT; /* NEON SIMD decode available */
#else
return ZUPT_CODEC_ZUPT_LZHP; /* Scalar only: LZHP is a better default */
#endif
}
static uint32_t auto_block_size(int level) {
if (level <= 2) return 131072;
if (level <= 4) return 131072;
if (level <= 6) return 262144;
if (level <= 7) return 262144;
return 524288;
}
void zupt_format_size(uint64_t b, char *buf, size_t cap) {
if (b < 1024) snprintf(buf, cap, "%llu B", (unsigned long long)b);
else if (b < 1048576) snprintf(buf, cap, "%.1f KB", (double)b/1024.0);
else if (b < 1073741824ULL) snprintf(buf, cap, "%.1f MB", (double)b/1048576.0);
else snprintf(buf, cap, "%.2f GB", (double)b/1073741824.0);
}
static uint64_t now_ns(void) { return (uint64_t)time(NULL)*1000000000ULL; }
static void gen_uuid(uint8_t u[16]) {
zupt_random_bytes(u, 16);
u[6]=(u[6]&0x0F)|0x40; u[8]=(u[8]&0x3F)|0x80;
}
/* ─── Progress bar ─── */
static void show_progress(const char *label, uint64_t done, uint64_t total) {
if (total == 0) return;
int pct = (int)(done * 100 / total);
int bar = pct / 2;
char buf[60]; memset(buf, ' ', 50); buf[50] = '\0';
for (int i = 0; i < bar && i < 50; i++) buf[i] = '#';
fprintf(stderr, "\r %s [%-50s] %3d%%", label, buf, pct);
if (done >= total) fprintf(stderr, "\n");
fflush(stderr);
}
/* ═══════════════════════════════════════════════════════════════════
* VARINT
* ═══════════════════════════════════════════════════════════════════ */
int zupt_encode_varint(uint8_t *b, uint64_t v) {
int n=0; do { uint8_t x=(uint8_t)(v&0x7F); v>>=7; if(v)x|=0x80; b[n++]=x; } while(v); return n;
}
int zupt_decode_varint(const uint8_t *b, size_t blen, uint64_t *v) {
*v=0; int s=0,n=0;
while(n<(int)blen&&n<9){uint64_t x=b[n];*v|=(x&0x7F)<<s;n++;if(!(x&0x80))return n;s+=7;}
return -1;
}
int zupt_write_varint(FILE *f, uint64_t v) {
uint8_t b[10]; int n=zupt_encode_varint(b,v); return fwrite(b,1,(size_t)n,f)==(size_t)n?n:-1;
}
int zupt_read_varint(FILE *f, uint64_t *v) {
*v=0; int s=0;
for(int i=0;i<9;i++){int c=fgetc(f);if(c==EOF)return -1;
*v|=(uint64_t)(c&0x7F)<<s;if(!(c&0x80))return i+1;s+=7;} return -1;
}
/* ═══════════════════════════════════════════════════════════════════
* DIRECTORY TRAVERSAL
* ═══════════════════════════════════════════════════════════════════ */
void zupt_filelist_init(zupt_filelist_t *fl) {
fl->paths = NULL; fl->arc_paths = NULL; fl->count = 0; fl->capacity = 0;
}
void zupt_filelist_free(zupt_filelist_t *fl) {
for (int i = 0; i < fl->count; i++) { free(fl->paths[i]); free(fl->arc_paths[i]); }
free(fl->paths); free(fl->arc_paths);
fl->paths = fl->arc_paths = NULL; fl->count = fl->capacity = 0;
}
void zupt_filelist_add(zupt_filelist_t *fl, const char *disk, const char *arc) {
if (fl->count >= fl->capacity) {
int new_cap = fl->capacity ? fl->capacity * 2 : 256;
char **new_paths = (char**)realloc(fl->paths, (size_t)new_cap * sizeof(char*));
char **new_arcs = (char**)realloc(fl->arc_paths, (size_t)new_cap * sizeof(char*));
if (!new_paths || !new_arcs) {
/* OOM: keep existing pointers intact, skip this file */
if (new_paths && new_paths != fl->paths) free(new_paths);
if (new_arcs && new_arcs != fl->arc_paths) free(new_arcs);
fprintf(stderr, " Warning: out of memory adding '%s'\n", disk);
return;
}
fl->paths = new_paths;
fl->arc_paths = new_arcs;
fl->capacity = new_cap;
}
fl->paths[fl->count] = strdup(disk);
fl->arc_paths[fl->count] = strdup(arc);
if (!fl->paths[fl->count] || !fl->arc_paths[fl->count]) {
free(fl->paths[fl->count]);
free(fl->arc_paths[fl->count]);
fprintf(stderr, " Warning: out of memory adding '%s'\n", disk);
return;
}
fl->count++;
}
static int is_dir(const char *path) {
#ifdef _WIN32
DWORD attr = GetFileAttributesA(path);
return (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY));
#else
struct stat st;
return (stat(path, &st) == 0 && S_ISDIR(st.st_mode));
#endif
}
void zupt_collect_files(zupt_filelist_t *fl, const char *path, const char *base) {
if (!is_dir(path)) {
/* Skip non-regular files (symlinks, devices, FIFOs, sockets) */
if (!zupt_is_regular_file(path)) {
fprintf(stderr, " Skipping non-regular file: %s\n", path);
return;
}
const char *arc = base;
while (arc[0]=='.' && (arc[1]=='/'||arc[1]=='\\')) arc+=2;
while (*arc=='/'||*arc=='\\') arc++;
if (*arc == '\0') arc = path;
while (*arc=='/'||*arc=='\\') arc++;
zupt_filelist_add(fl, path, arc);
return;
}
#ifdef _WIN32
char pattern[ZUPT_MAX_PATH];
snprintf(pattern, sizeof(pattern), "%s\\*", path);
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
if (fd.cFileName[0]=='.' && (fd.cFileName[1]=='\0' ||
(fd.cFileName[1]=='.' && fd.cFileName[2]=='\0'))) continue;
char child_disk[ZUPT_MAX_PATH], child_arc[ZUPT_MAX_PATH];
snprintf(child_disk, sizeof(child_disk), "%s\\%s", path, fd.cFileName);
snprintf(child_arc, sizeof(child_arc), "%s/%s", base, fd.cFileName);
zupt_collect_files(fl, child_disk, child_arc);
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
DIR *d = opendir(path);
if (!d) return;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
if (ent->d_name[0]=='.' && (ent->d_name[1]=='\0' ||
(ent->d_name[1]=='.' && ent->d_name[2]=='\0'))) continue;
char child_disk[ZUPT_MAX_PATH], child_arc[ZUPT_MAX_PATH];
snprintf(child_disk, sizeof(child_disk), "%s/%s", path, ent->d_name);
snprintf(child_arc, sizeof(child_arc), "%s/%s", base, ent->d_name);
zupt_collect_files(fl, child_disk, child_arc);
}
closedir(d);
#endif
}
/* ═══════════════════════════════════════════════════════════════════
* WRITE / READ HELPERS (LE-safe, error-checked)
* ═══════════════════════════════════════════════════════════════════ */
int zupt_w8(FILE*f,uint8_t v){return fwrite(&v,1,1,f)==1?0:-1;}
int zupt_w16le(FILE*f,uint16_t v){uint8_t b[2];zupt_le16_put(b,v);return fwrite(b,1,2,f)==2?0:-1;}
int zupt_w64le(FILE*f,uint64_t v){uint8_t b[8];zupt_le64_put(b,v);return fwrite(b,1,8,f)==8?0:-1;}
static int r16le(FILE*f,uint16_t*v){uint8_t b[2];if(fread(b,1,2,f)!=2)return -1;*v=zupt_le16_get(b);return 0;}
static int r64le(FILE*f,uint64_t*v){uint8_t b[8];if(fread(b,1,8,f)!=8)return -1;*v=zupt_le64_get(b);return 0;}
/* Aliases for internal use (backward compat with existing code) */
#define w8 zupt_w8
#define w16le zupt_w16le
#define w64le zupt_w64le
/* ═══════════════════════════════════════════════════════════════════
* SHARED ENCRYPTION HEADER WRITER
*
* Used by BOTH zupt_compress_files() and zupt_disk_backup().
* Writes the encryption header block and updates hdr.encryption_header_off.
* After return, the file position is at the end (ready for data blocks).
* ═══════════════════════════════════════════════════════════════════ */
zupt_error_t write_enc_header(FILE *out, zupt_archive_header_t *hdr,
zupt_options_t *opts) {
hdr->encryption_header_off = (uint64_t)ftello(out);
if (opts->pq_mode) {
/* ─── PQ HYBRID MODE ─── */
hdr->global_flags |= ZUPT_FLAG_PQ_HYBRID;
uint8_t enc_hdr_buf[1200];
size_t enc_hdr_len = 0;
if (!opts->quiet)
fprintf(stderr, " Post-quantum key encapsulation (ML-KEM-768 + X25519)...\n");
if (zupt_hybrid_encrypt_init(&opts->keyring, opts->keyfile,
enc_hdr_buf, &enc_hdr_len) != 0) {
fprintf(stderr, "Error: PQ hybrid key encapsulation failed.\n");
return ZUPT_ERR_AUTH_FAIL;
}
zupt_w8(out, ZUPT_BLOCK_MAGIC_0); zupt_w8(out, ZUPT_BLOCK_MAGIC_1);
zupt_w8(out, ZUPT_BLOCK_ENC_HEADER);
zupt_w16le(out, ZUPT_CODEC_STORE); zupt_w16le(out, 0);
zupt_write_varint(out, enc_hdr_len);
zupt_write_varint(out, enc_hdr_len);
zupt_w64le(out, zupt_xxh64(enc_hdr_buf, enc_hdr_len, 0));
if (fwrite(enc_hdr_buf, 1, enc_hdr_len, out) != enc_hdr_len)
return ZUPT_ERR_IO;
/* Re-write header with PQ flag */
fseeko(out, 0, SEEK_SET);
if (fwrite(hdr, sizeof(*hdr), 1, out) != 1) return ZUPT_ERR_IO;
fseeko(out, 0, SEEK_END);
if (!opts->quiet)
fprintf(stderr, " Encryption: PQ Hybrid (ML-KEM-768 + X25519) + AES-256-CTR + HMAC-SHA256\n\n");
} else {
/* ─── PASSWORD MODE (PBKDF2) ─── */
uint8_t salt[ZUPT_SALT_SIZE], nonce[ZUPT_NONCE_SIZE];
zupt_random_bytes(salt, ZUPT_SALT_SIZE);
zupt_random_bytes(nonce, ZUPT_NONCE_SIZE);
if (!opts->quiet)
fprintf(stderr, " Deriving encryption key (PBKDF2-SHA256, %d iterations)...\n",
ZUPT_KDF_ITERATIONS);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, ZUPT_KDF_ITERATIONS);
uint8_t enc_hdr[53];
enc_hdr[0] = ZUPT_ENC_PBKDF2;
memcpy(enc_hdr + 1, salt, 32);
memcpy(enc_hdr + 33, nonce, 16);
uint32_t iter = ZUPT_KDF_ITERATIONS;
memcpy(enc_hdr + 49, &iter, 4);
zupt_w8(out, ZUPT_BLOCK_MAGIC_0); zupt_w8(out, ZUPT_BLOCK_MAGIC_1);
zupt_w8(out, ZUPT_BLOCK_ENC_HEADER);
zupt_w16le(out, ZUPT_CODEC_STORE); zupt_w16le(out, 0);
zupt_write_varint(out, 53); zupt_write_varint(out, 53);
zupt_w64le(out, zupt_xxh64(enc_hdr, 53, 0));
if (fwrite(enc_hdr, 1, 53, out) != 53) return ZUPT_ERR_IO;
fseeko(out, 0, SEEK_SET);
if (fwrite(hdr, sizeof(*hdr), 1, out) != 1) return ZUPT_ERR_IO;
fseeko(out, 0, SEEK_END);
if (!opts->quiet)
fprintf(stderr, " Encryption: AES-256-CTR + HMAC-SHA256 (Encrypt-then-MAC)\n\n");
}
return ZUPT_OK;
}
static void ensure_dirs(const char *path) {
char tmp[ZUPT_MAX_PATH]; strncpy(tmp, path, sizeof(tmp)-1); tmp[sizeof(tmp)-1]='\0';
for (char *p=tmp+1;*p;p++)
if (*p=='/'||*p=='\\') { *p='\0'; zupt_mkdir(tmp); *p=ZUPT_PATH_SEP; }
}
static uint64_t get_mtime(const char *path) {
#ifdef _WIN32
(void)path; return now_ns();
#else
struct stat st;
if (stat(path, &st) == 0) return (uint64_t)st.st_mtime * 1000000000ULL;
return now_ns();
#endif
}
/* Safe ftello wrapper: returns 0 on error (caller should check context) */
static uint64_t safe_ftello(FILE *f) {
int64_t pos = ftello(f);
if (pos < 0) return 0;
return (uint64_t)pos;
}
/* ═══════════════════════════════════════════════════════════════════
* INDEX SERIALIZATION HELPERS (always LE)
* ═══════════════════════════════════════════════════════════════════ */
static size_t index_put_u64(uint8_t *buf, uint64_t v) {
zupt_le64_put(buf, v);
return 8;
}
static size_t index_put_u32(uint8_t *buf, uint32_t v) {
zupt_le32_put(buf, v);
return 4;
}
static uint64_t index_get_u64(const uint8_t *buf) {
return zupt_le64_get(buf);
}
static uint32_t index_get_u32(const uint8_t *buf) {
return zupt_le32_get(buf);
}
/* ═══════════════════════════════════════════════════════════════════
* COMPRESSION
* ═══════════════════════════════════════════════════════════════════ */
zupt_error_t zupt_compress_files(const char *output_path,
const char **arc_paths,
const char **disk_paths,
int num_files,
zupt_options_t *opts) {
if (opts->block_size == 0) opts->block_size = auto_block_size(opts->level);
/* Resolve AUTO codec before compression */
if (opts->codec_id == ZUPT_CODEC_AUTO)
opts->codec_id = zupt_resolve_auto_codec();
FILE *out = fopen(output_path, "wb");
if (!out) { fprintf(stderr, "Error: Cannot create '%s': %s\n", output_path, strerror(errno)); return ZUPT_ERR_IO; }
int write_err = 0; /* Accumulate write errors */
zupt_archive_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.magic[0]=ZUPT_MAGIC_0; hdr.magic[1]=ZUPT_MAGIC_1; hdr.magic[2]=ZUPT_MAGIC_2;
hdr.magic[3]=ZUPT_MAGIC_3; hdr.magic[4]=ZUPT_MAGIC_4; hdr.magic[5]=ZUPT_MAGIC_5;
hdr.version_major = ZUPT_FORMAT_MAJOR; hdr.version_minor = ZUPT_FORMAT_MINOR;
hdr.global_flags = ZUPT_FLAG_CKSUM_XXH64;
if (opts->encrypt) hdr.global_flags |= ZUPT_FLAG_ENCRYPTED;
if (opts->threads > 1) hdr.global_flags |= ZUPT_FLAG_MULTITHREADED;
hdr.creation_time = now_ns();
gen_uuid(hdr.archive_id);
if (fwrite(&hdr, sizeof(hdr), 1, out) != 1) write_err = 1;
if (opts->encrypt) {
zupt_error_t enc_err = write_enc_header(out, &hdr, opts);
if (enc_err != ZUPT_OK) { fclose(out); return enc_err; }
}
zupt_index_entry_t *index = (zupt_index_entry_t*)calloc((size_t)num_files, sizeof(zupt_index_entry_t));
uint8_t *rbuf = (uint8_t*)malloc(opts->block_size);
uint8_t *cbuf = (uint8_t*)malloc(zupt_lzh_bound(opts->block_size) + 512);
if (!index || !rbuf || !cbuf) { free(index); free(rbuf); free(cbuf); fclose(out); return ZUPT_ERR_NOMEM; }
uint64_t total_blocks = 0, total_in = 0, total_out = 0;
uint64_t block_seq = 0;
time_t start_time = time(NULL);
/* Create parallel context if multi-threaded */
zpar_ctx_t *pctx = NULL;
int effective_threads = opts->threads > 1 ? opts->threads : 1;
if (effective_threads > 1) {
pctx = zpar_create(effective_threads, opts->block_size, 0,
opts->encrypt ? &opts->keyring : NULL);
if (!pctx || pctx->threads_running == 0) {
if (pctx) zpar_destroy(pctx);
pctx = NULL;
effective_threads = 1;
if (!opts->quiet) fprintf(stderr, " Thread creation failed, using single thread\n");
}
}
for (int fi = 0; fi < num_files; fi++) {
FILE *inf = fopen(disk_paths[fi], "rb");
if (!inf) { fprintf(stderr, " Skipping: %s (%s)\n", disk_paths[fi], strerror(errno)); continue; }
fseeko(inf, 0, SEEK_END);
int64_t file_size = ftello(inf);
if (file_size < 0) { fclose(inf); continue; }
fseeko(inf, 0, SEEK_SET);
strncpy(index[fi].path, arc_paths[fi], ZUPT_MAX_PATH-1);
index[fi].uncompressed_size = (uint64_t)file_size;
index[fi].first_block_offset = safe_ftello(out);
index[fi].modification_time = get_mtime(disk_paths[fi]);
index[fi].attributes = 0644;
index[fi].block_count = 0;
char sz_buf[32]; zupt_format_size((uint64_t)file_size, sz_buf, sizeof(sz_buf));
if (opts->verbose)
fprintf(stderr, " %s (%s)\n", arc_paths[fi], sz_buf);
/* Chained hash: xxh64 over concatenated file content */
uint64_t file_hash_state = 0;
uint64_t file_comp = 0;
size_t remaining = (size_t)file_size;
uint64_t file_done = 0;
if (pctx && effective_threads > 1) {
/* ─── MULTI-THREADED COMPRESSION PATH ─── */
/* Batch: read up to N blocks, submit to workers, collect in order */
int *pending_slots = (int *)malloc((size_t)effective_threads * sizeof(int));
uint64_t *pending_seqs = (uint64_t *)malloc((size_t)effective_threads * sizeof(uint64_t));
if (!pending_slots || !pending_seqs) {
free(pending_slots); free(pending_seqs); fclose(inf);
write_err = 1; continue;
}
while (remaining > 0) {
int npending = 0;
/* Fill batch: read and submit up to N blocks */
while (remaining > 0 && npending < effective_threads) {
size_t chunk = remaining < opts->block_size ? remaining : opts->block_size;
size_t nread = fread(rbuf, 1, chunk, inf);
if (nread == 0) break;
/* Chained hash computed in main thread (sequential, fast) */
file_hash_state = zupt_xxh64(rbuf, nread, file_hash_state);
int slot = zpar_submit_compress(pctx, rbuf, nread,
block_seq, opts->level, opts->codec_id);
if (slot < 0) { write_err = 1; break; }
pending_slots[npending] = slot;
pending_seqs[npending] = block_seq;
npending++;
block_seq++;
remaining -= nread;
file_done += nread;
}
/* Collect results in order and write */
for (int pi = 0; pi < npending; pi++) {
zpar_slot_t *s = zpar_wait_slot(pctx, pending_slots[pi]);
if (!s || s->error != ZUPT_OK) {
if (!write_err) fprintf(stderr, " Block error: %s\n",
zupt_strerror(s ? s->error : ZUPT_ERR_CORRUPT));
write_err = 1;
zpar_release_slot(pctx, pending_slots[pi]);
continue;
}
/* Write block header */
w8(out, ZUPT_BLOCK_MAGIC_0); w8(out, ZUPT_BLOCK_MAGIC_1);
w8(out, ZUPT_BLOCK_DATA);
w16le(out, s->actual_codec); w16le(out, s->out_bflags);
zupt_write_varint(out, s->input_len); /* uncompressed size */
zupt_write_varint(out, (uint64_t)s->output_len);
w64le(out, s->checksum);
if (fwrite(s->output, 1, s->output_len, out) != s->output_len)
write_err = 1;
file_comp += s->output_len;
index[fi].block_count++;
total_blocks++;
zpar_release_slot(pctx, pending_slots[pi]);
}
if (write_err) break;
if (!opts->verbose && !opts->quiet && file_size > (int64_t)opts->block_size)
show_progress(arc_paths[fi], file_done, (uint64_t)file_size);
}
free(pending_slots);
free(pending_seqs);
} else {
/* ─── SINGLE-THREADED COMPRESSION PATH (bit-for-bit v0.5.1) ─── */
while (remaining > 0) {
size_t chunk = remaining < opts->block_size ? remaining : opts->block_size;
size_t nread = fread(rbuf, 1, chunk, inf);
if (nread == 0) break;
uint64_t checksum = zupt_xxh64(rbuf, nread, 0);
/* Chained hash: feed previous hash as seed for next block */
file_hash_state = zupt_xxh64(rbuf, nread, file_hash_state);
size_t comp_size = 0;
uint16_t codec = opts->codec_id;
if (codec == ZUPT_CODEC_ZUPT_LZHP) {
uint8_t pred[256];
float benefit = zupt_predict_benefit(rbuf, nread);
if (benefit > 0.03f && nread > 256) {
zupt_predict_build(rbuf, nread, pred);
uint8_t *transformed = (uint8_t *)malloc(nread);
if (transformed) {
zupt_predict_encode(rbuf, transformed, nread, pred);
size_t lzh_cap = zupt_lzh_bound(nread);
uint8_t *lzh_out = cbuf + 1 + 256;
size_t lzh_size = zupt_lzh_compress(transformed, nread, lzh_out,
lzh_cap, opts->level);
free(transformed);
if (lzh_size > 0 && 1 + 256 + lzh_size < nread) {
cbuf[0] = 0x01;
memcpy(cbuf + 1, pred, 256);
comp_size = 1 + 256 + lzh_size;
} else {
cbuf[0] = 0x00;
size_t plain = zupt_lzh_compress(rbuf, nread, cbuf + 1,
lzh_cap, opts->level);
if (plain > 0 && 1 + plain < nread)
comp_size = 1 + plain;
}
}
} else {
cbuf[0] = 0x00;
size_t lzh_cap = zupt_lzh_bound(nread);
size_t plain = zupt_lzh_compress(rbuf, nread, cbuf + 1,
lzh_cap, opts->level);
if (plain > 0 && 1 + plain < nread)
comp_size = 1 + plain;
}
} else if (codec == ZUPT_CODEC_ZUPT_LZH)
comp_size = zupt_lzh_compress(rbuf, nread, cbuf, zupt_lzh_bound(nread), opts->level);
else if (codec == ZUPT_CODEC_ZUPT_LZ)
comp_size = zupt_lz_compress(rbuf, nread, cbuf, zupt_lz_bound(nread), opts->level);
/* VAPTVUPT: VaptVupt codec compress path (v1.4.0 integration API) */
else if (codec == ZUPT_CODEC_VAPTVUPT) {
size_t vv_cap = vvz_compress_bound(nread);
if (vv_cap > zupt_lzh_bound(nread) + 512) {
uint8_t *vv_tmp = (uint8_t *)malloc(vv_cap);
if (vv_tmp) {
int64_t csz = vvz_compress(rbuf, nread, vv_tmp, vv_cap, opts->level);
if (csz > 0 && (size_t)csz < nread) {
memcpy(cbuf, vv_tmp, (size_t)csz);
comp_size = (size_t)csz;
}
free(vv_tmp);
}
} else {
int64_t csz = vvz_compress(rbuf, nread, cbuf, zupt_lzh_bound(nread) + 512, opts->level);
if (csz > 0 && (size_t)csz < nread)
comp_size = (size_t)csz;
}
}
const uint8_t *payload; uint64_t payload_size;
if (comp_size == 0 || comp_size >= nread) {
codec = ZUPT_CODEC_STORE; payload = rbuf; payload_size = nread;
} else {
payload = cbuf; payload_size = comp_size;
}
uint8_t *enc_payload = NULL;
uint16_t bflags = 0;
if (opts->encrypt && opts->keyring.active) {
size_t enc_len;
enc_payload = zupt_encrypt_buffer(&opts->keyring, payload, payload_size, block_seq, &enc_len);
if (!enc_payload) { fclose(inf); free(index); free(rbuf); free(cbuf); fclose(out); return ZUPT_ERR_NOMEM; }
payload = enc_payload;
payload_size = enc_len;
bflags |= ZUPT_BFLAG_ENCRYPTED;
}
w8(out, ZUPT_BLOCK_MAGIC_0); w8(out, ZUPT_BLOCK_MAGIC_1);
w8(out, ZUPT_BLOCK_DATA);
w16le(out, codec); w16le(out, bflags);
zupt_write_varint(out, (uint64_t)nread);
zupt_write_varint(out, payload_size);
w64le(out, checksum);
if (fwrite(payload, 1, (size_t)payload_size, out) != (size_t)payload_size) write_err = 1;
free(enc_payload);
file_comp += payload_size;
index[fi].block_count++;
total_blocks++;
block_seq++;
remaining -= nread;
file_done += nread;
if (!opts->verbose && !opts->quiet && file_size > (int64_t)opts->block_size)
show_progress(arc_paths[fi], file_done, (uint64_t)file_size);
} /* end while (remaining > 0) */
} /* end else (single-threaded) */
index[fi].compressed_size = file_comp;
index[fi].content_hash = file_hash_state;
total_in += index[fi].uncompressed_size;
total_out += index[fi].compressed_size;
fclose(inf);
if (opts->verbose) {
char in_s[32], out_s[32];
zupt_format_size(index[fi].uncompressed_size, in_s, sizeof(in_s));
zupt_format_size(index[fi].compressed_size, out_s, sizeof(out_s));
double ratio = index[fi].uncompressed_size > 0 ?
(double)index[fi].compressed_size / (double)index[fi].uncompressed_size * 100.0 : 100.0;
fprintf(stderr, " %s -> %s (%.1f%%)\n", in_s, out_s, ratio);
}
}
/* Destroy parallel context before writing index (single-threaded I/O) */
if (pctx) { zpar_destroy(pctx); pctx = NULL; }
/* Check for write errors before writing the index */
if (write_err) {
fprintf(stderr, "Error: Write errors occurred during compression.\n");
free(index); free(rbuf); free(cbuf); fclose(out);
return ZUPT_ERR_IO;
}
/* ─── Central Index ─── */
uint64_t index_offset = safe_ftello(out);
size_t icap = (size_t)num_files * (ZUPT_MAX_PATH + 128);
uint8_t *ibuf = (uint8_t*)malloc(icap);
if (!ibuf) { free(index); free(rbuf); free(cbuf); fclose(out); return ZUPT_ERR_NOMEM; }
size_t ip = 0;
ip += (size_t)zupt_encode_varint(ibuf + ip, (uint64_t)num_files);
for (int fi = 0; fi < num_files; fi++) {
if (index[fi].path[0] == '\0') continue;
size_t plen = strlen(index[fi].path);
ip += (size_t)zupt_encode_varint(ibuf + ip, plen);
memcpy(ibuf + ip, index[fi].path, plen); ip += plen;
ip += index_put_u64(ibuf + ip, index[fi].uncompressed_size);
ip += index_put_u64(ibuf + ip, index[fi].compressed_size);
ip += index_put_u64(ibuf + ip, index[fi].modification_time);
ip += index_put_u64(ibuf + ip, index[fi].content_hash);
ip += index_put_u64(ibuf + ip, index[fi].first_block_offset);
ip += (size_t)zupt_encode_varint(ibuf + ip, index[fi].block_count);
ip += index_put_u32(ibuf + ip, index[fi].attributes);
}
size_t ic_cap = zupt_lzh_bound(ip);
uint8_t *ic = (uint8_t*)malloc(ic_cap);
size_t ic_size = zupt_lzh_compress(ibuf, ip, ic, ic_cap, opts->level);
uint16_t ic_codec = ZUPT_CODEC_ZUPT_LZH;
const uint8_t *ic_pay; uint64_t ic_plen;
if (ic_size == 0 || ic_size >= ip) {
ic_codec = ZUPT_CODEC_STORE; ic_pay = ibuf; ic_plen = ip;
} else {
ic_pay = ic; ic_plen = ic_size;
}
uint8_t *enc_idx = NULL;
uint16_t idx_bflags = 0;
if (opts->encrypt && opts->keyring.active) {
size_t enc_len;
enc_idx = zupt_encrypt_buffer(&opts->keyring, ic_pay, ic_plen, block_seq, &enc_len);
ic_pay = enc_idx; ic_plen = enc_len;
idx_bflags |= ZUPT_BFLAG_ENCRYPTED;
}
uint64_t ic_ck = zupt_xxh64(ibuf, ip, 0);
w8(out, ZUPT_BLOCK_MAGIC_0); w8(out, ZUPT_BLOCK_MAGIC_1);
w8(out, ZUPT_BLOCK_INDEX);
w16le(out, ic_codec); w16le(out, idx_bflags);
zupt_write_varint(out, ip); zupt_write_varint(out, ic_plen);
w64le(out, ic_ck);
if (fwrite(ic_pay, 1, (size_t)ic_plen, out) != (size_t)ic_plen) write_err = 1;
free(enc_idx);
/* ─── Footer ─── */
zupt_footer_t ft;
memset(&ft, 0, sizeof(ft));
ft.index_offset = index_offset;
ft.total_blocks = total_blocks;
ft.archive_checksum = safe_ftello(out);
ft.footer_magic[0]='Z'; ft.footer_magic[1]='E'; ft.footer_magic[2]='N'; ft.footer_magic[3]='D';
ft.footer_version = 1;
if (fwrite(&ft, sizeof(ft), 1, out) != 1) write_err = 1;
fclose(out);
if (write_err) {
fprintf(stderr, "Error: Write errors occurred. Archive may be corrupt.\n");
free(ic); free(ibuf); free(index); free(rbuf); free(cbuf);
return ZUPT_ERR_IO;
}
/* Summary */
time_t elapsed = time(NULL) - start_time;
if (elapsed < 1) elapsed = 1;
char in_s[32], out_s[32];
zupt_format_size(total_in, in_s, sizeof(in_s));
zupt_format_size(total_out, out_s, sizeof(out_s));
double ratio = total_in > 0 ? (double)total_out/(double)total_in*100.0 : 100.0;
double speed = (double)total_in / (double)elapsed / 1048576.0;
if (!opts->quiet) {
fprintf(stderr, "\n Archive: %s\n", output_path);
fprintf(stderr, " Files: %d\n", num_files);
fprintf(stderr, " Original: %s\n", in_s);
fprintf(stderr, " Compressed: %s (%.1f%%)\n", out_s, ratio);
if (total_in > 0 && total_out > 0) {
double cr = (double)total_in / (double)total_out;
fprintf(stderr, " Ratio: %.2f:1\n", cr);
}
fprintf(stderr, " Blocks: %llu\n", (unsigned long long)total_blocks);
fprintf(stderr, " Codec: %s (level %d)\n", zupt_codec_name(opts->codec_id), opts->level);
if (opts->encrypt) fprintf(stderr, " Encryption: AES-256 + HMAC-SHA256\n");
fprintf(stderr, " Speed: %.1f MB/s (%llds)\n", speed, (long long)elapsed);
}
free(ic); free(ibuf); free(index); free(rbuf); free(cbuf);
return ZUPT_OK;
}
/* ═══════════════════════════════════════════════════════════════════
* SOLID-MODE COMPRESSION
* ═══════════════════════════════════════════════════════════════════ */
zupt_error_t zupt_compress_solid(const char *output_path,
const char **arc_paths,
const char **disk_paths,
int num_files,
zupt_options_t *opts) {
if (opts->block_size == 0) opts->block_size = auto_block_size(opts->level);
if (opts->block_size < 524288) opts->block_size = 524288;
/* Resolve AUTO codec before compression */
if (opts->codec_id == ZUPT_CODEC_AUTO)
opts->codec_id = zupt_resolve_auto_codec();
FILE *out = fopen(output_path, "wb");
if (!out) { fprintf(stderr, "Error: Cannot create '%s'\n", output_path); return ZUPT_ERR_IO; }
int write_err = 0;
zupt_archive_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
hdr.magic[0]=ZUPT_MAGIC_0; hdr.magic[1]=ZUPT_MAGIC_1; hdr.magic[2]=ZUPT_MAGIC_2;
hdr.magic[3]=ZUPT_MAGIC_3; hdr.magic[4]=ZUPT_MAGIC_4; hdr.magic[5]=ZUPT_MAGIC_5;
hdr.version_major = ZUPT_FORMAT_MAJOR; hdr.version_minor = ZUPT_FORMAT_MINOR;
hdr.global_flags = ZUPT_FLAG_CKSUM_XXH64 | ZUPT_FLAG_SOLID;
if (opts->encrypt) hdr.global_flags |= ZUPT_FLAG_ENCRYPTED;
hdr.creation_time = now_ns();
gen_uuid(hdr.archive_id);
if (fwrite(&hdr, sizeof(hdr), 1, out) != 1) write_err = 1;
if (opts->encrypt) {
zupt_error_t enc_err = write_enc_header(out, &hdr, opts);
if (enc_err != ZUPT_OK) { fclose(out); return enc_err; }
}
zupt_index_entry_t *index = (zupt_index_entry_t*)calloc((size_t)num_files, sizeof(zupt_index_entry_t));
if (!index) { fclose(out); return ZUPT_ERR_NOMEM; }
uint64_t total_uncompressed = 0;
for (int fi = 0; fi < num_files; fi++) {
FILE *inf = fopen(disk_paths[fi], "rb");
if (!inf) continue;
fseeko(inf, 0, SEEK_END);
int64_t sz = ftello(inf);
fclose(inf);
if (sz < 0) continue;
strncpy(index[fi].path, arc_paths[fi], ZUPT_MAX_PATH-1);
index[fi].uncompressed_size = (uint64_t)sz;
index[fi].first_block_offset = total_uncompressed;
index[fi].modification_time = get_mtime(disk_paths[fi]);
total_uncompressed += (uint64_t)sz;
if (!opts->quiet) {
char sz_s[32]; zupt_format_size((uint64_t)sz, sz_s, sizeof(sz_s));
fprintf(stderr, " %s (%s)\n", arc_paths[fi], sz_s);
}
}
uint8_t *solid_buf = (uint8_t*)malloc((size_t)total_uncompressed);
if (!solid_buf) { free(index); fclose(out); return ZUPT_ERR_NOMEM; }
size_t solid_pos = 0;
for (int fi = 0; fi < num_files; fi++) {
if (index[fi].uncompressed_size == 0) continue;
FILE *inf = fopen(disk_paths[fi], "rb");
if (!inf) continue;
if (fread(solid_buf + solid_pos, 1, (size_t)index[fi].uncompressed_size, inf) != (size_t)index[fi].uncompressed_size) { fclose(inf); continue; }
fclose(inf);
solid_pos += (size_t)index[fi].uncompressed_size;
}
uint64_t cum = 0;
for (int fi = 0; fi < num_files; fi++) {
uint64_t sz = index[fi].uncompressed_size;
if (sz > 0) index[fi].content_hash = zupt_xxh64(solid_buf + cum, (size_t)sz, 0);
cum += sz;
}
size_t block_cap = zupt_lzh_bound(opts->block_size) + 512;
uint8_t *cbuf = (uint8_t*)malloc(block_cap);
if (!cbuf) { free(solid_buf); free(index); fclose(out); return ZUPT_ERR_NOMEM; }
uint64_t total_blocks = 0, total_out = 0, block_seq = 0;
size_t remaining = (size_t)total_uncompressed;
size_t src_pos = 0;
time_t start_time = time(NULL);
while (remaining > 0) {
size_t chunk = remaining < opts->block_size ? remaining : opts->block_size;
uint8_t *src = solid_buf + src_pos;
uint64_t checksum = zupt_xxh64(src, chunk, 0);
size_t comp_size = 0;
uint16_t codec = opts->codec_id;
if (codec == ZUPT_CODEC_ZUPT_LZHP) {
uint8_t pred[256];
float benefit = zupt_predict_benefit(src, chunk);
if (benefit > 0.03f && chunk > 256) {
zupt_predict_build(src, chunk, pred);
uint8_t *trans = (uint8_t*)malloc(chunk);
if (trans) {
zupt_predict_encode(src, trans, chunk, pred);
size_t lzh_size = zupt_lzh_compress(trans, chunk, cbuf + 257, block_cap - 257, opts->level);
free(trans);
if (lzh_size > 0 && 257 + lzh_size < chunk) {
cbuf[0] = 0x01;
memcpy(cbuf + 1, pred, 256);
comp_size = 257 + lzh_size;
}
}
}
if (comp_size == 0) {
cbuf[0] = 0x00;
size_t plain = zupt_lzh_compress(src, chunk, cbuf + 1, block_cap - 1, opts->level);
if (plain > 0 && 1 + plain < chunk) comp_size = 1 + plain;
}
} else if (codec == ZUPT_CODEC_ZUPT_LZH) {
comp_size = zupt_lzh_compress(src, chunk, cbuf, block_cap, opts->level);
}
/* VAPTVUPT: VaptVupt codec in solid mode (v1.4.0 integration API) */
else if (codec == ZUPT_CODEC_VAPTVUPT) {
size_t vv_cap = vvz_compress_bound(chunk);
uint8_t *vv_tmp = (uint8_t *)malloc(vv_cap);
if (vv_tmp) {
int64_t csz = vvz_compress(src, chunk, vv_tmp, vv_cap, opts->level);
if (csz > 0 && (size_t)csz < chunk) {
if ((size_t)csz <= block_cap) {
memcpy(cbuf, vv_tmp, (size_t)csz);
comp_size = (size_t)csz;
}
}
free(vv_tmp);
}
}
const uint8_t *payload = cbuf; uint64_t payload_size = comp_size;
if (comp_size == 0 || comp_size >= chunk) {
codec = ZUPT_CODEC_STORE; payload = src; payload_size = chunk;
}
uint8_t *enc_pay = NULL;
uint16_t bflags = 0;
if (opts->encrypt && opts->keyring.active) {
size_t enc_len;
enc_pay = zupt_encrypt_buffer(&opts->keyring, payload, payload_size, block_seq, &enc_len);
if (enc_pay) { payload = enc_pay; payload_size = enc_len; bflags |= ZUPT_BFLAG_ENCRYPTED; }
}
w8(out, ZUPT_BLOCK_MAGIC_0); w8(out, ZUPT_BLOCK_MAGIC_1);
w8(out, ZUPT_BLOCK_DATA);
w16le(out, codec); w16le(out, bflags);
zupt_write_varint(out, (uint64_t)chunk);
zupt_write_varint(out, payload_size);
w64le(out, checksum);
if (fwrite(payload, 1, (size_t)payload_size, out) != (size_t)payload_size) write_err = 1;
free(enc_pay);
total_out += payload_size;
total_blocks++;
block_seq++;
src_pos += chunk;
remaining -= chunk;
}
for (int fi = 0; fi < num_files; fi++) index[fi].block_count = 0;
/* Write central index (LE serialization) */
uint64_t index_offset = safe_ftello(out);
size_t icap = (size_t)num_files * (ZUPT_MAX_PATH + 128);
uint8_t *ibuf = (uint8_t*)malloc(icap);
if (!ibuf) { free(solid_buf); free(cbuf); free(index); fclose(out); return ZUPT_ERR_NOMEM; }
size_t ip = 0;
ip += (size_t)zupt_encode_varint(ibuf + ip, (uint64_t)num_files);
for (int fi = 0; fi < num_files; fi++) {
if (index[fi].path[0] == '\0') continue;
size_t plen = strlen(index[fi].path);
ip += (size_t)zupt_encode_varint(ibuf + ip, plen);
memcpy(ibuf + ip, index[fi].path, plen); ip += plen;
ip += index_put_u64(ibuf + ip, index[fi].uncompressed_size);
ip += index_put_u64(ibuf + ip, index[fi].compressed_size);
ip += index_put_u64(ibuf + ip, index[fi].modification_time);
ip += index_put_u64(ibuf + ip, index[fi].content_hash);
ip += index_put_u64(ibuf + ip, index[fi].first_block_offset);
ip += (size_t)zupt_encode_varint(ibuf + ip, index[fi].block_count);
ip += index_put_u32(ibuf + ip, index[fi].attributes);
}
size_t ic_cap = zupt_lzh_bound(ip);
uint8_t *ic = (uint8_t*)malloc(ic_cap);
size_t ic_size = zupt_lzh_compress(ibuf, ip, ic, ic_cap, opts->level);
uint16_t ic_codec = ZUPT_CODEC_ZUPT_LZH;
const uint8_t *ic_pay; uint64_t ic_plen;
if (ic_size == 0 || ic_size >= ip) { ic_codec = ZUPT_CODEC_STORE; ic_pay = ibuf; ic_plen = ip; }
else { ic_pay = ic; ic_plen = ic_size; }
uint8_t *enc_idx = NULL; uint16_t idx_bflags = 0;
if (opts->encrypt && opts->keyring.active) {
size_t enc_len;
enc_idx = zupt_encrypt_buffer(&opts->keyring, ic_pay, ic_plen, block_seq, &enc_len);
if (enc_idx) { ic_pay = enc_idx; ic_plen = enc_len; idx_bflags |= ZUPT_BFLAG_ENCRYPTED; }
}
w8(out, ZUPT_BLOCK_MAGIC_0); w8(out, ZUPT_BLOCK_MAGIC_1);
w8(out, ZUPT_BLOCK_INDEX);
w16le(out, ic_codec); w16le(out, idx_bflags);
zupt_write_varint(out, (uint64_t)ip);
zupt_write_varint(out, ic_plen);
w64le(out, zupt_xxh64(ibuf, ip, 0));
if (fwrite(ic_pay, 1, (size_t)ic_plen, out) != (size_t)ic_plen) write_err = 1;
total_blocks++;
free(enc_idx);
zupt_footer_t ft;
memset(&ft, 0, sizeof(ft));
ft.index_offset = index_offset;
ft.total_blocks = total_blocks;
ft.footer_magic[0]='Z'; ft.footer_magic[1]='E'; ft.footer_magic[2]='N'; ft.footer_magic[3]='D';
ft.footer_version = 1;
if (fwrite(&ft, sizeof(ft), 1, out) != 1) write_err = 1;
fclose(out);
if (write_err) {
fprintf(stderr, "Error: Write errors occurred. Archive may be corrupt.\n");
free(ic); free(ibuf); free(solid_buf); free(cbuf); free(index);
return ZUPT_ERR_IO;
}
time_t elapsed = time(NULL) - start_time;
if (elapsed < 1) elapsed = 1;
char in_s[32], out_s[32];
zupt_format_size(total_uncompressed, in_s, sizeof(in_s));
zupt_format_size(total_out, out_s, sizeof(out_s));
if (!opts->quiet) {
fprintf(stderr, "\n Archive: %s\n", output_path);
fprintf(stderr, " Files: %d (SOLID)\n", num_files);
fprintf(stderr, " Original: %s\n", in_s);
fprintf(stderr, " Compressed: %s (%.1f%%)\n", out_s,
total_uncompressed > 0 ? (double)total_out / (double)total_uncompressed * 100.0 : 100.0);
if (total_uncompressed > 0 && total_out > 0)
fprintf(stderr, " Ratio: %.2f:1\n", (double)total_uncompressed / (double)total_out);
fprintf(stderr, " Blocks: %llu\n", (unsigned long long)total_blocks);
fprintf(stderr, " Codec: %s (level %d, SOLID)\n", zupt_codec_name(opts->codec_id), opts->level);
if (opts->encrypt) fprintf(stderr, " Encryption: AES-256 + HMAC-SHA256\n");
fprintf(stderr, " Speed: %.1f MB/s (%llds)\n",
(double)total_uncompressed / (double)elapsed / 1048576.0, (long long)elapsed);
}
free(ic); free(ibuf); free(solid_buf); free(cbuf); free(index);
return ZUPT_OK;
}
/* ═══════════════════════════════════════════════════════════════════
* READING HELPERS
* ═══════════════════════════════════════════════════════════════════ */
static zupt_error_t read_header(FILE *f, zupt_archive_header_t *h) {
if (fread(h, sizeof(*h), 1, f) != 1) return ZUPT_ERR_IO;
if (h->magic[0]!=ZUPT_MAGIC_0||h->magic[1]!=ZUPT_MAGIC_1||
h->magic[2]!=ZUPT_MAGIC_2||h->magic[3]!=ZUPT_MAGIC_3||
h->magic[4]!=ZUPT_MAGIC_4||h->magic[5]!=ZUPT_MAGIC_5) return ZUPT_ERR_BAD_MAGIC;
if (h->version_major != ZUPT_FORMAT_MAJOR) return ZUPT_ERR_BAD_VERSION;
return ZUPT_OK;
}
static zupt_error_t read_footer(FILE *f, zupt_footer_t *ft) {
fseeko(f, -(int64_t)sizeof(zupt_footer_t), SEEK_END);
if (fread(ft, sizeof(*ft), 1, f) != 1) return ZUPT_ERR_IO;
if (ft->footer_magic[0]!='Z'||ft->footer_magic[1]!='E'||
ft->footer_magic[2]!='N'||ft->footer_magic[3]!='D') return ZUPT_ERR_CORRUPT;
return ZUPT_OK;
}
zupt_error_t read_block(FILE *f, zupt_block_t *b) {
uint8_t m[2];
if (fread(m,1,2,f)!=2) return ZUPT_ERR_IO;
if (m[0]!=ZUPT_BLOCK_MAGIC_0||m[1]!=ZUPT_BLOCK_MAGIC_1) return ZUPT_ERR_CORRUPT;
uint8_t bt; if (fread(&bt,1,1,f)!=1) return ZUPT_ERR_IO; b->block_type = bt;
if (r16le(f,&b->codec_id)<0) return ZUPT_ERR_IO;
if (r16le(f,&b->block_flags)<0) return ZUPT_ERR_IO;
if (zupt_read_varint(f,&b->uncompressed_size)<0) return ZUPT_ERR_IO;
if (zupt_read_varint(f,&b->compressed_size)<0) return ZUPT_ERR_IO;
if (r64le(f,&b->checksum)<0) return ZUPT_ERR_IO;
if (b->compressed_size > ZUPT_MAX_BLOCK_SZ + 4096) return ZUPT_ERR_OVERFLOW;
if (b->uncompressed_size > ZUPT_MAX_BLOCK_SZ) return ZUPT_ERR_OVERFLOW;
b->payload = (uint8_t*)malloc((size_t)b->compressed_size);
if (!b->payload) return ZUPT_ERR_NOMEM;
if (fread(b->payload,1,(size_t)b->compressed_size,f)!=(size_t)b->compressed_size) {
free(b->payload); b->payload=NULL; return ZUPT_ERR_IO;
}
return ZUPT_OK;
}
zupt_error_t decompress_block(const zupt_block_t *b, const zupt_keyring_t *kr,
uint64_t block_seq, uint8_t **out, size_t *olen) {
const uint8_t *comp_data = b->payload;
size_t comp_len = (size_t)b->compressed_size;
uint8_t *dec_payload = NULL;
if (!b->payload && comp_len > 0) return ZUPT_ERR_CORRUPT;
if (b->uncompressed_size > ZUPT_MAX_BLOCK_SZ) return ZUPT_ERR_OVERFLOW;
if (comp_len > ZUPT_MAX_BLOCK_SZ + 1024) return ZUPT_ERR_OVERFLOW;
if (b->block_flags & ZUPT_BFLAG_ENCRYPTED) {
if (!kr || !kr->active) return ZUPT_ERR_AUTH_FAIL;
size_t dec_len;
dec_payload = zupt_decrypt_buffer(kr, comp_data, comp_len, block_seq, &dec_len);
if (!dec_payload) return ZUPT_ERR_AUTH_FAIL;
comp_data = dec_payload;
comp_len = dec_len;
}
*olen = (size_t)b->uncompressed_size;
if (*olen == 0) { *out = NULL; free(dec_payload); return ZUPT_OK; }
*out = (uint8_t*)malloc(*olen);
if (!*out) { free(dec_payload); return ZUPT_ERR_NOMEM; }
zupt_error_t result = ZUPT_OK;
if (b->codec_id == ZUPT_CODEC_STORE) {
if (comp_len < *olen) {
result = ZUPT_ERR_CORRUPT;
} else {
memcpy(*out, comp_data, *olen);
}
} else if (b->codec_id == ZUPT_CODEC_ZUPT_LZ) {
size_t r = zupt_lz_decompress(comp_data, comp_len, *out, *olen);
if (r != *olen) result = ZUPT_ERR_CORRUPT;
} else if (b->codec_id == ZUPT_CODEC_ZUPT_LZH) {
size_t r = zupt_lzh_decompress(comp_data, comp_len, *out, *olen);
if (r != *olen) result = ZUPT_ERR_CORRUPT;
} else if (b->codec_id == ZUPT_CODEC_ZUPT_LZHP) {
if (comp_len < 1) { result = ZUPT_ERR_CORRUPT; goto done; }
uint8_t pflag = comp_data[0];
int pred_active = (pflag & 0x01);
size_t hdr_size = 1;
uint8_t pred[256];
if (pred_active) {
if (comp_len < 257) { result = ZUPT_ERR_CORRUPT; goto done; }
memcpy(pred, comp_data + 1, 256);
hdr_size = 257;
}
if (comp_len <= hdr_size) { result = ZUPT_ERR_CORRUPT; goto done; }
const uint8_t *lzh_data = comp_data + hdr_size;
size_t lzh_len = comp_len - hdr_size;
if (pred_active) {
uint8_t *temp = (uint8_t *)malloc(*olen);
if (!temp) { result = ZUPT_ERR_NOMEM; goto done; }
size_t r = zupt_lzh_decompress(lzh_data, lzh_len, temp, *olen);
if (r != *olen) {
free(temp);
result = ZUPT_ERR_CORRUPT;
goto done;
}
zupt_predict_decode(temp, *out, *olen, pred);
free(temp);
} else {
size_t r = zupt_lzh_decompress(lzh_data, lzh_len, *out, *olen);
if (r != *olen) result = ZUPT_ERR_CORRUPT;
}
}
/* VAPTVUPT: VaptVupt codec decompress path (v1.4.0 cross-block decode) */
else if (b->codec_id == ZUPT_CODEC_VAPTVUPT) {
int64_t dsz = vvz_decompress(comp_data, comp_len, *out, *olen);
if (dsz < 0 || (size_t)dsz != *olen) result = ZUPT_ERR_CORRUPT;
} else {
result = ZUPT_ERR_UNSUPPORTED;
}
done:
free(dec_payload);
if (result != ZUPT_OK) { free(*out); *out = NULL; return result; }
uint64_t ck = zupt_xxh64(*out, *olen, 0);
if (ck != b->checksum) { free(*out); *out = NULL; return ZUPT_ERR_BAD_CHECKSUM; }
return ZUPT_OK;
}
zupt_error_t read_enc_header(FILE *f, zupt_archive_header_t *hdr, zupt_options_t *opts) {
if (!(hdr->global_flags & ZUPT_FLAG_ENCRYPTED)) return ZUPT_OK;
fseeko(f, (int64_t)hdr->encryption_header_off, SEEK_SET);
zupt_block_t eb;
zupt_error_t err = read_block(f, &eb);
if (err != ZUPT_OK) return err;
if (eb.compressed_size < 1) { free(eb.payload); return ZUPT_ERR_CORRUPT; }
uint8_t enc_type = eb.payload[0];
if (enc_type == ZUPT_ENC_PQ_HYBRID) {
/* ─── PQ HYBRID MODE ─── */
if (!opts->pq_mode || opts->keyfile[0] == '\0') {
fprintf(stderr, "Error: Archive uses post-quantum encryption. Use --pq <keyfile>.\n");
free(eb.payload);
return ZUPT_ERR_AUTH_FAIL;
}
if (zupt_hybrid_decrypt_init(&opts->keyring, opts->keyfile,
eb.payload, (size_t)eb.compressed_size) != 0) {
fprintf(stderr, "Error: PQ decryption key derivation failed (wrong key?).\n");
free(eb.payload);
return ZUPT_ERR_AUTH_FAIL;
}
free(eb.payload);
return ZUPT_OK;
} else if (enc_type == ZUPT_ENC_PBKDF2) {
/* ─── PASSWORD MODE (v0.7+ format with enc_type prefix) ─── */
if (opts->password[0] == '\0') {
fprintf(stderr, "Error: Archive is encrypted. Use -p to provide a password.\n");
free(eb.payload);
return ZUPT_ERR_AUTH_FAIL;
}
if (eb.compressed_size < 53) { free(eb.payload); return ZUPT_ERR_CORRUPT; }
uint8_t salt[32], nonce[16]; uint32_t iter;
memcpy(salt, eb.payload + 1, 32);
memcpy(nonce, eb.payload + 33, 16);
memcpy(&iter, eb.payload + 49, 4);
free(eb.payload);
fprintf(stderr, " Deriving decryption key (PBKDF2-SHA256, %u iterations)...\n", iter);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter);
return ZUPT_OK;
} else {
/* ─── LEGACY v0.5/v0.6 format (no enc_type prefix, raw salt at offset 0) ─── */
if (opts->password[0] == '\0') {
fprintf(stderr, "Error: Archive is encrypted. Use -p to provide a password.\n");
free(eb.payload);
return ZUPT_ERR_AUTH_FAIL;
}
if (eb.compressed_size < 52) { free(eb.payload); return ZUPT_ERR_CORRUPT; }
uint8_t salt[32], nonce[16]; uint32_t iter;
memcpy(salt, eb.payload, 32);
memcpy(nonce, eb.payload + 32, 16);
memcpy(&iter, eb.payload + 48, 4);
free(eb.payload);
fprintf(stderr, " Deriving decryption key (PBKDF2-SHA256, %u iterations)...\n", iter);
zupt_derive_keys(&opts->keyring, opts->password, salt, nonce, iter);
return ZUPT_OK;
}
}
static zupt_error_t parse_index(const uint8_t *buf, size_t blen,
zupt_index_entry_t **ents, int *n) {
size_t p = 0; uint64_t count;
int vn = zupt_decode_varint(buf+p, blen-p, &count);
if (vn < 0) return ZUPT_ERR_CORRUPT;
p += (size_t)vn;
if (count > ZUPT_MAX_FILES) return ZUPT_ERR_OVERFLOW;
*n = (int)count;
*ents = (zupt_index_entry_t*)calloc((size_t)count, sizeof(zupt_index_entry_t));
if (!*ents) return ZUPT_ERR_NOMEM;
for (uint64_t i = 0; i < count; i++) {
zupt_index_entry_t *e = &(*ents)[i];
uint64_t plen;
vn = zupt_decode_varint(buf+p, blen-p, &plen);
if (vn<0||p+(size_t)vn+plen>blen) { free(*ents); return ZUPT_ERR_CORRUPT; }
p += (size_t)vn;
if (plen >= ZUPT_MAX_PATH) plen = ZUPT_MAX_PATH-1;
memcpy(e->path, buf+p, (size_t)plen); e->path[plen]='\0'; p += (size_t)plen;
if (p+44>blen) { free(*ents); return ZUPT_ERR_CORRUPT; }
e->uncompressed_size = index_get_u64(buf+p); p+=8;
e->compressed_size = index_get_u64(buf+p); p+=8;
e->modification_time = index_get_u64(buf+p); p+=8;
e->content_hash = index_get_u64(buf+p); p+=8;
e->first_block_offset= index_get_u64(buf+p); p+=8;
uint64_t bc;
vn = zupt_decode_varint(buf+p, blen-p, &bc);
if (vn<0) { free(*ents); return ZUPT_ERR_CORRUPT; }
p += (size_t)vn; e->block_count = (uint32_t)bc;
if (p+4>blen) { free(*ents); return ZUPT_ERR_CORRUPT; }
e->attributes = index_get_u32(buf+p); p+=4;
}
return ZUPT_OK;
}
static zupt_error_t open_archive(FILE *f, zupt_options_t *opts,
zupt_archive_header_t *hdr, zupt_footer_t *ft,
zupt_index_entry_t **entries, int *num_entries) {
zupt_error_t err = read_header(f, hdr);
if (err != ZUPT_OK) return err;
err = read_footer(f, ft);
if (err != ZUPT_OK) return err;
err = read_enc_header(f, hdr, opts);
if (err != ZUPT_OK) return err;
fseeko(f, (int64_t)ft->index_offset, SEEK_SET);
zupt_block_t ib;
err = read_block(f, &ib);
if (err != ZUPT_OK) return err;
uint8_t *id; size_t idlen;
err = decompress_block(&ib, &opts->keyring, 0xFFFFFFFFFFFFFFFFULL, &id, &idlen);
free(ib.payload);
if (err != ZUPT_OK) return err;
err = parse_index(id, idlen, entries, num_entries);
free(id);
return err;
}
/* ═══════════════════════════════════════════════════════════════════
* LIST
* ═══════════════════════════════════════════════════════════════════ */
zupt_error_t zupt_list_archive(const char *arc, zupt_options_t *opts) {
FILE *f = fopen(arc, "rb");
if (!f) { fprintf(stderr, "Error: Cannot open '%s'\n", arc); return ZUPT_ERR_IO; }
zupt_archive_header_t hdr; zupt_footer_t ft;
zupt_index_entry_t *ents; int n;
zupt_error_t err = open_archive(f, opts, &hdr, &ft, &ents, &n);
if (err != ZUPT_OK) { fclose(f); return err; }
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");
printf("\n\n");
printf(" %-50s %12s %12s %s\n", "Path", "Original", "Compressed", "Ratio");
printf(" %s\n", "--------------------------------------------------------------------------------------------");
uint64_t ti=0, to=0;
for (int i=0;i<n;i++) {
zupt_index_entry_t *e = &ents[i];
char is[16],cs[16];
zupt_format_size(e->uncompressed_size, is, sizeof(is));
zupt_format_size(e->compressed_size, cs, sizeof(cs));
double r = e->uncompressed_size>0?(double)e->compressed_size/(double)e->uncompressed_size*100:100;
printf(" %-50s %12s %12s %5.1f%%\n", e->path, is, cs, r);
ti += e->uncompressed_size; to += e->compressed_size;
}
char tis[16],tos[16];
zupt_format_size(ti,tis,sizeof(tis)); zupt_format_size(to,tos,sizeof(tos));
double tr = ti>0?(double)to/(double)ti*100:100;
printf(" %s\n", "--------------------------------------------------------------------------------------------");
printf(" %-50s %12s %12s %5.1f%%\n", "TOTAL", tis, tos, tr);
printf(" %d file(s)\n\n", n);
free(ents); fclose(f);
return ZUPT_OK;
}
/* ═══════════════════════════════════════════════════════════════════
* EXTRACT
* ═══════════════════════════════════════════════════════════════════ */
zupt_error_t zupt_extract_archive(const char *arc, const char *dir, zupt_options_t *opts) {
FILE *f = fopen(arc, "rb");
if (!f) { fprintf(stderr, "Error: Cannot open '%s'\n", arc); return ZUPT_ERR_IO; }
zupt_archive_header_t hdr; zupt_footer_t ft;
zupt_index_entry_t *ents; int n;
zupt_error_t err = open_archive(f, opts, &hdr, &ft, &ents, &n);
if (err != ZUPT_OK) { fclose(f); fprintf(stderr, "Error: %s\n", zupt_strerror(err)); return err; }
if (dir) zupt_mkdir(dir);
int ok=0, fail=0;
uint64_t total_extracted = 0;
time_t start = time(NULL);
int is_solid = (hdr.global_flags & ZUPT_FLAG_SOLID) != 0;
if (is_solid) {
uint64_t total_size = 0;
for (int i = 0; i < n; i++) {
if (total_size + ents[i].uncompressed_size < total_size) {
fprintf(stderr, " Error: solid stream size overflow\n");
free(ents); fclose(f); return ZUPT_ERR_OVERFLOW;
}
total_size += ents[i].uncompressed_size;
}
if (total_size > (uint64_t)4 * 1024 * 1024 * 1024) {
fprintf(stderr, " Error: solid stream too large (%llu bytes)\n",
(unsigned long long)total_size);
free(ents); fclose(f); return ZUPT_ERR_OVERFLOW;
}
uint8_t *solid_buf = (uint8_t*)malloc((size_t)total_size);
if (!solid_buf) { free(ents); fclose(f); return ZUPT_ERR_NOMEM; }
fseeko(f, sizeof(zupt_archive_header_t), SEEK_SET);
if (hdr.global_flags & ZUPT_FLAG_ENCRYPTED) {
zupt_block_t enc_blk;
err = read_block(f, &enc_blk);
free(enc_blk.payload);
if (err != ZUPT_OK) { free(solid_buf); free(ents); fclose(f); return err; }
}
size_t solid_pos = 0;
uint64_t block_seq = 0;
int dec_error = 0;
while (solid_pos < (size_t)total_size) {
zupt_block_t blk;
err = read_block(f, &blk);
if (err != ZUPT_OK) { dec_error = 1; break; }
if (blk.block_type == ZUPT_BLOCK_INDEX) { free(blk.payload); break; }
uint8_t *dec; size_t dlen;
err = decompress_block(&blk, &opts->keyring, block_seq, &dec, &dlen);
free(blk.payload);
if (err != ZUPT_OK) {
fprintf(stderr, " Solid block %llu decompression failed: %s\n",
(unsigned long long)block_seq, zupt_strerror(err));
dec_error = 1; break;
}
if (solid_pos + dlen > (size_t)total_size) dlen = (size_t)total_size - solid_pos;
memcpy(solid_buf + solid_pos, dec, dlen);
solid_pos += dlen;
free(dec);
block_seq++;
}
if (dec_error) {
free(solid_buf); free(ents); fclose(f);
return ZUPT_ERR_CORRUPT;
}
for (int i = 0; i < n; i++) {
zupt_index_entry_t *e = &ents[i];
char out_path[ZUPT_MAX_PATH + 256];
if (dir) snprintf(out_path, sizeof(out_path), "%s%c%s", dir, ZUPT_PATH_SEP, e->path);
else { strncpy(out_path, e->path, sizeof(out_path)-1); out_path[sizeof(out_path)-1]='\0'; }
for (char *p=out_path;*p;p++) if (*p=='/') *p=ZUPT_PATH_SEP;
ensure_dirs(out_path);
FILE *of = fopen(out_path, "wb");
if (!of) { fail++; continue; }
uint64_t off = e->first_block_offset;
uint64_t sz = e->uncompressed_size;
if (off + sz <= total_size) {
fwrite(solid_buf + off, 1, (size_t)sz, of);
total_extracted += sz;
/* Verify content hash (empty files have content_hash=0) */
if (sz > 0) {
uint64_t ck = zupt_xxh64(solid_buf + off, (size_t)sz, 0);
if (ck == e->content_hash) ok++;
else { fprintf(stderr, " Checksum fail: %s\n", e->path); fail++; }
} else {
ok++; /* Empty file: nothing to verify */
}
} else {
fprintf(stderr, " Invalid offset: %s\n", e->path); fail++;
}
if (opts->verbose) {
char sz_s[16]; zupt_format_size(sz, sz_s, sizeof(sz_s));
fprintf(stderr, " %s (%s)\n", e->path, sz_s);
}
fclose(of);
}
free(solid_buf);
} else {
/* ─── NON-SOLID EXTRACTION ─── */
/* Multi-threaded decompression: dispatch blocks to N workers.
* Workers: decrypt → decompress → verify checksum.
* Main thread: read blocks, dispatch, write output in order. */
int effective_threads = opts->threads > 1 ? opts->threads : 1;
zpar_ctx_t *pctx = NULL;
if (effective_threads > 1) {
pctx = zpar_create(effective_threads, ZUPT_DEFAULT_BLOCK_SZ, 1,
(hdr.global_flags & ZUPT_FLAG_ENCRYPTED) ? &opts->keyring : NULL);
if (!pctx || pctx->threads_running == 0) {
if (pctx) zpar_destroy(pctx);
pctx = NULL;
effective_threads = 1;
}
}
for (int i=0; i<n; i++) {
zupt_index_entry_t *e = &ents[i];
char out_path[ZUPT_MAX_PATH + 256];
if (dir) snprintf(out_path, sizeof(out_path), "%s%c%s", dir, ZUPT_PATH_SEP, e->path);
else { strncpy(out_path, e->path, sizeof(out_path)-1); out_path[sizeof(out_path)-1]='\0'; }
for (char *p=out_path;*p;p++) if (*p=='/') *p=ZUPT_PATH_SEP;
ensure_dirs(out_path);
FILE *of = fopen(out_path, "wb");
if (!of) { fprintf(stderr, " Error: %s\n", out_path); fail++; continue; }
if (opts->verbose) {
char sz[16]; zupt_format_size(e->uncompressed_size, sz, sizeof(sz));
fprintf(stderr, " %s (%s)\n", e->path, sz);
}
fseeko(f, (int64_t)e->first_block_offset, SEEK_SET);
int berr = 0;
if (pctx && effective_threads > 1 && e->block_count > 1) {
/* ─── MT DECOMPRESSION PATH ─── */
int *pending_slots = (int *)malloc((size_t)effective_threads * sizeof(int));
if (!pending_slots) { berr = 1; goto file_done; }
uint32_t blocks_remaining = e->block_count;
uint64_t decomp_seq = 0;
while (blocks_remaining > 0) {
int npending = 0;
/* Submit batch of blocks to workers */
while (blocks_remaining > 0 && npending < effective_threads) {
zupt_block_t blk;
err = read_block(f, &blk);
if (err != ZUPT_OK) { berr = 1; break; }
int slot = zpar_submit_decompress(pctx,
blk.payload, (size_t)blk.compressed_size,
decomp_seq, blk.codec_id, blk.block_flags,
blk.checksum, blk.uncompressed_size);
free(blk.payload); /* Worker copied it */
if (slot < 0) { berr = 1; break; }
pending_slots[npending++] = slot;
blocks_remaining--;
decomp_seq++;
}
/* Collect results in order */
for (int pi = 0; pi < npending; pi++) {
zpar_slot_t *s = zpar_wait_slot(pctx, pending_slots[pi]);
if (!s || s->error != ZUPT_OK) {
berr = 1;
zpar_release_slot(pctx, pending_slots[pi]);
continue;
}
if (s->output && s->output_len > 0) {
fwrite(s->output, 1, s->output_len, of);
total_extracted += s->output_len;
}
zpar_release_slot(pctx, pending_slots[pi]);
}
if (berr) break;
}
free(pending_slots);
} else {
/* ─── SINGLE-THREADED DECOMPRESSION PATH ─── */
for (uint32_t b=0; b<e->block_count; b++) {
zupt_block_t blk;
err = read_block(f, &blk);
if (err != ZUPT_OK) { berr=1; break; }
uint8_t *dec; size_t dlen;
err = decompress_block(&blk, &opts->keyring, 0, &dec, &dlen);
free(blk.payload);
if (err != ZUPT_OK) { berr=1; break; }
fwrite(dec, 1, dlen, of);
total_extracted += dlen;
free(dec);
}
}
file_done:
fclose(of);
if (berr) fail++; else ok++;
}
if (pctx) zpar_destroy(pctx);
}
time_t elapsed = time(NULL) - start;
if (elapsed < 1) elapsed = 1;
char sz[16]; zupt_format_size(total_extracted, sz, sizeof(sz));
double speed = (double)total_extracted / (double)elapsed / 1048576.0;
fprintf(stderr, "\n Extracted %d file(s), %s (%.1f MB/s)", ok, sz, speed);
if (fail > 0) fprintf(stderr, ", %d error(s)", fail);
fprintf(stderr, "\n");
free(ents); fclose(f);
return fail>0 ? ZUPT_ERR_CORRUPT : ZUPT_OK;
}
/* ═══════════════════════════════════════════════════════════════════
* TEST
* ═══════════════════════════════════════════════════════════════════ */
zupt_error_t zupt_test_archive(const char *arc, zupt_options_t *opts) {
FILE *f = fopen(arc, "rb");
if (!f) { fprintf(stderr, "Error: Cannot open '%s'\n", arc); return ZUPT_ERR_IO; }
zupt_archive_header_t hdr; zupt_footer_t ft;
zupt_index_entry_t *ents; int n;
zupt_error_t err = open_archive(f, opts, &hdr, &ft, &ents, &n);
if (err != ZUPT_OK) { fclose(f); fprintf(stderr, "Error: %s\n", zupt_strerror(err)); return err; }
int pass=0, fail=0;
int is_solid = (hdr.global_flags & ZUPT_FLAG_SOLID) != 0;
if (is_solid) {
uint64_t total_size = 0;
for (int i = 0; i < n; i++) total_size += ents[i].uncompressed_size;
if (total_size > (uint64_t)ZUPT_MAX_BLOCK_SZ * 4096) {
fprintf(stderr, " Error: solid stream too large for test\n");
free(ents); fclose(f); return ZUPT_ERR_OVERFLOW;
}
uint8_t *solid_buf = (uint8_t*)malloc((size_t)total_size);
if (!solid_buf) { free(ents); fclose(f); return ZUPT_ERR_NOMEM; }
fseeko(f, sizeof(zupt_archive_header_t), SEEK_SET);
if (hdr.global_flags & ZUPT_FLAG_ENCRYPTED) {
zupt_block_t enc_blk;
err = read_block(f, &enc_blk);
if (err == ZUPT_OK) free(enc_blk.payload);
}
size_t solid_pos = 0;
uint64_t block_seq = 0;
int blocks_ok = 0, blocks_fail = 0;
while (solid_pos < (size_t)total_size) {
zupt_block_t blk;
err = read_block(f, &blk);
if (err != ZUPT_OK) { blocks_fail++; break; }
if (blk.block_type == ZUPT_BLOCK_INDEX) { free(blk.payload); break; }
uint8_t *dec; size_t dlen;
err = decompress_block(&blk, &opts->keyring, block_seq, &dec, &dlen);
free(blk.payload);
if (err != ZUPT_OK) {
fprintf(stderr, " Block %llu: FAIL (%s)\n",
(unsigned long long)block_seq, zupt_strerror(err));
blocks_fail++; break;
}
if (solid_pos + dlen > (size_t)total_size) dlen = (size_t)total_size - solid_pos;
memcpy(solid_buf + solid_pos, dec, dlen);
solid_pos += dlen;
free(dec);
blocks_ok++;
block_seq++;
}
if (blocks_fail > 0) {
fprintf(stderr, " Solid stream: %d blocks OK, %d failed\n", blocks_ok, blocks_fail);
free(solid_buf); free(ents); fclose(f);
return ZUPT_ERR_CORRUPT;
}
for (int i = 0; i < n; i++) {
zupt_index_entry_t *e = &ents[i];
uint64_t off = e->first_block_offset;
uint64_t sz = e->uncompressed_size;
int fok = 1;
if (off + sz > total_size) {
fok = 0;
} else if (sz > 0) {
uint64_t ck = zupt_xxh64(solid_buf + off, (size_t)sz, 0);
if (ck != e->content_hash) fok = 0;
}
if (fok) {
if (opts->verbose) fprintf(stderr, " OK: %s\n", e->path);
pass++;
} else {
fprintf(stderr, " FAIL: %s (checksum mismatch)\n", e->path);
fail++;
}
}
free(solid_buf);
} else {
for (int i = 0; i < n; i++) {
zupt_index_entry_t *e = &ents[i];
fseeko(f, (int64_t)e->first_block_offset, SEEK_SET);
int fok = 1;
for (uint32_t b = 0; b < e->block_count; b++) {
zupt_block_t blk;
err = read_block(f, &blk);
if (err != ZUPT_OK) { fok=0; break; }
uint8_t *dec; size_t dlen;
err = decompress_block(&blk, &opts->keyring, 0, &dec, &dlen);
free(blk.payload);
if (err != ZUPT_OK) { fok=0; break; }
free(dec);
}
if (fok) { if (opts->verbose) fprintf(stderr, " OK: %s\n", e->path); pass++; }
else { fprintf(stderr, " FAIL: %s (%s)\n", e->path, zupt_strerror(err)); fail++; }
}
}
printf("\n Test: %d passed, %d failed (%d files)\n", pass, fail, n);
free(ents); fclose(f);
return fail>0 ? ZUPT_ERR_BAD_CHECKSUM : ZUPT_OK;
}