# Zupt Archive Format Specification v1.4 **Status: FROZEN at v1.0.0.** Future format changes require v2.0 (new magic bytes). ## Overview A `.zupt` archive is a sequential byte stream: ``` [Archive Header (64B)] [Encryption Header Block?] [Data Blocks...] [Index Block] [Footer (32B)] ``` All multi-byte integers are **little-endian**. All variable-length integers use unsigned LEB128 (varint). ## Archive Header (64 bytes, offset 0) | Offset | Size | Field | Value | |--------|------|-------|-------| | 0 | 6 | magic | `5A 55 50 54 1A 00` ("ZUPT\x1a\0") | | 6 | 1 | version_major | 1 | | 7 | 1 | version_minor | 4 | | 8 | 4 | global_flags | Bitfield (LE uint32) | | 12 | 8 | creation_time | Nanoseconds since epoch (LE uint64) | | 20 | 16 | archive_id | Random UUID | | 36 | 8 | encryption_header_off | Offset to encryption header block (0 if unencrypted) | | 44 | 8 | comment_offset | Reserved (0) | | 52 | 12 | reserved | Zero-filled | ### Global Flags | Bit | Name | Description | |-----|------|-------------| | 0 | ENCRYPTED | Archive is encrypted | | 1 | SOLID | Solid-mode archive | | 2 | MULTITHREADED | Produced with multi-threaded compression (informational) | | 3 | PQ_HYBRID | Post-quantum hybrid encryption active | | 4 | FORMAT_STABLE | Format is frozen (v1.0+) | | 5 | — | Checksum type: 0 = XXH64 | ## Block Header Each block starts with: | Size | Field | Description | |------|-------|-------------| | 1 | magic_0 | `0xBB` | | 1 | magic_1 | `0x01` | | 1 | block_type | `0x00`=Data, `0x02`=Index, `0x03`=Encryption Header | | 2 | codec_id | LE uint16. See Codec IDs. | | 2 | block_flags | LE uint16. Bit 0 = encrypted. | | varint | uncompressed_size | Original data size | | varint | compressed_size | Payload size (= compressed, or = uncompressed if STORE) | | 8 | checksum | XXH64 of uncompressed data (LE uint64) | | ... | payload | `compressed_size` bytes | ### Codec IDs | ID | Name | Description | |----|------|-------------| | `0x0000` | STORE | No compression | | `0x0008` | Zupt-LZ | LZ77, 64KB window | | `0x0009` | Zupt-LZH | LZ77 + Huffman, 1MB window | | `0x000A` | Zupt-LZHP | LZ77 + Huffman + byte prediction (default) | ### Zupt-LZHP Payload Layout ``` [1B] prediction_flag (0x00=off, 0x01=on) if 0x01: [256B] prediction table [...] LZH compressed data ``` ## Encryption Header Block Located at `encryption_header_off` from the archive header. ### PBKDF2 Mode (enc_type = 0x01) | Size | Field | |------|-------| | 1 | enc_type = `0x01` | | 32 | salt | | 16 | base_nonce | | 4 | iteration_count (LE uint32) | ### PQ Hybrid Mode (enc_type = 0x02) | Size | Field | |------|-------| | 1 | enc_type = `0x02` | | 1088 | ML-KEM-768 ciphertext | | 32 | Ephemeral X25519 public key | | 16 | base_nonce | ### Legacy Mode (no enc_type prefix, v0.5 archives) | Size | Field | |------|-------| | 32 | salt | | 16 | nonce | | 4 | iteration_count | Detection: if first byte is not `0x01` or `0x02` and payload size is 52, treat as legacy. ## Encrypted Block Payload Each encrypted block payload contains: ``` [16B] per-block nonce (base_nonce XOR block_sequence_LE8) [...] AES-256-CTR ciphertext [32B] HMAC-SHA256(mac_key, nonce ‖ ciphertext) ``` **Decrypt order:** Verify HMAC first (Encrypt-then-MAC), then decrypt. ## Central Index Block Block type `0x02`. Codec: always Zupt-LZH (compressed). Contains: ``` [varint] file_count For each file: [varint] path_length [bytes] path (UTF-8) [8B] uncompressed_size (LE) [8B] compressed_size (LE) [8B] modification_time (LE, nanoseconds) [8B] content_hash (LE, chained XXH64) [8B] first_block_offset (LE) [4B] block_count (LE) [4B] attributes (LE) ``` If archive is encrypted, the entire index block payload is encrypted. ## Footer (32 bytes) | Offset | Size | Field | |--------|------|-------| | 0 | 8 | index_offset (LE uint64) | | 8 | 8 | total_blocks (LE uint64) | | 16 | 8 | archive_checksum (LE uint64, XXH64 of all block checksums) | | 24 | 4 | footer_magic = `"ZEND"` | | 28 | 4 | footer_version (LE uint32) | ## Backward Compatibility | Reader | Reads | |--------|-------| | v1.0+ | All v0.3+ archives | | v0.6 | v0.3–v1.3 (rejects v1.4 PQ archives with clean error) | | v0.5 | v0.3–v1.2 |