Skip to main content

tzap_core/
wire.rs

1use crc32c::crc32c;
2
3use crate::crypto::KdfParams;
4use crate::format::{
5    AeadAlgo, BlockKind, CompressionAlgo, FecAlgo, FormatError, KdfAlgo, BLOCK_RECORD_FRAMING_LEN,
6    BOOTSTRAP_SIDECAR_HEADER_LEN, CRYPTO_EXTENSION_HEADER_LEN, CRYPTO_EXTENSION_MAX_VALUE_LEN,
7    CRYPTO_HEADER_FIXED_LEN, CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, MANIFEST_FOOTER_LEN,
8    READER_MAX_BLOCK_SIZE, READER_MAX_CHUNK_SIZE, READER_MAX_CRYPTO_HEADER_LEN,
9    READER_MAX_ENVELOPE_TARGET_SIZE, READER_MAX_FEC_CLASS_SHARDS,
10    READER_MAX_INDEX_FEC_CLASS_SHARDS, READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS,
11    READER_MAX_PATH_LENGTH, READER_MAX_STRIPE_WIDTH, VOLUME_FORMAT_REV, VOLUME_HEADER_LEN,
12    VOLUME_TRAILER_LEN,
13};
14
15const TZAP_MAGIC: [u8; 4] = *b"TZAP";
16const TZCH_MAGIC: [u8; 4] = *b"TZCH";
17const TZBK_MAGIC: [u8; 4] = *b"TZBK";
18const TZMF_MAGIC: [u8; 4] = *b"TZMF";
19const TZVT_MAGIC: [u8; 4] = *b"TZVT";
20const TZBS_MAGIC: [u8; 4] = *b"TZBS";
21
22const BLOCK_LAST_DATA_FLAG: u8 = 0x01;
23const BLOCK_RESERVED_FLAGS: u8 = !BLOCK_LAST_DATA_FLAG;
24
25const SIDECAR_MANIFEST_PRESENT: u32 = 0x01;
26const SIDECAR_INDEX_ROOT_PRESENT: u32 = 0x02;
27const SIDECAR_DICTIONARY_PRESENT: u32 = 0x04;
28const SIDECAR_KNOWN_FLAGS: u32 =
29    SIDECAR_MANIFEST_PRESENT | SIDECAR_INDEX_ROOT_PRESENT | SIDECAR_DICTIONARY_PRESENT;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct VolumeHeader {
33    pub format_version: u16,
34    pub volume_format_rev: u16,
35    pub volume_index: u32,
36    pub stripe_width: u32,
37    pub archive_uuid: [u8; 16],
38    pub session_id: [u8; 16],
39    pub crypto_header_offset: u32,
40    pub crypto_header_length: u32,
41    pub header_crc32c: u32,
42}
43
44impl VolumeHeader {
45    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
46        expect_len("VolumeHeader", VOLUME_HEADER_LEN, bytes.len())?;
47        expect_magic("VolumeHeader", TZAP_MAGIC, &bytes[0..4])?;
48        expect_crc("VolumeHeader", &bytes[..124], read_u32(bytes, 124)?)?;
49        expect_zero("VolumeHeader", &bytes[56..124])?;
50
51        let header = Self {
52            format_version: read_u16(bytes, 4)?,
53            volume_format_rev: read_u16(bytes, 6)?,
54            volume_index: read_u32(bytes, 8)?,
55            stripe_width: read_u32(bytes, 12)?,
56            archive_uuid: read_array_16(bytes, 16)?,
57            session_id: read_array_16(bytes, 32)?,
58            crypto_header_offset: read_u32(bytes, 48)?,
59            crypto_header_length: read_u32(bytes, 52)?,
60            header_crc32c: read_u32(bytes, 124)?,
61        };
62        header.validate()?;
63        Ok(header)
64    }
65
66    pub fn validate(&self) -> Result<(), FormatError> {
67        if self.format_version != FORMAT_VERSION {
68            return Err(FormatError::UnsupportedFormatVersion(self.format_version));
69        }
70        if self.volume_format_rev != VOLUME_FORMAT_REV {
71            return Err(FormatError::UnsupportedVolumeFormatRevision(
72                self.volume_format_rev,
73            ));
74        }
75        if self.stripe_width == 0 {
76            return Err(FormatError::ZeroStripeWidth);
77        }
78        if self.volume_index >= self.stripe_width {
79            return Err(FormatError::VolumeIndexOutOfRange {
80                volume_index: self.volume_index,
81                stripe_width: self.stripe_width,
82            });
83        }
84        if self.crypto_header_offset != VOLUME_HEADER_LEN as u32 {
85            return Err(FormatError::NonCanonicalCryptoHeaderOffset(
86                self.crypto_header_offset,
87            ));
88        }
89        if self.crypto_header_length > READER_MAX_CRYPTO_HEADER_LEN {
90            return Err(FormatError::ReaderResourceLimitExceeded {
91                field: "CryptoHeader length",
92                cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
93                actual: self.crypto_header_length as u64,
94            });
95        }
96        Ok(())
97    }
98
99    pub fn to_bytes(&self) -> [u8; VOLUME_HEADER_LEN] {
100        let mut bytes = [0u8; VOLUME_HEADER_LEN];
101        bytes[0..4].copy_from_slice(&TZAP_MAGIC);
102        write_u16(&mut bytes, 4, self.format_version);
103        write_u16(&mut bytes, 6, self.volume_format_rev);
104        write_u32(&mut bytes, 8, self.volume_index);
105        write_u32(&mut bytes, 12, self.stripe_width);
106        bytes[16..32].copy_from_slice(&self.archive_uuid);
107        bytes[32..48].copy_from_slice(&self.session_id);
108        write_u32(&mut bytes, 48, self.crypto_header_offset);
109        write_u32(&mut bytes, 52, self.crypto_header_length);
110        let crc = crc32c(&bytes[..124]);
111        write_u32(&mut bytes, 124, crc);
112        bytes
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct CryptoHeaderFixed {
118    pub length: u32,
119    pub compression_algo: CompressionAlgo,
120    pub aead_algo: AeadAlgo,
121    pub fec_algo: FecAlgo,
122    pub kdf_algo: KdfAlgo,
123    pub chunk_size: u32,
124    pub envelope_target_size: u32,
125    pub block_size: u32,
126    pub fec_data_shards: u16,
127    pub fec_parity_shards: u16,
128    pub index_fec_data_shards: u16,
129    pub index_fec_parity_shards: u16,
130    pub index_root_fec_data_shards: u16,
131    pub index_root_fec_parity_shards: u16,
132    pub stripe_width: u32,
133    pub volume_loss_tolerance: u8,
134    pub bit_rot_buffer_pct: u8,
135    pub has_dictionary: u8,
136    pub max_path_length: u32,
137    pub expected_volume_size: u64,
138}
139
140impl CryptoHeaderFixed {
141    pub fn parse(bytes: &[u8], volume_crypto_header_length: u32) -> Result<Self, FormatError> {
142        expect_len("CryptoHeaderFixed", CRYPTO_HEADER_FIXED_LEN, bytes.len())?;
143        expect_magic("CryptoHeaderFixed", TZCH_MAGIC, &bytes[0..4])?;
144        expect_zero("CryptoHeaderFixed", &bytes[47..48])?;
145        expect_zero("CryptoHeaderFixed", &bytes[60..76])?;
146
147        let length = read_u32(bytes, 4)?;
148        if length != volume_crypto_header_length {
149            return Err(FormatError::CryptoHeaderLengthMismatch {
150                fixed: length,
151                volume: volume_crypto_header_length,
152            });
153        }
154        if length > READER_MAX_CRYPTO_HEADER_LEN {
155            return Err(FormatError::ReaderResourceLimitExceeded {
156                field: "CryptoHeader length",
157                cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
158                actual: length as u64,
159            });
160        }
161
162        let header = Self {
163            length,
164            compression_algo: CompressionAlgo::try_from(read_u16(bytes, 8)?)?,
165            aead_algo: AeadAlgo::try_from(read_u16(bytes, 10)?)?,
166            fec_algo: FecAlgo::try_from(read_u16(bytes, 12)?)?,
167            kdf_algo: KdfAlgo::try_from(read_u16(bytes, 14)?)?,
168            chunk_size: read_u32(bytes, 16)?,
169            envelope_target_size: read_u32(bytes, 20)?,
170            block_size: read_u32(bytes, 24)?,
171            fec_data_shards: read_u16(bytes, 28)?,
172            fec_parity_shards: read_u16(bytes, 30)?,
173            index_fec_data_shards: read_u16(bytes, 32)?,
174            index_fec_parity_shards: read_u16(bytes, 34)?,
175            index_root_fec_data_shards: read_u16(bytes, 36)?,
176            index_root_fec_parity_shards: read_u16(bytes, 38)?,
177            stripe_width: read_u32(bytes, 40)?,
178            volume_loss_tolerance: bytes[44],
179            bit_rot_buffer_pct: bytes[45],
180            has_dictionary: bytes[46],
181            max_path_length: read_u32(bytes, 48)?,
182            expected_volume_size: read_u64(bytes, 52)?,
183        };
184        header.validate_v36()?;
185        Ok(header)
186    }
187
188    pub fn validate_v36(&self) -> Result<(), FormatError> {
189        if self.compression_algo != CompressionAlgo::ZstdFramed {
190            return Err(FormatError::UnsupportedCompressionForV36(
191                self.compression_algo,
192            ));
193        }
194        if self.fec_algo != FecAlgo::ReedSolomonGF16 {
195            return Err(FormatError::UnsupportedFecForV36(self.fec_algo));
196        }
197        if self.has_dictionary > 1 {
198            return Err(FormatError::InvalidBoolean {
199                field: "has_dictionary",
200                value: self.has_dictionary,
201            });
202        }
203        if self.stripe_width == 0 {
204            return Err(FormatError::ZeroStripeWidth);
205        }
206        if self.stripe_width > READER_MAX_STRIPE_WIDTH {
207            return Err(FormatError::ReaderResourceLimitExceeded {
208                field: "stripe_width",
209                cap: READER_MAX_STRIPE_WIDTH as u64,
210                actual: self.stripe_width as u64,
211            });
212        }
213        if self.volume_loss_tolerance as u32 >= self.stripe_width {
214            return Err(FormatError::VolumeLossToleranceOutOfRange {
215                volume_loss_tolerance: self.volume_loss_tolerance,
216                stripe_width: self.stripe_width,
217            });
218        }
219        if self.bit_rot_buffer_pct > 100 {
220            return Err(FormatError::BitRotBufferPctTooLarge(
221                self.bit_rot_buffer_pct,
222            ));
223        }
224        if self.fec_data_shards == 0 {
225            return Err(FormatError::ZeroDataShardMaximum {
226                field: "fec_data_shards",
227            });
228        }
229        if self.index_fec_data_shards == 0 {
230            return Err(FormatError::ZeroDataShardMaximum {
231                field: "index_fec_data_shards",
232            });
233        }
234        if self.index_root_fec_data_shards == 0 {
235            return Err(FormatError::ZeroDataShardMaximum {
236                field: "index_root_fec_data_shards",
237            });
238        }
239        validate_fec_class_shards(
240            "fec_data_shards + fec_parity_shards",
241            self.fec_data_shards,
242            self.fec_parity_shards,
243            READER_MAX_FEC_CLASS_SHARDS,
244        )?;
245        validate_fec_class_shards(
246            "index_fec_data_shards + index_fec_parity_shards",
247            self.index_fec_data_shards,
248            self.index_fec_parity_shards,
249            READER_MAX_INDEX_FEC_CLASS_SHARDS,
250        )?;
251        validate_fec_class_shards(
252            "index_root_fec_data_shards + index_root_fec_parity_shards",
253            self.index_root_fec_data_shards,
254            self.index_root_fec_parity_shards,
255            READER_MAX_INDEX_ROOT_FEC_CLASS_SHARDS,
256        )?;
257        if self.chunk_size == 0 {
258            return Err(FormatError::ZeroChunkSize);
259        }
260        if self.envelope_target_size == 0 {
261            return Err(FormatError::ZeroEnvelopeTargetSize);
262        }
263        if self.chunk_size > self.envelope_target_size {
264            return Err(FormatError::ChunkSizeExceedsEnvelopeTarget {
265                chunk_size: self.chunk_size,
266                envelope_target_size: self.envelope_target_size,
267            });
268        }
269        if self.chunk_size > READER_MAX_CHUNK_SIZE {
270            return Err(FormatError::ReaderResourceLimitExceeded {
271                field: "chunk_size",
272                cap: READER_MAX_CHUNK_SIZE as u64,
273                actual: self.chunk_size as u64,
274            });
275        }
276        if self.envelope_target_size > READER_MAX_ENVELOPE_TARGET_SIZE {
277            return Err(FormatError::ReaderResourceLimitExceeded {
278                field: "envelope_target_size",
279                cap: READER_MAX_ENVELOPE_TARGET_SIZE as u64,
280                actual: self.envelope_target_size as u64,
281            });
282        }
283        if self.block_size < 4096 {
284            return Err(FormatError::BlockSizeTooSmall(self.block_size));
285        }
286        if self.block_size % 2 != 0 {
287            return Err(FormatError::OddBlockSize(self.block_size));
288        }
289        if self.block_size > READER_MAX_BLOCK_SIZE {
290            return Err(FormatError::ReaderResourceLimitExceeded {
291                field: "block_size",
292                cap: READER_MAX_BLOCK_SIZE as u64,
293                actual: self.block_size as u64,
294            });
295        }
296        if self.max_path_length > READER_MAX_PATH_LENGTH {
297            return Err(FormatError::ReaderResourceLimitExceeded {
298                field: "max_path_length",
299                cap: READER_MAX_PATH_LENGTH as u64,
300                actual: self.max_path_length as u64,
301            });
302        }
303        Ok(())
304    }
305
306    pub fn to_bytes(&self) -> [u8; CRYPTO_HEADER_FIXED_LEN] {
307        let mut bytes = [0u8; CRYPTO_HEADER_FIXED_LEN];
308        bytes[0..4].copy_from_slice(&TZCH_MAGIC);
309        write_u32(&mut bytes, 4, self.length);
310        write_u16(&mut bytes, 8, self.compression_algo as u16);
311        write_u16(&mut bytes, 10, self.aead_algo as u16);
312        write_u16(&mut bytes, 12, self.fec_algo as u16);
313        write_u16(&mut bytes, 14, self.kdf_algo as u16);
314        write_u32(&mut bytes, 16, self.chunk_size);
315        write_u32(&mut bytes, 20, self.envelope_target_size);
316        write_u32(&mut bytes, 24, self.block_size);
317        write_u16(&mut bytes, 28, self.fec_data_shards);
318        write_u16(&mut bytes, 30, self.fec_parity_shards);
319        write_u16(&mut bytes, 32, self.index_fec_data_shards);
320        write_u16(&mut bytes, 34, self.index_fec_parity_shards);
321        write_u16(&mut bytes, 36, self.index_root_fec_data_shards);
322        write_u16(&mut bytes, 38, self.index_root_fec_parity_shards);
323        write_u32(&mut bytes, 40, self.stripe_width);
324        bytes[44] = self.volume_loss_tolerance;
325        bytes[45] = self.bit_rot_buffer_pct;
326        bytes[46] = self.has_dictionary;
327        write_u32(&mut bytes, 48, self.max_path_length);
328        write_u64(&mut bytes, 52, self.expected_volume_size);
329        bytes
330    }
331}
332
333fn validate_fec_class_shards(
334    field: &'static str,
335    data_shards: u16,
336    parity_shards: u16,
337    cap: u32,
338) -> Result<(), FormatError> {
339    let total = data_shards as u32 + parity_shards as u32;
340    if total > cap {
341        return Err(FormatError::ReaderResourceLimitExceeded {
342            field,
343            cap: cap as u64,
344            actual: total as u64,
345        });
346    }
347    Ok(())
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct ExtensionTlv<'a> {
352    pub tag: u16,
353    pub value: &'a [u8],
354}
355
356pub fn scan_crypto_extension_tlvs(bytes: &[u8]) -> Result<Vec<ExtensionTlv<'_>>, FormatError> {
357    let mut offset = 0usize;
358    let mut extensions = Vec::new();
359    loop {
360        if offset == bytes.len() {
361            return Err(FormatError::MissingExtensionTerminator);
362        }
363        if bytes.len() - offset < CRYPTO_EXTENSION_HEADER_LEN {
364            return Err(FormatError::TruncatedExtensionHeader);
365        }
366        let tag = read_u16(bytes, offset)?;
367        let length = read_u32(bytes, offset + 2)?;
368        offset += CRYPTO_EXTENSION_HEADER_LEN;
369        if tag == 0 {
370            if length != 0 {
371                return Err(FormatError::MalformedExtensionTerminator);
372            }
373            if offset != bytes.len() {
374                return Err(FormatError::BytesAfterExtensionTerminator);
375            }
376            return Ok(extensions);
377        }
378        if length > CRYPTO_EXTENSION_MAX_VALUE_LEN {
379            return Err(FormatError::ExtensionPayloadTooLarge(length));
380        }
381        let length = length as usize;
382        if bytes.len() - offset < length {
383            return Err(FormatError::TruncatedExtensionPayload);
384        }
385        extensions.push(ExtensionTlv {
386            tag,
387            value: &bytes[offset..offset + length],
388        });
389        offset += length;
390    }
391}
392
393pub fn validate_crypto_extension_semantics(
394    extensions: &[ExtensionTlv<'_>],
395) -> Result<(), FormatError> {
396    let mut seen_known = Vec::new();
397    for extension in extensions {
398        let ext_tag = extension.tag & 0x7fff;
399        let is_critical = extension.tag & 0x8000 != 0;
400        if matches!(ext_tag, 0x0004 | 0x0006) {
401            return Err(FormatError::ForbiddenExtensionTag(ext_tag));
402        }
403        if is_known_extension(ext_tag) {
404            if seen_known.contains(&ext_tag) {
405                return Err(FormatError::DuplicateKnownExtension(ext_tag));
406            }
407            validate_known_extension(ext_tag, extension.value)?;
408            seen_known.push(ext_tag);
409        } else if is_critical {
410            return Err(FormatError::UnknownCriticalExtension(ext_tag));
411        }
412    }
413    Ok(())
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
417pub struct CryptoHeader<'a> {
418    pub fixed: CryptoHeaderFixed,
419    pub kdf_params: KdfParams,
420    pub extensions: Vec<ExtensionTlv<'a>>,
421    pub header_hmac: [u8; 32],
422    pub hmac_covered_bytes: &'a [u8],
423}
424
425impl<'a> CryptoHeader<'a> {
426    pub fn parse(bytes: &'a [u8], volume_crypto_header_length: u32) -> Result<Self, FormatError> {
427        let declared_len = volume_crypto_header_length as usize;
428        if volume_crypto_header_length > READER_MAX_CRYPTO_HEADER_LEN {
429            return Err(FormatError::ReaderResourceLimitExceeded {
430                field: "CryptoHeader length",
431                cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
432                actual: volume_crypto_header_length as u64,
433            });
434        }
435        if bytes.len() != declared_len {
436            return Err(FormatError::InvalidLength {
437                structure: "CryptoHeader",
438                expected: declared_len,
439                actual: bytes.len(),
440            });
441        }
442        let min_len =
443            CRYPTO_HEADER_FIXED_LEN + 2 + CRYPTO_EXTENSION_HEADER_LEN + CRYPTO_HEADER_HMAC_LEN;
444        if bytes.len() < min_len {
445            return Err(FormatError::CryptoHeaderTooShort {
446                min: min_len,
447                actual: bytes.len(),
448            });
449        }
450
451        let fixed = CryptoHeaderFixed::parse(
452            &bytes[..CRYPTO_HEADER_FIXED_LEN],
453            volume_crypto_header_length,
454        )?;
455        let hmac_offset = bytes.len() - CRYPTO_HEADER_HMAC_LEN;
456        let (kdf_params, kdf_len) =
457            KdfParams::parse(fixed.kdf_algo, &bytes[CRYPTO_HEADER_FIXED_LEN..hmac_offset])?;
458        let extension_bytes = &bytes[CRYPTO_HEADER_FIXED_LEN + kdf_len..hmac_offset];
459        let extensions = scan_crypto_extension_tlvs(extension_bytes)?;
460        let header_hmac = read_array_32(bytes, hmac_offset)?;
461
462        Ok(Self {
463            fixed,
464            kdf_params,
465            extensions,
466            header_hmac,
467            hmac_covered_bytes: &bytes[..hmac_offset],
468        })
469    }
470
471    pub fn validate_extension_semantics(&self) -> Result<(), FormatError> {
472        validate_crypto_extension_semantics(&self.extensions)
473    }
474}
475
476#[derive(Debug, Clone, PartialEq, Eq)]
477pub struct BlockRecord {
478    pub block_index: u64,
479    pub kind: BlockKind,
480    pub flags: u8,
481    pub payload: Vec<u8>,
482    pub record_crc32c: u32,
483}
484
485impl BlockRecord {
486    pub fn parse(bytes: &[u8], block_size: usize) -> Result<Self, FormatError> {
487        let expected = block_size + BLOCK_RECORD_FRAMING_LEN;
488        expect_len("BlockRecord", expected, bytes.len())?;
489        expect_magic("BlockRecord", TZBK_MAGIC, &bytes[0..4])?;
490        expect_zero("BlockRecord", &bytes[14..16])?;
491        expect_crc(
492            "BlockRecord",
493            &bytes[..16 + block_size],
494            read_u32(bytes, 16 + block_size)?,
495        )?;
496
497        let kind = BlockKind::try_from(bytes[12])?;
498        let flags = bytes[13];
499        if flags & BLOCK_RESERVED_FLAGS != 0 {
500            return Err(FormatError::InvalidBlockFlags(flags));
501        }
502        if kind.is_parity() && flags & BLOCK_LAST_DATA_FLAG != 0 {
503            return Err(FormatError::ParityBlockHasLastDataFlag);
504        }
505
506        Ok(Self {
507            block_index: read_u64(bytes, 4)?,
508            kind,
509            flags,
510            payload: bytes[16..16 + block_size].to_vec(),
511            record_crc32c: read_u32(bytes, 16 + block_size)?,
512        })
513    }
514
515    pub fn is_last_data(&self) -> bool {
516        self.flags & BLOCK_LAST_DATA_FLAG != 0
517    }
518
519    pub fn to_bytes(&self) -> Vec<u8> {
520        let mut bytes = vec![0u8; self.payload.len() + BLOCK_RECORD_FRAMING_LEN];
521        bytes[0..4].copy_from_slice(&TZBK_MAGIC);
522        write_u64(&mut bytes, 4, self.block_index);
523        bytes[12] = self.kind as u8;
524        bytes[13] = self.flags;
525        bytes[16..16 + self.payload.len()].copy_from_slice(&self.payload);
526        let crc = crc32c(&bytes[..16 + self.payload.len()]);
527        let crc_offset = 16 + self.payload.len();
528        write_u32(&mut bytes, crc_offset, crc);
529        bytes
530    }
531}
532
533#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct ManifestFooter {
535    pub archive_uuid: [u8; 16],
536    pub session_id: [u8; 16],
537    pub volume_index: u32,
538    pub is_authoritative: u8,
539    pub total_volumes: u32,
540    pub index_root_first_block: u64,
541    pub index_root_data_block_count: u32,
542    pub index_root_parity_block_count: u32,
543    pub index_root_encrypted_size: u32,
544    pub index_root_decompressed_size: u32,
545    pub manifest_hmac: [u8; 32],
546}
547
548impl ManifestFooter {
549    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
550        expect_len("ManifestFooter", MANIFEST_FOOTER_LEN, bytes.len())?;
551        expect_magic("ManifestFooter", TZMF_MAGIC, &bytes[0..4])?;
552        expect_zero("ManifestFooter", &bytes[41..44])?;
553        expect_zero("ManifestFooter", &bytes[72..104])?;
554        let is_authoritative = bytes[40];
555        if is_authoritative > 1 {
556            return Err(FormatError::InvalidAuthoritativeFlag(is_authoritative));
557        }
558
559        Ok(Self {
560            archive_uuid: read_array_16(bytes, 4)?,
561            session_id: read_array_16(bytes, 20)?,
562            volume_index: read_u32(bytes, 36)?,
563            is_authoritative,
564            total_volumes: read_u32(bytes, 44)?,
565            index_root_first_block: read_u64(bytes, 48)?,
566            index_root_data_block_count: read_u32(bytes, 56)?,
567            index_root_parity_block_count: read_u32(bytes, 60)?,
568            index_root_encrypted_size: read_u32(bytes, 64)?,
569            index_root_decompressed_size: read_u32(bytes, 68)?,
570            manifest_hmac: read_array_32(bytes, 104)?,
571        })
572    }
573
574    pub fn validate_index_root_extent(&self, block_size: u32) -> Result<(), FormatError> {
575        if self.index_root_data_block_count == 0 || self.index_root_encrypted_size == 0 {
576            return Err(FormatError::EmptyIndexRootExtent);
577        }
578        let expected = self
579            .index_root_data_block_count
580            .checked_mul(block_size)
581            .ok_or(FormatError::IndexRootSizeMismatch)?;
582        if expected != self.index_root_encrypted_size {
583            return Err(FormatError::IndexRootSizeMismatch);
584        }
585        Ok(())
586    }
587
588    pub fn to_bytes(&self) -> [u8; MANIFEST_FOOTER_LEN] {
589        let mut bytes = [0u8; MANIFEST_FOOTER_LEN];
590        bytes[0..4].copy_from_slice(&TZMF_MAGIC);
591        bytes[4..20].copy_from_slice(&self.archive_uuid);
592        bytes[20..36].copy_from_slice(&self.session_id);
593        write_u32(&mut bytes, 36, self.volume_index);
594        bytes[40] = self.is_authoritative;
595        write_u32(&mut bytes, 44, self.total_volumes);
596        write_u64(&mut bytes, 48, self.index_root_first_block);
597        write_u32(&mut bytes, 56, self.index_root_data_block_count);
598        write_u32(&mut bytes, 60, self.index_root_parity_block_count);
599        write_u32(&mut bytes, 64, self.index_root_encrypted_size);
600        write_u32(&mut bytes, 68, self.index_root_decompressed_size);
601        bytes[104..136].copy_from_slice(&self.manifest_hmac);
602        bytes
603    }
604}
605
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub struct VolumeTrailer {
608    pub archive_uuid: [u8; 16],
609    pub session_id: [u8; 16],
610    pub volume_index: u32,
611    pub block_count: u64,
612    pub bytes_written: u64,
613    pub manifest_footer_offset: u64,
614    pub manifest_footer_length: u32,
615    pub closed_at_ns: i64,
616    pub trailer_hmac: [u8; 32],
617}
618
619impl VolumeTrailer {
620    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
621        expect_len("VolumeTrailer", VOLUME_TRAILER_LEN, bytes.len())?;
622        expect_magic("VolumeTrailer", TZVT_MAGIC, &bytes[0..4])?;
623        expect_zero("VolumeTrailer", &bytes[76..96])?;
624        let manifest_footer_length = read_u32(bytes, 64)?;
625        if manifest_footer_length != MANIFEST_FOOTER_LEN as u32 {
626            return Err(FormatError::InvalidManifestFooterLength(
627                manifest_footer_length,
628            ));
629        }
630
631        Ok(Self {
632            archive_uuid: read_array_16(bytes, 4)?,
633            session_id: read_array_16(bytes, 20)?,
634            volume_index: read_u32(bytes, 36)?,
635            block_count: read_u64(bytes, 40)?,
636            bytes_written: read_u64(bytes, 48)?,
637            manifest_footer_offset: read_u64(bytes, 56)?,
638            manifest_footer_length,
639            closed_at_ns: read_i64(bytes, 68)?,
640            trailer_hmac: read_array_32(bytes, 96)?,
641        })
642    }
643
644    pub fn to_bytes(&self) -> [u8; VOLUME_TRAILER_LEN] {
645        let mut bytes = [0u8; VOLUME_TRAILER_LEN];
646        bytes[0..4].copy_from_slice(&TZVT_MAGIC);
647        bytes[4..20].copy_from_slice(&self.archive_uuid);
648        bytes[20..36].copy_from_slice(&self.session_id);
649        write_u32(&mut bytes, 36, self.volume_index);
650        write_u64(&mut bytes, 40, self.block_count);
651        write_u64(&mut bytes, 48, self.bytes_written);
652        write_u64(&mut bytes, 56, self.manifest_footer_offset);
653        write_u32(&mut bytes, 64, self.manifest_footer_length);
654        write_i64(&mut bytes, 68, self.closed_at_ns);
655        bytes[96..128].copy_from_slice(&self.trailer_hmac);
656        bytes
657    }
658}
659
660#[derive(Debug, Clone, PartialEq, Eq)]
661pub struct BootstrapSidecarHeader {
662    pub archive_uuid: [u8; 16],
663    pub session_id: [u8; 16],
664    pub flags: u32,
665    pub manifest_footer_offset: u64,
666    pub manifest_footer_length: u32,
667    pub index_root_records_offset: u64,
668    pub index_root_records_length: u64,
669    pub dictionary_records_offset: u64,
670    pub dictionary_records_length: u64,
671    pub sidecar_hmac: [u8; 32],
672    pub header_crc32c: u32,
673}
674
675impl BootstrapSidecarHeader {
676    pub fn parse(bytes: &[u8]) -> Result<Self, FormatError> {
677        expect_len(
678            "BootstrapSidecarHeader",
679            BOOTSTRAP_SIDECAR_HEADER_LEN,
680            bytes.len(),
681        )?;
682        expect_magic("BootstrapSidecarHeader", TZBS_MAGIC, &bytes[0..4])?;
683        let version = read_u32(bytes, 4)?;
684        if version != 1 {
685            return Err(FormatError::UnsupportedBootstrapSidecarVersion(version));
686        }
687        expect_zero("BootstrapSidecarHeader", &bytes[88..92])?;
688        expect_crc(
689            "BootstrapSidecarHeader",
690            &bytes[..124],
691            read_u32(bytes, 124)?,
692        )?;
693
694        let header = Self {
695            archive_uuid: read_array_16(bytes, 8)?,
696            session_id: read_array_16(bytes, 24)?,
697            flags: read_u32(bytes, 40)?,
698            manifest_footer_offset: read_u64(bytes, 44)?,
699            manifest_footer_length: read_u32(bytes, 52)?,
700            index_root_records_offset: read_u64(bytes, 56)?,
701            index_root_records_length: read_u64(bytes, 64)?,
702            dictionary_records_offset: read_u64(bytes, 72)?,
703            dictionary_records_length: read_u64(bytes, 80)?,
704            sidecar_hmac: read_array_32(bytes, 92)?,
705            header_crc32c: read_u32(bytes, 124)?,
706        };
707        header.validate_sections()?;
708        Ok(header)
709    }
710
711    pub fn validate_packed_layout(&self, file_size: u64) -> Result<(), FormatError> {
712        let mut cursor = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
713        cursor = self.validate_section_cursor(
714            self.has_manifest_footer(),
715            self.manifest_footer_offset,
716            self.manifest_footer_length as u64,
717            cursor,
718        )?;
719        cursor = self.validate_section_cursor(
720            self.has_index_root_records(),
721            self.index_root_records_offset,
722            self.index_root_records_length,
723            cursor,
724        )?;
725        cursor = self.validate_section_cursor(
726            self.has_dictionary_records(),
727            self.dictionary_records_offset,
728            self.dictionary_records_length,
729            cursor,
730        )?;
731        if cursor != file_size {
732            return Err(FormatError::NonCanonicalBootstrapSidecarLayout);
733        }
734        Ok(())
735    }
736
737    pub fn has_manifest_footer(&self) -> bool {
738        self.flags & SIDECAR_MANIFEST_PRESENT != 0
739    }
740
741    pub fn has_index_root_records(&self) -> bool {
742        self.flags & SIDECAR_INDEX_ROOT_PRESENT != 0
743    }
744
745    pub fn has_dictionary_records(&self) -> bool {
746        self.flags & SIDECAR_DICTIONARY_PRESENT != 0
747    }
748
749    pub fn to_bytes(&self) -> [u8; BOOTSTRAP_SIDECAR_HEADER_LEN] {
750        let mut bytes = [0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
751        bytes[0..4].copy_from_slice(&TZBS_MAGIC);
752        write_u32(&mut bytes, 4, 1);
753        bytes[8..24].copy_from_slice(&self.archive_uuid);
754        bytes[24..40].copy_from_slice(&self.session_id);
755        write_u32(&mut bytes, 40, self.flags);
756        write_u64(&mut bytes, 44, self.manifest_footer_offset);
757        write_u32(&mut bytes, 52, self.manifest_footer_length);
758        write_u64(&mut bytes, 56, self.index_root_records_offset);
759        write_u64(&mut bytes, 64, self.index_root_records_length);
760        write_u64(&mut bytes, 72, self.dictionary_records_offset);
761        write_u64(&mut bytes, 80, self.dictionary_records_length);
762        bytes[92..124].copy_from_slice(&self.sidecar_hmac);
763        let crc = crc32c(&bytes[..124]);
764        write_u32(&mut bytes, 124, crc);
765        bytes
766    }
767
768    fn validate_sections(&self) -> Result<(), FormatError> {
769        if self.flags & !SIDECAR_KNOWN_FLAGS != 0 {
770            return Err(FormatError::UnknownBootstrapSidecarFlags(self.flags));
771        }
772        self.validate_presence_fields(
773            self.has_manifest_footer(),
774            self.manifest_footer_offset,
775            self.manifest_footer_length as u64,
776        )?;
777        if self.has_manifest_footer() && self.manifest_footer_length != MANIFEST_FOOTER_LEN as u32 {
778            return Err(FormatError::InvalidManifestFooterLength(
779                self.manifest_footer_length,
780            ));
781        }
782        self.validate_presence_fields(
783            self.has_index_root_records(),
784            self.index_root_records_offset,
785            self.index_root_records_length,
786        )?;
787        self.validate_presence_fields(
788            self.has_dictionary_records(),
789            self.dictionary_records_offset,
790            self.dictionary_records_length,
791        )?;
792        Ok(())
793    }
794
795    fn validate_presence_fields(
796        &self,
797        present: bool,
798        offset: u64,
799        length: u64,
800    ) -> Result<(), FormatError> {
801        match (present, offset, length) {
802            (true, 0, _) | (true, _, 0) => Err(FormatError::EmptyBootstrapSidecarSection),
803            (false, 0, 0) => Ok(()),
804            (false, _, _) => Err(FormatError::NonZeroAbsentBootstrapSidecarSection),
805            (true, _, _) => Ok(()),
806        }
807    }
808
809    fn validate_section_cursor(
810        &self,
811        present: bool,
812        offset: u64,
813        length: u64,
814        cursor: u64,
815    ) -> Result<u64, FormatError> {
816        if present {
817            if offset != cursor {
818                return Err(FormatError::NonCanonicalBootstrapSidecarLayout);
819            }
820            cursor
821                .checked_add(length)
822                .ok_or(FormatError::NonCanonicalBootstrapSidecarLayout)
823        } else {
824            Ok(cursor)
825        }
826    }
827}
828
829fn is_known_extension(ext_tag: u16) -> bool {
830    matches!(ext_tag, 0x0001 | 0x0002 | 0x0003 | 0x0005)
831}
832
833fn validate_known_extension(ext_tag: u16, value: &[u8]) -> Result<(), FormatError> {
834    match ext_tag {
835        0x0001 | 0x0002 | 0x0005 => std::str::from_utf8(value)
836            .map(|_| ())
837            .map_err(|_| FormatError::MalformedKnownExtension(ext_tag)),
838        0x0003 => {
839            if value.len() == 8 {
840                Ok(())
841            } else {
842                Err(FormatError::MalformedKnownExtension(ext_tag))
843            }
844        }
845        _ => Ok(()),
846    }
847}
848
849fn expect_len(structure: &'static str, expected: usize, actual: usize) -> Result<(), FormatError> {
850    if actual != expected {
851        return Err(FormatError::InvalidLength {
852            structure,
853            expected,
854            actual,
855        });
856    }
857    Ok(())
858}
859
860fn expect_magic(
861    structure: &'static str,
862    expected: [u8; 4],
863    actual: &[u8],
864) -> Result<(), FormatError> {
865    if actual != expected {
866        return Err(FormatError::BadMagic { structure });
867    }
868    Ok(())
869}
870
871fn expect_zero(structure: &'static str, bytes: &[u8]) -> Result<(), FormatError> {
872    if bytes.iter().any(|byte| *byte != 0) {
873        return Err(FormatError::NonZeroReserved { structure });
874    }
875    Ok(())
876}
877
878fn expect_crc(structure: &'static str, covered: &[u8], expected: u32) -> Result<(), FormatError> {
879    if crc32c(covered) != expected {
880        return Err(FormatError::BadCrc { structure });
881    }
882    Ok(())
883}
884
885fn read_array_16(bytes: &[u8], offset: usize) -> Result<[u8; 16], FormatError> {
886    let mut value = [0u8; 16];
887    value.copy_from_slice(
888        bytes
889            .get(offset..offset + 16)
890            .ok_or(FormatError::InvalidLength {
891                structure: "array16",
892                expected: offset + 16,
893                actual: bytes.len(),
894            })?,
895    );
896    Ok(value)
897}
898
899fn read_array_32(bytes: &[u8], offset: usize) -> Result<[u8; 32], FormatError> {
900    let mut value = [0u8; 32];
901    value.copy_from_slice(
902        bytes
903            .get(offset..offset + 32)
904            .ok_or(FormatError::InvalidLength {
905                structure: "array32",
906                expected: offset + 32,
907                actual: bytes.len(),
908            })?,
909    );
910    Ok(value)
911}
912
913fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, FormatError> {
914    let array: [u8; 2] = bytes
915        .get(offset..offset + 2)
916        .ok_or(FormatError::InvalidLength {
917            structure: "u16",
918            expected: offset + 2,
919            actual: bytes.len(),
920        })?
921        .try_into()
922        .expect("slice length checked");
923    Ok(u16::from_le_bytes(array))
924}
925
926fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, FormatError> {
927    let array: [u8; 4] = bytes
928        .get(offset..offset + 4)
929        .ok_or(FormatError::InvalidLength {
930            structure: "u32",
931            expected: offset + 4,
932            actual: bytes.len(),
933        })?
934        .try_into()
935        .expect("slice length checked");
936    Ok(u32::from_le_bytes(array))
937}
938
939fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, FormatError> {
940    let array: [u8; 8] = bytes
941        .get(offset..offset + 8)
942        .ok_or(FormatError::InvalidLength {
943            structure: "u64",
944            expected: offset + 8,
945            actual: bytes.len(),
946        })?
947        .try_into()
948        .expect("slice length checked");
949    Ok(u64::from_le_bytes(array))
950}
951
952fn read_i64(bytes: &[u8], offset: usize) -> Result<i64, FormatError> {
953    let array: [u8; 8] = bytes
954        .get(offset..offset + 8)
955        .ok_or(FormatError::InvalidLength {
956            structure: "i64",
957            expected: offset + 8,
958            actual: bytes.len(),
959        })?
960        .try_into()
961        .expect("slice length checked");
962    Ok(i64::from_le_bytes(array))
963}
964
965fn write_u16(bytes: &mut [u8], offset: usize, value: u16) {
966    bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
967}
968
969fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
970    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
971}
972
973fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
974    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
975}
976
977fn write_i64(bytes: &mut [u8], offset: usize, value: i64) {
978    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
979}
980
981#[cfg(test)]
982mod tests {
983    use super::*;
984    use crate::format::CRYPTO_HEADER_HMAC_LEN;
985
986    fn uuid() -> [u8; 16] {
987        [0x11; 16]
988    }
989
990    fn session() -> [u8; 16] {
991        [0x22; 16]
992    }
993
994    fn volume_header() -> VolumeHeader {
995        VolumeHeader {
996            format_version: FORMAT_VERSION,
997            volume_format_rev: VOLUME_FORMAT_REV,
998            volume_index: 0,
999            stripe_width: 1,
1000            archive_uuid: uuid(),
1001            session_id: session(),
1002            crypto_header_offset: VOLUME_HEADER_LEN as u32,
1003            crypto_header_length: 128,
1004            header_crc32c: 0,
1005        }
1006    }
1007
1008    fn crypto_fixed() -> CryptoHeaderFixed {
1009        CryptoHeaderFixed {
1010            length: (CRYPTO_HEADER_FIXED_LEN + 2 + 6 + CRYPTO_HEADER_HMAC_LEN) as u32,
1011            compression_algo: CompressionAlgo::ZstdFramed,
1012            aead_algo: AeadAlgo::AesGcmSiv256,
1013            fec_algo: FecAlgo::ReedSolomonGF16,
1014            kdf_algo: KdfAlgo::Raw,
1015            chunk_size: 262_144,
1016            envelope_target_size: 4 * 1024 * 1024,
1017            block_size: 4096,
1018            fec_data_shards: 16,
1019            fec_parity_shards: 2,
1020            index_fec_data_shards: 16,
1021            index_fec_parity_shards: 2,
1022            index_root_fec_data_shards: 16,
1023            index_root_fec_parity_shards: 2,
1024            stripe_width: 1,
1025            volume_loss_tolerance: 0,
1026            bit_rot_buffer_pct: 5,
1027            has_dictionary: 0,
1028            max_path_length: 4096,
1029            expected_volume_size: 0,
1030        }
1031    }
1032
1033    fn raw_crypto_header_bytes() -> Vec<u8> {
1034        let fixed = crypto_fixed();
1035        let mut bytes = Vec::new();
1036        bytes.extend_from_slice(&fixed.to_bytes());
1037        bytes.extend_from_slice(&(KdfAlgo::Raw as u16).to_le_bytes());
1038        bytes.extend_from_slice(&0u16.to_le_bytes());
1039        bytes.extend_from_slice(&0u32.to_le_bytes());
1040        bytes.extend_from_slice(&[0xab; CRYPTO_HEADER_HMAC_LEN]);
1041        bytes
1042    }
1043
1044    #[test]
1045    fn volume_header_round_trips_and_validates() {
1046        let bytes = volume_header().to_bytes();
1047        let parsed = VolumeHeader::parse(&bytes).unwrap();
1048        assert_eq!(parsed.format_version, FORMAT_VERSION);
1049        assert_eq!(parsed.volume_format_rev, VOLUME_FORMAT_REV);
1050        assert_eq!(parsed.crypto_header_offset, VOLUME_HEADER_LEN as u32);
1051    }
1052
1053    #[test]
1054    fn volume_header_rejects_mutations() {
1055        let mut bytes = volume_header().to_bytes();
1056        bytes[0] = b'X';
1057        assert_eq!(
1058            VolumeHeader::parse(&bytes).unwrap_err(),
1059            FormatError::BadMagic {
1060                structure: "VolumeHeader"
1061            }
1062        );
1063
1064        let mut bytes = volume_header().to_bytes();
1065        write_u16(&mut bytes, 6, 35);
1066        let crc = crc32c(&bytes[..124]);
1067        write_u32(&mut bytes, 124, crc);
1068        assert_eq!(
1069            VolumeHeader::parse(&bytes).unwrap_err(),
1070            FormatError::UnsupportedVolumeFormatRevision(35)
1071        );
1072
1073        let mut bytes = volume_header().to_bytes();
1074        bytes[124] ^= 1;
1075        assert_eq!(
1076            VolumeHeader::parse(&bytes).unwrap_err(),
1077            FormatError::BadCrc {
1078                structure: "VolumeHeader"
1079            }
1080        );
1081
1082        let mut bytes = volume_header().to_bytes();
1083        write_u32(&mut bytes, 48, 129);
1084        let crc = crc32c(&bytes[..124]);
1085        write_u32(&mut bytes, 124, crc);
1086        assert_eq!(
1087            VolumeHeader::parse(&bytes).unwrap_err(),
1088            FormatError::NonCanonicalCryptoHeaderOffset(129)
1089        );
1090
1091        let mut bytes = volume_header().to_bytes();
1092        write_u32(&mut bytes, 52, READER_MAX_CRYPTO_HEADER_LEN + 1);
1093        let crc = crc32c(&bytes[..124]);
1094        write_u32(&mut bytes, 124, crc);
1095        assert_eq!(
1096            VolumeHeader::parse(&bytes).unwrap_err(),
1097            FormatError::ReaderResourceLimitExceeded {
1098                field: "CryptoHeader length",
1099                cap: READER_MAX_CRYPTO_HEADER_LEN as u64,
1100                actual: (READER_MAX_CRYPTO_HEADER_LEN + 1) as u64,
1101            }
1102        );
1103    }
1104
1105    #[test]
1106    fn crypto_header_fixed_round_trips_and_validates() {
1107        let header = crypto_fixed();
1108        let bytes = header.to_bytes();
1109        let parsed = CryptoHeaderFixed::parse(&bytes, header.length).unwrap();
1110        assert_eq!(parsed.compression_algo, CompressionAlgo::ZstdFramed);
1111        assert_eq!(parsed.fec_algo, FecAlgo::ReedSolomonGF16);
1112    }
1113
1114    #[test]
1115    fn crypto_header_fixed_rejects_v36_incompatible_values() {
1116        let mut header = crypto_fixed();
1117        header.compression_algo = CompressionAlgo::None;
1118        assert_eq!(
1119            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1120            FormatError::UnsupportedCompressionForV36(CompressionAlgo::None)
1121        );
1122
1123        let mut header = crypto_fixed();
1124        header.block_size = 4097;
1125        assert_eq!(
1126            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1127            FormatError::OddBlockSize(4097)
1128        );
1129
1130        let mut bytes = crypto_fixed().to_bytes();
1131        bytes[47] = 1;
1132        assert_eq!(
1133            CryptoHeaderFixed::parse(&bytes, crypto_fixed().length).unwrap_err(),
1134            FormatError::NonZeroReserved {
1135                structure: "CryptoHeaderFixed"
1136            }
1137        );
1138    }
1139
1140    #[test]
1141    fn crypto_header_fixed_treats_expected_volume_size_as_advisory_not_reserved() {
1142        let mut header = crypto_fixed();
1143        header.expected_volume_size = 1u64 << 56;
1144
1145        let parsed = CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap();
1146
1147        assert_eq!(parsed.expected_volume_size, 1u64 << 56);
1148    }
1149
1150    #[test]
1151    fn crypto_header_fixed_rejects_reader_cap_excesses() {
1152        let mut header = crypto_fixed();
1153        header.chunk_size = READER_MAX_CHUNK_SIZE + 1;
1154        header.envelope_target_size = header.chunk_size;
1155        assert_eq!(
1156            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1157            FormatError::ReaderResourceLimitExceeded {
1158                field: "chunk_size",
1159                cap: READER_MAX_CHUNK_SIZE as u64,
1160                actual: (READER_MAX_CHUNK_SIZE + 1) as u64,
1161            }
1162        );
1163
1164        let mut header = crypto_fixed();
1165        header.block_size = READER_MAX_BLOCK_SIZE + 2;
1166        assert_eq!(
1167            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1168            FormatError::ReaderResourceLimitExceeded {
1169                field: "block_size",
1170                cap: READER_MAX_BLOCK_SIZE as u64,
1171                actual: (READER_MAX_BLOCK_SIZE + 2) as u64,
1172            }
1173        );
1174
1175        let mut header = crypto_fixed();
1176        header.max_path_length = READER_MAX_PATH_LENGTH + 1;
1177        assert_eq!(
1178            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1179            FormatError::ReaderResourceLimitExceeded {
1180                field: "max_path_length",
1181                cap: READER_MAX_PATH_LENGTH as u64,
1182                actual: (READER_MAX_PATH_LENGTH + 1) as u64,
1183            }
1184        );
1185
1186        let mut header = crypto_fixed();
1187        header.fec_data_shards = READER_MAX_FEC_CLASS_SHARDS as u16;
1188        header.fec_parity_shards = 1;
1189        assert_eq!(
1190            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1191            FormatError::ReaderResourceLimitExceeded {
1192                field: "fec_data_shards + fec_parity_shards",
1193                cap: READER_MAX_FEC_CLASS_SHARDS as u64,
1194                actual: (READER_MAX_FEC_CLASS_SHARDS + 1) as u64,
1195            }
1196        );
1197
1198        let mut header = crypto_fixed();
1199        header.index_fec_data_shards = READER_MAX_INDEX_FEC_CLASS_SHARDS as u16;
1200        header.index_fec_parity_shards = 1;
1201        assert_eq!(
1202            CryptoHeaderFixed::parse(&header.to_bytes(), header.length).unwrap_err(),
1203            FormatError::ReaderResourceLimitExceeded {
1204                field: "index_fec_data_shards + index_fec_parity_shards",
1205                cap: READER_MAX_INDEX_FEC_CLASS_SHARDS as u64,
1206                actual: (READER_MAX_INDEX_FEC_CLASS_SHARDS + 1) as u64,
1207            }
1208        );
1209    }
1210
1211    #[test]
1212    fn crypto_extension_scanner_enforces_terminator_and_caps() {
1213        let bytes = [0u8; CRYPTO_EXTENSION_HEADER_LEN];
1214        assert!(scan_crypto_extension_tlvs(&bytes).unwrap().is_empty());
1215
1216        let mut bytes = Vec::new();
1217        bytes.extend_from_slice(&0x0001u16.to_le_bytes());
1218        bytes.extend_from_slice(&3u32.to_le_bytes());
1219        bytes.extend_from_slice(b"hey");
1220        bytes.extend_from_slice(&0u16.to_le_bytes());
1221        bytes.extend_from_slice(&0u32.to_le_bytes());
1222        let tlvs = scan_crypto_extension_tlvs(&bytes).unwrap();
1223        assert_eq!(tlvs[0].tag, 1);
1224        assert_eq!(tlvs[0].value, b"hey");
1225
1226        let mut bytes = Vec::new();
1227        bytes.extend_from_slice(&0x0001u16.to_le_bytes());
1228        bytes.extend_from_slice(&257u32.to_le_bytes());
1229        assert_eq!(
1230            scan_crypto_extension_tlvs(&bytes).unwrap_err(),
1231            FormatError::ExtensionPayloadTooLarge(257)
1232        );
1233
1234        assert_eq!(
1235            scan_crypto_extension_tlvs(&[]).unwrap_err(),
1236            FormatError::MissingExtensionTerminator
1237        );
1238    }
1239
1240    #[test]
1241    fn crypto_header_parse_splits_fixed_kdf_extensions_and_hmac() {
1242        let bytes = raw_crypto_header_bytes();
1243        let header = CryptoHeader::parse(&bytes, bytes.len() as u32).unwrap();
1244        assert_eq!(header.fixed.kdf_algo, KdfAlgo::Raw);
1245        assert_eq!(header.kdf_params, KdfParams::Raw);
1246        assert!(header.extensions.is_empty());
1247        assert_eq!(header.header_hmac, [0xab; CRYPTO_HEADER_HMAC_LEN]);
1248        assert_eq!(
1249            header.hmac_covered_bytes.len(),
1250            bytes.len() - CRYPTO_HEADER_HMAC_LEN
1251        );
1252    }
1253
1254    #[test]
1255    fn crypto_header_parse_rejects_truncated_and_bad_kdf_params() {
1256        let mut bytes = raw_crypto_header_bytes();
1257        bytes.truncate(CRYPTO_HEADER_FIXED_LEN + 1);
1258        assert_eq!(
1259            CryptoHeader::parse(&bytes, bytes.len() as u32).unwrap_err(),
1260            FormatError::CryptoHeaderTooShort {
1261                min: CRYPTO_HEADER_FIXED_LEN
1262                    + 2
1263                    + CRYPTO_EXTENSION_HEADER_LEN
1264                    + CRYPTO_HEADER_HMAC_LEN,
1265                actual: CRYPTO_HEADER_FIXED_LEN + 1
1266            }
1267        );
1268
1269        let mut bytes = raw_crypto_header_bytes();
1270        write_u16(
1271            &mut bytes,
1272            CRYPTO_HEADER_FIXED_LEN,
1273            KdfAlgo::Argon2id as u16,
1274        );
1275        assert_eq!(
1276            CryptoHeader::parse(&bytes, bytes.len() as u32).unwrap_err(),
1277            FormatError::KdfAlgoTagMismatch {
1278                expected: 0,
1279                actual: 1
1280            }
1281        );
1282    }
1283
1284    #[test]
1285    fn crypto_extension_semantics_reject_forbidden_duplicate_and_critical() {
1286        let duplicate = vec![
1287            ExtensionTlv {
1288                tag: 0x0001,
1289                value: b"one",
1290            },
1291            ExtensionTlv {
1292                tag: 0x0001,
1293                value: b"two",
1294            },
1295        ];
1296        assert_eq!(
1297            validate_crypto_extension_semantics(&duplicate).unwrap_err(),
1298            FormatError::DuplicateKnownExtension(0x0001)
1299        );
1300
1301        let forbidden = vec![ExtensionTlv {
1302            tag: 0x8004,
1303            value: b"",
1304        }];
1305        assert_eq!(
1306            validate_crypto_extension_semantics(&forbidden).unwrap_err(),
1307            FormatError::ForbiddenExtensionTag(0x0004)
1308        );
1309
1310        let unknown_critical = vec![ExtensionTlv {
1311            tag: 0x8123,
1312            value: b"",
1313        }];
1314        assert_eq!(
1315            validate_crypto_extension_semantics(&unknown_critical).unwrap_err(),
1316            FormatError::UnknownCriticalExtension(0x0123)
1317        );
1318
1319        let malformed_known = vec![ExtensionTlv {
1320            tag: 0x0003,
1321            value: b"short",
1322        }];
1323        assert_eq!(
1324            validate_crypto_extension_semantics(&malformed_known).unwrap_err(),
1325            FormatError::MalformedKnownExtension(0x0003)
1326        );
1327    }
1328
1329    #[test]
1330    fn block_record_round_trips_and_validates_crc() {
1331        let record = BlockRecord {
1332            block_index: 0,
1333            kind: BlockKind::PayloadData,
1334            flags: BLOCK_LAST_DATA_FLAG,
1335            payload: vec![7; 4096],
1336            record_crc32c: 0,
1337        };
1338        let bytes = record.to_bytes();
1339        let parsed = BlockRecord::parse(&bytes, 4096).unwrap();
1340        assert_eq!(parsed.kind, BlockKind::PayloadData);
1341        assert!(parsed.is_last_data());
1342
1343        let mut corrupted = bytes;
1344        corrupted[20] ^= 1;
1345        assert_eq!(
1346            BlockRecord::parse(&corrupted, 4096).unwrap_err(),
1347            FormatError::BadCrc {
1348                structure: "BlockRecord"
1349            }
1350        );
1351    }
1352
1353    #[test]
1354    fn block_record_rejects_reserved_kind_flags_and_parity_last_flag() {
1355        let mut record = BlockRecord {
1356            block_index: 0,
1357            kind: BlockKind::PayloadData,
1358            flags: 0x02,
1359            payload: vec![0; 4096],
1360            record_crc32c: 0,
1361        };
1362        assert_eq!(
1363            BlockRecord::parse(&record.to_bytes(), 4096).unwrap_err(),
1364            FormatError::InvalidBlockFlags(0x02)
1365        );
1366
1367        record.kind = BlockKind::PayloadParity;
1368        record.flags = BLOCK_LAST_DATA_FLAG;
1369        assert_eq!(
1370            BlockRecord::parse(&record.to_bytes(), 4096).unwrap_err(),
1371            FormatError::ParityBlockHasLastDataFlag
1372        );
1373
1374        let mut bytes = record.to_bytes();
1375        bytes[12] = 10;
1376        let crc = crc32c(&bytes[..4112]);
1377        write_u32(&mut bytes, 4112, crc);
1378        assert_eq!(
1379            BlockRecord::parse(&bytes, 4096).unwrap_err(),
1380            FormatError::UnknownBlockKind(10)
1381        );
1382    }
1383
1384    #[test]
1385    fn manifest_footer_round_trips_and_validates_index_extent() {
1386        let footer = ManifestFooter {
1387            archive_uuid: uuid(),
1388            session_id: session(),
1389            volume_index: 0,
1390            is_authoritative: 1,
1391            total_volumes: 1,
1392            index_root_first_block: 0,
1393            index_root_data_block_count: 2,
1394            index_root_parity_block_count: 1,
1395            index_root_encrypted_size: 8192,
1396            index_root_decompressed_size: 120,
1397            manifest_hmac: [0xaa; 32],
1398        };
1399        let parsed = ManifestFooter::parse(&footer.to_bytes()).unwrap();
1400        parsed.validate_index_root_extent(4096).unwrap();
1401
1402        let mut bad = footer.clone();
1403        bad.index_root_encrypted_size = 4096;
1404        assert_eq!(
1405            ManifestFooter::parse(&bad.to_bytes())
1406                .unwrap()
1407                .validate_index_root_extent(4096)
1408                .unwrap_err(),
1409            FormatError::IndexRootSizeMismatch
1410        );
1411    }
1412
1413    #[test]
1414    fn volume_trailer_round_trips_and_requires_manifest_length() {
1415        let trailer = VolumeTrailer {
1416            archive_uuid: uuid(),
1417            session_id: session(),
1418            volume_index: 0,
1419            block_count: 3,
1420            bytes_written: 10_000,
1421            manifest_footer_offset: 9_864,
1422            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
1423            closed_at_ns: 123,
1424            trailer_hmac: [0xbb; 32],
1425        };
1426        let parsed = VolumeTrailer::parse(&trailer.to_bytes()).unwrap();
1427        assert_eq!(parsed.block_count, 3);
1428
1429        let mut bad = trailer;
1430        bad.manifest_footer_length = 100;
1431        assert_eq!(
1432            VolumeTrailer::parse(&bad.to_bytes()).unwrap_err(),
1433            FormatError::InvalidManifestFooterLength(100)
1434        );
1435    }
1436
1437    #[test]
1438    fn bootstrap_sidecar_header_validates_presence_and_layout() {
1439        let header = BootstrapSidecarHeader {
1440            archive_uuid: uuid(),
1441            session_id: session(),
1442            flags: SIDECAR_MANIFEST_PRESENT | SIDECAR_INDEX_ROOT_PRESENT,
1443            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
1444            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
1445            index_root_records_offset: (BOOTSTRAP_SIDECAR_HEADER_LEN + MANIFEST_FOOTER_LEN) as u64,
1446            index_root_records_length: 40,
1447            dictionary_records_offset: 0,
1448            dictionary_records_length: 0,
1449            sidecar_hmac: [0xcc; 32],
1450            header_crc32c: 0,
1451        };
1452        let parsed = BootstrapSidecarHeader::parse(&header.to_bytes()).unwrap();
1453        parsed
1454            .validate_packed_layout(
1455                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + MANIFEST_FOOTER_LEN as u64 + 40,
1456            )
1457            .unwrap();
1458
1459        let mut bad = header.clone();
1460        bad.flags |= 0x08;
1461        assert_eq!(
1462            BootstrapSidecarHeader::parse(&bad.to_bytes()).unwrap_err(),
1463            FormatError::UnknownBootstrapSidecarFlags(0x0b)
1464        );
1465
1466        let mut bad = header;
1467        bad.index_root_records_offset += 1;
1468        let parsed = BootstrapSidecarHeader::parse(&bad.to_bytes()).unwrap();
1469        assert_eq!(
1470            parsed
1471                .validate_packed_layout(
1472                    BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + MANIFEST_FOOTER_LEN as u64 + 40
1473                )
1474                .unwrap_err(),
1475            FormatError::NonCanonicalBootstrapSidecarLayout
1476        );
1477    }
1478}