1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::io::Read;
3
4use sha2::{Digest, Sha256};
5
6use crate::compression::{
7 decompress_exact_zstd_frame, decompress_exact_zstd_frame_with_dictionary,
8 validate_exact_zstd_frame,
9};
10use crate::crypto::{decrypt_padded_aead_object, verify_hmac, HmacDomain, MasterKey, Subkeys};
11use crate::fec::repair_data_gf16;
12use crate::format::{
13 BlockKind, FormatError, BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN,
14 MANIFEST_FOOTER_LEN, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
15};
16use crate::metadata::{
17 hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
18 EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
19};
20use crate::tar_model::{
21 parse_tar_member_group, restore_tar_member, validate_tar_stream_total_extraction_size,
22 MetadataDiagnostic, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
23};
24use crate::wire::{
25 BlockRecord, BootstrapSidecarHeader, CryptoHeader, CryptoHeaderFixed, ManifestFooter,
26 VolumeHeader, VolumeTrailer,
27};
28
29const TRAILER_HMAC_COVERED_LEN: usize = 96;
30const MANIFEST_HMAC_COVERED_LEN: usize = 104;
31const SIDECAR_HMAC_COVERED_LEN: usize = 92;
32const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
33const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
34const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
35const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct ReaderOptions {
39 pub max_trailing_garbage_scan: usize,
40 pub max_verify_tar_size: usize,
41 pub max_total_extraction_size: u64,
42}
43
44impl Default for ReaderOptions {
45 fn default() -> Self {
46 Self {
47 max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
48 max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
49 max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ArchiveEntry {
56 pub path: String,
57 pub file_data_size: u64,
58 pub kind: TarEntryKind,
59 pub mode: u32,
60 pub mtime: u64,
61 pub diagnostics: Vec<MetadataDiagnostic>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ExtractedArchiveMember {
66 pub path: String,
67 pub kind: TarEntryKind,
68 pub data: Vec<u8>,
69 pub link_target: Option<String>,
70 pub diagnostics: Vec<MetadataDiagnostic>,
71}
72
73#[derive(Debug, Clone)]
74pub struct OpenedArchive {
75 options: ReaderOptions,
76 observed_archive_bytes: u64,
77 subkeys: Subkeys,
78 blocks: BTreeMap<u64, BlockRecord>,
79 pub volume_header: VolumeHeader,
80 pub crypto_header: CryptoHeaderFixed,
81 pub manifest_footer: ManifestFooter,
82 pub volume_trailer: VolumeTrailer,
83 pub index_root: IndexRoot,
84 payload_dictionary: Option<Vec<u8>>,
85}
86
87#[derive(Debug, Clone, Copy)]
88struct ObjectExtent {
89 first_block_index: u64,
90 data_block_count: u32,
91 parity_block_count: u32,
92 encrypted_size: u32,
93}
94
95type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
96
97pub fn open_archive<'a>(
98 bytes: &'a [u8],
99 master_key: &MasterKey,
100) -> Result<OpenedArchive, FormatError> {
101 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
102}
103
104pub fn open_archive_volumes(
105 volumes: &[&[u8]],
106 master_key: &MasterKey,
107) -> Result<OpenedArchive, FormatError> {
108 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
109}
110
111pub fn open_archive_with_bootstrap_sidecar(
112 bytes: &[u8],
113 bootstrap_sidecar: &[u8],
114 master_key: &MasterKey,
115) -> Result<OpenedArchive, FormatError> {
116 OpenedArchive::open_with_bootstrap_sidecar_options(
117 bytes,
118 bootstrap_sidecar,
119 master_key,
120 ReaderOptions::default(),
121 )
122}
123
124pub fn open_non_seekable_archive(
125 bytes: &[u8],
126 master_key: &MasterKey,
127 bootstrap_sidecar: Option<&[u8]>,
128) -> Result<OpenedArchive, FormatError> {
129 match bootstrap_sidecar {
130 Some(sidecar) => open_archive_with_bootstrap_sidecar(bytes, sidecar, master_key),
131 None => Err(FormatError::ReaderUnsupported(
132 "non-seekable random access requires a bootstrap sidecar",
133 )),
134 }
135}
136
137pub fn sequential_extract_tar_stream(
138 bytes: &[u8],
139 master_key: &MasterKey,
140) -> Result<Vec<u8>, FormatError> {
141 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
142}
143
144impl OpenedArchive {
145 pub fn open_with_options(
146 bytes: &[u8],
147 master_key: &MasterKey,
148 options: ReaderOptions,
149 ) -> Result<Self, FormatError> {
150 Self::open_volumes_with_options(&[bytes], master_key, options)
151 }
152
153 pub fn open_volumes_with_options(
154 volumes: &[&[u8]],
155 master_key: &MasterKey,
156 options: ReaderOptions,
157 ) -> Result<Self, FormatError> {
158 if volumes.is_empty() {
159 return Err(FormatError::InvalidArchive("no volumes supplied"));
160 }
161
162 let observed_archive_bytes =
163 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
164 let mut first: Option<ParsedSeekableVolume> = None;
165 let mut seen_volume_indexes = BTreeSet::new();
166 let mut blocks = BTreeMap::new();
167 let mut erased_block_indices = BTreeSet::new();
168
169 for volume_bytes in volumes {
170 let parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
171 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
172 return Err(FormatError::InvalidArchive(
173 "duplicate authenticated volume index",
174 ));
175 }
176
177 if let Some(first) = &first {
178 validate_volume_set_member(first, &parsed)?;
179 }
180
181 for (block_index, record) in &parsed.blocks {
182 if blocks.insert(*block_index, record.clone()).is_some() {
183 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
184 }
185 }
186 for block_index in &parsed.erased_block_indices {
187 erased_block_indices.insert(*block_index);
188 }
189
190 if first.is_none() {
191 first = Some(parsed);
192 }
193 }
194
195 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
196 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
197 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
198 }
199
200 let limits = metadata_limits(&first.crypto_header);
201 let index_root_plaintext = load_metadata_object_from_parts(
202 &blocks,
203 &first.subkeys,
204 &first.volume_header,
205 &first.crypto_header,
206 ObjectExtent {
207 first_block_index: first.manifest_footer.index_root_first_block,
208 data_block_count: first.manifest_footer.index_root_data_block_count,
209 parity_block_count: first.manifest_footer.index_root_parity_block_count,
210 encrypted_size: first.manifest_footer.index_root_encrypted_size,
211 },
212 BlockKind::IndexRootData,
213 BlockKind::IndexRootParity,
214 &first.subkeys.index_root_key,
215 &first.subkeys.index_nonce_seed,
216 b"idxroot",
217 0,
218 first.crypto_header.index_root_fec_data_shards,
219 first.crypto_header.index_root_fec_parity_shards,
220 first.manifest_footer.index_root_decompressed_size,
221 )?;
222 let index_root = IndexRoot::parse(
223 &index_root_plaintext,
224 first.crypto_header.has_dictionary != 0,
225 limits,
226 )?;
227 let payload_dictionary = load_archive_dictionary(
228 &blocks,
229 &first.subkeys,
230 &first.volume_header,
231 &first.crypto_header,
232 &index_root,
233 )?;
234
235 Ok(Self {
236 options,
237 observed_archive_bytes,
238 subkeys: first.subkeys,
239 blocks,
240 volume_header: first.volume_header,
241 crypto_header: first.crypto_header,
242 manifest_footer: first.manifest_footer,
243 volume_trailer: first.volume_trailer,
244 index_root,
245 payload_dictionary,
246 })
247 }
248
249 pub fn open_with_bootstrap_sidecar_options(
250 bytes: &[u8],
251 bootstrap_sidecar: &[u8],
252 master_key: &MasterKey,
253 options: ReaderOptions,
254 ) -> Result<Self, FormatError> {
255 let observed_archive_bytes =
256 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
257 if bytes.len() < VOLUME_HEADER_LEN {
258 return Err(FormatError::InvalidLength {
259 structure: "archive",
260 expected: VOLUME_HEADER_LEN,
261 actual: bytes.len(),
262 });
263 }
264
265 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
266 let crypto_start = volume_header.crypto_header_offset as usize;
267 let crypto_len = volume_header.crypto_header_length as usize;
268 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
269 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
270 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
271 let subkeys = Subkeys::derive(
272 master_key,
273 &volume_header.archive_uuid,
274 &volume_header.session_id,
275 )?;
276 verify_hmac(
277 HmacDomain::CryptoHeader,
278 &subkeys.mac_key,
279 &volume_header.archive_uuid,
280 &volume_header.session_id,
281 parsed_crypto.hmac_covered_bytes,
282 &parsed_crypto.header_hmac,
283 )?;
284 parsed_crypto.validate_extension_semantics()?;
285 validate_m9_supported_volume(&volume_header, &parsed_crypto.fixed)?;
286
287 let sidecar = parse_trusted_bootstrap_sidecar(
288 bootstrap_sidecar,
289 &volume_header,
290 &parsed_crypto.fixed,
291 &subkeys,
292 )?;
293
294 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
295 bytes,
296 crypto_end,
297 parsed_crypto.fixed.block_size as usize,
298 &volume_header,
299 )?;
300 insert_sidecar_records(&mut blocks, sidecar.index_root_records)?;
301
302 let (terminal_manifest, volume_trailer) = parse_terminal_material(
303 bytes,
304 terminal_offset,
305 observed_block_count,
306 &subkeys,
307 &volume_header,
308 parsed_crypto.fixed.block_size,
309 )?;
310 if terminal_manifest != sidecar.manifest_footer {
311 return Err(FormatError::InvalidArchive(
312 "bootstrap sidecar conflicts with terminal ManifestFooter",
313 ));
314 }
315
316 let limits = metadata_limits(&parsed_crypto.fixed);
317 let index_root_plaintext = load_metadata_object_from_parts(
318 &blocks,
319 &subkeys,
320 &volume_header,
321 &parsed_crypto.fixed,
322 ObjectExtent {
323 first_block_index: sidecar.manifest_footer.index_root_first_block,
324 data_block_count: sidecar.manifest_footer.index_root_data_block_count,
325 parity_block_count: sidecar.manifest_footer.index_root_parity_block_count,
326 encrypted_size: sidecar.manifest_footer.index_root_encrypted_size,
327 },
328 BlockKind::IndexRootData,
329 BlockKind::IndexRootParity,
330 &subkeys.index_root_key,
331 &subkeys.index_nonce_seed,
332 b"idxroot",
333 0,
334 parsed_crypto.fixed.index_root_fec_data_shards,
335 parsed_crypto.fixed.index_root_fec_parity_shards,
336 sidecar.manifest_footer.index_root_decompressed_size,
337 )?;
338 let index_root = IndexRoot::parse(
339 &index_root_plaintext,
340 parsed_crypto.fixed.has_dictionary != 0,
341 limits,
342 )?;
343 if parsed_crypto.fixed.has_dictionary != 0 {
344 let (offset, length) =
345 sidecar
346 .dictionary_records_section
347 .ok_or(FormatError::ReaderUnsupported(
348 "dictionary bootstrap required",
349 ))?;
350 let dictionary_records = parse_sidecar_block_records(
351 bootstrap_sidecar,
352 offset,
353 length,
354 parsed_crypto.fixed.block_size as usize,
355 dictionary_extent_from_index_root(&index_root)?,
356 BlockKind::DictionaryData,
357 BlockKind::DictionaryParity,
358 "dictionary",
359 )?;
360 insert_sidecar_records(&mut blocks, dictionary_records)?;
361 }
362 let payload_dictionary = load_archive_dictionary(
363 &blocks,
364 &subkeys,
365 &volume_header,
366 &parsed_crypto.fixed,
367 &index_root,
368 )?;
369
370 Ok(Self {
371 options,
372 observed_archive_bytes,
373 subkeys,
374 blocks,
375 volume_header,
376 crypto_header: parsed_crypto.fixed,
377 manifest_footer: sidecar.manifest_footer,
378 volume_trailer,
379 index_root,
380 payload_dictionary,
381 })
382 }
383
384 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
385 #[derive(Clone, Copy)]
386 struct WinningEntry {
387 start: u64,
388 file_data_size: u64,
389 shard_index: usize,
390 file_index: usize,
391 }
392
393 let shards = self.load_all_index_shards()?;
394 let mut final_entries = BTreeMap::<String, WinningEntry>::new();
395 for (shard_index, shard) in shards.iter().enumerate() {
396 for (idx, file) in shard.files.iter().enumerate() {
397 let path = utf8_path(
398 shard
399 .file_path(idx)
400 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
401 )?;
402 let start =
403 shard
404 .tar_member_group_start(idx)
405 .ok_or(FormatError::InvalidArchive(
406 "FileEntry tar member start is missing",
407 ))?;
408 if let Some(winner) = final_entries.get_mut(&path) {
409 if start >= winner.start {
410 winner.start = start;
411 winner.file_data_size = file.file_data_size;
412 winner.shard_index = shard_index;
413 winner.file_index = idx;
414 }
415 } else {
416 final_entries.insert(
417 path,
418 WinningEntry {
419 start,
420 file_data_size: file.file_data_size,
421 shard_index,
422 file_index: idx,
423 },
424 );
425 }
426 }
427 }
428 final_entries
429 .into_iter()
430 .map(|(path, winner)| {
431 let shard = &shards[winner.shard_index];
432 let member =
433 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
434 Ok(ArchiveEntry {
435 path,
436 file_data_size: winner.file_data_size,
437 kind: member.kind,
438 mode: member.mode,
439 mtime: member.mtime,
440 diagnostics: member.diagnostics,
441 })
442 })
443 .collect()
444 }
445
446 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
447 self.extract_member(path)?
448 .map(|member| {
449 if member.kind != TarEntryKind::Regular {
450 return Err(FormatError::ReaderUnsupported(
451 "extract_file returns only regular file payloads",
452 ));
453 }
454 Ok(member.data)
455 })
456 .transpose()
457 }
458
459 pub fn extract_member(
460 &self,
461 path: &str,
462 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
463 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
464 let candidate_indexes = self
465 .index_root
466 .candidate_shards_for_path(&normalized, self.metadata_limits())?;
467 let mut winner: Option<(IndexShard, usize, u64)> = None;
468
469 for row_index in candidate_indexes {
470 let locating =
471 self.index_root
472 .shards
473 .get(row_index)
474 .ok_or(FormatError::InvalidArchive(
475 "candidate shard row is out of bounds",
476 ))?;
477 let shard = self.load_index_shard(locating)?;
478 if let Some(file_index) = shard.lookup_file_index(&normalized) {
479 let start =
480 shard
481 .tar_member_group_start(file_index)
482 .ok_or(FormatError::InvalidArchive(
483 "FileEntry tar member start is missing",
484 ))?;
485 if winner
486 .as_ref()
487 .map(|(_, _, best_start)| start > *best_start)
488 .unwrap_or(true)
489 {
490 winner = Some((shard, file_index, start));
491 }
492 }
493 }
494
495 winner
496 .map(|(shard, file_index, _)| self.extract_loaded_member(&shard, file_index))
497 .transpose()
498 }
499
500 pub fn extract_file_to(
501 &self,
502 path: &str,
503 root: &std::path::Path,
504 options: SafeExtractionOptions,
505 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
506 self.extract_owned_tar_member(path)?
507 .map(|member| restore_tar_member(root, &member, options))
508 .transpose()
509 }
510
511 pub fn verify(&self) -> Result<(), FormatError> {
512 let shards = self.load_all_index_shards()?;
513 let mut file_count = 0u64;
514 let mut frames = BTreeMap::<u64, FrameEntry>::new();
515 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
516
517 for shard in &shards {
518 file_count = file_count
519 .checked_add(shard.files.len() as u64)
520 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
521 for frame in &shard.frames {
522 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
523 if existing != *frame {
524 return Err(FormatError::InvalidArchive(
525 "duplicate FrameEntry rows do not match",
526 ));
527 }
528 }
529 }
530 for envelope in &shard.envelopes {
531 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
532 {
533 if existing != *envelope {
534 return Err(FormatError::InvalidArchive(
535 "duplicate EnvelopeEntry rows do not match",
536 ));
537 }
538 }
539 }
540 }
541
542 if file_count != self.index_root.header.file_count {
543 return Err(FormatError::InvalidArchive(
544 "IndexRoot file_count does not match decoded shards",
545 ));
546 }
547 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
548 && self.index_root.directory_hint_shards.is_empty()
549 {
550 return Err(FormatError::InvalidArchive(
551 "IndexRoot file_count requires directory hints",
552 ));
553 }
554 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
555 verify_dense_keys(
556 &envelopes,
557 self.index_root.header.envelope_count,
558 "EnvelopeEntry",
559 )?;
560 validate_envelope_frame_coverage(&frames, &envelopes)?;
561 self.validate_encrypted_object_block_ranges(&envelopes)?;
562
563 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
564 sum.checked_add(envelope.data_block_count as u64)
565 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
566 })?;
567 if payload_block_count != self.index_root.header.payload_block_count {
568 return Err(FormatError::InvalidArchive(
569 "IndexRoot payload_block_count does not match envelopes",
570 ));
571 }
572
573 let tar_len = to_usize(self.index_root.header.tar_total_size, "tar stream")?;
574 if tar_len > self.options.max_verify_tar_size {
575 return Err(FormatError::ReaderUnsupported(
576 "verify tar stream exceeds configured in-memory cap",
577 ));
578 }
579 let mut tar_stream = vec![0u8; tar_len];
580 let mut covered = vec![false; tar_len];
581 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
582
583 for frame in frames.values() {
584 let envelope =
585 envelopes
586 .get(&frame.envelope_index)
587 .ok_or(FormatError::InvalidArchive(
588 "FrameEntry references missing EnvelopeEntry",
589 ))?;
590 if !envelope_cache.contains_key(&envelope.envelope_index) {
591 envelope_cache.insert(
592 envelope.envelope_index,
593 self.load_payload_envelope(envelope)?,
594 );
595 }
596 let envelope_plaintext = envelope_cache
597 .get(&envelope.envelope_index)
598 .expect("inserted above");
599 let compressed = slice(
600 envelope_plaintext,
601 frame.offset_in_envelope as usize,
602 frame.compressed_size as usize,
603 "FrameEntry",
604 )?;
605 let decoded = self.decompress_payload_frame(compressed, frame.decompressed_size)?;
606 let start = to_usize(frame.tar_stream_offset, "tar stream")?;
607 let end = checked_add(start, decoded.len(), "tar stream")?;
608 if end > tar_stream.len() {
609 return Err(FormatError::InvalidArchive(
610 "FrameEntry exceeds IndexRoot tar_total_size",
611 ));
612 }
613 if covered[start..end].iter().any(|value| *value) {
614 return Err(FormatError::InvalidArchive("decoded frames overlap"));
615 }
616 tar_stream[start..end].copy_from_slice(&decoded);
617 covered[start..end].fill(true);
618 }
619
620 if covered.iter().any(|value| !*value) {
621 return Err(FormatError::InvalidArchive("decoded frames leave tar gap"));
622 }
623 if sha256_bytes(&tar_stream) != self.index_root.header.content_sha256 {
624 return Err(FormatError::InvalidArchive(
625 "IndexRoot content_sha256 does not match decoded tar stream",
626 ));
627 }
628
629 let mut file_extents = Vec::new();
630 let mut directory_hint_map = DirectoryHintMap::new();
631 for (shard_row_index, shard) in shards.iter().enumerate() {
632 let shard_row_index = u32::try_from(shard_row_index)
633 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
634 for idx in 0..shard.files.len() {
635 let file = &shard.files[idx];
636 let start =
637 shard
638 .tar_member_group_start(idx)
639 .ok_or(FormatError::InvalidArchive(
640 "FileEntry tar member start is missing",
641 ))?;
642 file_extents.push((start, file.tar_member_group_size));
643 let member = self.decode_loaded_owned_tar_member(shard, idx, false)?;
644 let path = shard
645 .file_path(idx)
646 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
647 add_expected_directory_hint_rows(
648 &mut directory_hint_map,
649 shard_row_index,
650 path,
651 member.kind,
652 );
653 }
654 }
655 validate_file_extent_coverage_ranges(&file_extents, tar_len)?;
656 if !self.index_root.directory_hint_shards.is_empty() {
657 let hint_tables = self.load_all_directory_hint_tables()?;
658 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
659 }
660
661 Ok(())
662 }
663
664 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
665 self.index_root
666 .shards
667 .iter()
668 .map(|entry| self.load_index_shard(entry))
669 .collect()
670 }
671
672 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
673 let plaintext = load_metadata_object_from_parts(
674 &self.blocks,
675 &self.subkeys,
676 &self.volume_header,
677 &self.crypto_header,
678 ObjectExtent {
679 first_block_index: entry.first_block_index,
680 data_block_count: entry.data_block_count,
681 parity_block_count: entry.parity_block_count,
682 encrypted_size: entry.encrypted_size,
683 },
684 BlockKind::IndexShardData,
685 BlockKind::IndexShardParity,
686 &self.subkeys.index_shard_key,
687 &self.subkeys.index_nonce_seed,
688 b"idxshard",
689 entry.shard_index,
690 self.crypto_header.index_fec_data_shards,
691 self.crypto_header.index_fec_parity_shards,
692 entry.decompressed_size,
693 )?;
694 IndexShard::parse(&plaintext, entry, self.metadata_limits())
695 }
696
697 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
698 self.index_root
699 .directory_hint_shards
700 .iter()
701 .map(|entry| self.load_directory_hint_table(entry))
702 .collect()
703 }
704
705 fn load_directory_hint_table(
706 &self,
707 entry: &DirectoryHintShardEntry,
708 ) -> Result<DirectoryHintTable, FormatError> {
709 let plaintext = load_metadata_object_from_parts(
710 &self.blocks,
711 &self.subkeys,
712 &self.volume_header,
713 &self.crypto_header,
714 ObjectExtent {
715 first_block_index: entry.first_block_index,
716 data_block_count: entry.data_block_count,
717 parity_block_count: entry.parity_block_count,
718 encrypted_size: entry.encrypted_size,
719 },
720 BlockKind::DirectoryHintData,
721 BlockKind::DirectoryHintParity,
722 &self.subkeys.dir_hint_key,
723 &self.subkeys.index_nonce_seed,
724 b"dirhint",
725 entry.hint_shard_index,
726 self.crypto_header.index_fec_data_shards,
727 self.crypto_header.index_fec_parity_shards,
728 entry.decompressed_size,
729 )?;
730 DirectoryHintTable::parse(
731 &plaintext,
732 entry,
733 self.index_root.header.shard_count,
734 self.metadata_limits(),
735 )
736 }
737
738 fn load_payload_envelope(&self, envelope: &EnvelopeEntry) -> Result<Vec<u8>, FormatError> {
739 let plaintext = load_decrypted_object_from_parts(
740 &self.blocks,
741 &self.volume_header,
742 &self.crypto_header,
743 ObjectExtent {
744 first_block_index: envelope.first_block_index,
745 data_block_count: envelope.data_block_count,
746 parity_block_count: envelope.parity_block_count,
747 encrypted_size: envelope.encrypted_size,
748 },
749 BlockKind::PayloadData,
750 BlockKind::PayloadParity,
751 &self.subkeys.enc_key,
752 &self.subkeys.nonce_seed,
753 b"envelope",
754 envelope.envelope_index,
755 self.crypto_header.fec_data_shards,
756 self.crypto_header.fec_parity_shards,
757 )?;
758 if plaintext.len() != envelope.plaintext_size as usize {
759 return Err(FormatError::InvalidArchive(
760 "payload envelope plaintext_size mismatch",
761 ));
762 }
763 Ok(plaintext)
764 }
765
766 fn extract_owned_tar_member(&self, path: &str) -> Result<Option<OwnedTarMember>, FormatError> {
767 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
768 let candidate_indexes = self
769 .index_root
770 .candidate_shards_for_path(&normalized, self.metadata_limits())?;
771 let mut winner: Option<(IndexShard, usize, u64)> = None;
772
773 for row_index in candidate_indexes {
774 let locating =
775 self.index_root
776 .shards
777 .get(row_index)
778 .ok_or(FormatError::InvalidArchive(
779 "candidate shard row is out of bounds",
780 ))?;
781 let shard = self.load_index_shard(locating)?;
782 if let Some(file_index) = shard.lookup_file_index(&normalized) {
783 let start =
784 shard
785 .tar_member_group_start(file_index)
786 .ok_or(FormatError::InvalidArchive(
787 "FileEntry tar member start is missing",
788 ))?;
789 if winner
790 .as_ref()
791 .map(|(_, _, best_start)| start > *best_start)
792 .unwrap_or(true)
793 {
794 winner = Some((shard, file_index, start));
795 }
796 }
797 }
798
799 winner
800 .map(|(shard, file_index, _)| self.extract_loaded_owned_tar_member(&shard, file_index))
801 .transpose()
802 }
803
804 fn extract_loaded_member(
805 &self,
806 shard: &IndexShard,
807 file_index: usize,
808 ) -> Result<ExtractedArchiveMember, FormatError> {
809 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
810 Ok(ExtractedArchiveMember {
811 path: utf8_path(&member.path)?,
812 kind: member.kind,
813 data: member.data,
814 link_target: member
815 .link_target
816 .map(|target| utf8_path(&target))
817 .transpose()?,
818 diagnostics: member.diagnostics,
819 })
820 }
821
822 fn extract_loaded_owned_tar_member(
823 &self,
824 shard: &IndexShard,
825 file_index: usize,
826 ) -> Result<OwnedTarMember, FormatError> {
827 self.decode_loaded_owned_tar_member(shard, file_index, true)
828 }
829
830 fn decode_loaded_owned_tar_member(
831 &self,
832 shard: &IndexShard,
833 file_index: usize,
834 enforce_extraction_cap: bool,
835 ) -> Result<OwnedTarMember, FormatError> {
836 let file = shard
837 .files
838 .get(file_index)
839 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
840 if enforce_extraction_cap {
841 self.validate_total_extraction_size(file.file_data_size)?;
842 }
843 let expected_path = shard
844 .file_path(file_index)
845 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
846 let frames = frame_range_for_file(shard, file)?;
847 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
848 let mut decoded = Vec::new();
849
850 for frame in frames {
851 let envelope = shard
852 .envelopes
853 .iter()
854 .find(|entry| entry.envelope_index == frame.envelope_index)
855 .ok_or(FormatError::InvalidArchive(
856 "FrameEntry references missing EnvelopeEntry",
857 ))?;
858 if !envelope_cache.contains_key(&envelope.envelope_index) {
859 envelope_cache.insert(
860 envelope.envelope_index,
861 self.load_payload_envelope(envelope)?,
862 );
863 }
864 let envelope_plaintext = envelope_cache
865 .get(&envelope.envelope_index)
866 .expect("inserted above");
867 let compressed = slice(
868 envelope_plaintext,
869 frame.offset_in_envelope as usize,
870 frame.compressed_size as usize,
871 "FrameEntry",
872 )?;
873 decoded.extend_from_slice(
874 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
875 );
876 }
877
878 let offset = file.offset_in_first_frame_plaintext as usize;
879 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
880 let group = slice(&decoded, offset, group_len, "FileEntry")?;
881 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
882 if member.path != expected_path {
883 return Err(FormatError::InvalidArchive(
884 "tar member path does not match FileEntry path",
885 ));
886 }
887 if member.logical_size != file.file_data_size {
888 return Err(FormatError::InvalidArchive(
889 "tar member size does not match FileEntry file_data_size",
890 ));
891 }
892 Ok(member.to_owned_member())
893 }
894
895 fn metadata_limits(&self) -> MetadataLimits {
896 metadata_limits(&self.crypto_header)
897 }
898
899 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
900 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
901 if logical_size > cap {
902 return Err(FormatError::ReaderUnsupported(
903 "total extraction size exceeds configured cap",
904 ));
905 }
906 Ok(())
907 }
908
909 fn decompress_payload_frame(
910 &self,
911 compressed: &[u8],
912 decompressed_size: u32,
913 ) -> Result<Vec<u8>, FormatError> {
914 if let Some(dictionary) = &self.payload_dictionary {
915 decompress_exact_zstd_frame_with_dictionary(
916 compressed,
917 decompressed_size as usize,
918 dictionary,
919 )
920 } else {
921 decompress_exact_zstd_frame(compressed, decompressed_size as usize)
922 }
923 }
924
925 fn validate_encrypted_object_block_ranges(
926 &self,
927 envelopes: &BTreeMap<u64, EnvelopeEntry>,
928 ) -> Result<(), FormatError> {
929 let mut ranges = Vec::new();
930 ranges.push(object_block_range(
931 self.manifest_footer.index_root_first_block,
932 self.manifest_footer.index_root_data_block_count,
933 self.manifest_footer.index_root_parity_block_count,
934 "IndexRoot",
935 )?);
936 for shard in &self.index_root.shards {
937 ranges.push(object_block_range(
938 shard.first_block_index,
939 shard.data_block_count,
940 shard.parity_block_count,
941 "IndexShard",
942 )?);
943 }
944 for hint in &self.index_root.directory_hint_shards {
945 ranges.push(object_block_range(
946 hint.first_block_index,
947 hint.data_block_count,
948 hint.parity_block_count,
949 "DirectoryHintShardEntry",
950 )?);
951 }
952 if self.crypto_header.has_dictionary != 0 {
953 ranges.push(object_block_range(
954 self.index_root.header.dictionary_first_block,
955 self.index_root.header.dictionary_data_block_count,
956 self.index_root.header.dictionary_parity_block_count,
957 "dictionary",
958 )?);
959 }
960 for envelope in envelopes.values() {
961 ranges.push(object_block_range(
962 envelope.first_block_index,
963 envelope.data_block_count,
964 envelope.parity_block_count,
965 "EnvelopeEntry",
966 )?);
967 }
968 validate_non_overlapping_object_ranges(&mut ranges)
969 }
970}
971
972#[derive(Debug)]
973struct ParsedSeekableVolume {
974 volume_header: VolumeHeader,
975 crypto_header: CryptoHeaderFixed,
976 crypto_header_bytes: Vec<u8>,
977 subkeys: Subkeys,
978 manifest_footer: ManifestFooter,
979 volume_trailer: VolumeTrailer,
980 blocks: BTreeMap<u64, BlockRecord>,
981 erased_block_indices: BTreeSet<u64>,
982}
983
984fn parse_seekable_volume(
985 bytes: &[u8],
986 master_key: &MasterKey,
987 options: ReaderOptions,
988) -> Result<ParsedSeekableVolume, FormatError> {
989 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
990 return Err(FormatError::InvalidLength {
991 structure: "archive",
992 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
993 actual: bytes.len(),
994 });
995 }
996
997 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
998 let crypto_start = volume_header.crypto_header_offset as usize;
999 let crypto_len = volume_header.crypto_header_length as usize;
1000 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1001 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1002 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1003 let subkeys = Subkeys::derive(
1004 master_key,
1005 &volume_header.archive_uuid,
1006 &volume_header.session_id,
1007 )?;
1008 verify_hmac(
1009 HmacDomain::CryptoHeader,
1010 &subkeys.mac_key,
1011 &volume_header.archive_uuid,
1012 &volume_header.session_id,
1013 parsed_crypto.hmac_covered_bytes,
1014 &parsed_crypto.header_hmac,
1015 )?;
1016 parsed_crypto.validate_extension_semantics()?;
1017 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed)?;
1018
1019 let (trailer_offset, volume_trailer) =
1020 locate_trailer(bytes, &subkeys, &volume_header, options)?;
1021 validate_trailer_identity(&volume_header, &volume_trailer)?;
1022
1023 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
1024 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
1025 if manifest_end != trailer_offset {
1026 return Err(FormatError::InvalidArchive(
1027 "ManifestFooter does not end at selected trailer",
1028 ));
1029 }
1030 let manifest_bytes = slice(
1031 bytes,
1032 manifest_offset,
1033 MANIFEST_FOOTER_LEN,
1034 "ManifestFooter",
1035 )?;
1036 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
1037 validate_manifest_footer(&volume_header, &manifest_footer, &subkeys, manifest_bytes)?;
1038 manifest_footer.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1039
1040 let block_region = parse_block_region(
1041 bytes,
1042 crypto_end,
1043 manifest_offset,
1044 parsed_crypto.fixed.block_size as usize,
1045 &volume_header,
1046 &volume_trailer,
1047 )?;
1048
1049 Ok(ParsedSeekableVolume {
1050 volume_header,
1051 crypto_header: parsed_crypto.fixed,
1052 crypto_header_bytes: crypto_bytes.to_vec(),
1053 subkeys,
1054 manifest_footer,
1055 volume_trailer,
1056 blocks: block_region.blocks,
1057 erased_block_indices: block_region.erased_block_indices,
1058 })
1059}
1060
1061fn validate_seekable_supported_volume(
1062 volume_header: &VolumeHeader,
1063 crypto_header: &CryptoHeaderFixed,
1064) -> Result<(), FormatError> {
1065 if crypto_header.stripe_width != volume_header.stripe_width {
1066 return Err(FormatError::InvalidArchive(
1067 "VolumeHeader and CryptoHeader stripe_width differ",
1068 ));
1069 }
1070 Ok(())
1071}
1072
1073fn validate_volume_set_member(
1074 first: &ParsedSeekableVolume,
1075 candidate: &ParsedSeekableVolume,
1076) -> Result<(), FormatError> {
1077 if candidate.volume_header.archive_uuid != first.volume_header.archive_uuid
1078 || candidate.volume_header.session_id != first.volume_header.session_id
1079 {
1080 return Err(FormatError::InvalidArchive(
1081 "mixed archive or session IDs in volume set",
1082 ));
1083 }
1084 if candidate.crypto_header_bytes != first.crypto_header_bytes
1085 || candidate.crypto_header != first.crypto_header
1086 {
1087 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
1088 }
1089 if !manifest_bootstrap_fields_match(&first.manifest_footer, &candidate.manifest_footer) {
1090 return Err(FormatError::InvalidArchive(
1091 "ManifestFooter bootstrap fields differ",
1092 ));
1093 }
1094 Ok(())
1095}
1096
1097fn manifest_bootstrap_fields_match(left: &ManifestFooter, right: &ManifestFooter) -> bool {
1098 left.archive_uuid == right.archive_uuid
1099 && left.session_id == right.session_id
1100 && left.is_authoritative == right.is_authoritative
1101 && left.total_volumes == right.total_volumes
1102 && left.index_root_first_block == right.index_root_first_block
1103 && left.index_root_data_block_count == right.index_root_data_block_count
1104 && left.index_root_parity_block_count == right.index_root_parity_block_count
1105 && left.index_root_encrypted_size == right.index_root_encrypted_size
1106 && left.index_root_decompressed_size == right.index_root_decompressed_size
1107}
1108
1109fn validate_complete_global_block_coverage(
1110 blocks: &BTreeMap<u64, BlockRecord>,
1111 erased_block_indices: &BTreeSet<u64>,
1112) -> Result<(), FormatError> {
1113 let mut expected = 0u64;
1114 let mut block_iter = blocks.keys().copied().peekable();
1115 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
1116
1117 loop {
1118 let next_block = block_iter.peek().copied();
1119 let next_erasure = erasure_iter.peek().copied();
1120 let next = match (next_block, next_erasure) {
1121 (Some(block), Some(erasure)) if block == erasure => {
1122 return Err(FormatError::InvalidArchive(
1123 "BlockRecord index is both present and erased",
1124 ));
1125 }
1126 (Some(block), Some(erasure)) => block.min(erasure),
1127 (Some(block), None) => block,
1128 (None, Some(erasure)) => erasure,
1129 (None, None) => return Ok(()),
1130 };
1131
1132 if next != expected {
1133 return Err(FormatError::InvalidArchive(
1134 "complete volume set has missing global blocks",
1135 ));
1136 }
1137 if next_block == Some(next) {
1138 block_iter.next();
1139 }
1140 if next_erasure == Some(next) {
1141 erasure_iter.next();
1142 }
1143 expected = expected
1144 .checked_add(1)
1145 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
1146 }
1147}
1148
1149fn locate_trailer(
1150 bytes: &[u8],
1151 subkeys: &Subkeys,
1152 volume_header: &VolumeHeader,
1153 options: ReaderOptions,
1154) -> Result<(usize, VolumeTrailer), FormatError> {
1155 let canonical_offset =
1156 bytes
1157 .len()
1158 .checked_sub(VOLUME_TRAILER_LEN)
1159 .ok_or(FormatError::InvalidLength {
1160 structure: "VolumeTrailer",
1161 expected: VOLUME_TRAILER_LEN,
1162 actual: bytes.len(),
1163 })?;
1164 match parse_authenticated_trailer(bytes, canonical_offset, subkeys, volume_header) {
1165 Ok(trailer) => {
1166 if trailer.bytes_written != canonical_offset as u64 {
1167 return Err(FormatError::InvalidArchive(
1168 "VolumeTrailer bytes_written does not match selected trailer offset",
1169 ));
1170 }
1171 return Ok((canonical_offset, trailer));
1172 }
1173 Err(err) if options.max_trailing_garbage_scan == 0 => return Err(err),
1174 Err(_) => {}
1175 }
1176
1177 let scan_start = canonical_offset.saturating_sub(options.max_trailing_garbage_scan);
1178 for offset in (scan_start..canonical_offset).rev() {
1179 if let Ok(trailer) = parse_authenticated_trailer(bytes, offset, subkeys, volume_header) {
1180 if trailer.bytes_written == offset as u64 {
1181 return Ok((offset, trailer));
1182 }
1183 }
1184 }
1185
1186 Err(FormatError::InvalidArchive(
1187 "no authenticated VolumeTrailer found",
1188 ))
1189}
1190
1191fn parse_authenticated_trailer(
1192 bytes: &[u8],
1193 offset: usize,
1194 subkeys: &Subkeys,
1195 volume_header: &VolumeHeader,
1196) -> Result<VolumeTrailer, FormatError> {
1197 let raw = slice(bytes, offset, VOLUME_TRAILER_LEN, "VolumeTrailer")?;
1198 let trailer = VolumeTrailer::parse(raw)?;
1199 verify_hmac(
1200 HmacDomain::VolumeTrailer,
1201 &subkeys.mac_key,
1202 &volume_header.archive_uuid,
1203 &volume_header.session_id,
1204 &raw[..TRAILER_HMAC_COVERED_LEN],
1205 &trailer.trailer_hmac,
1206 )?;
1207 Ok(trailer)
1208}
1209
1210fn validate_m9_supported_volume(
1211 volume_header: &VolumeHeader,
1212 crypto_header: &CryptoHeaderFixed,
1213) -> Result<(), FormatError> {
1214 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
1215 return Err(FormatError::ReaderUnsupported(
1216 "M9 reader supports only single-volume archives",
1217 ));
1218 }
1219 if crypto_header.stripe_width != volume_header.stripe_width {
1220 return Err(FormatError::InvalidArchive(
1221 "VolumeHeader and CryptoHeader stripe_width differ",
1222 ));
1223 }
1224 Ok(())
1225}
1226
1227#[derive(Debug)]
1228struct TrustedBootstrapSidecar {
1229 manifest_footer: ManifestFooter,
1230 index_root_records: Vec<BlockRecord>,
1231 dictionary_records_section: Option<(u64, u64)>,
1232}
1233
1234fn parse_trusted_bootstrap_sidecar(
1235 bytes: &[u8],
1236 volume_header: &VolumeHeader,
1237 crypto_header: &CryptoHeaderFixed,
1238 subkeys: &Subkeys,
1239) -> Result<TrustedBootstrapSidecar, FormatError> {
1240 let header_bytes = slice(
1241 bytes,
1242 0,
1243 BOOTSTRAP_SIDECAR_HEADER_LEN,
1244 "BootstrapSidecarHeader",
1245 )?;
1246 let header = BootstrapSidecarHeader::parse(header_bytes)?;
1247 if header.archive_uuid != volume_header.archive_uuid
1248 || header.session_id != volume_header.session_id
1249 {
1250 return Err(FormatError::InvalidArchive(
1251 "bootstrap sidecar identity does not match VolumeHeader",
1252 ));
1253 }
1254 verify_hmac(
1255 HmacDomain::BootstrapSidecar,
1256 &subkeys.mac_key,
1257 &volume_header.archive_uuid,
1258 &volume_header.session_id,
1259 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
1260 &header.sidecar_hmac,
1261 )?;
1262 header.validate_packed_layout(bytes.len() as u64)?;
1263 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
1264
1265 if !header.has_manifest_footer() || !header.has_index_root_records() {
1266 return Err(FormatError::ReaderUnsupported(
1267 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1268 ));
1269 }
1270 if header.has_dictionary_records() {
1271 if crypto_header.has_dictionary == 0 {
1272 return Err(FormatError::InvalidArchive(
1273 "bootstrap sidecar has dictionary records while has_dictionary is false",
1274 ));
1275 }
1276 } else if crypto_header.has_dictionary != 0 {
1277 return Err(FormatError::ReaderUnsupported(
1278 "dictionary bootstrap required",
1279 ));
1280 }
1281
1282 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
1283 let manifest_bytes = slice(
1284 bytes,
1285 manifest_offset,
1286 MANIFEST_FOOTER_LEN,
1287 "ManifestFooter",
1288 )?;
1289 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
1290 validate_sidecar_manifest_footer(
1291 volume_header,
1292 crypto_header,
1293 &manifest_footer,
1294 subkeys,
1295 manifest_bytes,
1296 )?;
1297 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
1298
1299 let index_root_records = parse_sidecar_block_records(
1300 bytes,
1301 header.index_root_records_offset,
1302 header.index_root_records_length,
1303 crypto_header.block_size as usize,
1304 ObjectExtent {
1305 first_block_index: manifest_footer.index_root_first_block,
1306 data_block_count: manifest_footer.index_root_data_block_count,
1307 parity_block_count: manifest_footer.index_root_parity_block_count,
1308 encrypted_size: manifest_footer.index_root_encrypted_size,
1309 },
1310 BlockKind::IndexRootData,
1311 BlockKind::IndexRootParity,
1312 "IndexRoot",
1313 )?;
1314
1315 Ok(TrustedBootstrapSidecar {
1316 manifest_footer,
1317 index_root_records,
1318 dictionary_records_section: header.has_dictionary_records().then_some((
1319 header.dictionary_records_offset,
1320 header.dictionary_records_length,
1321 )),
1322 })
1323}
1324
1325fn insert_sidecar_records(
1326 blocks: &mut BTreeMap<u64, BlockRecord>,
1327 records: Vec<BlockRecord>,
1328) -> Result<(), FormatError> {
1329 for record in records {
1330 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
1331 if existing != record {
1332 return Err(FormatError::InvalidArchive(
1333 "bootstrap sidecar conflicts with volume BlockRecord",
1334 ));
1335 }
1336 }
1337 }
1338 Ok(())
1339}
1340
1341fn validate_sidecar_manifest_footer(
1342 volume_header: &VolumeHeader,
1343 crypto_header: &CryptoHeaderFixed,
1344 footer: &ManifestFooter,
1345 subkeys: &Subkeys,
1346 raw: &[u8],
1347) -> Result<(), FormatError> {
1348 if footer.archive_uuid != volume_header.archive_uuid
1349 || footer.session_id != volume_header.session_id
1350 {
1351 return Err(FormatError::InvalidArchive(
1352 "sidecar ManifestFooter identity does not match VolumeHeader",
1353 ));
1354 }
1355 if footer.volume_index != 0 {
1356 return Err(FormatError::InvalidArchive(
1357 "sidecar ManifestFooter volume_index must be zero",
1358 ));
1359 }
1360 if footer.total_volumes != crypto_header.stripe_width {
1361 return Err(FormatError::InvalidArchive(
1362 "sidecar ManifestFooter total_volumes does not match stripe_width",
1363 ));
1364 }
1365 if footer.is_authoritative != 1 {
1366 return Err(FormatError::InvalidArchive(
1367 "sidecar ManifestFooter is not authoritative",
1368 ));
1369 }
1370 verify_hmac(
1371 HmacDomain::ManifestFooter,
1372 &subkeys.mac_key,
1373 &volume_header.archive_uuid,
1374 &volume_header.session_id,
1375 &raw[..MANIFEST_HMAC_COVERED_LEN],
1376 &footer.manifest_hmac,
1377 )
1378}
1379
1380fn validate_sidecar_size_cap(
1381 header: &BootstrapSidecarHeader,
1382 crypto_header: &CryptoHeaderFixed,
1383 file_size: u64,
1384) -> Result<(), FormatError> {
1385 let record_len = checked_u64_add(
1386 crypto_header.block_size as u64,
1387 BLOCK_RECORD_FRAMING_LEN as u64,
1388 "bootstrap sidecar cap overflow",
1389 )?;
1390 let max_index_records = crypto_header.index_root_fec_data_shards as u64
1391 + crypto_header.index_root_fec_parity_shards as u64;
1392 let max_record_section_bytes = checked_u64_mul(
1393 max_index_records,
1394 record_len,
1395 "bootstrap sidecar cap overflow",
1396 )?;
1397 if header.index_root_records_length % record_len != 0 {
1398 return Err(FormatError::InvalidArchive(
1399 "bootstrap sidecar IndexRoot records length is not aligned",
1400 ));
1401 }
1402 if header.index_root_records_length / record_len > max_index_records {
1403 return Err(FormatError::InvalidArchive(
1404 "bootstrap sidecar IndexRoot records exceed resource cap",
1405 ));
1406 }
1407 if header.dictionary_records_length % record_len != 0 {
1408 return Err(FormatError::InvalidArchive(
1409 "bootstrap sidecar dictionary records length is not aligned",
1410 ));
1411 }
1412 if header.dictionary_records_length / record_len > max_index_records {
1413 return Err(FormatError::InvalidArchive(
1414 "bootstrap sidecar dictionary records exceed resource cap",
1415 ));
1416 }
1417
1418 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
1419 if header.has_manifest_footer() {
1420 cap = cap
1421 .checked_add(MANIFEST_FOOTER_LEN as u64)
1422 .ok_or(FormatError::InvalidArchive(
1423 "bootstrap sidecar cap overflow",
1424 ))?;
1425 }
1426 if header.has_index_root_records() {
1427 cap = checked_u64_add(
1428 cap,
1429 max_record_section_bytes,
1430 "bootstrap sidecar cap overflow",
1431 )?;
1432 }
1433 if header.has_dictionary_records() {
1434 cap = checked_u64_add(
1435 cap,
1436 max_record_section_bytes,
1437 "bootstrap sidecar cap overflow",
1438 )?;
1439 }
1440 if file_size > cap {
1441 return Err(FormatError::InvalidArchive(
1442 "bootstrap sidecar exceeds resource cap",
1443 ));
1444 }
1445 Ok(())
1446}
1447
1448fn parse_sidecar_block_records(
1449 sidecar_bytes: &[u8],
1450 offset: u64,
1451 length: u64,
1452 block_size: usize,
1453 extent: ObjectExtent,
1454 data_kind: BlockKind,
1455 parity_kind: BlockKind,
1456 structure: &'static str,
1457) -> Result<Vec<BlockRecord>, FormatError> {
1458 let record_len = block_size
1459 .checked_add(BLOCK_RECORD_FRAMING_LEN)
1460 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
1461 if length % record_len as u64 != 0 {
1462 return Err(FormatError::InvalidArchive(
1463 "sidecar BlockRecord section is not aligned",
1464 ));
1465 }
1466 let expected_count = extent.data_block_count as usize + extent.parity_block_count as usize;
1467 let actual_count = usize::try_from(length / record_len as u64)
1468 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
1469 if actual_count != expected_count {
1470 return Err(FormatError::InvalidArchive(
1471 "sidecar BlockRecord section does not match declared extent",
1472 ));
1473 }
1474 let start = to_usize(offset, "BootstrapSidecarHeader")?;
1475 let raw = slice(
1476 sidecar_bytes,
1477 start,
1478 to_usize(length, "BootstrapSidecarHeader")?,
1479 "BootstrapSidecarHeader",
1480 )?;
1481 let mut records = Vec::with_capacity(expected_count);
1482
1483 for idx in 0..expected_count {
1484 let record = BlockRecord::parse(
1485 slice(raw, idx * record_len, record_len, "BlockRecord")?,
1486 block_size,
1487 )?;
1488 let expected_block_index =
1489 checked_u64_add(extent.first_block_index, idx as u64, structure)?;
1490 if record.block_index != expected_block_index {
1491 return Err(FormatError::InvalidArchive(
1492 "sidecar BlockRecord section has missing or duplicate blocks",
1493 ));
1494 }
1495 let expected_kind = if idx < extent.data_block_count as usize {
1496 data_kind
1497 } else {
1498 parity_kind
1499 };
1500 if record.kind != expected_kind {
1501 return Err(FormatError::InvalidArchive(
1502 "sidecar BlockRecord section has wrong kind",
1503 ));
1504 }
1505 let should_be_last = idx + 1 == extent.data_block_count as usize;
1506 if idx < extent.data_block_count as usize && record.is_last_data() != should_be_last {
1507 return Err(FormatError::InvalidArchive(
1508 "sidecar BlockRecord section has wrong last-data flag",
1509 ));
1510 }
1511 records.push(record);
1512 }
1513
1514 Ok(records)
1515}
1516
1517fn validate_trailer_identity(
1518 volume_header: &VolumeHeader,
1519 trailer: &VolumeTrailer,
1520) -> Result<(), FormatError> {
1521 if trailer.archive_uuid != volume_header.archive_uuid
1522 || trailer.session_id != volume_header.session_id
1523 || trailer.volume_index != volume_header.volume_index
1524 {
1525 return Err(FormatError::InvalidArchive(
1526 "VolumeTrailer identity does not match VolumeHeader",
1527 ));
1528 }
1529 Ok(())
1530}
1531
1532fn validate_manifest_footer(
1533 volume_header: &VolumeHeader,
1534 footer: &ManifestFooter,
1535 subkeys: &Subkeys,
1536 raw: &[u8],
1537) -> Result<(), FormatError> {
1538 if footer.archive_uuid != volume_header.archive_uuid
1539 || footer.session_id != volume_header.session_id
1540 || footer.volume_index != volume_header.volume_index
1541 {
1542 return Err(FormatError::InvalidArchive(
1543 "ManifestFooter identity does not match VolumeHeader",
1544 ));
1545 }
1546 if footer.total_volumes != volume_header.stripe_width {
1547 return Err(FormatError::InvalidArchive(
1548 "ManifestFooter total_volumes does not match stripe_width",
1549 ));
1550 }
1551 if footer.is_authoritative != 1 {
1552 return Err(FormatError::InvalidArchive(
1553 "ManifestFooter is not authoritative",
1554 ));
1555 }
1556 verify_hmac(
1557 HmacDomain::ManifestFooter,
1558 &subkeys.mac_key,
1559 &volume_header.archive_uuid,
1560 &volume_header.session_id,
1561 &raw[..MANIFEST_HMAC_COVERED_LEN],
1562 &footer.manifest_hmac,
1563 )
1564}
1565
1566#[derive(Debug)]
1567struct ParsedBlockRegion {
1568 blocks: BTreeMap<u64, BlockRecord>,
1569 erased_block_indices: BTreeSet<u64>,
1570}
1571
1572fn parse_block_region(
1573 bytes: &[u8],
1574 start: usize,
1575 end: usize,
1576 block_size: usize,
1577 volume_header: &VolumeHeader,
1578 trailer: &VolumeTrailer,
1579) -> Result<ParsedBlockRegion, FormatError> {
1580 if end < start {
1581 return Err(FormatError::InvalidArchive(
1582 "ManifestFooter starts before BlockRecord region",
1583 ));
1584 }
1585 let record_len = block_size
1586 .checked_add(BLOCK_RECORD_FRAMING_LEN)
1587 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
1588 let region_len = end - start;
1589 if region_len % record_len != 0 {
1590 return Err(FormatError::InvalidArchive(
1591 "BlockRecord region length is not aligned",
1592 ));
1593 }
1594 let observed_count = region_len / record_len;
1595 if observed_count as u64 != trailer.block_count {
1596 return Err(FormatError::InvalidArchive(
1597 "VolumeTrailer block_count does not match BlockRecord region",
1598 ));
1599 }
1600
1601 let mut blocks = BTreeMap::new();
1602 let mut erased_block_indices = BTreeSet::new();
1603 for idx in 0..observed_count {
1604 let offset = start + idx * record_len;
1605 let expected_block_index = checked_u64_add(
1606 volume_header.volume_index as u64,
1607 checked_u64_mul(
1608 idx as u64,
1609 volume_header.stripe_width as u64,
1610 "BlockRecord index overflow",
1611 )?,
1612 "BlockRecord index overflow",
1613 )?;
1614 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
1615 match BlockRecord::parse(raw, block_size) {
1616 Ok(record) => {
1617 if record.block_index != expected_block_index {
1618 return Err(FormatError::InvalidArchive(
1619 "BlockRecord index does not match volume position",
1620 ));
1621 }
1622 if blocks.insert(record.block_index, record).is_some() {
1623 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1624 }
1625 }
1626 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
1627 if !erased_block_indices.insert(expected_block_index) {
1628 return Err(FormatError::InvalidArchive(
1629 "duplicate erased BlockRecord index",
1630 ));
1631 }
1632 }
1633 Err(err) => return Err(err),
1634 }
1635 }
1636
1637 Ok(ParsedBlockRegion {
1638 blocks,
1639 erased_block_indices,
1640 })
1641}
1642
1643fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
1644 match error {
1645 FormatError::BadCrc { structure }
1646 | FormatError::BadMagic { structure }
1647 | FormatError::NonZeroReserved { structure } => *structure == "BlockRecord",
1648 _ => false,
1649 }
1650}
1651
1652fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
1653 lhs.checked_mul(rhs)
1654 .ok_or(FormatError::InvalidArchive(reason))
1655}
1656
1657fn parse_stream_block_prefix(
1658 bytes: &[u8],
1659 start: usize,
1660 block_size: usize,
1661 volume_header: &VolumeHeader,
1662) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
1663 let record_len = block_size
1664 .checked_add(BLOCK_RECORD_FRAMING_LEN)
1665 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
1666 let mut blocks = BTreeMap::new();
1667 let mut offset = start;
1668 let mut observed_block_count = 0u64;
1669
1670 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
1671 let expected_block_index =
1672 expected_stream_block_index(volume_header, observed_block_count)?;
1673 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
1674 match BlockRecord::parse(raw, block_size) {
1675 Ok(record) => {
1676 if record.block_index != expected_block_index {
1677 return Err(FormatError::InvalidArchive(
1678 "BlockRecord index does not match stream position",
1679 ));
1680 }
1681 if blocks.insert(record.block_index, record).is_some() {
1682 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1683 }
1684 }
1685 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
1686 Err(err) => return Err(err),
1687 }
1688 offset = checked_add(offset, record_len, "BlockRecord")?;
1689 observed_block_count = observed_block_count
1690 .checked_add(1)
1691 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
1692 }
1693
1694 Ok((blocks, offset, observed_block_count))
1695}
1696
1697fn expected_stream_block_index(
1698 volume_header: &VolumeHeader,
1699 observed_block_count: u64,
1700) -> Result<u64, FormatError> {
1701 checked_u64_add(
1702 volume_header.volume_index as u64,
1703 checked_u64_mul(
1704 observed_block_count,
1705 volume_header.stripe_width as u64,
1706 "BlockRecord index overflow",
1707 )?,
1708 "BlockRecord index overflow",
1709 )
1710}
1711
1712fn parse_sequential_block_or_erasure(
1713 bytes: &[u8],
1714 offset: usize,
1715 record_len: usize,
1716 block_size: usize,
1717 volume_header: &VolumeHeader,
1718 observed_block_count: u64,
1719) -> Result<Option<BlockRecord>, FormatError> {
1720 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
1721 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
1722 match BlockRecord::parse(raw, block_size) {
1723 Ok(record) => {
1724 if record.block_index != expected_block_index {
1725 return Err(FormatError::InvalidArchive(
1726 "BlockRecord index does not match stream position",
1727 ));
1728 }
1729 Ok(Some(record))
1730 }
1731 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
1732 Err(err) => Err(err),
1733 }
1734}
1735
1736fn parse_terminal_material(
1737 bytes: &[u8],
1738 manifest_offset: usize,
1739 observed_block_count: u64,
1740 subkeys: &Subkeys,
1741 volume_header: &VolumeHeader,
1742 block_size: u32,
1743) -> Result<(ManifestFooter, VolumeTrailer), FormatError> {
1744 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
1745 let trailer_end = checked_add(manifest_end, VOLUME_TRAILER_LEN, "VolumeTrailer")?;
1746 if trailer_end != bytes.len() {
1747 return Err(FormatError::InvalidArchive(
1748 "terminal ManifestFooter/VolumeTrailer is not packed at stream end",
1749 ));
1750 }
1751
1752 let manifest_bytes = slice(
1753 bytes,
1754 manifest_offset,
1755 MANIFEST_FOOTER_LEN,
1756 "ManifestFooter",
1757 )?;
1758 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
1759 validate_manifest_footer(volume_header, &manifest_footer, subkeys, manifest_bytes)?;
1760 manifest_footer.validate_index_root_extent(block_size)?;
1761
1762 let trailer = parse_authenticated_trailer(bytes, manifest_end, subkeys, volume_header)?;
1763 validate_trailer_identity(volume_header, &trailer)?;
1764 if trailer.manifest_footer_offset != manifest_offset as u64 {
1765 return Err(FormatError::InvalidArchive(
1766 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
1767 ));
1768 }
1769 if trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32 {
1770 return Err(FormatError::InvalidManifestFooterLength(
1771 trailer.manifest_footer_length,
1772 ));
1773 }
1774 if trailer.bytes_written != manifest_end as u64 {
1775 return Err(FormatError::InvalidArchive(
1776 "VolumeTrailer bytes_written does not match observed trailer offset",
1777 ));
1778 }
1779 if trailer.block_count != observed_block_count {
1780 return Err(FormatError::InvalidArchive(
1781 "VolumeTrailer block_count does not match observed stream",
1782 ));
1783 }
1784
1785 Ok((manifest_footer, trailer))
1786}
1787
1788#[derive(Debug, Default)]
1789struct PendingSequentialEnvelope {
1790 data_shards: Vec<Option<Vec<u8>>>,
1791 parity_shards: Vec<Option<Vec<u8>>>,
1792 saw_last_data: bool,
1793 awaiting_tentative_parity: bool,
1794}
1795
1796impl PendingSequentialEnvelope {
1797 fn is_empty(&self) -> bool {
1798 self.data_shards.is_empty() && self.parity_shards.is_empty()
1799 }
1800}
1801
1802fn handle_sequential_payload_erasure(
1803 pending: &mut PendingSequentialEnvelope,
1804 crypto_header: &CryptoHeaderFixed,
1805 metadata_seen: bool,
1806) -> Result<(), FormatError> {
1807 if metadata_seen || pending.saw_last_data {
1808 return Err(FormatError::BadCrc {
1809 structure: "BlockRecord",
1810 });
1811 }
1812 if !sequential_payload_parity_is_guaranteed(crypto_header) {
1813 return Err(FormatError::BadCrc {
1814 structure: "BlockRecord",
1815 });
1816 }
1817 pending.data_shards.push(None);
1818 pending.awaiting_tentative_parity = true;
1819 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
1820 return Err(FormatError::InvalidArchive(
1821 "sequential payload envelope exceeds data-shard cap",
1822 ));
1823 }
1824 Ok(())
1825}
1826
1827fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
1828 crypto_header.fec_parity_shards > 0
1829 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
1830}
1831
1832fn sequential_extract_tar_stream_with_options(
1833 bytes: &[u8],
1834 master_key: &MasterKey,
1835 options: ReaderOptions,
1836) -> Result<Vec<u8>, FormatError> {
1837 if bytes.len() < VOLUME_HEADER_LEN {
1838 return Err(FormatError::InvalidLength {
1839 structure: "archive",
1840 expected: VOLUME_HEADER_LEN,
1841 actual: bytes.len(),
1842 });
1843 }
1844
1845 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1846 let crypto_start = volume_header.crypto_header_offset as usize;
1847 let crypto_len = volume_header.crypto_header_length as usize;
1848 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1849 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1850 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1851 let subkeys = Subkeys::derive(
1852 master_key,
1853 &volume_header.archive_uuid,
1854 &volume_header.session_id,
1855 )?;
1856 verify_hmac(
1857 HmacDomain::CryptoHeader,
1858 &subkeys.mac_key,
1859 &volume_header.archive_uuid,
1860 &volume_header.session_id,
1861 parsed_crypto.hmac_covered_bytes,
1862 &parsed_crypto.header_hmac,
1863 )?;
1864 parsed_crypto.validate_extension_semantics()?;
1865 validate_sequential_supported_volume(&volume_header, &parsed_crypto.fixed)?;
1866
1867 let block_size = parsed_crypto.fixed.block_size as usize;
1868 let record_len = block_size
1869 .checked_add(BLOCK_RECORD_FRAMING_LEN)
1870 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
1871 let mut offset = crypto_end;
1872 let mut observed_block_count = 0u64;
1873 let mut metadata_seen = false;
1874 let mut pending = PendingSequentialEnvelope::default();
1875 let mut next_envelope_index = 0u64;
1876 let mut tar_stream = Vec::new();
1877
1878 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
1879 let record = parse_sequential_block_or_erasure(
1880 bytes,
1881 offset,
1882 record_len,
1883 block_size,
1884 &volume_header,
1885 observed_block_count,
1886 )?;
1887 observed_block_count = observed_block_count
1888 .checked_add(1)
1889 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
1890 let Some(record) = record else {
1891 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
1892 offset = checked_add(offset, record_len, "BlockRecord")?;
1893 continue;
1894 };
1895
1896 match record.kind {
1897 BlockKind::PayloadData => {
1898 if metadata_seen {
1899 return Err(FormatError::InvalidArchive(
1900 "payload BlockRecord appears after metadata",
1901 ));
1902 }
1903 if pending.awaiting_tentative_parity {
1904 return Err(FormatError::InvalidArchive(
1905 "sequential payload envelope boundary is ambiguous after CRC erasure",
1906 ));
1907 }
1908 if pending.saw_last_data {
1909 finalize_sequential_envelope(
1910 &mut pending,
1911 &parsed_crypto.fixed,
1912 &subkeys,
1913 &volume_header,
1914 &mut next_envelope_index,
1915 &mut tar_stream,
1916 )?;
1917 }
1918 let is_last_data = record.is_last_data();
1919 pending.data_shards.push(Some(record.payload));
1920 if is_last_data {
1921 pending.saw_last_data = true;
1922 }
1923 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
1924 return Err(FormatError::InvalidArchive(
1925 "sequential payload envelope exceeds data-shard cap",
1926 ));
1927 }
1928 }
1929 BlockKind::PayloadParity => {
1930 if metadata_seen {
1931 return Err(FormatError::InvalidArchive(
1932 "payload parity BlockRecord appears after metadata",
1933 ));
1934 }
1935 if pending.awaiting_tentative_parity {
1936 pending.awaiting_tentative_parity = false;
1937 pending.saw_last_data = true;
1938 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
1939 return Err(FormatError::InvalidArchive(
1940 "payload parity appears before envelope data is complete",
1941 ));
1942 }
1943 pending.parity_shards.push(Some(record.payload));
1944 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
1945 return Err(FormatError::InvalidArchive(
1946 "sequential payload envelope exceeds parity-shard cap",
1947 ));
1948 }
1949 }
1950 _ => {
1951 if !pending.is_empty() {
1952 finalize_sequential_envelope(
1953 &mut pending,
1954 &parsed_crypto.fixed,
1955 &subkeys,
1956 &volume_header,
1957 &mut next_envelope_index,
1958 &mut tar_stream,
1959 )?;
1960 }
1961 metadata_seen = true;
1962 }
1963 }
1964
1965 offset = checked_add(offset, record_len, "BlockRecord")?;
1966 }
1967
1968 if !pending.is_empty() {
1969 finalize_sequential_envelope(
1970 &mut pending,
1971 &parsed_crypto.fixed,
1972 &subkeys,
1973 &volume_header,
1974 &mut next_envelope_index,
1975 &mut tar_stream,
1976 )?;
1977 }
1978
1979 parse_terminal_material(
1980 bytes,
1981 offset,
1982 observed_block_count,
1983 &subkeys,
1984 &volume_header,
1985 parsed_crypto.fixed.block_size,
1986 )?;
1987 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
1988 validate_tar_stream_total_extraction_size(
1989 &tar_stream,
1990 parsed_crypto.fixed.max_path_length,
1991 total_extraction_size_cap(options, observed_archive_bytes),
1992 )?;
1993 Ok(tar_stream)
1994}
1995
1996fn validate_sequential_supported_volume(
1997 volume_header: &VolumeHeader,
1998 crypto_header: &CryptoHeaderFixed,
1999) -> Result<(), FormatError> {
2000 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
2001 return Err(FormatError::ReaderUnsupported(
2002 "M9 sequential reader supports only single-volume archives",
2003 ));
2004 }
2005 if crypto_header.stripe_width != volume_header.stripe_width {
2006 return Err(FormatError::InvalidArchive(
2007 "VolumeHeader and CryptoHeader stripe_width differ",
2008 ));
2009 }
2010 if crypto_header.has_dictionary != 0 {
2011 return Err(FormatError::ReaderUnsupported(
2012 "dictionary bootstrap required for non-seekable sequential extraction",
2013 ));
2014 }
2015 Ok(())
2016}
2017
2018fn finalize_sequential_envelope(
2019 pending: &mut PendingSequentialEnvelope,
2020 crypto_header: &CryptoHeaderFixed,
2021 subkeys: &Subkeys,
2022 volume_header: &VolumeHeader,
2023 next_envelope_index: &mut u64,
2024 tar_stream: &mut Vec<u8>,
2025) -> Result<(), FormatError> {
2026 if !pending.saw_last_data {
2027 return Err(FormatError::InvalidArchive(
2028 "sequential payload envelope is missing last-data flag",
2029 ));
2030 }
2031 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
2032 return Err(FormatError::InvalidArchive(
2033 "sequential payload envelope exceeds data-shard cap",
2034 ));
2035 }
2036 if pending.parity_shards.len() > crypto_header.fec_parity_shards as usize {
2037 return Err(FormatError::InvalidArchive(
2038 "sequential payload envelope exceeds parity-shard cap",
2039 ));
2040 }
2041 let required_parity = required_object_parity(pending.data_shards.len() as u64, crypto_header)?;
2042 if pending.parity_shards.len() < required_parity as usize {
2043 return Err(FormatError::InvalidArchive(
2044 "sequential payload envelope has insufficient parity for recovery settings",
2045 ));
2046 }
2047
2048 let repaired = repair_data_gf16(
2049 &pending.data_shards,
2050 &pending.parity_shards,
2051 crypto_header.block_size as usize,
2052 )?;
2053 let mut encrypted = Vec::with_capacity(repaired.len() * crypto_header.block_size as usize);
2054 for shard in repaired {
2055 encrypted.extend_from_slice(&shard);
2056 }
2057 let plaintext = decrypt_padded_aead_object(
2058 crypto_header.aead_algo,
2059 &subkeys.enc_key,
2060 &subkeys.nonce_seed,
2061 b"envelope",
2062 &volume_header.archive_uuid,
2063 &volume_header.session_id,
2064 *next_envelope_index,
2065 &encrypted,
2066 )?;
2067 decode_concatenated_zstd_frames(&plaintext, None, tar_stream)?;
2068 *next_envelope_index = next_envelope_index
2069 .checked_add(1)
2070 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
2071 *pending = PendingSequentialEnvelope::default();
2072 Ok(())
2073}
2074
2075fn decode_concatenated_zstd_frames(
2076 plaintext: &[u8],
2077 dictionary: Option<&[u8]>,
2078 output: &mut Vec<u8>,
2079) -> Result<(), FormatError> {
2080 let mut cursor = 0usize;
2081 while cursor < plaintext.len() {
2082 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
2083 .map_err(|_| FormatError::InvalidZstdFrame)?;
2084 if frame_len == 0 {
2085 return Err(FormatError::InvalidZstdFrame);
2086 }
2087 let end = checked_add(cursor, frame_len, "zstd frame")?;
2088 validate_exact_zstd_frame(&plaintext[cursor..end])?;
2089 let decoded = if let Some(dictionary) = dictionary {
2090 let mut decoder =
2091 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
2092 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
2093 let mut decoded = Vec::new();
2094 decoder
2095 .read_to_end(&mut decoded)
2096 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
2097 decoded
2098 } else {
2099 zstd::stream::decode_all(&plaintext[cursor..end])
2100 .map_err(|_| FormatError::ZstdDecompressionFailure)?
2101 };
2102 output.extend_from_slice(&decoded);
2103 cursor = end;
2104 }
2105 Ok(())
2106}
2107
2108fn load_archive_dictionary(
2109 blocks: &BTreeMap<u64, BlockRecord>,
2110 subkeys: &Subkeys,
2111 volume_header: &VolumeHeader,
2112 crypto_header: &CryptoHeaderFixed,
2113 index_root: &IndexRoot,
2114) -> Result<Option<Vec<u8>>, FormatError> {
2115 if crypto_header.has_dictionary == 0 {
2116 return Ok(None);
2117 }
2118 let plaintext = load_metadata_object_from_parts(
2119 blocks,
2120 subkeys,
2121 volume_header,
2122 crypto_header,
2123 dictionary_extent_from_index_root(index_root)?,
2124 BlockKind::DictionaryData,
2125 BlockKind::DictionaryParity,
2126 &subkeys.dictionary_key,
2127 &subkeys.index_nonce_seed,
2128 b"dict",
2129 0,
2130 crypto_header.index_root_fec_data_shards,
2131 crypto_header.index_root_fec_parity_shards,
2132 index_root.header.dictionary_decompressed_size,
2133 )?;
2134 Ok(Some(plaintext))
2135}
2136
2137fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
2138 if index_root.header.dictionary_data_block_count == 0
2139 || index_root.header.dictionary_encrypted_size == 0
2140 || index_root.header.dictionary_decompressed_size == 0
2141 {
2142 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
2143 }
2144 Ok(ObjectExtent {
2145 first_block_index: index_root.header.dictionary_first_block,
2146 data_block_count: index_root.header.dictionary_data_block_count,
2147 parity_block_count: index_root.header.dictionary_parity_block_count,
2148 encrypted_size: index_root.header.dictionary_encrypted_size,
2149 })
2150}
2151
2152fn load_metadata_object_from_parts(
2153 blocks: &BTreeMap<u64, BlockRecord>,
2154 subkeys: &Subkeys,
2155 volume_header: &VolumeHeader,
2156 crypto_header: &CryptoHeaderFixed,
2157 extent: ObjectExtent,
2158 data_kind: BlockKind,
2159 parity_kind: BlockKind,
2160 key: &[u8; 32],
2161 nonce_seed: &[u8; 32],
2162 domain: &[u8],
2163 counter: u64,
2164 class_data_shard_max: u16,
2165 class_parity_shard_max: u16,
2166 decompressed_size: u32,
2167) -> Result<Vec<u8>, FormatError> {
2168 let compressed = load_decrypted_object_from_parts(
2169 blocks,
2170 volume_header,
2171 crypto_header,
2172 extent,
2173 data_kind,
2174 parity_kind,
2175 key,
2176 nonce_seed,
2177 domain,
2178 counter,
2179 class_data_shard_max,
2180 class_parity_shard_max,
2181 )?;
2182 let _ = subkeys;
2183 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
2184}
2185
2186fn load_decrypted_object_from_parts(
2187 blocks: &BTreeMap<u64, BlockRecord>,
2188 volume_header: &VolumeHeader,
2189 crypto_header: &CryptoHeaderFixed,
2190 extent: ObjectExtent,
2191 data_kind: BlockKind,
2192 parity_kind: BlockKind,
2193 key: &[u8; 32],
2194 nonce_seed: &[u8; 32],
2195 domain: &[u8],
2196 counter: u64,
2197 class_data_shard_max: u16,
2198 class_parity_shard_max: u16,
2199) -> Result<Vec<u8>, FormatError> {
2200 validate_object_extent(
2201 extent,
2202 crypto_header,
2203 class_data_shard_max,
2204 class_parity_shard_max,
2205 )?;
2206 let block_size = crypto_header.block_size as usize;
2207 let data_count = extent.data_block_count as usize;
2208 let parity_count = extent.parity_block_count as usize;
2209 let mut data_shards = Vec::with_capacity(data_count);
2210 let mut parity_shards = Vec::with_capacity(parity_count);
2211
2212 for offset in 0..data_count {
2213 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2214 if let Some(record) = blocks.get(&block_index) {
2215 if record.kind != data_kind {
2216 return Err(FormatError::InvalidArchive(
2217 "object data block has unexpected kind",
2218 ));
2219 }
2220 let should_be_last = offset + 1 == data_count;
2221 if record.is_last_data() != should_be_last {
2222 return Err(FormatError::InvalidArchive(
2223 "object last-data flag is not on the final data block",
2224 ));
2225 }
2226 data_shards.push(Some(record.payload.clone()));
2227 } else {
2228 data_shards.push(None);
2229 }
2230 }
2231
2232 for offset in 0..parity_count {
2233 let block_index = checked_u64_add(
2234 extent.first_block_index,
2235 data_count as u64 + offset as u64,
2236 "object",
2237 )?;
2238 if let Some(record) = blocks.get(&block_index) {
2239 if record.kind != parity_kind {
2240 return Err(FormatError::InvalidArchive(
2241 "object parity block has unexpected kind",
2242 ));
2243 }
2244 if record.is_last_data() {
2245 return Err(FormatError::InvalidArchive(
2246 "object parity block has last-data flag",
2247 ));
2248 }
2249 parity_shards.push(Some(record.payload.clone()));
2250 } else {
2251 parity_shards.push(None);
2252 }
2253 }
2254
2255 let repaired = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2256 let mut encrypted = Vec::with_capacity(extent.encrypted_size as usize);
2257 for shard in repaired {
2258 encrypted.extend_from_slice(&shard);
2259 }
2260 if encrypted.len() != extent.encrypted_size as usize {
2261 return Err(FormatError::InvalidArchive(
2262 "object encrypted size does not match repaired shards",
2263 ));
2264 }
2265
2266 decrypt_padded_aead_object(
2267 crypto_header.aead_algo,
2268 key,
2269 nonce_seed,
2270 domain,
2271 &volume_header.archive_uuid,
2272 &volume_header.session_id,
2273 counter,
2274 &encrypted,
2275 )
2276}
2277
2278fn validate_object_extent(
2279 extent: ObjectExtent,
2280 crypto_header: &CryptoHeaderFixed,
2281 class_data_shard_max: u16,
2282 class_parity_shard_max: u16,
2283) -> Result<(), FormatError> {
2284 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
2285 return Err(FormatError::InvalidArchive(
2286 "encrypted object has zero data blocks or size",
2287 ));
2288 }
2289 if extent.data_block_count > class_data_shard_max as u32 {
2290 return Err(FormatError::InvalidArchive(
2291 "encrypted object exceeds its class data-shard maximum",
2292 ));
2293 }
2294 if extent.parity_block_count > class_parity_shard_max as u32 {
2295 return Err(FormatError::InvalidArchive(
2296 "encrypted object exceeds its class parity-shard maximum",
2297 ));
2298 }
2299 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
2300 if extent.parity_block_count < required_parity {
2301 return Err(FormatError::InvalidArchive(
2302 "encrypted object has insufficient parity for recovery settings",
2303 ));
2304 }
2305 let total = checked_u64_add(
2306 extent.data_block_count as u64,
2307 extent.parity_block_count as u64,
2308 "encrypted object shard count overflow",
2309 )?;
2310 if total > 65_535 {
2311 return Err(FormatError::FecTooManyShards(total as usize));
2312 }
2313 let expected = checked_u64_mul(
2314 extent.data_block_count as u64,
2315 crypto_header.block_size as u64,
2316 "encrypted object size overflow",
2317 )?;
2318 if expected != extent.encrypted_size as u64 {
2319 return Err(FormatError::InvalidArchive(
2320 "encrypted object size is not data_block_count * block_size",
2321 ));
2322 }
2323 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
2324 return Err(FormatError::InvalidArchive(
2325 "encrypted object is too small for AEAD tag",
2326 ));
2327 }
2328 Ok(())
2329}
2330
2331fn required_object_parity(
2332 data_block_count: u64,
2333 crypto_header: &CryptoHeaderFixed,
2334) -> Result<u32, FormatError> {
2335 let min_parity =
2336 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
2337 1
2338 } else {
2339 0
2340 };
2341 let mut parity = 0u64;
2342 for _ in 0..100 {
2343 let total = data_block_count
2344 .checked_add(parity)
2345 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
2346 let by_volume = checked_u64_mul(
2347 crypto_header.volume_loss_tolerance as u64,
2348 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
2349 "volume-loss parity overflow",
2350 )?;
2351 let by_bitrot = ceil_div_u64(
2352 checked_u64_mul(
2353 total,
2354 crypto_header.bit_rot_buffer_pct as u64,
2355 "bit-rot parity overflow",
2356 )?,
2357 100,
2358 )?;
2359 let next = by_volume
2360 .checked_add(by_bitrot)
2361 .ok_or(FormatError::InvalidArchive("parity overflow"))?
2362 .max(min_parity);
2363 if next == parity {
2364 return u32::try_from(next)
2365 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
2366 }
2367 parity = next;
2368 }
2369 Err(FormatError::InvalidArchive(
2370 "parity calculation did not converge",
2371 ))
2372}
2373
2374fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
2375 if denominator == 0 {
2376 return Err(FormatError::InvalidArchive("division by zero"));
2377 }
2378 numerator
2379 .checked_add(denominator - 1)
2380 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
2381 .map(|value| value / denominator)
2382}
2383
2384fn frame_range_for_file<'b>(
2385 shard: &'b IndexShard,
2386 file: &FileEntry,
2387) -> Result<Vec<&'b FrameEntry>, FormatError> {
2388 let mut frames = Vec::with_capacity(file.frame_count as usize);
2389 for offset in 0..file.frame_count as u64 {
2390 let frame_index =
2391 file.first_frame_index
2392 .checked_add(offset)
2393 .ok_or(FormatError::InvalidArchive(
2394 "FileEntry frame range overflow",
2395 ))?;
2396 let frame = shard
2397 .frames
2398 .iter()
2399 .find(|entry| entry.frame_index == frame_index)
2400 .ok_or(FormatError::InvalidArchive(
2401 "FileEntry references missing FrameEntry",
2402 ))?;
2403 frames.push(frame);
2404 }
2405 Ok(frames)
2406}
2407
2408fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
2409 MetadataLimits {
2410 block_size: crypto_header.block_size,
2411 max_path_length: crypto_header.max_path_length,
2412 max_payload_data_shards: crypto_header.fec_data_shards,
2413 max_payload_parity_shards: crypto_header.fec_parity_shards,
2414 max_index_data_shards: crypto_header.index_fec_data_shards,
2415 max_index_parity_shards: crypto_header.index_fec_parity_shards,
2416 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
2417 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
2418 ..MetadataLimits::default()
2419 }
2420}
2421
2422fn verify_dense_keys<T>(
2423 entries: &BTreeMap<u64, T>,
2424 expected_count: u64,
2425 structure: &'static str,
2426) -> Result<(), FormatError> {
2427 if entries.len() as u64 != expected_count {
2428 return Err(FormatError::InvalidArchive(
2429 "decoded table count does not match IndexRoot",
2430 ));
2431 }
2432 for expected in 0..expected_count {
2433 if !entries.contains_key(&expected) {
2434 return Err(FormatError::InvalidMetadata {
2435 structure,
2436 reason: "global index coverage has a gap",
2437 });
2438 }
2439 }
2440 Ok(())
2441}
2442
2443fn validate_envelope_frame_coverage(
2444 frames: &BTreeMap<u64, FrameEntry>,
2445 envelopes: &BTreeMap<u64, EnvelopeEntry>,
2446) -> Result<(), FormatError> {
2447 let mut accounted_frames = BTreeSet::new();
2448 for envelope in envelopes.values() {
2449 let first = envelope.first_frame_index;
2450 let end =
2451 first
2452 .checked_add(envelope.frame_count as u64)
2453 .ok_or(FormatError::InvalidArchive(
2454 "EnvelopeEntry frame range overflow",
2455 ))?;
2456 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
2457 for frame_index in first..end {
2458 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
2459 "EnvelopeEntry references missing FrameEntry",
2460 ))?;
2461 if frame.envelope_index != envelope.envelope_index {
2462 return Err(FormatError::InvalidArchive(
2463 "FrameEntry envelope_index does not match containing EnvelopeEntry",
2464 ));
2465 }
2466 if !accounted_frames.insert(frame_index) {
2467 return Err(FormatError::InvalidArchive(
2468 "FrameEntry is covered by multiple EnvelopeEntries",
2469 ));
2470 }
2471 let start = frame.offset_in_envelope as usize;
2472 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
2473 if end > envelope.plaintext_size as usize {
2474 return Err(FormatError::InvalidArchive(
2475 "FrameEntry exceeds EnvelopeEntry plaintext_size",
2476 ));
2477 }
2478 ranges.push((start, end));
2479 }
2480 validate_exact_coverage_ranges(
2481 &mut ranges,
2482 envelope.plaintext_size as usize,
2483 "EnvelopeEntry frame coverage has a gap or overlap",
2484 )?;
2485 }
2486
2487 for frame_index in frames.keys() {
2488 if !accounted_frames.contains(frame_index) {
2489 return Err(FormatError::InvalidArchive(
2490 "FrameEntry is not covered by any EnvelopeEntry",
2491 ));
2492 }
2493 }
2494 Ok(())
2495}
2496
2497fn validate_file_extent_coverage_ranges(
2498 extents: &[(u64, u64)],
2499 tar_len: usize,
2500) -> Result<(), FormatError> {
2501 let mut ranges = Vec::with_capacity(extents.len());
2502 for (start, len) in extents {
2503 let start = to_usize(*start, "FileEntry")?;
2504 let len = to_usize(*len, "FileEntry")?;
2505 let end = checked_add(start, len, "FileEntry")?;
2506 if end > tar_len {
2507 return Err(FormatError::InvalidArchive(
2508 "FileEntry extent exceeds IndexRoot tar_total_size",
2509 ));
2510 }
2511 ranges.push((start, end));
2512 }
2513 validate_exact_coverage_ranges(
2514 &mut ranges,
2515 tar_len,
2516 "FileEntry extents do not cover tar stream exactly",
2517 )
2518}
2519
2520fn add_expected_directory_hint_rows(
2521 map: &mut DirectoryHintMap,
2522 shard_row_index: u32,
2523 path: &[u8],
2524 kind: TarEntryKind,
2525) {
2526 map.entry(Vec::new()).or_default().insert(shard_row_index);
2527 for (idx, byte) in path.iter().enumerate() {
2528 if *byte == b'/' {
2529 map.entry(path[..idx].to_vec())
2530 .or_default()
2531 .insert(shard_row_index);
2532 }
2533 }
2534 if kind == TarEntryKind::Directory {
2535 map.entry(path.to_vec())
2536 .or_default()
2537 .insert(shard_row_index);
2538 }
2539}
2540
2541fn validate_directory_hint_tables_against_expected(
2542 tables: &[DirectoryHintTable],
2543 expected: &DirectoryHintMap,
2544) -> Result<(), FormatError> {
2545 let mut actual = Vec::new();
2546 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
2547
2548 for table in tables {
2549 for entry_index in 0..table.entries.len() {
2550 let path = table
2551 .entry_path(entry_index)
2552 .ok_or(FormatError::InvalidArchive(
2553 "DirectoryHintEntry path is missing",
2554 ))?;
2555 let key = (hash_prefix(path), path.to_vec());
2556 if let Some(previous) = &previous_key {
2557 if previous >= &key {
2558 return Err(FormatError::InvalidArchive(
2559 "DirectoryHintEntry rows are not globally sorted",
2560 ));
2561 }
2562 }
2563 previous_key = Some(key);
2564
2565 let rows =
2566 table
2567 .shard_rows_for_entry(entry_index)
2568 .ok_or(FormatError::InvalidArchive(
2569 "DirectoryHintEntry shard rows are missing",
2570 ))?;
2571 actual.push((path.to_vec(), rows.to_vec()));
2572 }
2573 }
2574
2575 if actual != sorted_directory_hint_rows(expected) {
2576 return Err(FormatError::InvalidArchive(
2577 "directory hint map does not match decoded files",
2578 ));
2579 }
2580 Ok(())
2581}
2582
2583fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
2584 let mut rows = map
2585 .iter()
2586 .map(|(path, shard_rows)| {
2587 (
2588 path.clone(),
2589 shard_rows.iter().copied().collect::<Vec<u32>>(),
2590 )
2591 })
2592 .collect::<Vec<_>>();
2593 rows.sort_by(|(left_path, _), (right_path, _)| {
2594 hash_prefix(left_path)
2595 .cmp(&hash_prefix(right_path))
2596 .then_with(|| left_path.cmp(right_path))
2597 });
2598 rows
2599}
2600
2601fn validate_exact_coverage_ranges(
2602 ranges: &mut [(usize, usize)],
2603 expected_end: usize,
2604 reason: &'static str,
2605) -> Result<(), FormatError> {
2606 ranges.sort_unstable();
2607 let mut cursor = 0usize;
2608 for (start, end) in ranges.iter().copied() {
2609 if start != cursor || end < start {
2610 return Err(FormatError::InvalidArchive(reason));
2611 }
2612 cursor = end;
2613 }
2614 if cursor != expected_end {
2615 return Err(FormatError::InvalidArchive(reason));
2616 }
2617 Ok(())
2618}
2619
2620fn object_block_range(
2621 first_block_index: u64,
2622 data_block_count: u32,
2623 parity_block_count: u32,
2624 structure: &'static str,
2625) -> Result<(u64, u64), FormatError> {
2626 let total = data_block_count as u64 + parity_block_count as u64;
2627 if total == 0 {
2628 return Err(FormatError::InvalidArchive(structure));
2629 }
2630 let end = checked_u64_add(first_block_index, total, structure)?;
2631 Ok((first_block_index, end))
2632}
2633
2634fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
2635 ranges.sort_unstable();
2636 for pair in ranges.windows(2) {
2637 if pair[0].1 > pair[1].0 {
2638 return Err(FormatError::InvalidArchive(
2639 "encrypted object block ranges overlap",
2640 ));
2641 }
2642 }
2643 Ok(())
2644}
2645
2646fn observed_archive_size(sizes: impl IntoIterator<Item = u64>) -> Result<u64, FormatError> {
2647 sizes.into_iter().try_fold(0u64, |sum, size| {
2648 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
2649 "observed archive size overflow",
2650 ))
2651 })
2652}
2653
2654fn total_extraction_size_cap(options: ReaderOptions, observed_archive_bytes: u64) -> u64 {
2655 options
2656 .max_total_extraction_size
2657 .min(observed_archive_bytes.saturating_mul(10))
2658}
2659
2660fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
2661 std::str::from_utf8(bytes)
2662 .map(|path| path.to_owned())
2663 .map_err(|_| FormatError::UnsafeArchivePath)
2664}
2665
2666fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
2667 let digest = Sha256::digest(bytes);
2668 let mut out = [0u8; 32];
2669 out.copy_from_slice(&digest);
2670 out
2671}
2672
2673fn slice<'b>(
2674 bytes: &'b [u8],
2675 offset: usize,
2676 len: usize,
2677 structure: &'static str,
2678) -> Result<&'b [u8], FormatError> {
2679 let end = checked_add(offset, len, structure)?;
2680 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
2681 structure,
2682 expected: end,
2683 actual: bytes.len(),
2684 })
2685}
2686
2687fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
2688 lhs.checked_add(rhs)
2689 .ok_or(FormatError::InvalidArchive(structure))
2690}
2691
2692fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
2693 lhs.checked_add(rhs)
2694 .ok_or(FormatError::InvalidArchive(structure))
2695}
2696
2697fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
2698 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
2699}
2700
2701#[cfg(test)]
2702mod tests {
2703 use super::*;
2704 use crate::crypto::compute_hmac;
2705 use crate::format::{AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo};
2706 use crate::metadata::{DirectoryHintEntry, DirectoryHintTableHeader};
2707 use crate::writer::{write_archive, write_archive_with_dictionary, RegularFile, WriterOptions};
2708
2709 fn master_key() -> MasterKey {
2710 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
2711 }
2712
2713 fn dictionary() -> &'static [u8] {
2714 b"dir/dict.txt common words common words common words dictionary payload"
2715 }
2716
2717 fn single_stream_options() -> WriterOptions {
2718 WriterOptions {
2719 stripe_width: 1,
2720 volume_loss_tolerance: 0,
2721 ..WriterOptions::default()
2722 }
2723 }
2724
2725 #[test]
2726 fn opens_lists_verifies_and_extracts_one_file_archive() {
2727 let archive = write_archive(
2728 &[RegularFile::new("dir/hello.txt", b"hello m7")],
2729 &master_key(),
2730 single_stream_options(),
2731 )
2732 .unwrap();
2733 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2734
2735 assert_eq!(
2736 opened.list_files().unwrap(),
2737 vec![ArchiveEntry {
2738 path: "dir/hello.txt".to_string(),
2739 file_data_size: 8,
2740 kind: TarEntryKind::Regular,
2741 mode: 0o644,
2742 mtime: 0,
2743 diagnostics: Vec::new(),
2744 }]
2745 );
2746 opened.verify().unwrap();
2747 assert_eq!(
2748 opened.extract_file("dir/hello.txt").unwrap(),
2749 Some(b"hello m7".to_vec())
2750 );
2751 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
2752 }
2753
2754 #[test]
2755 fn safe_extract_writes_regular_file_under_root() {
2756 let archive = write_archive(
2757 &[RegularFile::new("dir/hello.txt", b"safe m8")],
2758 &master_key(),
2759 single_stream_options(),
2760 )
2761 .unwrap();
2762 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2763 let tmp = tempfile::tempdir().unwrap();
2764
2765 opened
2766 .extract_file_to(
2767 "dir/hello.txt",
2768 tmp.path(),
2769 SafeExtractionOptions::default(),
2770 )
2771 .unwrap()
2772 .unwrap();
2773
2774 assert_eq!(
2775 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
2776 b"safe m8"
2777 );
2778 }
2779
2780 #[test]
2781 fn safe_extract_rejects_overwriting_existing_file_by_default() {
2782 let archive = write_archive(
2783 &[RegularFile::new("hello.txt", b"new")],
2784 &master_key(),
2785 single_stream_options(),
2786 )
2787 .unwrap();
2788 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2789 let tmp = tempfile::tempdir().unwrap();
2790 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
2791
2792 assert_eq!(
2793 opened
2794 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
2795 .unwrap_err(),
2796 FormatError::UnsafeOverwrite
2797 );
2798 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
2799 }
2800
2801 #[test]
2802 fn opens_and_verifies_empty_archive() {
2803 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
2804 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2805
2806 assert!(opened.list_files().unwrap().is_empty());
2807 opened.verify().unwrap();
2808 }
2809
2810 #[test]
2811 fn default_reader_options_allow_v36_trailing_garbage_scan() {
2812 let archive = write_archive(
2813 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
2814 &master_key(),
2815 single_stream_options(),
2816 )
2817 .unwrap();
2818 let mut with_trailing_garbage = archive.bytes.clone();
2819 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
2820
2821 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
2822 assert_eq!(
2823 opened.extract_file("garbage-tolerant.txt").unwrap(),
2824 Some(b"still intact".to_vec())
2825 );
2826 }
2827
2828 #[test]
2829 fn rejects_wrong_key_before_metadata_release() {
2830 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
2831 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
2832
2833 assert_eq!(
2834 open_archive(&archive.bytes, &wrong).unwrap_err(),
2835 FormatError::HmacMismatch {
2836 structure: "CryptoHeader"
2837 }
2838 );
2839 }
2840
2841 #[test]
2842 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
2843 let mut archive = write_archive(
2844 &[RegularFile::new("file.txt", b"authenticated")],
2845 &master_key(),
2846 single_stream_options(),
2847 )
2848 .unwrap()
2849 .bytes;
2850 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
2851 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
2852 let crypto = CryptoHeader::parse(
2853 &archive[VOLUME_HEADER_LEN..crypto_end],
2854 volume.crypto_header_length,
2855 )
2856 .unwrap();
2857 let block_size = crypto.fixed.block_size as usize;
2858 archive[crypto_end + 16] ^= 1;
2859 let crc_offset = crypto_end + 16 + block_size;
2860 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
2861 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
2862
2863 let opened = open_archive(&archive, &master_key()).unwrap();
2864 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
2865 }
2866
2867 #[test]
2868 fn list_and_extract_use_final_view_for_duplicate_paths() {
2869 let archive = write_archive(
2870 &[
2871 RegularFile::new("same.txt", b"old"),
2872 RegularFile::new("same.txt", b"newer"),
2873 ],
2874 &master_key(),
2875 single_stream_options(),
2876 )
2877 .unwrap();
2878 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2879
2880 assert_eq!(
2881 opened.list_files().unwrap(),
2882 vec![ArchiveEntry {
2883 path: "same.txt".to_string(),
2884 file_data_size: 5,
2885 kind: TarEntryKind::Regular,
2886 mode: 0o644,
2887 mtime: 0,
2888 diagnostics: Vec::new(),
2889 }]
2890 );
2891 assert_eq!(
2892 opened.extract_file("same.txt").unwrap(),
2893 Some(b"newer".to_vec())
2894 );
2895 opened.verify().unwrap();
2896 }
2897
2898 #[test]
2899 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
2900 let archive = write_archive(
2901 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
2902 &master_key(),
2903 single_stream_options(),
2904 )
2905 .unwrap();
2906 let opened = open_archive_with_bootstrap_sidecar(
2907 &archive.bytes,
2908 &archive.bootstrap_sidecar,
2909 &master_key(),
2910 )
2911 .unwrap();
2912
2913 assert_eq!(
2914 opened.list_files().unwrap(),
2915 vec![ArchiveEntry {
2916 path: "dir/sidecar.txt".to_string(),
2917 file_data_size: 13,
2918 kind: TarEntryKind::Regular,
2919 mode: 0o644,
2920 mtime: 0,
2921 diagnostics: Vec::new(),
2922 }]
2923 );
2924 assert_eq!(
2925 opened.extract_file("dir/sidecar.txt").unwrap(),
2926 Some(b"hello sidecar".to_vec())
2927 );
2928 opened.verify().unwrap();
2929 }
2930
2931 #[test]
2932 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
2933 let archive = write_archive_with_dictionary(
2934 &[RegularFile::new(
2935 "dir/dict.txt",
2936 b"common words common words dictionary payload",
2937 )],
2938 &master_key(),
2939 single_stream_options(),
2940 dictionary(),
2941 )
2942 .unwrap();
2943 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2944
2945 assert_eq!(opened.crypto_header.has_dictionary, 1);
2946 assert!(opened.index_root.header.dictionary_data_block_count > 0);
2947 assert_eq!(
2948 opened.list_files().unwrap(),
2949 vec![ArchiveEntry {
2950 path: "dir/dict.txt".to_string(),
2951 file_data_size: 44,
2952 kind: TarEntryKind::Regular,
2953 mode: 0o644,
2954 mtime: 0,
2955 diagnostics: Vec::new(),
2956 }]
2957 );
2958 assert_eq!(
2959 opened.extract_file("dir/dict.txt").unwrap(),
2960 Some(b"common words common words dictionary payload".to_vec())
2961 );
2962 opened.verify().unwrap();
2963 }
2964
2965 #[test]
2966 fn dictionary_object_tamper_fails_before_payload_decompression() {
2967 let archive = write_archive_with_dictionary(
2968 &[RegularFile::new(
2969 "dir/dict.txt",
2970 b"common words common words dictionary payload",
2971 )],
2972 &master_key(),
2973 single_stream_options(),
2974 dictionary(),
2975 )
2976 .unwrap();
2977 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
2978 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
2979 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
2980 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
2981 let dictionary_offset =
2982 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
2983
2984 let mut tampered = archive.bytes.clone();
2985 tampered[dictionary_offset + 16] ^= 0x01;
2986 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
2987 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
2988 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
2989
2990 assert_eq!(
2991 open_archive(&tampered, &master_key()).unwrap_err(),
2992 FormatError::AeadFailure
2993 );
2994 }
2995
2996 #[test]
2997 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
2998 let archive = write_archive_with_dictionary(
2999 &[RegularFile::new(
3000 "dict-sidecar.txt",
3001 b"common words common words sidecar payload",
3002 )],
3003 &master_key(),
3004 single_stream_options(),
3005 dictionary(),
3006 )
3007 .unwrap();
3008 let opened = open_non_seekable_archive(
3009 &archive.bytes,
3010 &master_key(),
3011 Some(&archive.bootstrap_sidecar),
3012 )
3013 .unwrap();
3014
3015 assert_eq!(
3016 opened.extract_file("dict-sidecar.txt").unwrap(),
3017 Some(b"common words common words sidecar payload".to_vec())
3018 );
3019 opened.verify().unwrap();
3020 }
3021
3022 #[test]
3023 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
3024 let archive = write_archive(
3025 &[RegularFile::new(
3026 "sidecar-erasure.txt",
3027 b"repair through sidecar",
3028 )],
3029 &master_key(),
3030 single_stream_options(),
3031 )
3032 .unwrap();
3033 let mut corrupted = archive.bytes.clone();
3034 corrupt_first_block_record_payload(&mut corrupted);
3035
3036 let opened = open_archive_with_bootstrap_sidecar(
3037 &corrupted,
3038 &archive.bootstrap_sidecar,
3039 &master_key(),
3040 )
3041 .unwrap();
3042 assert_eq!(
3043 opened.extract_file("sidecar-erasure.txt").unwrap(),
3044 Some(b"repair through sidecar".to_vec())
3045 );
3046 }
3047
3048 #[test]
3049 fn extraction_rejects_logical_payload_above_total_size_cap() {
3050 let archive = write_archive(
3051 &[RegularFile::new("cap.txt", b"payload")],
3052 &master_key(),
3053 single_stream_options(),
3054 )
3055 .unwrap();
3056 let mut options = ReaderOptions::default();
3057 options.max_total_extraction_size = 3;
3058 let opened =
3059 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
3060
3061 assert_eq!(
3062 opened.extract_file("cap.txt").unwrap_err(),
3063 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
3064 );
3065 }
3066
3067 #[test]
3068 fn verify_does_not_apply_extraction_payload_cap() {
3069 let archive = write_archive(
3070 &[RegularFile::new("verify-cap.txt", b"payload")],
3071 &master_key(),
3072 single_stream_options(),
3073 )
3074 .unwrap();
3075 let mut options = ReaderOptions::default();
3076 options.max_total_extraction_size = 3;
3077 let opened =
3078 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
3079
3080 opened.verify().unwrap();
3081 assert_eq!(
3082 opened.extract_file("verify-cap.txt").unwrap_err(),
3083 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
3084 );
3085 }
3086
3087 #[test]
3088 fn dictionary_sidecar_requires_dictionary_record_section() {
3089 let archive = write_archive_with_dictionary(
3090 &[RegularFile::new("dict-missing.txt", b"common words")],
3091 &master_key(),
3092 single_stream_options(),
3093 dictionary(),
3094 )
3095 .unwrap();
3096 let header = BootstrapSidecarHeader::parse(
3097 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
3098 )
3099 .unwrap();
3100 let mut missing_dictionary =
3101 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
3102 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
3103 header.flags &= !0x04;
3104 header.dictionary_records_offset = 0;
3105 header.dictionary_records_length = 0;
3106 });
3107
3108 assert_eq!(
3109 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing_dictionary, &master_key())
3110 .unwrap_err(),
3111 FormatError::ReaderUnsupported("dictionary bootstrap required")
3112 );
3113 }
3114
3115 #[test]
3116 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
3117 let archive = write_archive_with_dictionary(
3118 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
3119 &master_key(),
3120 single_stream_options(),
3121 dictionary(),
3122 )
3123 .unwrap();
3124
3125 let mut wrong_kind = archive.bootstrap_sidecar.clone();
3126 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
3127 record.kind = BlockKind::IndexRootData;
3128 });
3129 assert_eq!(
3130 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
3131 .unwrap_err(),
3132 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
3133 );
3134
3135 let mut wrong_last = archive.bootstrap_sidecar.clone();
3136 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
3137 record.flags = 0;
3138 });
3139 assert_eq!(
3140 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
3141 .unwrap_err(),
3142 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
3143 );
3144 }
3145
3146 #[test]
3147 fn non_seekable_random_access_requires_sidecar() {
3148 let archive = write_archive(
3149 &[RegularFile::new("file.txt", b"payload")],
3150 &master_key(),
3151 single_stream_options(),
3152 )
3153 .unwrap();
3154
3155 assert_eq!(
3156 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
3157 FormatError::ReaderUnsupported(
3158 "non-seekable random access requires a bootstrap sidecar"
3159 )
3160 );
3161 assert!(open_non_seekable_archive(
3162 &archive.bytes,
3163 &master_key(),
3164 Some(&archive.bootstrap_sidecar)
3165 )
3166 .is_ok());
3167 }
3168
3169 #[test]
3170 fn sequential_extracts_dictionary_free_tar_stream() {
3171 let archive = write_archive(
3172 &[RegularFile::new("seq.txt", b"streaming")],
3173 &master_key(),
3174 single_stream_options(),
3175 )
3176 .unwrap();
3177
3178 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
3179 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
3180 assert_eq!(member.path, b"seq.txt");
3181 assert_eq!(member.data, b"streaming");
3182 }
3183
3184 #[test]
3185 fn sequential_rejects_logical_payload_above_total_size_cap() {
3186 let archive = write_archive(
3187 &[RegularFile::new("seq-cap.txt", b"payload")],
3188 &master_key(),
3189 single_stream_options(),
3190 )
3191 .unwrap();
3192 let mut options = ReaderOptions::default();
3193 options.max_total_extraction_size = 3;
3194
3195 assert_eq!(
3196 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
3197 .unwrap_err(),
3198 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
3199 );
3200 }
3201
3202 #[test]
3203 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
3204 let archive = write_archive(
3205 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
3206 &master_key(),
3207 single_stream_options(),
3208 )
3209 .unwrap();
3210 let mut corrupted = archive.bytes;
3211 corrupt_first_block_record_payload(&mut corrupted);
3212
3213 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
3214 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
3215 assert_eq!(member.path, b"seq-erasure.txt");
3216 assert_eq!(member.data, b"stream repair");
3217 }
3218
3219 #[test]
3220 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
3221 let archive = write_archive(
3222 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
3223 &master_key(),
3224 WriterOptions {
3225 bit_rot_buffer_pct: 0,
3226 fec_parity_shards: 0,
3227 index_fec_parity_shards: 0,
3228 index_root_fec_parity_shards: 0,
3229 ..single_stream_options()
3230 },
3231 )
3232 .unwrap();
3233 let mut corrupted = archive.bytes;
3234 corrupt_first_block_record_payload(&mut corrupted);
3235
3236 assert_eq!(
3237 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
3238 FormatError::BadCrc {
3239 structure: "BlockRecord"
3240 }
3241 );
3242 }
3243
3244 #[test]
3245 fn sequential_rejects_when_terminal_authentication_fails() {
3246 let archive = write_archive(
3247 &[RegularFile::new("seq.txt", b"streaming")],
3248 &master_key(),
3249 single_stream_options(),
3250 )
3251 .unwrap();
3252 let mut corrupted = archive.bytes;
3253 let trailer_hmac_offset = corrupted.len() - VOLUME_TRAILER_LEN + TRAILER_HMAC_COVERED_LEN;
3254 corrupted[trailer_hmac_offset] ^= 0x01;
3255
3256 assert_eq!(
3257 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
3258 FormatError::HmacMismatch {
3259 structure: "VolumeTrailer"
3260 }
3261 );
3262 }
3263
3264 #[test]
3265 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
3266 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
3267 let mut output = Vec::new();
3268
3269 assert_eq!(
3270 decode_concatenated_zstd_frames(&skippable, None, &mut output).unwrap_err(),
3271 FormatError::NotStandardZstdFrame
3272 );
3273 assert!(output.is_empty());
3274 }
3275
3276 #[test]
3277 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
3278 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
3279 let mut bad_flags = archive.bootstrap_sidecar.clone();
3280 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
3281 header.flags |= 0x08;
3282 });
3283 assert_eq!(
3284 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
3285 .unwrap_err(),
3286 FormatError::UnknownBootstrapSidecarFlags(0x0b)
3287 );
3288
3289 let mut trailing = archive.bootstrap_sidecar.clone();
3290 trailing.push(0);
3291 assert_eq!(
3292 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
3293 .unwrap_err(),
3294 FormatError::NonCanonicalBootstrapSidecarLayout
3295 );
3296 }
3297
3298 #[test]
3299 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
3300 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
3301 let mut wrong_volume = archive.bootstrap_sidecar.clone();
3302 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
3303 footer.volume_index = 1;
3304 });
3305 assert_eq!(
3306 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
3307 .unwrap_err(),
3308 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
3309 );
3310
3311 let mut non_authoritative = archive.bootstrap_sidecar.clone();
3312 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
3313 footer.is_authoritative = 0;
3314 });
3315 assert_eq!(
3316 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
3317 .unwrap_err(),
3318 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
3319 );
3320 }
3321
3322 #[test]
3323 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
3324 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
3325 let mut with_dictionary = archive.bootstrap_sidecar.clone();
3326 let header =
3327 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
3328 .unwrap();
3329 let record_len = sidecar_record_len(&with_dictionary);
3330 let first_record = header.index_root_records_offset as usize;
3331 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
3332 let dictionary_offset = with_dictionary.len() as u64;
3333 with_dictionary.extend_from_slice(&copied_record);
3334 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
3335 header.flags |= 0x04;
3336 header.dictionary_records_offset = dictionary_offset;
3337 header.dictionary_records_length = record_len as u64;
3338 });
3339
3340 assert_eq!(
3341 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
3342 .unwrap_err(),
3343 FormatError::InvalidArchive(
3344 "bootstrap sidecar has dictionary records while has_dictionary is false"
3345 )
3346 );
3347 }
3348
3349 #[test]
3350 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
3351 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
3352 let mut missing = archive.bootstrap_sidecar.clone();
3353 let record_len = sidecar_record_len(&missing);
3354 let new_len = missing.len() - record_len;
3355 missing.truncate(new_len);
3356 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
3357 header.index_root_records_length -= record_len as u64;
3358 });
3359 assert_eq!(
3360 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
3361 .unwrap_err(),
3362 FormatError::InvalidArchive(
3363 "sidecar BlockRecord section does not match declared extent"
3364 )
3365 );
3366
3367 let mut duplicate = archive.bootstrap_sidecar.clone();
3368 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
3369 record.block_index -= 1;
3370 });
3371 assert_eq!(
3372 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
3373 .unwrap_err(),
3374 FormatError::InvalidArchive(
3375 "sidecar BlockRecord section has missing or duplicate blocks"
3376 )
3377 );
3378
3379 let mut misordered = archive.bootstrap_sidecar.clone();
3380 swap_sidecar_index_records(&mut misordered, 0, 1);
3381 assert_eq!(
3382 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
3383 .unwrap_err(),
3384 FormatError::InvalidArchive(
3385 "sidecar BlockRecord section has missing or duplicate blocks"
3386 )
3387 );
3388
3389 let mut wrong_kind = archive.bootstrap_sidecar.clone();
3390 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
3391 record.kind = BlockKind::PayloadData;
3392 });
3393 assert_eq!(
3394 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
3395 .unwrap_err(),
3396 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
3397 );
3398
3399 let mut wrong_last = archive.bootstrap_sidecar.clone();
3400 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
3401 record.flags = 0;
3402 });
3403 assert_eq!(
3404 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
3405 .unwrap_err(),
3406 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
3407 );
3408 }
3409
3410 #[test]
3411 fn verify_helper_rejects_envelope_frame_coverage_gap() {
3412 let frames = BTreeMap::from([(
3413 0,
3414 FrameEntry {
3415 frame_index: 0,
3416 envelope_index: 0,
3417 offset_in_envelope: 0,
3418 compressed_size: 10,
3419 decompressed_size: 512,
3420 flags: 0,
3421 tar_stream_offset: 0,
3422 },
3423 )]);
3424 let envelopes = BTreeMap::from([(
3425 0,
3426 EnvelopeEntry {
3427 envelope_index: 0,
3428 first_block_index: 0,
3429 data_block_count: 1,
3430 parity_block_count: 1,
3431 encrypted_size: 4096,
3432 plaintext_size: 11,
3433 first_frame_index: 0,
3434 frame_count: 1,
3435 },
3436 )]);
3437
3438 assert_eq!(
3439 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
3440 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
3441 );
3442 }
3443
3444 #[test]
3445 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
3446 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
3447 assert_eq!(
3448 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
3449 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
3450 );
3451 assert_eq!(
3452 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
3453 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
3454 );
3455 }
3456
3457 #[test]
3458 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
3459 let mut map = DirectoryHintMap::new();
3460 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
3461 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
3462
3463 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
3464 assert_eq!(map.get(&b"foo".to_vec()), Some(&BTreeSet::from([2, 4])));
3465 assert_eq!(map.get(&b"foo/bar".to_vec()), Some(&BTreeSet::from([2, 4])));
3466 assert!(!map.contains_key(&b"foo/bar/baz.txt".to_vec()));
3467 assert!(!map.contains_key(&b"foobar".to_vec()));
3468 }
3469
3470 #[test]
3471 fn directory_hint_validation_requires_exact_global_map() {
3472 let mut expected = DirectoryHintMap::new();
3473 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
3474 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
3475 let rows = sorted_directory_hint_rows(&expected);
3476 let table = directory_hint_table_from_rows(7, &rows, 2);
3477
3478 validate_directory_hint_tables_against_expected(&[table.clone()], &expected).unwrap();
3479
3480 let mut incomplete = expected.clone();
3481 incomplete.get_mut(&b"foo".to_vec()).unwrap().remove(&1);
3482 assert_eq!(
3483 validate_directory_hint_tables_against_expected(&[table], &incomplete).unwrap_err(),
3484 FormatError::InvalidArchive("directory hint map does not match decoded files")
3485 );
3486 }
3487
3488 #[test]
3489 fn directory_hint_validation_rejects_global_order_mismatch() {
3490 let mut expected = DirectoryHintMap::new();
3491 expected.insert(Vec::new(), BTreeSet::from([0]));
3492 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
3493 let rows = sorted_directory_hint_rows(&expected);
3494 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
3495 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
3496
3497 assert_eq!(
3498 validate_directory_hint_tables_against_expected(&[second, first], &expected)
3499 .unwrap_err(),
3500 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
3501 );
3502 }
3503
3504 #[test]
3505 fn object_extent_rejects_parity_above_class_cap() {
3506 let crypto_header = CryptoHeaderFixed {
3507 length: 0,
3508 compression_algo: CompressionAlgo::ZstdFramed,
3509 aead_algo: AeadAlgo::AesGcmSiv256,
3510 fec_algo: FecAlgo::ReedSolomonGF16,
3511 kdf_algo: KdfAlgo::Raw,
3512 chunk_size: 1024,
3513 envelope_target_size: 4096,
3514 block_size: 4096,
3515 fec_data_shards: 1,
3516 fec_parity_shards: 1,
3517 index_fec_data_shards: 1,
3518 index_fec_parity_shards: 1,
3519 index_root_fec_data_shards: 1,
3520 index_root_fec_parity_shards: 1,
3521 stripe_width: 1,
3522 volume_loss_tolerance: 0,
3523 bit_rot_buffer_pct: 0,
3524 has_dictionary: 0,
3525 max_path_length: 4096,
3526 expected_volume_size: 0,
3527 };
3528 let extent = ObjectExtent {
3529 first_block_index: 0,
3530 data_block_count: 1,
3531 parity_block_count: 2,
3532 encrypted_size: 4096,
3533 };
3534
3535 assert_eq!(
3536 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
3537 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
3538 );
3539 }
3540
3541 #[test]
3542 fn object_extent_rejects_parity_below_recoverability_requirement() {
3543 let crypto_header = CryptoHeaderFixed {
3544 length: 0,
3545 compression_algo: CompressionAlgo::ZstdFramed,
3546 aead_algo: AeadAlgo::AesGcmSiv256,
3547 fec_algo: FecAlgo::ReedSolomonGF16,
3548 kdf_algo: KdfAlgo::Raw,
3549 chunk_size: 1024,
3550 envelope_target_size: 4096,
3551 block_size: 4096,
3552 fec_data_shards: 1,
3553 fec_parity_shards: 1,
3554 index_fec_data_shards: 1,
3555 index_fec_parity_shards: 1,
3556 index_root_fec_data_shards: 1,
3557 index_root_fec_parity_shards: 1,
3558 stripe_width: 2,
3559 volume_loss_tolerance: 1,
3560 bit_rot_buffer_pct: 0,
3561 has_dictionary: 0,
3562 max_path_length: 4096,
3563 expected_volume_size: 0,
3564 };
3565 let extent = ObjectExtent {
3566 first_block_index: 0,
3567 data_block_count: 1,
3568 parity_block_count: 0,
3569 encrypted_size: 4096,
3570 };
3571
3572 assert_eq!(
3573 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
3574 FormatError::InvalidArchive(
3575 "encrypted object has insufficient parity for recovery settings"
3576 )
3577 );
3578 }
3579
3580 #[test]
3581 fn opens_complete_multi_volume_archive() {
3582 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
3583 let archive = write_archive(
3584 &files,
3585 &master_key(),
3586 WriterOptions {
3587 stripe_width: 2,
3588 volume_loss_tolerance: 1,
3589 ..single_stream_options()
3590 },
3591 )
3592 .unwrap();
3593 assert_eq!(archive.volumes.len(), 2);
3594
3595 let volume_refs = archive
3596 .volumes
3597 .iter()
3598 .map(Vec::as_slice)
3599 .collect::<Vec<_>>();
3600 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
3601
3602 assert_eq!(opened.volume_header.stripe_width, 2);
3603 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
3604 assert_eq!(
3605 opened.extract_file("alpha.txt").unwrap(),
3606 Some(b"hello from volume stripes".to_vec())
3607 );
3608 opened.verify().unwrap();
3609 }
3610
3611 #[test]
3612 fn recovers_from_one_missing_volume_when_parity_allows() {
3613 let files = [RegularFile::new("alpha.txt", b"recover me")];
3614 let archive = write_archive(
3615 &files,
3616 &master_key(),
3617 WriterOptions {
3618 stripe_width: 2,
3619 volume_loss_tolerance: 1,
3620 ..single_stream_options()
3621 },
3622 )
3623 .unwrap();
3624
3625 let recovered =
3626 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
3627 assert_eq!(
3628 recovered.extract_file("alpha.txt").unwrap(),
3629 Some(b"recover me".to_vec())
3630 );
3631 recovered.verify().unwrap();
3632 }
3633
3634 #[test]
3635 fn recovers_from_crc_corrupted_block_when_parity_allows() {
3636 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
3637 let archive = write_archive(
3638 &files,
3639 &master_key(),
3640 WriterOptions {
3641 stripe_width: 2,
3642 volume_loss_tolerance: 1,
3643 ..single_stream_options()
3644 },
3645 )
3646 .unwrap();
3647 let mut volumes = archive.volumes.clone();
3648 corrupt_first_block_record_payload(&mut volumes[0]);
3649
3650 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
3651 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
3652
3653 assert_eq!(
3654 recovered.extract_file("alpha.txt").unwrap(),
3655 Some(b"repair corrupt block".to_vec())
3656 );
3657 recovered.verify().unwrap();
3658 }
3659
3660 #[test]
3661 fn rejects_block_record_at_wrong_stripe_position() {
3662 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
3663 let archive = write_archive(
3664 &files,
3665 &master_key(),
3666 WriterOptions {
3667 stripe_width: 2,
3668 volume_loss_tolerance: 1,
3669 ..single_stream_options()
3670 },
3671 )
3672 .unwrap();
3673 let mut volumes = archive.volumes.clone();
3674 mutate_first_block_record(&mut volumes[0], |record| {
3675 record.block_index += 2;
3676 });
3677
3678 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
3679 assert_eq!(
3680 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
3681 FormatError::InvalidArchive("BlockRecord index does not match volume position")
3682 );
3683 }
3684
3685 #[test]
3686 fn rejects_duplicate_authenticated_volume_indexes() {
3687 let files = [RegularFile::new("alpha.txt", b"duplicates")];
3688 let archive = write_archive(
3689 &files,
3690 &master_key(),
3691 WriterOptions {
3692 stripe_width: 2,
3693 volume_loss_tolerance: 1,
3694 ..single_stream_options()
3695 },
3696 )
3697 .unwrap();
3698
3699 assert_eq!(
3700 open_archive_volumes(
3701 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
3702 &master_key()
3703 )
3704 .unwrap_err(),
3705 FormatError::InvalidArchive("duplicate authenticated volume index")
3706 );
3707 }
3708
3709 fn directory_hint_table_from_rows(
3710 hint_shard_index: u64,
3711 rows: &[(Vec<u8>, Vec<u32>)],
3712 shard_count: u32,
3713 ) -> DirectoryHintTable {
3714 let mut entries = Vec::new();
3715 let mut shard_row_indexes = Vec::new();
3716 let mut string_pool = Vec::new();
3717
3718 for (path, rows) in rows {
3719 let path_offset = if path.is_empty() {
3720 0
3721 } else {
3722 let offset = string_pool.len() as u64;
3723 string_pool.extend_from_slice(path);
3724 offset
3725 };
3726 let shard_list_start_index = shard_row_indexes.len() as u32;
3727 shard_row_indexes.extend_from_slice(rows);
3728 entries.push(DirectoryHintEntry {
3729 dir_hash: hash_prefix(path),
3730 path_offset,
3731 path_length: path.len() as u32,
3732 shard_list_start_index,
3733 shard_count: rows.len() as u32,
3734 });
3735 }
3736
3737 let table_bytes =
3738 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
3739 let locating = DirectoryHintShardEntry {
3740 hint_shard_index,
3741 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
3742 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
3743 first_block_index: 0,
3744 data_block_count: 1,
3745 parity_block_count: 0,
3746 encrypted_size: 4096,
3747 decompressed_size: table_bytes.len() as u32,
3748 entry_count: rows.len() as u64,
3749 };
3750 DirectoryHintTable::parse(
3751 &table_bytes,
3752 &locating,
3753 shard_count,
3754 MetadataLimits::default(),
3755 )
3756 .unwrap()
3757 }
3758
3759 fn directory_hint_table_bytes(
3760 hint_shard_index: u64,
3761 entries: Vec<DirectoryHintEntry>,
3762 shard_row_indexes: Vec<u32>,
3763 string_pool: Vec<u8>,
3764 ) -> Vec<u8> {
3765 let header_len = DirectoryHintTableHeader {
3766 version: 1,
3767 hint_shard_index,
3768 entry_count: 0,
3769 entry_table_offset: 0,
3770 shard_list_offset: 0,
3771 string_pool_offset: 0,
3772 string_pool_size: 0,
3773 }
3774 .to_bytes()
3775 .len();
3776 let entry_len = entries
3777 .first()
3778 .map(|entry| entry.to_bytes().len())
3779 .unwrap_or(0);
3780 let shard_list_offset = if entries.is_empty() {
3781 0
3782 } else {
3783 header_len + entries.len() * entry_len
3784 };
3785 let string_pool_offset = if string_pool.is_empty() {
3786 0
3787 } else {
3788 shard_list_offset + shard_row_indexes.len() * 4
3789 };
3790
3791 let header = DirectoryHintTableHeader {
3792 version: 1,
3793 hint_shard_index,
3794 entry_count: entries.len() as u64,
3795 entry_table_offset: if entries.is_empty() {
3796 0
3797 } else {
3798 header_len as u64
3799 },
3800 shard_list_offset: shard_list_offset as u64,
3801 string_pool_offset: string_pool_offset as u64,
3802 string_pool_size: string_pool.len() as u64,
3803 };
3804
3805 let mut out = Vec::new();
3806 out.extend_from_slice(&header.to_bytes());
3807 for entry in entries {
3808 out.extend_from_slice(&entry.to_bytes());
3809 }
3810 for row in shard_row_indexes {
3811 out.extend_from_slice(&row.to_le_bytes());
3812 }
3813 out.extend_from_slice(&string_pool);
3814 out
3815 }
3816
3817 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
3818 let (record_offset, _) = first_block_record(volume);
3819 volume[record_offset + 16] ^= 0x55;
3820 }
3821
3822 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
3823 let (record_offset, record_len) = first_block_record(volume);
3824 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
3825 let mut record = BlockRecord::parse(
3826 &volume[record_offset..record_offset + record_len],
3827 block_size,
3828 )
3829 .unwrap();
3830 mutate(&mut record);
3831 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
3832 }
3833
3834 fn first_block_record(volume: &[u8]) -> (usize, usize) {
3835 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
3836 let crypto_start = volume_header.crypto_header_offset as usize;
3837 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
3838 let crypto_header = CryptoHeader::parse(
3839 &volume[crypto_start..crypto_end],
3840 volume_header.crypto_header_length,
3841 )
3842 .unwrap();
3843 let record_offset = crypto_end;
3844 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
3845 assert!(volume.len() >= record_offset + record_len);
3846 (record_offset, record_len)
3847 }
3848
3849 fn rewrite_sidecar_header(
3850 sidecar: &mut [u8],
3851 master_key: &MasterKey,
3852 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
3853 ) {
3854 let mut header =
3855 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
3856 mutate(&mut header);
3857 header.sidecar_hmac = [0u8; 32];
3858 let mut header_bytes = header.to_bytes();
3859 let subkeys =
3860 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
3861 header.sidecar_hmac = compute_hmac(
3862 HmacDomain::BootstrapSidecar,
3863 &subkeys.mac_key,
3864 &header.archive_uuid,
3865 &header.session_id,
3866 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
3867 );
3868 header_bytes = header.to_bytes();
3869 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
3870 }
3871
3872 fn mutate_sidecar_manifest(
3873 sidecar: &mut [u8],
3874 master_key: &MasterKey,
3875 mutate: impl FnOnce(&mut ManifestFooter),
3876 ) {
3877 let header =
3878 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
3879 let offset = header.manifest_footer_offset as usize;
3880 let mut footer =
3881 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
3882 mutate(&mut footer);
3883 footer.manifest_hmac = [0u8; 32];
3884 let mut footer_bytes = footer.to_bytes();
3885 let subkeys =
3886 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
3887 footer.manifest_hmac = compute_hmac(
3888 HmacDomain::ManifestFooter,
3889 &subkeys.mac_key,
3890 &footer.archive_uuid,
3891 &footer.session_id,
3892 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
3893 );
3894 footer_bytes = footer.to_bytes();
3895 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
3896 }
3897
3898 fn mutate_sidecar_index_record(
3899 sidecar: &mut [u8],
3900 record_index: usize,
3901 mutate: impl FnOnce(&mut BlockRecord),
3902 ) {
3903 let header =
3904 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
3905 let record_len = sidecar_record_len(sidecar);
3906 let offset = header.index_root_records_offset as usize + record_index * record_len;
3907 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
3908 let mut record =
3909 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
3910 mutate(&mut record);
3911 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
3912 }
3913
3914 fn mutate_sidecar_dictionary_record(
3915 sidecar: &mut [u8],
3916 record_index: usize,
3917 mutate: impl FnOnce(&mut BlockRecord),
3918 ) {
3919 let header =
3920 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
3921 let record_len = sidecar_record_len(sidecar);
3922 let offset = header.dictionary_records_offset as usize + record_index * record_len;
3923 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
3924 let mut record =
3925 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
3926 mutate(&mut record);
3927 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
3928 }
3929
3930 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
3931 let header =
3932 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
3933 let record_len = sidecar_record_len(sidecar);
3934 let left_offset = header.index_root_records_offset as usize + left * record_len;
3935 let right_offset = header.index_root_records_offset as usize + right * record_len;
3936 for idx in 0..record_len {
3937 sidecar.swap(left_offset + idx, right_offset + idx);
3938 }
3939 }
3940
3941 fn sidecar_record_len(sidecar: &[u8]) -> usize {
3942 let header =
3943 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
3944 let footer_offset = header.manifest_footer_offset as usize;
3945 let footer =
3946 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
3947 .unwrap();
3948 let index_record_count = footer.index_root_data_block_count as usize
3949 + footer.index_root_parity_block_count as usize;
3950 header.index_root_records_length as usize / index_record_count
3951 }
3952}