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}