1use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom, Write};
4use std::sync::Arc;
5
6use sha2::{Digest, Sha256};
7
8use crate::compression::{
9 decompress_exact_zstd_frame, decompress_exact_zstd_frame_with_dictionary,
10 validate_exact_zstd_frame,
11};
12use crate::crypto::{
13 decrypt_padded_aead_object, verify_hmac, AeadObjectContext, HmacDomain, MasterKey, Subkeys,
14};
15use crate::fec::repair_data_gf16;
16use crate::format::{
17 BlockKind, ExtractError, FormatError, BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN,
18 CRITICAL_METADATA_IMAGE_FIXED_LEN, CRITICAL_METADATA_RECOVERY_HEADER_LEN,
19 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN, CRITICAL_RECOVERY_LOCATOR_LEN,
20 CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN, MANIFEST_FOOTER_LEN,
21 READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN,
22 SERIALIZED_REGION_HEADER_LEN, VOLUME_FORMAT_REV, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
23};
24use crate::metadata::{
25 hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
26 EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
27};
28use crate::non_seekable_reader::StreamedPayloadSummary;
29use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
30use crate::root_auth::{
31 archive_root, critical_metadata_digest, data_block_merkle_root, fec_layout_digest,
32 index_digest, root_auth_descriptor_digest, signer_identity_digest, ArchiveRootInputs,
33 CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
34};
35use crate::tar_model::{
36 parse_tar_member_group, restore_streaming_tar_member_group,
37 stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
38 MetadataDiagnostic, OwnedTarMember, SafeExtractionOptions, TarEntryKind, TarMemberGroupReader,
39 TarStreamTotalExtractionSizeValidator,
40};
41use crate::wire::{
42 BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage, CriticalMetadataRecoveryHeader,
43 CriticalMetadataRecoveryShard, CriticalRecoveryLocator, CryptoHeader, CryptoHeaderFixed,
44 ExtensionTlv, ManifestFooter, RootAuthFooterV1, VolumeHeader, VolumeTrailer,
45};
46
47const TRAILER_HMAC_COVERED_LEN: usize = 96;
48const MANIFEST_HMAC_COVERED_LEN: usize = 104;
49const SIDECAR_HMAC_COVERED_LEN: usize = 92;
50const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
51const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
52const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
53const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
54
55pub trait ArchiveReadAt: Send + Sync + 'static {
56 fn len(&self) -> Result<u64, FormatError>;
57 fn is_empty(&self) -> Result<bool, FormatError> {
58 Ok(self.len()? == 0)
59 }
60 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
61}
62
63impl ArchiveReadAt for File {
64 fn len(&self) -> Result<u64, FormatError> {
65 self.metadata()
66 .map(|metadata| metadata.len())
67 .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
68 }
69
70 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
71 let mut file = self
72 .try_clone()
73 .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
74 file.seek(SeekFrom::Start(offset))
75 .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
76 file.read_exact(buf)
77 .map_err(|_| FormatError::InvalidArchive("archive read failed"))
78 }
79}
80
81impl ArchiveReadAt for Vec<u8> {
82 fn len(&self) -> Result<u64, FormatError> {
83 u64::try_from(self.len())
84 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
85 }
86
87 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
88 let offset = to_usize(offset, "archive")?;
89 let end = checked_add(offset, buf.len(), "archive")?;
90 let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
91 structure: "archive",
92 expected: end,
93 actual: self.len(),
94 })?;
95 buf.copy_from_slice(source);
96 Ok(())
97 }
98}
99
100impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
101 fn len(&self) -> Result<u64, FormatError> {
102 (**self).len()
103 }
104
105 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
106 (**self).read_exact_at(offset, buf)
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct ReaderOptions {
112 pub max_trailing_garbage_scan: usize,
113 pub max_verify_tar_size: usize,
114 pub max_total_extraction_size: u64,
115}
116
117impl Default for ReaderOptions {
118 fn default() -> Self {
119 Self {
120 max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
121 max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
122 max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct ArchiveEntry {
129 pub path: String,
130 pub file_data_size: u64,
131 pub kind: TarEntryKind,
132 pub mode: u32,
133 pub mtime: u64,
134 pub diagnostics: Vec<MetadataDiagnostic>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct ArchiveIndexEntry {
139 pub path: String,
140 pub file_data_size: u64,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct ExtractedArchiveMember {
145 pub path: String,
146 pub kind: TarEntryKind,
147 pub data: Vec<u8>,
148 pub link_target: Option<String>,
149 pub diagnostics: Vec<MetadataDiagnostic>,
150}
151
152#[derive(Debug, Clone)]
153pub struct OpenedArchive {
154 options: ReaderOptions,
155 observed_archive_bytes: u64,
156 observed_volume_count: u32,
157 subkeys: Subkeys,
158 blocks: BTreeMap<u64, BlockRecord>,
159 lazy_blocks: Option<Arc<SeekableBlockSource>>,
160 crypto_header_bytes: Vec<u8>,
161 pub volume_header: VolumeHeader,
162 pub crypto_header: CryptoHeaderFixed,
163 pub manifest_footer: ManifestFooter,
164 pub volume_trailer: Option<VolumeTrailer>,
165 pub root_auth_footer: Option<RootAuthFooterV1>,
166 pub index_root: IndexRoot,
167 payload_dictionary: Option<Vec<u8>>,
168}
169
170#[derive(Debug)]
171pub struct ArchiveContentVerification<'a> {
172 archive: &'a OpenedArchive,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum RootAuthDiagnostic {
177 RootAuthContentVerified,
178 RootAuthDeferredFullArchiveScanRequired,
179 AuthenticatedMetadataNotRootSigned,
180 RecoveryMarginNotRootAuthenticated,
181 ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
182 RecoveryMarginChecked,
183 RecoveryMarginFailed,
184 RecoveryMarginUnchecked,
185}
186
187impl RootAuthDiagnostic {
188 pub const fn label(self) -> &'static str {
189 match self {
190 Self::RootAuthContentVerified => "root_auth_content_verified",
191 Self::RootAuthDeferredFullArchiveScanRequired => {
192 "root_auth_deferred_full_archive_scan_required"
193 }
194 Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
195 Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
196 Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
197 "replicated_global_copy_unchecked_due_to_volume_loss"
198 }
199 Self::RecoveryMarginChecked => "recovery_margin_checked",
200 Self::RecoveryMarginFailed => "recovery_margin_failed",
201 Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum PublicNoKeyDiagnostic {
208 PublicDataBlockCommitmentVerified,
209 PublicPhysicalCompletenessUnverified,
210 PublicRecoveryMarginUnchecked,
211}
212
213impl PublicNoKeyDiagnostic {
214 pub const fn label(self) -> &'static str {
215 match self {
216 Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
217 Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
218 Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
219 }
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct RootAuthVerification {
225 pub archive_root: [u8; 32],
226 pub authenticator_id: u16,
227 pub signer_identity_type: u16,
228 pub signer_identity_bytes: Vec<u8>,
229 pub total_data_block_count: u64,
230 pub diagnostics: Vec<RootAuthDiagnostic>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct PublicNoKeyVerification {
235 pub archive_root: [u8; 32],
236 pub authenticator_id: u16,
237 pub signer_identity_type: u16,
238 pub signer_identity_bytes: Vec<u8>,
239 pub total_data_block_count: u64,
240 pub diagnostics: Vec<PublicNoKeyDiagnostic>,
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244struct RootAuthMaterial {
245 critical_metadata_digest: [u8; 32],
246 index_digest: [u8; 32],
247 fec_layout_digest: [u8; 32],
248 data_block_merkle_root: [u8; 32],
249 signer_identity_digest: [u8; 32],
250 archive_root: [u8; 32],
251 total_data_block_count: u64,
252}
253
254#[derive(Debug, Clone, Copy)]
255struct ObjectExtent {
256 first_block_index: u64,
257 data_block_count: u32,
258 parity_block_count: u32,
259 encrypted_size: u32,
260}
261
262pub(crate) struct StreamedArchiveOpenParts {
263 pub(crate) options: ReaderOptions,
264 pub(crate) observed_archive_bytes: u64,
265 pub(crate) subkeys: Subkeys,
266 pub(crate) blocks: BTreeMap<u64, BlockRecord>,
267 pub(crate) crypto_header_bytes: Vec<u8>,
268 pub(crate) volume_header: VolumeHeader,
269 pub(crate) crypto_header: CryptoHeaderFixed,
270 pub(crate) manifest_footer: ManifestFooter,
271 pub(crate) volume_trailer: VolumeTrailer,
272 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
273}
274
275#[derive(Clone, Copy)]
276struct WinningIndexEntry {
277 start: u64,
278 file_data_size: u64,
279 shard_index: usize,
280 file_index: usize,
281}
282
283struct LocatedIndexFile {
284 shard: IndexShard,
285 file_index: usize,
286 start: u64,
287}
288
289struct DecodedTarMemberGroupReader<'a> {
290 archive: &'a OpenedArchive,
291 shard: &'a IndexShard,
292 file: &'a FileEntry,
293 next_frame_offset: u64,
294 cached_envelope_index: Option<u64>,
295 cached_envelope_plaintext: Vec<u8>,
296 current_frame: Vec<u8>,
297 current_frame_offset: usize,
298 remaining_group_bytes: u64,
299}
300
301struct SeekableVolumeSource {
302 reader: Arc<dyn ArchiveReadAt>,
303 volume_index: u32,
304 crypto_end: u64,
305 block_count: u64,
306 record_len: u64,
307 block_size: usize,
308}
309
310impl std::fmt::Debug for SeekableVolumeSource {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.debug_struct("SeekableVolumeSource")
313 .field("volume_index", &self.volume_index)
314 .field("crypto_end", &self.crypto_end)
315 .field("block_count", &self.block_count)
316 .field("record_len", &self.record_len)
317 .field("block_size", &self.block_size)
318 .finish_non_exhaustive()
319 }
320}
321
322#[derive(Debug)]
323struct SeekableBlockSource {
324 stripe_width: u32,
325 volumes: Vec<Option<SeekableVolumeSource>>,
326}
327
328trait BlockProvider {
329 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
330}
331
332struct OpenedBlockProvider<'a> {
333 memory_blocks: &'a BTreeMap<u64, BlockRecord>,
334 lazy_blocks: Option<&'a SeekableBlockSource>,
335}
336
337impl SeekableBlockSource {
338 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
339 if self.stripe_width == 0 {
340 return Err(FormatError::ZeroStripeWidth);
341 }
342 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
343 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
344 let Some(volume) = self
345 .volumes
346 .get(volume_index as usize)
347 .and_then(Option::as_ref)
348 else {
349 return Ok(None);
350 };
351 let slot = block_index / self.stripe_width as u64;
352 if slot >= volume.block_count {
353 return Ok(None);
354 }
355 match volume.read_slot(slot, block_index) {
356 Ok(record) => Ok(Some(record)),
357 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
358 Err(err) => Err(err),
359 }
360 }
361
362 fn validate_complete_coverage(&self) -> Result<(), FormatError> {
363 let mut total_block_count = 0u64;
364 for volume in &self.volumes {
365 let volume = volume.as_ref().ok_or(FormatError::InvalidArchive(
366 "missing volume in complete set",
367 ))?;
368 total_block_count = checked_u64_add(
369 total_block_count,
370 volume.block_count,
371 "BlockRecord count overflow",
372 )?;
373 for slot in 0..volume.block_count {
374 let block_index = checked_u64_add(
375 volume.volume_index as u64,
376 checked_u64_mul(slot, self.stripe_width as u64, "BlockRecord index overflow")?,
377 "BlockRecord index overflow",
378 )?;
379 match volume.read_slot(slot, block_index) {
380 Ok(_) => {}
381 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
382 Err(err) => return Err(err),
383 }
384 }
385 }
386 for volume in self.volumes.iter().flatten() {
387 let expected = expected_striped_volume_block_count(
388 total_block_count,
389 self.stripe_width,
390 volume.volume_index,
391 )?;
392 if volume.block_count != expected {
393 return Err(FormatError::InvalidArchive(
394 "BlockRecord global coverage has a gap",
395 ));
396 }
397 }
398 Ok(())
399 }
400
401 fn is_complete_volume_set(&self) -> bool {
402 self.volumes.iter().all(Option::is_some)
403 }
404}
405
406impl SeekableVolumeSource {
407 fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
408 let record_offset = self
409 .crypto_end
410 .checked_add(checked_u64_mul(
411 slot,
412 self.record_len,
413 "BlockRecord offset overflow",
414 )?)
415 .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))?;
416 let raw = read_at_vec(
417 self.reader.as_ref(),
418 record_offset,
419 usize::try_from(self.record_len)
420 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
421 "BlockRecord",
422 )?;
423 let record = BlockRecord::parse(&raw, self.block_size)?;
424 if record.block_index != expected_block_index {
425 return Err(FormatError::InvalidArchive(
426 "BlockRecord index does not match volume position",
427 ));
428 }
429 Ok(record)
430 }
431}
432
433impl BlockProvider for BTreeMap<u64, BlockRecord> {
434 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
435 Ok(self.get(&block_index).cloned())
436 }
437}
438
439impl BlockProvider for OpenedBlockProvider<'_> {
440 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
441 if let Some(record) = self.memory_blocks.get(&block_index) {
442 return Ok(Some(record.clone()));
443 }
444 match self.lazy_blocks {
445 Some(source) => source.block(block_index),
446 None => Ok(None),
447 }
448 }
449}
450
451type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
452pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
453
454pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
455 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
456}
457
458pub fn open_archive_volumes(
459 volumes: &[&[u8]],
460 master_key: &MasterKey,
461) -> Result<OpenedArchive, FormatError> {
462 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
463}
464
465pub fn open_archive_with_bootstrap_sidecar(
466 bytes: &[u8],
467 bootstrap_sidecar: &[u8],
468 master_key: &MasterKey,
469) -> Result<OpenedArchive, FormatError> {
470 OpenedArchive::open_with_bootstrap_sidecar_options(
471 bytes,
472 bootstrap_sidecar,
473 master_key,
474 ReaderOptions::default(),
475 )
476}
477
478pub fn open_seekable_archive<R: ArchiveReadAt>(
479 reader: R,
480 master_key: &MasterKey,
481) -> Result<OpenedArchive, FormatError> {
482 OpenedArchive::open_seekable_volumes_with_options(
483 vec![reader],
484 master_key,
485 ReaderOptions::default(),
486 )
487}
488
489pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
490 readers: Vec<R>,
491 master_key: &MasterKey,
492) -> Result<OpenedArchive, FormatError> {
493 OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
494}
495
496pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
497 reader: R,
498 bootstrap_sidecar: &[u8],
499 master_key: &MasterKey,
500) -> Result<OpenedArchive, FormatError> {
501 OpenedArchive::open_seekable_volumes_with_options_for_mode(
502 vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
503 master_key,
504 ReaderOptions::default(),
505 Some(bootstrap_sidecar),
506 )
507}
508
509pub fn open_non_seekable_archive(
510 bytes: &[u8],
511 master_key: &MasterKey,
512 bootstrap_sidecar: Option<&[u8]>,
513) -> Result<OpenedArchive, FormatError> {
514 match bootstrap_sidecar {
515 Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
516 bytes,
517 sidecar,
518 master_key,
519 ReaderOptions::default(),
520 BootstrapSidecarUse::NonSeekableRandomAccess,
521 ),
522 None => Err(FormatError::ReaderUnsupported(
523 "non-seekable random access requires a bootstrap sidecar",
524 )),
525 }
526}
527
528pub fn public_no_key_verify_archive_with<F>(
529 bytes: &[u8],
530 verifier: F,
531) -> Result<PublicNoKeyVerification, FormatError>
532where
533 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
534{
535 public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
536}
537
538pub fn public_no_key_verify_volumes_with<F>(
539 volumes: &[&[u8]],
540 verifier: F,
541) -> Result<PublicNoKeyVerification, FormatError>
542where
543 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
544{
545 public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
546}
547
548pub fn sequential_extract_tar_stream(
554 bytes: &[u8],
555 master_key: &MasterKey,
556) -> Result<Vec<u8>, FormatError> {
557 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
558}
559
560impl OpenedArchive {
561 fn block_provider(&self) -> OpenedBlockProvider<'_> {
562 OpenedBlockProvider {
563 memory_blocks: &self.blocks,
564 lazy_blocks: self.lazy_blocks.as_deref(),
565 }
566 }
567
568 fn missing_volume_count(&self) -> u32 {
569 self.crypto_header
570 .stripe_width
571 .saturating_sub(self.observed_volume_count)
572 }
573
574 fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
575 let mut diagnostics = vec![
576 RootAuthDiagnostic::RootAuthContentVerified,
577 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
578 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
579 ];
580 if self.missing_volume_count() > 0 {
581 diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
582 }
583 diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
584 diagnostics
585 }
586
587 pub fn open_with_options(
588 bytes: &[u8],
589 master_key: &MasterKey,
590 options: ReaderOptions,
591 ) -> Result<Self, FormatError> {
592 Self::open_volumes_with_options(&[bytes], master_key, options)
593 }
594
595 pub fn open_volumes_with_options(
596 volumes: &[&[u8]],
597 master_key: &MasterKey,
598 options: ReaderOptions,
599 ) -> Result<Self, FormatError> {
600 if volumes.is_empty() {
601 return Err(FormatError::InvalidArchive("no volumes supplied"));
602 }
603
604 let observed_archive_bytes =
605 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
606 let mut first: Option<ParsedSeekableVolume> = None;
607 let mut manifest_authority: Option<ManifestFooter> = None;
608 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
609 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
610 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
611 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
612 let mut saw_root_auth_absent = false;
613 let mut first_manifest_footer_error: Option<FormatError> = None;
614 let mut seen_volume_indexes = BTreeSet::new();
615 let mut blocks = BTreeMap::new();
616 let mut erased_block_indices = BTreeSet::new();
617
618 for volume_bytes in volumes {
619 let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
620 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
621 return Err(FormatError::InvalidArchive(
622 "duplicate authenticated volume index",
623 ));
624 }
625
626 if let Some(first) = &first {
627 validate_volume_set_member(first, &parsed)?;
628 }
629
630 if let Some(footer) = &parsed.manifest_footer {
631 if let Some(authority) = &manifest_authority {
632 if !manifest_bootstrap_fields_match(authority, footer) {
633 return Err(FormatError::InvalidArchive(
634 "ManifestFooter bootstrap fields differ",
635 ));
636 }
637 } else {
638 manifest_authority = Some(footer.clone());
639 manifest_authority_volume_header = Some(parsed.volume_header.clone());
640 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
641 }
642 } else if first_manifest_footer_error.is_none() {
643 first_manifest_footer_error = parsed.manifest_footer_error.take();
644 }
645
646 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
647 (Some(footer), Some(bytes)) => {
648 if saw_root_auth_absent {
649 return Err(FormatError::InvalidArchive(
650 "root-auth footer presence differs across volumes",
651 ));
652 }
653 if let Some(authority_bytes) = &root_auth_authority_bytes {
654 if authority_bytes != bytes {
655 return Err(FormatError::InvalidArchive(
656 "RootAuthFooter copies differ",
657 ));
658 }
659 } else {
660 root_auth_authority = Some(footer.clone());
661 root_auth_authority_bytes = Some(bytes.clone());
662 }
663 }
664 (None, None) => {
665 if root_auth_authority_bytes.is_some() {
666 return Err(FormatError::InvalidArchive(
667 "root-auth footer presence differs across volumes",
668 ));
669 }
670 saw_root_auth_absent = true;
671 }
672 _ => {
673 return Err(FormatError::InvalidArchive(
674 "root-auth footer terminal state is inconsistent",
675 ));
676 }
677 }
678
679 for (block_index, record) in &parsed.blocks {
680 if blocks.insert(*block_index, record.clone()).is_some() {
681 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
682 }
683 }
684 for block_index in &parsed.erased_block_indices {
685 erased_block_indices.insert(*block_index);
686 }
687
688 if first.is_none() {
689 first = Some(parsed);
690 }
691 }
692
693 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
694 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
695 Some(err) => err,
696 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
697 })?;
698 let authority_volume_header = manifest_authority_volume_header.ok_or(
699 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
700 )?;
701 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
702 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
703 )?;
704 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
705 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
706 let missing_volume_count = first
707 .crypto_header
708 .stripe_width
709 .checked_sub(observed_volume_count)
710 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
711 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
712 return Err(FormatError::InvalidArchive(
713 "missing volume count exceeds volume_loss_tolerance",
714 ));
715 }
716 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
717 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
718 }
719
720 let limits = metadata_limits(&first.crypto_header);
721 let index_root_plaintext = load_metadata_object_from_parts(
722 &blocks,
723 ObjectLoadContext::index_root(
724 &first.volume_header,
725 &first.crypto_header,
726 &first.subkeys,
727 ObjectExtent {
728 first_block_index: manifest_footer.index_root_first_block,
729 data_block_count: manifest_footer.index_root_data_block_count,
730 parity_block_count: manifest_footer.index_root_parity_block_count,
731 encrypted_size: manifest_footer.index_root_encrypted_size,
732 },
733 ),
734 manifest_footer.index_root_decompressed_size,
735 )?;
736 let index_root = IndexRoot::parse(
737 &index_root_plaintext,
738 first.crypto_header.has_dictionary != 0,
739 limits,
740 )?;
741 let payload_dictionary = load_archive_dictionary(
742 &blocks,
743 &first.subkeys,
744 &first.volume_header,
745 &first.crypto_header,
746 &index_root,
747 )?;
748
749 Ok(Self {
750 options,
751 observed_archive_bytes,
752 observed_volume_count,
753 subkeys: first.subkeys,
754 blocks,
755 lazy_blocks: None,
756 crypto_header_bytes: first.crypto_header_bytes,
757 volume_header: authority_volume_header,
758 crypto_header: first.crypto_header,
759 manifest_footer,
760 volume_trailer: Some(authority_volume_trailer),
761 root_auth_footer: root_auth_authority,
762 index_root,
763 payload_dictionary,
764 })
765 }
766
767 pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
768 readers: Vec<R>,
769 master_key: &MasterKey,
770 options: ReaderOptions,
771 ) -> Result<Self, FormatError> {
772 let readers = readers
773 .into_iter()
774 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
775 .collect::<Vec<_>>();
776 Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
777 }
778
779 fn open_seekable_volumes_with_options_for_mode(
780 readers: Vec<Arc<dyn ArchiveReadAt>>,
781 master_key: &MasterKey,
782 options: ReaderOptions,
783 bootstrap_sidecar: Option<&[u8]>,
784 ) -> Result<Self, FormatError> {
785 if readers.is_empty() {
786 return Err(FormatError::InvalidArchive("no volumes supplied"));
787 }
788 if bootstrap_sidecar.is_some() && readers.len() > 1 {
789 return Err(FormatError::ReaderUnsupported(
790 "multi-volume inputs with bootstrap sidecar are not supported",
791 ));
792 }
793
794 let observed_archive_bytes = observed_archive_size(
795 readers
796 .iter()
797 .map(|reader| reader.len())
798 .collect::<Result<Vec<_>, _>>()?
799 .into_iter()
800 .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
801 )?;
802 let mut first: Option<ParsedSeekableReadAtVolume> = None;
803 let mut manifest_authority: Option<ManifestFooter> = None;
804 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
805 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
806 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
807 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
808 let mut saw_root_auth_absent = false;
809 let mut first_manifest_footer_error: Option<FormatError> = None;
810 let mut seen_volume_indexes = BTreeSet::new();
811 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
812
813 for reader in readers {
814 let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
815 if bootstrap_sidecar.is_some() {
816 validate_bootstrap_single_volume_input(
817 &parsed.volume_header,
818 &parsed.crypto_header,
819 )?;
820 }
821 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
822 return Err(FormatError::InvalidArchive(
823 "duplicate authenticated volume index",
824 ));
825 }
826
827 if let Some(first) = &first {
828 validate_volume_set_member_metadata(
829 &first.volume_header,
830 &first.crypto_header,
831 &first.crypto_header_bytes,
832 &parsed.volume_header,
833 &parsed.crypto_header,
834 &parsed.crypto_header_bytes,
835 )?;
836 } else {
837 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
838 }
839
840 if let Some(footer) = &parsed.manifest_footer {
841 if let Some(authority) = &manifest_authority {
842 if !manifest_bootstrap_fields_match(authority, footer) {
843 return Err(FormatError::InvalidArchive(
844 "ManifestFooter bootstrap fields differ",
845 ));
846 }
847 } else {
848 manifest_authority = Some(footer.clone());
849 manifest_authority_volume_header = Some(parsed.volume_header.clone());
850 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
851 }
852 } else if first_manifest_footer_error.is_none() {
853 first_manifest_footer_error = parsed.manifest_footer_error.take();
854 }
855
856 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
857 (Some(footer), Some(bytes)) => {
858 if saw_root_auth_absent {
859 return Err(FormatError::InvalidArchive(
860 "root-auth footer presence differs across volumes",
861 ));
862 }
863 if let Some(authority_bytes) = &root_auth_authority_bytes {
864 if authority_bytes != bytes {
865 return Err(FormatError::InvalidArchive(
866 "RootAuthFooter copies differ",
867 ));
868 }
869 } else {
870 root_auth_authority = Some(footer.clone());
871 root_auth_authority_bytes = Some(bytes.clone());
872 }
873 }
874 (None, None) => {
875 if root_auth_authority_bytes.is_some() {
876 return Err(FormatError::InvalidArchive(
877 "root-auth footer presence differs across volumes",
878 ));
879 }
880 saw_root_auth_absent = true;
881 }
882 _ => {
883 return Err(FormatError::InvalidArchive(
884 "root-auth footer terminal state is inconsistent",
885 ));
886 }
887 }
888
889 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
890 let source = SeekableVolumeSource {
891 reader: parsed.reader.clone(),
892 volume_index: parsed.volume_header.volume_index,
893 crypto_end: parsed.crypto_end,
894 block_count: parsed.volume_trailer.block_count,
895 record_len,
896 block_size: parsed.crypto_header.block_size as usize,
897 };
898 let slot = parsed.volume_header.volume_index as usize;
899 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
900 {
901 return Err(FormatError::InvalidArchive(
902 "duplicate authenticated volume index",
903 ));
904 }
905
906 if first.is_none() {
907 first = Some(parsed);
908 }
909 }
910
911 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
912 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
913 Some(err) => err,
914 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
915 })?;
916 let authority_volume_header = manifest_authority_volume_header.ok_or(
917 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
918 )?;
919 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
920 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
921 )?;
922 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
923 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
924 let missing_volume_count = first
925 .crypto_header
926 .stripe_width
927 .checked_sub(observed_volume_count)
928 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
929 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
930 return Err(FormatError::InvalidArchive(
931 "missing volume count exceeds volume_loss_tolerance",
932 ));
933 }
934
935 let mut blocks = BTreeMap::new();
936 let sidecar = if let Some(bytes) = bootstrap_sidecar {
937 let sidecar = parse_bootstrap_sidecar(
938 bytes,
939 &first.volume_header,
940 &first.crypto_header,
941 &first.subkeys,
942 )?;
943 sidecar
944 .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
945 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
946 if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
947 return Err(FormatError::InvalidArchive(
948 "bootstrap sidecar conflicts with terminal ManifestFooter",
949 ));
950 }
951 }
952 Some((bytes, sidecar))
953 } else {
954 None
955 };
956
957 if let Some((sidecar_bytes, sidecar)) = &sidecar {
958 if let Some((offset, length)) = sidecar.index_root_records_section {
959 let index_root_records = parse_sidecar_block_records(
960 sidecar_bytes,
961 first.crypto_header.block_size as usize,
962 SidecarBlockRecordsSection {
963 offset,
964 length,
965 extent: index_root_extent_from_manifest(&manifest_footer),
966 data_kind: BlockKind::IndexRootData,
967 parity_kind: BlockKind::IndexRootParity,
968 structure: "IndexRoot",
969 },
970 )?;
971 insert_sidecar_records(&mut blocks, index_root_records)?;
972 }
973 }
974
975 let lazy_source = Arc::new(SeekableBlockSource {
976 stripe_width: first.crypto_header.stripe_width,
977 volumes: lazy_volume_slots,
978 });
979 let block_provider = OpenedBlockProvider {
980 memory_blocks: &blocks,
981 lazy_blocks: Some(lazy_source.as_ref()),
982 };
983 let limits = metadata_limits(&first.crypto_header);
984 let index_root_plaintext = load_metadata_object_from_parts(
985 &block_provider,
986 ObjectLoadContext::index_root(
987 &first.volume_header,
988 &first.crypto_header,
989 &first.subkeys,
990 index_root_extent_from_manifest(&manifest_footer),
991 ),
992 manifest_footer.index_root_decompressed_size,
993 )?;
994 let index_root = IndexRoot::parse(
995 &index_root_plaintext,
996 first.crypto_header.has_dictionary != 0,
997 limits,
998 )?;
999 if first.crypto_header.has_dictionary != 0 {
1000 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1001 if let Some((offset, length)) = sidecar.dictionary_records_section {
1002 let dictionary_records = parse_sidecar_block_records(
1003 sidecar_bytes,
1004 first.crypto_header.block_size as usize,
1005 SidecarBlockRecordsSection {
1006 offset,
1007 length,
1008 extent: dictionary_extent_from_index_root(&index_root)?,
1009 data_kind: BlockKind::DictionaryData,
1010 parity_kind: BlockKind::DictionaryParity,
1011 structure: "Dictionary",
1012 },
1013 )?;
1014 insert_sidecar_records(&mut blocks, dictionary_records)?;
1015 }
1016 }
1017 }
1018 let block_provider = OpenedBlockProvider {
1019 memory_blocks: &blocks,
1020 lazy_blocks: Some(lazy_source.as_ref()),
1021 };
1022 let payload_dictionary = load_archive_dictionary(
1023 &block_provider,
1024 &first.subkeys,
1025 &first.volume_header,
1026 &first.crypto_header,
1027 &index_root,
1028 )?;
1029
1030 Ok(Self {
1031 options,
1032 observed_archive_bytes,
1033 observed_volume_count,
1034 subkeys: first.subkeys,
1035 blocks,
1036 lazy_blocks: Some(lazy_source),
1037 crypto_header_bytes: first.crypto_header_bytes,
1038 volume_header: authority_volume_header,
1039 crypto_header: first.crypto_header,
1040 manifest_footer,
1041 volume_trailer: Some(authority_volume_trailer),
1042 root_auth_footer: root_auth_authority,
1043 index_root,
1044 payload_dictionary,
1045 })
1046 }
1047
1048 pub fn open_with_bootstrap_sidecar_options(
1049 bytes: &[u8],
1050 bootstrap_sidecar: &[u8],
1051 master_key: &MasterKey,
1052 options: ReaderOptions,
1053 ) -> Result<Self, FormatError> {
1054 Self::open_with_bootstrap_sidecar_options_for_mode(
1055 bytes,
1056 bootstrap_sidecar,
1057 master_key,
1058 options,
1059 BootstrapSidecarUse::SeekableAssist,
1060 )
1061 }
1062
1063 fn open_with_bootstrap_sidecar_options_for_mode(
1064 bytes: &[u8],
1065 bootstrap_sidecar: &[u8],
1066 master_key: &MasterKey,
1067 options: ReaderOptions,
1068 sidecar_use: BootstrapSidecarUse,
1069 ) -> Result<Self, FormatError> {
1070 let observed_archive_bytes =
1071 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1072 if bytes.len() < VOLUME_HEADER_LEN {
1073 return Err(FormatError::InvalidLength {
1074 structure: "archive",
1075 expected: VOLUME_HEADER_LEN,
1076 actual: bytes.len(),
1077 });
1078 }
1079
1080 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1081 let crypto_start = volume_header.crypto_header_offset as usize;
1082 let crypto_len = volume_header.crypto_header_length as usize;
1083 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1084 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1085 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1086 let subkeys = Subkeys::derive(
1087 master_key,
1088 &volume_header.archive_uuid,
1089 &volume_header.session_id,
1090 )?;
1091 verify_hmac(
1092 HmacDomain::CryptoHeader,
1093 &subkeys.mac_key,
1094 &volume_header.archive_uuid,
1095 &volume_header.session_id,
1096 parsed_crypto.hmac_covered_bytes,
1097 &parsed_crypto.header_hmac,
1098 )?;
1099 parsed_crypto.validate_extension_semantics()?;
1100 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1101 validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1102 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1103
1104 let sidecar = parse_bootstrap_sidecar(
1105 bootstrap_sidecar,
1106 &volume_header,
1107 &parsed_crypto.fixed,
1108 &subkeys,
1109 )?;
1110 sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1111
1112 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1113 bytes,
1114 crypto_end,
1115 parsed_crypto.fixed.block_size as usize,
1116 &volume_header,
1117 )?;
1118 let terminal_material = match sidecar_use {
1119 BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1120 bytes,
1121 terminal_offset,
1122 observed_block_count,
1123 KeyHoldingTerminalContext {
1124 subkeys: &subkeys,
1125 volume_header: &volume_header,
1126 crypto_header: &parsed_crypto.fixed,
1127 crypto_header_bytes: crypto_bytes,
1128 },
1129 options,
1130 )?),
1131 BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1132 bytes,
1133 terminal_offset,
1134 observed_block_count,
1135 KeyHoldingTerminalContext {
1136 subkeys: &subkeys,
1137 volume_header: &volume_header,
1138 crypto_header: &parsed_crypto.fixed,
1139 crypto_header_bytes: crypto_bytes,
1140 },
1141 options,
1142 )
1143 .ok(),
1144 };
1145 let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1146 let manifest_authority = match sidecar_use {
1147 BootstrapSidecarUse::SeekableAssist => {
1148 let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1149 "terminal ManifestFooter/VolumeTrailer is required",
1150 ))?;
1151 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1152 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1153 return Err(FormatError::InvalidArchive(
1154 "bootstrap sidecar conflicts with terminal ManifestFooter",
1155 ));
1156 }
1157 }
1158 terminal_manifest.clone()
1159 }
1160 BootstrapSidecarUse::NonSeekableRandomAccess => {
1161 let sidecar_manifest = sidecar
1162 .manifest_footer
1163 .as_ref()
1164 .ok_or(FormatError::ReaderUnsupported(
1165 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1166 ))?;
1167 if let Some(terminal_manifest) = terminal_manifest {
1168 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1169 return Err(FormatError::InvalidArchive(
1170 "bootstrap sidecar conflicts with terminal ManifestFooter",
1171 ));
1172 }
1173 }
1174 sidecar_manifest.clone()
1175 }
1176 };
1177 manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1178
1179 if let Some((offset, length)) = sidecar.index_root_records_section {
1180 let index_root_records = parse_sidecar_block_records(
1181 bootstrap_sidecar,
1182 parsed_crypto.fixed.block_size as usize,
1183 SidecarBlockRecordsSection {
1184 offset,
1185 length,
1186 extent: index_root_extent_from_manifest(&manifest_authority),
1187 data_kind: BlockKind::IndexRootData,
1188 parity_kind: BlockKind::IndexRootParity,
1189 structure: "IndexRoot",
1190 },
1191 )?;
1192 insert_sidecar_records(&mut blocks, index_root_records)?;
1193 }
1194
1195 let limits = metadata_limits(&parsed_crypto.fixed);
1196 let index_root_plaintext = load_metadata_object_from_parts(
1197 &blocks,
1198 ObjectLoadContext::index_root(
1199 &volume_header,
1200 &parsed_crypto.fixed,
1201 &subkeys,
1202 index_root_extent_from_manifest(&manifest_authority),
1203 ),
1204 manifest_authority.index_root_decompressed_size,
1205 )?;
1206 let index_root = IndexRoot::parse(
1207 &index_root_plaintext,
1208 parsed_crypto.fixed.has_dictionary != 0,
1209 limits,
1210 )?;
1211 if parsed_crypto.fixed.has_dictionary != 0 {
1212 if let Some((offset, length)) = sidecar.dictionary_records_section {
1213 let dictionary_records = parse_sidecar_block_records(
1214 bootstrap_sidecar,
1215 parsed_crypto.fixed.block_size as usize,
1216 SidecarBlockRecordsSection {
1217 offset,
1218 length,
1219 extent: dictionary_extent_from_index_root(&index_root)?,
1220 data_kind: BlockKind::DictionaryData,
1221 parity_kind: BlockKind::DictionaryParity,
1222 structure: "dictionary",
1223 },
1224 )?;
1225 insert_sidecar_records(&mut blocks, dictionary_records)?;
1226 }
1227 }
1228 let payload_dictionary = load_archive_dictionary(
1229 &blocks,
1230 &subkeys,
1231 &volume_header,
1232 &parsed_crypto.fixed,
1233 &index_root,
1234 )?;
1235
1236 Ok(Self {
1237 options,
1238 observed_archive_bytes,
1239 observed_volume_count: 1,
1240 subkeys,
1241 blocks,
1242 lazy_blocks: None,
1243 crypto_header_bytes: crypto_bytes.to_vec(),
1244 volume_header,
1245 crypto_header: parsed_crypto.fixed,
1246 manifest_footer: manifest_authority,
1247 volume_trailer: terminal_material
1248 .as_ref()
1249 .map(|(_, trailer, _)| trailer.clone()),
1250 root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
1251 index_root,
1252 payload_dictionary,
1253 })
1254 }
1255
1256 pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
1262 let shards = self.load_all_index_shards()?;
1263 final_index_entry_winners(&shards)?
1264 .into_iter()
1265 .map(|(path, winner)| {
1266 Ok(ArchiveIndexEntry {
1267 path,
1268 file_data_size: winner.file_data_size,
1269 })
1270 })
1271 .collect()
1272 }
1273
1274 pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
1276 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1277 self.locate_index_file(&normalized)?
1278 .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
1279 .transpose()
1280 }
1281
1282 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
1283 let shards = self.load_all_index_shards()?;
1284 final_index_entry_winners(&shards)?
1285 .into_iter()
1286 .map(|(path, winner)| {
1287 let shard = &shards[winner.shard_index];
1288 let member =
1289 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
1290 Ok(ArchiveEntry {
1291 path,
1292 file_data_size: winner.file_data_size,
1293 kind: member.kind,
1294 mode: member.mode,
1295 mtime: member.mtime,
1296 diagnostics: member.diagnostics,
1297 })
1298 })
1299 .collect()
1300 }
1301
1302 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
1309 self.extract_member(path)?
1310 .map(|member| {
1311 if member.kind != TarEntryKind::Regular {
1312 return Err(FormatError::ReaderUnsupported(
1313 "extract_file returns only regular file payloads",
1314 ));
1315 }
1316 Ok(member.data)
1317 })
1318 .transpose()
1319 }
1320
1321 pub fn extract_file_with_diagnostics(
1324 &self,
1325 path: &str,
1326 ) -> Result<Option<ExtractedRegularFile>, FormatError> {
1327 self.extract_member(path)?
1328 .map(|member| {
1329 if member.kind != TarEntryKind::Regular {
1330 return Err(FormatError::ReaderUnsupported(
1331 "extract_file_with_diagnostics returns only regular file payloads",
1332 ));
1333 }
1334 Ok((member.data, member.diagnostics))
1335 })
1336 .transpose()
1337 }
1338
1339 pub fn extract_file_to_writer<W: Write>(
1345 &self,
1346 path: &str,
1347 writer: &mut W,
1348 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1349 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1350 self.locate_index_file(&normalized)?
1351 .map(|located| {
1352 self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
1353 })
1354 .transpose()
1355 }
1356
1357 pub fn extract_member(
1358 &self,
1359 path: &str,
1360 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
1361 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1362 self.locate_index_file(&normalized)?
1363 .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
1364 .transpose()
1365 }
1366
1367 pub fn extract_file_to(
1368 &self,
1369 path: &str,
1370 root: &std::path::Path,
1371 options: SafeExtractionOptions,
1372 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
1373 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1374 self.locate_index_file(&normalized)?
1375 .map(|located| {
1376 self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
1377 })
1378 .transpose()
1379 }
1380
1381 pub fn verify(&self) -> Result<(), FormatError> {
1382 self.verify_content().map(|_| ())
1383 }
1384
1385 pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
1386 if let Some(source) = &self.lazy_blocks {
1387 if source.is_complete_volume_set() {
1388 source.validate_complete_coverage()?;
1389 }
1390 }
1391 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
1392 && self.index_root.directory_hint_shards.is_empty()
1393 {
1394 return Err(FormatError::InvalidArchive(
1395 "IndexRoot file_count requires directory hints",
1396 ));
1397 }
1398
1399 let shards = self.load_all_index_shards()?;
1400 let mut file_count = 0u64;
1401 let mut frames = BTreeMap::<u64, FrameEntry>::new();
1402 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
1403
1404 for shard in &shards {
1405 file_count = file_count
1406 .checked_add(shard.files.len() as u64)
1407 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
1408 for frame in &shard.frames {
1409 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
1410 if existing != *frame {
1411 return Err(FormatError::InvalidArchive(
1412 "duplicate FrameEntry rows do not match",
1413 ));
1414 }
1415 }
1416 }
1417 for envelope in &shard.envelopes {
1418 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
1419 {
1420 if existing != *envelope {
1421 return Err(FormatError::InvalidArchive(
1422 "duplicate EnvelopeEntry rows do not match",
1423 ));
1424 }
1425 }
1426 }
1427 }
1428 validate_global_file_table_order(&shards)?;
1429
1430 if file_count != self.index_root.header.file_count {
1431 return Err(FormatError::InvalidArchive(
1432 "IndexRoot file_count does not match decoded shards",
1433 ));
1434 }
1435 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
1436 verify_dense_keys(
1437 &envelopes,
1438 self.index_root.header.envelope_count,
1439 "EnvelopeEntry",
1440 )?;
1441 validate_envelope_frame_coverage(&frames, &envelopes)?;
1442 self.validate_encrypted_object_block_ranges(&envelopes)?;
1443
1444 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
1445 sum.checked_add(envelope.data_block_count as u64)
1446 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1447 })?;
1448 if payload_block_count != self.index_root.header.payload_block_count {
1449 return Err(FormatError::InvalidArchive(
1450 "IndexRoot payload_block_count does not match envelopes",
1451 ));
1452 }
1453
1454 let tar_len = self.index_root.header.tar_total_size;
1455 let mut content_hasher = Sha256::new();
1456 let mut tar_cursor = 0u64;
1457 let mut cached_envelope_index = None;
1458 let mut cached_envelope_plaintext = Vec::new();
1459
1460 for frame in frames.values() {
1461 let envelope =
1462 envelopes
1463 .get(&frame.envelope_index)
1464 .ok_or(FormatError::InvalidArchive(
1465 "FrameEntry references missing EnvelopeEntry",
1466 ))?;
1467 if cached_envelope_index != Some(envelope.envelope_index) {
1468 cached_envelope_plaintext = self.load_payload_envelope(envelope)?;
1469 cached_envelope_index = Some(envelope.envelope_index);
1470 }
1471 let compressed = slice(
1472 &cached_envelope_plaintext,
1473 frame.offset_in_envelope as usize,
1474 frame.compressed_size as usize,
1475 "FrameEntry",
1476 )?;
1477 let decoded = self.decompress_payload_frame(compressed, frame.decompressed_size)?;
1478 if frame.tar_stream_offset != tar_cursor {
1479 return Err(FormatError::InvalidArchive(
1480 "decoded frames leave tar gap or overlap",
1481 ));
1482 }
1483 tar_cursor = tar_cursor
1484 .checked_add(decoded.len() as u64)
1485 .ok_or(FormatError::InvalidArchive("tar stream size overflow"))?;
1486 if tar_cursor > tar_len {
1487 return Err(FormatError::InvalidArchive(
1488 "FrameEntry exceeds IndexRoot tar_total_size",
1489 ));
1490 }
1491 content_hasher.update(&decoded);
1492 }
1493
1494 if tar_cursor != tar_len {
1495 return Err(FormatError::InvalidArchive("decoded frames leave tar gap"));
1496 }
1497 if content_hasher.finalize().as_slice() != self.index_root.header.content_sha256 {
1498 return Err(FormatError::InvalidArchive(
1499 "IndexRoot content_sha256 does not match decoded tar stream",
1500 ));
1501 }
1502
1503 let mut file_extents = Vec::new();
1504 let mut directory_hint_map = DirectoryHintMap::new();
1505 for (shard_row_index, shard) in shards.iter().enumerate() {
1506 let shard_row_index = u32::try_from(shard_row_index)
1507 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
1508 for idx in 0..shard.files.len() {
1509 let file = &shard.files[idx];
1510 let start =
1511 shard
1512 .tar_member_group_start(idx)
1513 .ok_or(FormatError::InvalidArchive(
1514 "FileEntry tar member start is missing",
1515 ))?;
1516 file_extents.push((start, file.tar_member_group_size));
1517 let member = self.decode_loaded_owned_tar_member(shard, idx, false)?;
1518 let path = shard
1519 .file_path(idx)
1520 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
1521 add_expected_directory_hint_rows(
1522 &mut directory_hint_map,
1523 shard_row_index,
1524 path,
1525 member.kind,
1526 );
1527 }
1528 }
1529 validate_file_extent_coverage_ranges(&file_extents, tar_len)?;
1530 if !self.index_root.directory_hint_shards.is_empty() {
1531 let hint_tables = self.load_all_directory_hint_tables()?;
1532 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
1533 }
1534
1535 Ok(ArchiveContentVerification { archive: self })
1536 }
1537
1538 pub(crate) fn from_streamed_parts(
1539 parts: StreamedArchiveOpenParts,
1540 ) -> Result<Self, FormatError> {
1541 let limits = metadata_limits(&parts.crypto_header);
1542 let index_root_plaintext = load_metadata_object_from_parts(
1543 &parts.blocks,
1544 ObjectLoadContext::index_root(
1545 &parts.volume_header,
1546 &parts.crypto_header,
1547 &parts.subkeys,
1548 ObjectExtent {
1549 first_block_index: parts.manifest_footer.index_root_first_block,
1550 data_block_count: parts.manifest_footer.index_root_data_block_count,
1551 parity_block_count: parts.manifest_footer.index_root_parity_block_count,
1552 encrypted_size: parts.manifest_footer.index_root_encrypted_size,
1553 },
1554 ),
1555 parts.manifest_footer.index_root_decompressed_size,
1556 )?;
1557 let index_root = IndexRoot::parse(
1558 &index_root_plaintext,
1559 parts.crypto_header.has_dictionary != 0,
1560 limits,
1561 )?;
1562 let payload_dictionary = load_archive_dictionary(
1563 &parts.blocks,
1564 &parts.subkeys,
1565 &parts.volume_header,
1566 &parts.crypto_header,
1567 &index_root,
1568 )?;
1569
1570 Ok(Self {
1571 options: parts.options,
1572 observed_archive_bytes: parts.observed_archive_bytes,
1573 observed_volume_count: 1,
1574 subkeys: parts.subkeys,
1575 blocks: parts.blocks,
1576 lazy_blocks: None,
1577 crypto_header_bytes: parts.crypto_header_bytes,
1578 volume_header: parts.volume_header,
1579 crypto_header: parts.crypto_header,
1580 manifest_footer: parts.manifest_footer,
1581 volume_trailer: Some(parts.volume_trailer),
1582 root_auth_footer: parts.root_auth_footer,
1583 index_root,
1584 payload_dictionary,
1585 })
1586 }
1587
1588 pub(crate) fn verify_streamed_payload_summary(
1589 &self,
1590 streamed: &StreamedPayloadSummary,
1591 ) -> Result<(), FormatError> {
1592 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
1593 && self.index_root.directory_hint_shards.is_empty()
1594 {
1595 return Err(FormatError::InvalidArchive(
1596 "IndexRoot file_count requires directory hints",
1597 ));
1598 }
1599 if streamed.tar.total_extraction_size
1600 > total_extraction_size_cap(self.options, self.observed_archive_bytes)
1601 {
1602 return Err(FormatError::ReaderUnsupported(
1603 "total extraction size exceeds configured cap",
1604 ));
1605 }
1606
1607 let shards = self.load_all_index_shards()?;
1608 let mut file_count = 0u64;
1609 let mut frames = BTreeMap::<u64, FrameEntry>::new();
1610 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
1611
1612 for shard in &shards {
1613 file_count = file_count
1614 .checked_add(shard.files.len() as u64)
1615 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
1616 for frame in &shard.frames {
1617 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
1618 if existing != *frame {
1619 return Err(FormatError::InvalidArchive(
1620 "duplicate FrameEntry rows do not match",
1621 ));
1622 }
1623 }
1624 }
1625 for envelope in &shard.envelopes {
1626 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
1627 {
1628 if existing != *envelope {
1629 return Err(FormatError::InvalidArchive(
1630 "duplicate EnvelopeEntry rows do not match",
1631 ));
1632 }
1633 }
1634 }
1635 }
1636 validate_global_file_table_order(&shards)?;
1637
1638 if file_count != self.index_root.header.file_count {
1639 return Err(FormatError::InvalidArchive(
1640 "IndexRoot file_count does not match decoded shards",
1641 ));
1642 }
1643 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
1644 verify_dense_keys(
1645 &envelopes,
1646 self.index_root.header.envelope_count,
1647 "EnvelopeEntry",
1648 )?;
1649 validate_envelope_frame_coverage(&frames, &envelopes)?;
1650 self.validate_encrypted_object_block_ranges(&envelopes)?;
1651
1652 let metadata_payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
1653 sum.checked_add(envelope.data_block_count as u64)
1654 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1655 })?;
1656 if metadata_payload_block_count != self.index_root.header.payload_block_count {
1657 return Err(FormatError::InvalidArchive(
1658 "IndexRoot payload_block_count does not match envelopes",
1659 ));
1660 }
1661 let streamed_payload_block_count =
1662 streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
1663 sum.checked_add(envelope.data_block_count as u64)
1664 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1665 })?;
1666 if streamed_payload_block_count != self.index_root.header.payload_block_count {
1667 return Err(FormatError::InvalidArchive(
1668 "streamed payload block count does not match IndexRoot",
1669 ));
1670 }
1671
1672 if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
1673 return Err(FormatError::InvalidArchive(
1674 "IndexRoot tar_total_size does not match streamed tar stream",
1675 ));
1676 }
1677 if streamed.content_sha256 != self.index_root.header.content_sha256 {
1678 return Err(FormatError::InvalidArchive(
1679 "IndexRoot content_sha256 does not match streamed tar stream",
1680 ));
1681 }
1682
1683 let streamed_envelopes = streamed.envelope_map()?;
1684 for envelope in envelopes.values() {
1685 let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
1686 FormatError::InvalidArchive(
1687 "metadata references missing streamed payload envelope",
1688 ),
1689 )?;
1690 if actual.first_block_index != envelope.first_block_index
1691 || actual.data_block_count != envelope.data_block_count
1692 || actual.parity_block_count != envelope.parity_block_count
1693 || actual.encrypted_size != envelope.encrypted_size
1694 || actual.plaintext_size != envelope.plaintext_size
1695 || actual.first_frame_index != envelope.first_frame_index
1696 || actual.frame_count != envelope.frame_count
1697 {
1698 return Err(FormatError::InvalidArchive(
1699 "EnvelopeEntry does not match streamed payload envelope",
1700 ));
1701 }
1702 }
1703
1704 let streamed_frames = streamed.frame_map()?;
1705 for frame in frames.values() {
1706 let actual =
1707 streamed_frames
1708 .get(&frame.frame_index)
1709 .ok_or(FormatError::InvalidArchive(
1710 "metadata references missing streamed payload frame",
1711 ))?;
1712 if actual.envelope_index != frame.envelope_index
1713 || actual.offset_in_envelope != frame.offset_in_envelope
1714 || actual.compressed_size != frame.compressed_size
1715 || actual.decompressed_size != frame.decompressed_size
1716 || actual.tar_stream_offset != frame.tar_stream_offset
1717 || streamed.frame_flags(actual)? != frame.flags
1718 {
1719 return Err(FormatError::InvalidArchive(
1720 "FrameEntry does not match streamed payload frame",
1721 ));
1722 }
1723 }
1724
1725 let streamed_members = streamed.member_start_map()?;
1726 if streamed.tar.members.len() as u64 != file_count {
1727 return Err(FormatError::InvalidArchive(
1728 "streamed tar member count does not match decoded shards",
1729 ));
1730 }
1731 let mut file_extents = Vec::new();
1732 let mut directory_hint_map = DirectoryHintMap::new();
1733 for (shard_row_index, shard) in shards.iter().enumerate() {
1734 let shard_row_index = u32::try_from(shard_row_index)
1735 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
1736 for idx in 0..shard.files.len() {
1737 let file = &shard.files[idx];
1738 let start =
1739 shard
1740 .tar_member_group_start(idx)
1741 .ok_or(FormatError::InvalidArchive(
1742 "FileEntry tar member start is missing",
1743 ))?;
1744 file_extents.push((start, file.tar_member_group_size));
1745 let path = shard
1746 .file_path(idx)
1747 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
1748 let member = streamed_members
1749 .get(&start)
1750 .ok_or(FormatError::InvalidArchive(
1751 "FileEntry tar member start is missing from streamed tar",
1752 ))?;
1753 if member.path != path
1754 || member.logical_size != file.file_data_size
1755 || member.group_size != file.tar_member_group_size
1756 {
1757 return Err(FormatError::InvalidArchive(
1758 "FileEntry does not match streamed tar member",
1759 ));
1760 }
1761 add_expected_directory_hint_rows(
1762 &mut directory_hint_map,
1763 shard_row_index,
1764 path,
1765 member.kind,
1766 );
1767 }
1768 }
1769 validate_file_extent_coverage_ranges(&file_extents, streamed.tar.tar_total_size)?;
1770 if !self.index_root.directory_hint_shards.is_empty() {
1771 let hint_tables = self.load_all_directory_hint_tables()?;
1772 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
1773 }
1774
1775 Ok(())
1776 }
1777
1778 pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
1779 where
1780 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
1781 {
1782 let content_verification = self.verify_content()?;
1783 self.verify_root_auth_with_verified_content(&content_verification, verifier)
1784 }
1785
1786 pub fn verify_root_auth_with_verified_content<F>(
1787 &self,
1788 content_verification: &ArchiveContentVerification<'_>,
1789 mut verifier: F,
1790 ) -> Result<RootAuthVerification, FormatError>
1791 where
1792 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
1793 {
1794 if !std::ptr::eq(content_verification.archive, self) {
1795 return Err(FormatError::InvalidArchive(
1796 "content verification does not match archive",
1797 ));
1798 }
1799 let footer = self
1800 .root_auth_footer
1801 .as_ref()
1802 .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
1803 let material = self.recompute_root_auth_material(footer)?;
1804 if material.critical_metadata_digest != footer.critical_metadata_digest
1805 || material.index_digest != footer.index_digest
1806 || material.fec_layout_digest != footer.fec_layout_digest
1807 || material.data_block_merkle_root != footer.data_block_merkle_root
1808 || material.signer_identity_digest != footer.signer_identity_digest
1809 || material.archive_root != footer.archive_root
1810 || material.total_data_block_count != footer.total_data_block_count
1811 {
1812 return Err(FormatError::InvalidArchive(
1813 "RootAuthFooter commitments do not match recomputed archive root",
1814 ));
1815 }
1816 if !verifier(footer, &material.archive_root)? {
1817 return Err(FormatError::InvalidArchive(
1818 "root-auth authenticator verification failed",
1819 ));
1820 }
1821 Ok(RootAuthVerification {
1822 archive_root: material.archive_root,
1823 authenticator_id: footer.authenticator_id,
1824 signer_identity_type: footer.signer_identity_type,
1825 signer_identity_bytes: footer.signer_identity_bytes.clone(),
1826 total_data_block_count: footer.total_data_block_count,
1827 diagnostics: self.root_auth_success_diagnostics(),
1828 })
1829 }
1830
1831 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
1832 self.index_root
1833 .shards
1834 .iter()
1835 .map(|entry| self.load_index_shard(entry))
1836 .collect()
1837 }
1838
1839 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
1840 let block_provider = self.block_provider();
1841 let plaintext = load_metadata_object_from_parts(
1842 &block_provider,
1843 ObjectLoadContext::index_shard(
1844 &self.volume_header,
1845 &self.crypto_header,
1846 &self.subkeys,
1847 entry,
1848 ),
1849 entry.decompressed_size,
1850 )?;
1851 IndexShard::parse(&plaintext, entry, self.metadata_limits())
1852 }
1853
1854 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
1855 self.index_root
1856 .directory_hint_shards
1857 .iter()
1858 .map(|entry| self.load_directory_hint_table(entry))
1859 .collect()
1860 }
1861
1862 fn load_directory_hint_table(
1863 &self,
1864 entry: &DirectoryHintShardEntry,
1865 ) -> Result<DirectoryHintTable, FormatError> {
1866 let block_provider = self.block_provider();
1867 let plaintext = load_metadata_object_from_parts(
1868 &block_provider,
1869 ObjectLoadContext::directory_hint(
1870 &self.volume_header,
1871 &self.crypto_header,
1872 &self.subkeys,
1873 entry,
1874 ),
1875 entry.decompressed_size,
1876 )?;
1877 DirectoryHintTable::parse(
1878 &plaintext,
1879 entry,
1880 self.index_root.header.shard_count,
1881 self.metadata_limits(),
1882 )
1883 }
1884
1885 fn load_payload_envelope(&self, envelope: &EnvelopeEntry) -> Result<Vec<u8>, FormatError> {
1886 let block_provider = self.block_provider();
1887 let plaintext = load_decrypted_object_from_parts(
1888 &block_provider,
1889 ObjectLoadContext::payload(
1890 &self.volume_header,
1891 &self.crypto_header,
1892 &self.subkeys,
1893 envelope,
1894 ),
1895 )?;
1896 if plaintext.len() != envelope.plaintext_size as usize {
1897 return Err(FormatError::InvalidArchive(
1898 "payload envelope plaintext_size mismatch",
1899 ));
1900 }
1901 Ok(plaintext)
1902 }
1903
1904 fn locate_index_file(
1905 &self,
1906 normalized: &[u8],
1907 ) -> Result<Option<LocatedIndexFile>, FormatError> {
1908 let candidate_indexes = self
1909 .index_root
1910 .candidate_shards_for_path(normalized, self.metadata_limits())?;
1911 let mut winner: Option<LocatedIndexFile> = None;
1912
1913 for row_index in candidate_indexes {
1914 let locating =
1915 self.index_root
1916 .shards
1917 .get(row_index)
1918 .ok_or(FormatError::InvalidArchive(
1919 "candidate shard row is out of bounds",
1920 ))?;
1921 let shard = self.load_index_shard(locating)?;
1922 if let Some(file_index) = shard.lookup_file_index(normalized) {
1923 let start =
1924 shard
1925 .tar_member_group_start(file_index)
1926 .ok_or(FormatError::InvalidArchive(
1927 "FileEntry tar member start is missing",
1928 ))?;
1929 if winner
1930 .as_ref()
1931 .map(|existing| start > existing.start)
1932 .unwrap_or(true)
1933 {
1934 winner = Some(LocatedIndexFile {
1935 shard,
1936 file_index,
1937 start,
1938 });
1939 }
1940 }
1941 }
1942
1943 Ok(winner)
1944 }
1945
1946 fn extract_loaded_member(
1947 &self,
1948 shard: &IndexShard,
1949 file_index: usize,
1950 ) -> Result<ExtractedArchiveMember, FormatError> {
1951 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
1952 Ok(ExtractedArchiveMember {
1953 path: utf8_path(&member.path)?,
1954 kind: member.kind,
1955 data: member.data,
1956 link_target: member
1957 .link_target
1958 .map(|target| utf8_path(&target))
1959 .transpose()?,
1960 diagnostics: member.diagnostics,
1961 })
1962 }
1963
1964 fn extract_loaded_owned_tar_member(
1965 &self,
1966 shard: &IndexShard,
1967 file_index: usize,
1968 ) -> Result<OwnedTarMember, FormatError> {
1969 self.decode_loaded_owned_tar_member(shard, file_index, true)
1970 }
1971
1972 fn stream_loaded_file_to_writer<W: Write>(
1973 &self,
1974 shard: &IndexShard,
1975 file_index: usize,
1976 writer: &mut W,
1977 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
1978 let file = shard
1979 .files
1980 .get(file_index)
1981 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
1982 self.validate_total_extraction_size(file.file_data_size)?;
1983 let expected_path = shard
1984 .file_path(file_index)
1985 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
1986 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file);
1987 stream_regular_tar_member_group_to_writer(
1988 &mut reader,
1989 expected_path,
1990 file.file_data_size,
1991 file.tar_member_group_size,
1992 self.crypto_header.max_path_length,
1993 writer,
1994 )
1995 }
1996
1997 fn stream_loaded_file_to_path(
1998 &self,
1999 shard: &IndexShard,
2000 file_index: usize,
2001 root: &std::path::Path,
2002 options: SafeExtractionOptions,
2003 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
2004 let file = shard
2005 .files
2006 .get(file_index)
2007 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2008 self.validate_total_extraction_size(file.file_data_size)?;
2009 let expected_path = shard
2010 .file_path(file_index)
2011 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2012 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file);
2013 restore_streaming_tar_member_group(
2014 root,
2015 expected_path,
2016 file.file_data_size,
2017 file.tar_member_group_size,
2018 self.crypto_header.max_path_length,
2019 options,
2020 &mut reader,
2021 )
2022 .map_err(format_error_from_extract_error)
2023 }
2024
2025 fn decode_loaded_owned_tar_member(
2026 &self,
2027 shard: &IndexShard,
2028 file_index: usize,
2029 enforce_extraction_cap: bool,
2030 ) -> Result<OwnedTarMember, FormatError> {
2031 let file = shard
2032 .files
2033 .get(file_index)
2034 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2035 if enforce_extraction_cap {
2036 self.validate_total_extraction_size(file.file_data_size)?;
2037 }
2038 let expected_path = shard
2039 .file_path(file_index)
2040 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2041 let frames = frame_range_for_file(shard, file)?;
2042 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
2043 let mut decoded = Vec::new();
2044
2045 for frame in frames {
2046 let envelope = shard
2047 .envelopes
2048 .iter()
2049 .find(|entry| entry.envelope_index == frame.envelope_index)
2050 .ok_or(FormatError::InvalidArchive(
2051 "FrameEntry references missing EnvelopeEntry",
2052 ))?;
2053 if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
2054 entry.insert(self.load_payload_envelope(envelope)?);
2055 }
2056 let envelope_plaintext = envelope_cache
2057 .get(&envelope.envelope_index)
2058 .expect("inserted above");
2059 let compressed = slice(
2060 envelope_plaintext,
2061 frame.offset_in_envelope as usize,
2062 frame.compressed_size as usize,
2063 "FrameEntry",
2064 )?;
2065 decoded.extend_from_slice(
2066 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
2067 );
2068 }
2069
2070 let offset = file.offset_in_first_frame_plaintext as usize;
2071 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
2072 let group = slice(&decoded, offset, group_len, "FileEntry")?;
2073 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
2074 if member.path != expected_path {
2075 return Err(FormatError::InvalidArchive(
2076 "tar member path does not match FileEntry path",
2077 ));
2078 }
2079 if member.logical_size != file.file_data_size {
2080 return Err(FormatError::InvalidArchive(
2081 "tar member size does not match FileEntry file_data_size",
2082 ));
2083 }
2084 Ok(member.to_owned_member())
2085 }
2086
2087 fn metadata_limits(&self) -> MetadataLimits {
2088 metadata_limits(&self.crypto_header)
2089 }
2090
2091 fn recompute_root_auth_material(
2092 &self,
2093 footer: &RootAuthFooterV1,
2094 ) -> Result<RootAuthMaterial, FormatError> {
2095 let footer_length = footer.footer_length()?;
2096 let root_auth_descriptor_digest = root_auth_descriptor_digest(
2097 footer.authenticator_id,
2098 footer.signer_identity_type,
2099 &footer.signer_identity_bytes,
2100 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
2101 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
2102 })?,
2103 footer_length,
2104 )?;
2105 let signer_identity_digest =
2106 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
2107 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
2108 let crypto_pre_hmac_len = self
2109 .crypto_header_bytes
2110 .len()
2111 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
2112 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
2113 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
2114 archive_uuid: self.volume_header.archive_uuid,
2115 session_id: self.volume_header.session_id,
2116 stripe_width: self.crypto_header.stripe_width,
2117 total_volumes: self.manifest_footer.total_volumes,
2118 compression_algo: self.crypto_header.compression_algo,
2119 aead_algo: self.crypto_header.aead_algo,
2120 fec_algo: self.crypto_header.fec_algo,
2121 kdf_algo: self.crypto_header.kdf_algo,
2122 crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
2123 chunk_size: self.crypto_header.chunk_size,
2124 envelope_target_size: self.crypto_header.envelope_target_size,
2125 block_size: self.crypto_header.block_size,
2126 fec_data_shards: self.crypto_header.fec_data_shards,
2127 fec_parity_shards: self.crypto_header.fec_parity_shards,
2128 index_fec_data_shards: self.crypto_header.index_fec_data_shards,
2129 index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
2130 index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
2131 index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
2132 volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
2133 bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
2134 has_dictionary: self.crypto_header.has_dictionary,
2135 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
2136 index_root_first_block: self.manifest_footer.index_root_first_block,
2137 index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
2138 index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
2139 index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
2140 index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
2141 root_auth_descriptor_digest,
2142 })?;
2143 let index_root_plaintext = self.index_root.to_bytes();
2144 let index_digest = index_digest(&index_root_plaintext);
2145 let shards = self.load_all_index_shards()?;
2146 let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
2147 let fec_layout_digest = fec_layout_digest(&fec_layout_rows)?;
2148 let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
2149 let total_data_block_count = u64::try_from(data_leaves.len())
2150 .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
2151 let data_block_merkle_root = data_block_merkle_root(&data_leaves);
2152 let archive_root = archive_root(ArchiveRootInputs {
2153 archive_uuid: self.volume_header.archive_uuid,
2154 session_id: self.volume_header.session_id,
2155 format_version: FORMAT_VERSION,
2156 volume_format_rev: VOLUME_FORMAT_REV,
2157 compression_algo: self.crypto_header.compression_algo,
2158 aead_algo: self.crypto_header.aead_algo,
2159 fec_algo: self.crypto_header.fec_algo,
2160 kdf_algo: self.crypto_header.kdf_algo,
2161 critical_metadata_digest,
2162 index_digest,
2163 fec_layout_digest,
2164 total_data_block_count,
2165 data_block_merkle_root,
2166 root_auth_descriptor_digest,
2167 signer_identity_digest,
2168 });
2169 Ok(RootAuthMaterial {
2170 critical_metadata_digest,
2171 index_digest,
2172 fec_layout_digest,
2173 data_block_merkle_root,
2174 signer_identity_digest,
2175 archive_root,
2176 total_data_block_count,
2177 })
2178 }
2179
2180 fn root_auth_fec_layout_rows(
2181 &self,
2182 shards: &[IndexShard],
2183 ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
2184 let mut rows = Vec::new();
2185 rows.push(FecLayoutObjectRow {
2186 object_class: 1,
2187 present: true,
2188 object_id: 0,
2189 first_block_index: self.manifest_footer.index_root_first_block,
2190 data_block_count: self.manifest_footer.index_root_data_block_count,
2191 parity_block_count: self.manifest_footer.index_root_parity_block_count,
2192 encrypted_size: self.manifest_footer.index_root_encrypted_size,
2193 plain_size: self.manifest_footer.index_root_decompressed_size,
2194 });
2195 if self.crypto_header.has_dictionary != 0 {
2196 rows.push(FecLayoutObjectRow {
2197 object_class: 2,
2198 present: true,
2199 object_id: 0,
2200 first_block_index: self.index_root.header.dictionary_first_block,
2201 data_block_count: self.index_root.header.dictionary_data_block_count,
2202 parity_block_count: self.index_root.header.dictionary_parity_block_count,
2203 encrypted_size: self.index_root.header.dictionary_encrypted_size,
2204 plain_size: self.index_root.header.dictionary_decompressed_size,
2205 });
2206 } else {
2207 rows.push(FecLayoutObjectRow {
2208 object_class: 2,
2209 present: false,
2210 object_id: 0,
2211 first_block_index: 0,
2212 data_block_count: 0,
2213 parity_block_count: 0,
2214 encrypted_size: 0,
2215 plain_size: 0,
2216 });
2217 }
2218 for entry in &self.index_root.shards {
2219 rows.push(FecLayoutObjectRow {
2220 object_class: 3,
2221 present: true,
2222 object_id: entry.shard_index,
2223 first_block_index: entry.first_block_index,
2224 data_block_count: entry.data_block_count,
2225 parity_block_count: entry.parity_block_count,
2226 encrypted_size: entry.encrypted_size,
2227 plain_size: entry.decompressed_size,
2228 });
2229 }
2230 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2231 for shard in shards {
2232 for envelope in &shard.envelopes {
2233 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2234 {
2235 if existing != *envelope {
2236 return Err(FormatError::InvalidArchive(
2237 "duplicate EnvelopeEntry rows do not match",
2238 ));
2239 }
2240 }
2241 }
2242 }
2243 for envelope in envelopes.values() {
2244 rows.push(FecLayoutObjectRow {
2245 object_class: 4,
2246 present: true,
2247 object_id: envelope.envelope_index,
2248 first_block_index: envelope.first_block_index,
2249 data_block_count: envelope.data_block_count,
2250 parity_block_count: envelope.parity_block_count,
2251 encrypted_size: envelope.encrypted_size,
2252 plain_size: envelope.plaintext_size,
2253 });
2254 }
2255 for entry in &self.index_root.directory_hint_shards {
2256 rows.push(FecLayoutObjectRow {
2257 object_class: 5,
2258 present: true,
2259 object_id: entry.hint_shard_index,
2260 first_block_index: entry.first_block_index,
2261 data_block_count: entry.data_block_count,
2262 parity_block_count: entry.parity_block_count,
2263 encrypted_size: entry.encrypted_size,
2264 plain_size: entry.decompressed_size,
2265 });
2266 }
2267 Ok(rows)
2268 }
2269
2270 fn root_auth_data_block_leaves(
2271 &self,
2272 rows: &[FecLayoutObjectRow],
2273 ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
2274 let mut leaves = Vec::new();
2275 let block_provider = self.block_provider();
2276 for row in rows.iter().filter(|row| row.present) {
2277 let (data_kind, parity_kind, data_max, parity_max) = match row.object_class {
2278 1 => (
2279 BlockKind::IndexRootData,
2280 BlockKind::IndexRootParity,
2281 self.crypto_header.index_root_fec_data_shards,
2282 self.crypto_header.index_root_fec_parity_shards,
2283 ),
2284 2 => (
2285 BlockKind::DictionaryData,
2286 BlockKind::DictionaryParity,
2287 self.crypto_header.index_root_fec_data_shards,
2288 self.crypto_header.index_root_fec_parity_shards,
2289 ),
2290 3 => (
2291 BlockKind::IndexShardData,
2292 BlockKind::IndexShardParity,
2293 self.crypto_header.index_fec_data_shards,
2294 self.crypto_header.index_fec_parity_shards,
2295 ),
2296 4 => (
2297 BlockKind::PayloadData,
2298 BlockKind::PayloadParity,
2299 self.crypto_header.fec_data_shards,
2300 self.crypto_header.fec_parity_shards,
2301 ),
2302 5 => (
2303 BlockKind::DirectoryHintData,
2304 BlockKind::DirectoryHintParity,
2305 self.crypto_header.index_fec_data_shards,
2306 self.crypto_header.index_fec_parity_shards,
2307 ),
2308 _ => {
2309 return Err(FormatError::InvalidArchive(
2310 "unknown root-auth FEC row class",
2311 ))
2312 }
2313 };
2314 let extent = ObjectExtent {
2315 first_block_index: row.first_block_index,
2316 data_block_count: row.data_block_count,
2317 parity_block_count: row.parity_block_count,
2318 encrypted_size: row.encrypted_size,
2319 };
2320 let repaired = load_repaired_object_data_shards_from_parts(
2321 &block_provider,
2322 &self.crypto_header,
2323 extent,
2324 data_kind,
2325 parity_kind,
2326 data_max,
2327 parity_max,
2328 )?;
2329 for (offset, payload) in repaired.into_iter().enumerate() {
2330 leaves.push(DataBlockMerkleLeaf {
2331 block_index: checked_u64_add(
2332 row.first_block_index,
2333 offset as u64,
2334 "root-auth data block",
2335 )?,
2336 kind: data_kind,
2337 flags: if offset + 1 == row.data_block_count as usize {
2338 0x01
2339 } else {
2340 0
2341 },
2342 payload,
2343 });
2344 }
2345 }
2346 leaves.sort_by_key(|leaf| leaf.block_index);
2347 Ok(leaves)
2348 }
2349
2350 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
2351 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
2352 if logical_size > cap {
2353 return Err(FormatError::ReaderUnsupported(
2354 "total extraction size exceeds configured cap",
2355 ));
2356 }
2357 Ok(())
2358 }
2359
2360 fn decompress_payload_frame(
2361 &self,
2362 compressed: &[u8],
2363 decompressed_size: u32,
2364 ) -> Result<Vec<u8>, FormatError> {
2365 if let Some(dictionary) = &self.payload_dictionary {
2366 decompress_exact_zstd_frame_with_dictionary(
2367 compressed,
2368 decompressed_size as usize,
2369 dictionary,
2370 )
2371 } else {
2372 decompress_exact_zstd_frame(compressed, decompressed_size as usize)
2373 }
2374 }
2375
2376 fn validate_encrypted_object_block_ranges(
2377 &self,
2378 envelopes: &BTreeMap<u64, EnvelopeEntry>,
2379 ) -> Result<(), FormatError> {
2380 let mut ranges = Vec::new();
2381 ranges.push(object_block_range(
2382 self.manifest_footer.index_root_first_block,
2383 self.manifest_footer.index_root_data_block_count,
2384 self.manifest_footer.index_root_parity_block_count,
2385 "IndexRoot",
2386 )?);
2387 for shard in &self.index_root.shards {
2388 ranges.push(object_block_range(
2389 shard.first_block_index,
2390 shard.data_block_count,
2391 shard.parity_block_count,
2392 "IndexShard",
2393 )?);
2394 }
2395 for hint in &self.index_root.directory_hint_shards {
2396 ranges.push(object_block_range(
2397 hint.first_block_index,
2398 hint.data_block_count,
2399 hint.parity_block_count,
2400 "DirectoryHintShardEntry",
2401 )?);
2402 }
2403 if self.crypto_header.has_dictionary != 0 {
2404 ranges.push(object_block_range(
2405 self.index_root.header.dictionary_first_block,
2406 self.index_root.header.dictionary_data_block_count,
2407 self.index_root.header.dictionary_parity_block_count,
2408 "dictionary",
2409 )?);
2410 }
2411 for envelope in envelopes.values() {
2412 ranges.push(object_block_range(
2413 envelope.first_block_index,
2414 envelope.data_block_count,
2415 envelope.parity_block_count,
2416 "EnvelopeEntry",
2417 )?);
2418 }
2419 validate_non_overlapping_object_ranges(&mut ranges)
2420 }
2421}
2422
2423impl<'a> DecodedTarMemberGroupReader<'a> {
2424 fn new(archive: &'a OpenedArchive, shard: &'a IndexShard, file: &'a FileEntry) -> Self {
2425 Self {
2426 archive,
2427 shard,
2428 file,
2429 next_frame_offset: 0,
2430 cached_envelope_index: None,
2431 cached_envelope_plaintext: Vec::new(),
2432 current_frame: Vec::new(),
2433 current_frame_offset: 0,
2434 remaining_group_bytes: file.tar_member_group_size,
2435 }
2436 }
2437
2438 fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
2439 while self.current_frame_offset >= self.current_frame.len() {
2440 if self.next_frame_offset >= self.file.frame_count as u64 {
2441 return Err(
2442 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
2443 );
2444 }
2445 let frame_index = self
2446 .file
2447 .first_frame_index
2448 .checked_add(self.next_frame_offset)
2449 .ok_or(FormatError::InvalidArchive(
2450 "FileEntry frame range overflow",
2451 ))?;
2452 let frame = frame_by_index(self.shard, frame_index)?;
2453 let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
2454 if self.cached_envelope_index != Some(envelope.envelope_index) {
2455 self.cached_envelope_plaintext = self.archive.load_payload_envelope(envelope)?;
2456 self.cached_envelope_index = Some(envelope.envelope_index);
2457 }
2458 let compressed = slice(
2459 &self.cached_envelope_plaintext,
2460 frame.offset_in_envelope as usize,
2461 frame.compressed_size as usize,
2462 "FrameEntry",
2463 )?;
2464 let decoded = self
2465 .archive
2466 .decompress_payload_frame(compressed, frame.decompressed_size)?;
2467 let offset = if self.next_frame_offset == 0 {
2468 self.file.offset_in_first_frame_plaintext as usize
2469 } else {
2470 0
2471 };
2472 if offset > decoded.len() {
2473 return Err(FormatError::InvalidArchive(
2474 "offset in first frame is outside the first referenced frame",
2475 )
2476 .into());
2477 }
2478 self.next_frame_offset += 1;
2479 self.current_frame = decoded;
2480 self.current_frame_offset = offset;
2481 }
2482 Ok(())
2483 }
2484}
2485
2486impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
2487 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
2488 if buf.is_empty() {
2489 return Ok(0);
2490 }
2491 if self.remaining_group_bytes == 0 {
2492 return Ok(0);
2493 }
2494 self.ensure_frame_available()?;
2495 let available = self.current_frame.len() - self.current_frame_offset;
2496 let len = available
2497 .min(buf.len())
2498 .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
2499 if len == 0 {
2500 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
2501 }
2502 buf[..len].copy_from_slice(
2503 &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
2504 );
2505 self.current_frame_offset += len;
2506 self.remaining_group_bytes -= len as u64;
2507 Ok(len)
2508 }
2509}
2510
2511fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
2512 shard
2513 .frames
2514 .binary_search_by_key(&frame_index, |entry| entry.frame_index)
2515 .map(|idx| &shard.frames[idx])
2516 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
2517}
2518
2519fn envelope_by_index(
2520 shard: &IndexShard,
2521 envelope_index: u64,
2522) -> Result<&EnvelopeEntry, FormatError> {
2523 shard
2524 .envelopes
2525 .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
2526 .map(|idx| &shard.envelopes[idx])
2527 .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
2528}
2529
2530fn format_error_from_extract_error(err: ExtractError) -> FormatError {
2531 match err {
2532 ExtractError::Format(err) => err,
2533 ExtractError::Output(_) => {
2534 FormatError::FilesystemExtractionFailed("failed to write regular file")
2535 }
2536 }
2537}
2538
2539fn final_index_entry_winners(
2540 shards: &[IndexShard],
2541) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
2542 let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
2543 for (shard_index, shard) in shards.iter().enumerate() {
2544 for (idx, file) in shard.files.iter().enumerate() {
2545 let path = utf8_path(
2546 shard
2547 .file_path(idx)
2548 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
2549 )?;
2550 let start = shard
2551 .tar_member_group_start(idx)
2552 .ok_or(FormatError::InvalidArchive(
2553 "FileEntry tar member start is missing",
2554 ))?;
2555 if let Some(winner) = final_entries.get_mut(&path) {
2556 if start >= winner.start {
2557 winner.start = start;
2558 winner.file_data_size = file.file_data_size;
2559 winner.shard_index = shard_index;
2560 winner.file_index = idx;
2561 }
2562 } else {
2563 final_entries.insert(
2564 path,
2565 WinningIndexEntry {
2566 start,
2567 file_data_size: file.file_data_size,
2568 shard_index,
2569 file_index: idx,
2570 },
2571 );
2572 }
2573 }
2574 }
2575 Ok(final_entries)
2576}
2577
2578fn archive_index_entry_from_loaded_file(
2579 shard: &IndexShard,
2580 file_index: usize,
2581) -> Result<ArchiveIndexEntry, FormatError> {
2582 let file = shard
2583 .files
2584 .get(file_index)
2585 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2586 let path = utf8_path(
2587 shard
2588 .file_path(file_index)
2589 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
2590 )?;
2591 Ok(ArchiveIndexEntry {
2592 path,
2593 file_data_size: file.file_data_size,
2594 })
2595}
2596
2597#[derive(Debug)]
2598struct ParsedSeekableVolume {
2599 volume_header: VolumeHeader,
2600 crypto_header: CryptoHeaderFixed,
2601 crypto_header_bytes: Vec<u8>,
2602 subkeys: Subkeys,
2603 manifest_footer: Option<ManifestFooter>,
2604 manifest_footer_error: Option<FormatError>,
2605 root_auth_footer: Option<RootAuthFooterV1>,
2606 root_auth_footer_bytes: Option<Vec<u8>>,
2607 volume_trailer: VolumeTrailer,
2608 blocks: BTreeMap<u64, BlockRecord>,
2609 erased_block_indices: BTreeSet<u64>,
2610}
2611
2612struct ParsedSeekableReadAtVolume {
2613 reader: Arc<dyn ArchiveReadAt>,
2614 volume_header: VolumeHeader,
2615 crypto_header: CryptoHeaderFixed,
2616 crypto_header_bytes: Vec<u8>,
2617 subkeys: Subkeys,
2618 manifest_footer: Option<ManifestFooter>,
2619 manifest_footer_error: Option<FormatError>,
2620 root_auth_footer: Option<RootAuthFooterV1>,
2621 root_auth_footer_bytes: Option<Vec<u8>>,
2622 volume_trailer: VolumeTrailer,
2623 crypto_end: u64,
2624}
2625
2626fn parse_seekable_volume(
2627 bytes: &[u8],
2628 master_key: &MasterKey,
2629 options: ReaderOptions,
2630) -> Result<ParsedSeekableVolume, FormatError> {
2631 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
2632 return Err(FormatError::InvalidLength {
2633 structure: "archive",
2634 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
2635 actual: bytes.len(),
2636 });
2637 }
2638
2639 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
2640 let crypto_start = volume_header.crypto_header_offset as usize;
2641 let crypto_len = volume_header.crypto_header_length as usize;
2642 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
2643 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
2644 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
2645 let subkeys = Subkeys::derive(
2646 master_key,
2647 &volume_header.archive_uuid,
2648 &volume_header.session_id,
2649 )?;
2650 verify_hmac(
2651 HmacDomain::CryptoHeader,
2652 &subkeys.mac_key,
2653 &volume_header.archive_uuid,
2654 &volume_header.session_id,
2655 parsed_crypto.hmac_covered_bytes,
2656 &parsed_crypto.header_hmac,
2657 )?;
2658 parsed_crypto.validate_extension_semantics()?;
2659 validate_seekable_supported_volume(
2660 &volume_header,
2661 &parsed_crypto.fixed,
2662 &parsed_crypto.extensions,
2663 )?;
2664 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
2665
2666 let terminal = locate_v41_terminal(
2667 bytes,
2668 KeyHoldingTerminalContext {
2669 subkeys: &subkeys,
2670 volume_header: &volume_header,
2671 crypto_header: &parsed_crypto.fixed,
2672 crypto_header_bytes: crypto_bytes,
2673 },
2674 options,
2675 )?;
2676 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
2677 let volume_trailer = terminal.volume_trailer.clone();
2678 validate_trailer_identity(&volume_header, &volume_trailer)?;
2679
2680 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
2681 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
2682 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
2683 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
2684 || volume_trailer
2685 .root_auth_footer_offset
2686 .checked_add(volume_trailer.root_auth_footer_length as u64)
2687 .ok_or(FormatError::InvalidArchive(
2688 "RootAuthFooter terminal boundary overflow",
2689 ))?
2690 != trailer_offset as u64
2691 {
2692 return Err(FormatError::InvalidArchive(
2693 "RootAuthFooter does not sit before selected trailer",
2694 ));
2695 }
2696 } else if manifest_end != trailer_offset {
2697 return Err(FormatError::InvalidArchive(
2698 "ManifestFooter does not end at selected trailer",
2699 ));
2700 }
2701 let manifest_bytes = &terminal.manifest_footer_bytes;
2702 let (manifest_footer, manifest_footer_error) = match parse_valid_manifest_footer(
2703 &volume_header,
2704 &subkeys,
2705 manifest_bytes,
2706 parsed_crypto.fixed.block_size,
2707 ) {
2708 Ok(footer) => (Some(footer), None),
2709 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
2710 Err(err) => return Err(err),
2711 };
2712
2713 let block_region = parse_block_region(
2714 bytes,
2715 crypto_end,
2716 manifest_offset,
2717 parsed_crypto.fixed.block_size as usize,
2718 &volume_header,
2719 &volume_trailer,
2720 )?;
2721
2722 Ok(ParsedSeekableVolume {
2723 volume_header,
2724 crypto_header: parsed_crypto.fixed,
2725 crypto_header_bytes: crypto_bytes.to_vec(),
2726 subkeys,
2727 manifest_footer,
2728 manifest_footer_error,
2729 root_auth_footer: terminal.root_auth_footer,
2730 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
2731 volume_trailer,
2732 blocks: block_region.blocks,
2733 erased_block_indices: block_region.erased_block_indices,
2734 })
2735}
2736
2737fn parse_seekable_read_at_volume(
2738 reader: Arc<dyn ArchiveReadAt>,
2739 master_key: &MasterKey,
2740 options: ReaderOptions,
2741) -> Result<ParsedSeekableReadAtVolume, FormatError> {
2742 let observed_len = reader.len()?;
2743 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
2744 return Err(FormatError::InvalidLength {
2745 structure: "archive",
2746 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
2747 actual: to_usize(observed_len, "archive")?,
2748 });
2749 }
2750
2751 let volume_header_bytes = read_at_vec(reader.as_ref(), 0, VOLUME_HEADER_LEN, "archive")?;
2752 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
2753 let crypto_start = volume_header.crypto_header_offset as u64;
2754 let crypto_len = volume_header.crypto_header_length as u64;
2755 let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
2756 let crypto_bytes = read_at_vec(
2757 reader.as_ref(),
2758 crypto_start,
2759 to_usize(crypto_len, "CryptoHeader")?,
2760 "CryptoHeader",
2761 )?;
2762 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
2763 let subkeys = Subkeys::derive(
2764 master_key,
2765 &volume_header.archive_uuid,
2766 &volume_header.session_id,
2767 )?;
2768 verify_hmac(
2769 HmacDomain::CryptoHeader,
2770 &subkeys.mac_key,
2771 &volume_header.archive_uuid,
2772 &volume_header.session_id,
2773 parsed_crypto.hmac_covered_bytes,
2774 &parsed_crypto.header_hmac,
2775 )?;
2776 parsed_crypto.validate_extension_semantics()?;
2777 validate_seekable_supported_volume(
2778 &volume_header,
2779 &parsed_crypto.fixed,
2780 &parsed_crypto.extensions,
2781 )?;
2782 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
2783
2784 let terminal = locate_v41_terminal_read_at(
2785 reader.as_ref(),
2786 observed_len,
2787 KeyHoldingTerminalContext {
2788 subkeys: &subkeys,
2789 volume_header: &volume_header,
2790 crypto_header: &parsed_crypto.fixed,
2791 crypto_header_bytes: &crypto_bytes,
2792 },
2793 options,
2794 )?;
2795 let volume_trailer = terminal.volume_trailer.clone();
2796 validate_trailer_identity(&volume_header, &volume_trailer)?;
2797
2798 let manifest_offset = volume_trailer.manifest_footer_offset;
2799 let manifest_end = checked_u64_add(
2800 manifest_offset,
2801 MANIFEST_FOOTER_LEN as u64,
2802 "ManifestFooter",
2803 )?;
2804 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
2805 if volume_trailer.root_auth_footer_offset != manifest_end
2806 || volume_trailer
2807 .root_auth_footer_offset
2808 .checked_add(volume_trailer.root_auth_footer_length as u64)
2809 .ok_or(FormatError::InvalidArchive(
2810 "RootAuthFooter terminal boundary overflow",
2811 ))?
2812 != terminal.image.volume_trailer_offset
2813 {
2814 return Err(FormatError::InvalidArchive(
2815 "RootAuthFooter does not sit before selected trailer",
2816 ));
2817 }
2818 } else if manifest_end != terminal.image.volume_trailer_offset {
2819 return Err(FormatError::InvalidArchive(
2820 "ManifestFooter does not end at selected trailer",
2821 ));
2822 }
2823 validate_seekable_block_region_layout(
2824 crypto_end,
2825 manifest_offset,
2826 parsed_crypto.fixed.block_size as usize,
2827 &volume_trailer,
2828 )?;
2829
2830 let manifest_bytes = &terminal.manifest_footer_bytes;
2831 let (manifest_footer, manifest_footer_error) = match parse_valid_manifest_footer(
2832 &volume_header,
2833 &subkeys,
2834 manifest_bytes,
2835 parsed_crypto.fixed.block_size,
2836 ) {
2837 Ok(footer) => (Some(footer), None),
2838 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
2839 Err(err) => return Err(err),
2840 };
2841
2842 Ok(ParsedSeekableReadAtVolume {
2843 reader,
2844 volume_header,
2845 crypto_header: parsed_crypto.fixed,
2846 crypto_header_bytes: crypto_bytes,
2847 subkeys,
2848 manifest_footer,
2849 manifest_footer_error,
2850 root_auth_footer: terminal.root_auth_footer,
2851 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
2852 volume_trailer,
2853 crypto_end,
2854 })
2855}
2856
2857#[derive(Debug)]
2858struct ParsedPublicNoKeyVolume {
2859 volume_header: VolumeHeader,
2860 crypto_header: CryptoHeaderFixed,
2861 root_auth_footer: RootAuthFooterV1,
2862 root_auth_footer_bytes: Vec<u8>,
2863 blocks: BTreeMap<u64, BlockRecord>,
2864}
2865
2866fn public_no_key_verify_volumes_with_options<F>(
2867 volumes: &[&[u8]],
2868 mut verifier: F,
2869 options: ReaderOptions,
2870) -> Result<PublicNoKeyVerification, FormatError>
2871where
2872 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2873{
2874 if volumes.is_empty() {
2875 return Err(FormatError::InvalidArchive("no volumes supplied"));
2876 }
2877 let mut parsed = Vec::with_capacity(volumes.len());
2878 for volume in volumes {
2879 parsed.push(parse_public_no_key_volume(volume, options)?);
2880 }
2881 let first = parsed
2882 .first()
2883 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
2884 if parsed.len() != first.crypto_header.stripe_width as usize {
2885 return Err(FormatError::ReaderUnsupported(
2886 "public no-key verification requires a complete volume set",
2887 ));
2888 }
2889
2890 let mut seen_volume_indexes = BTreeSet::new();
2891 let mut blocks = BTreeMap::new();
2892 for volume in &parsed {
2893 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
2894 || volume.volume_header.session_id != first.volume_header.session_id
2895 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
2896 {
2897 return Err(FormatError::InvalidArchive(
2898 "public no-key volume global metadata differs",
2899 ));
2900 }
2901 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
2902 return Err(FormatError::InvalidArchive(
2903 "public no-key RootAuthFooter copies differ",
2904 ));
2905 }
2906 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
2907 return Err(FormatError::InvalidArchive(
2908 "duplicate public no-key volume index",
2909 ));
2910 }
2911 for (block_index, record) in &volume.blocks {
2912 if blocks.insert(*block_index, record.clone()).is_some() {
2913 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
2914 }
2915 }
2916 }
2917 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
2918
2919 let footer = &first.root_auth_footer;
2920 let mut data_leaves = blocks
2921 .values()
2922 .filter(|record| record.kind.is_data())
2923 .map(|record| DataBlockMerkleLeaf {
2924 block_index: record.block_index,
2925 kind: record.kind,
2926 flags: record.flags,
2927 payload: record.payload.clone(),
2928 })
2929 .collect::<Vec<_>>();
2930 data_leaves.sort_by_key(|leaf| leaf.block_index);
2931 let total_data_block_count = u64::try_from(data_leaves.len())
2932 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
2933 let observed_data_root = data_block_merkle_root(&data_leaves);
2934 if total_data_block_count != footer.total_data_block_count
2935 || observed_data_root != footer.data_block_merkle_root
2936 {
2937 return Err(FormatError::InvalidArchive(
2938 "public no-key data-block commitment mismatch",
2939 ));
2940 }
2941 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
2942 if archive_root != footer.archive_root {
2943 return Err(FormatError::InvalidArchive(
2944 "public no-key archive_root mismatch",
2945 ));
2946 }
2947 if !verifier(footer, &archive_root)? {
2948 return Err(FormatError::InvalidArchive(
2949 "public no-key authenticator verification failed",
2950 ));
2951 }
2952 Ok(PublicNoKeyVerification {
2953 archive_root,
2954 authenticator_id: footer.authenticator_id,
2955 signer_identity_type: footer.signer_identity_type,
2956 signer_identity_bytes: footer.signer_identity_bytes.clone(),
2957 total_data_block_count,
2958 diagnostics: vec![
2959 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
2960 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
2961 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
2962 ],
2963 })
2964}
2965
2966fn parse_public_no_key_volume(
2967 bytes: &[u8],
2968 options: ReaderOptions,
2969) -> Result<ParsedPublicNoKeyVolume, FormatError> {
2970 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
2971 return Err(FormatError::InvalidLength {
2972 structure: "archive",
2973 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
2974 actual: bytes.len(),
2975 });
2976 }
2977 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
2978 let crypto_start = volume_header.crypto_header_offset as usize;
2979 let crypto_len = volume_header.crypto_header_length as usize;
2980 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
2981 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
2982 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
2983 parsed_crypto.validate_extension_semantics()?;
2984 validate_seekable_supported_volume(
2985 &volume_header,
2986 &parsed_crypto.fixed,
2987 &parsed_crypto.extensions,
2988 )?;
2989 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
2990
2991 let terminal =
2992 locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
2993 let block_region = parse_public_block_observation(
2994 bytes,
2995 crypto_end,
2996 &terminal.image,
2997 parsed_crypto.fixed.block_size as usize,
2998 &volume_header,
2999 )?;
3000 Ok(ParsedPublicNoKeyVolume {
3001 volume_header,
3002 crypto_header: parsed_crypto.fixed,
3003 root_auth_footer: terminal.root_auth_footer,
3004 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3005 blocks: block_region,
3006 })
3007}
3008
3009fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3010 left.length == right.length
3011 && left.stripe_width == right.stripe_width
3012 && left.block_size == right.block_size
3013 && left.compression_algo == right.compression_algo
3014 && left.aead_algo == right.aead_algo
3015 && left.fec_algo == right.fec_algo
3016 && left.kdf_algo == right.kdf_algo
3017}
3018
3019fn recompute_public_archive_root(
3020 footer: &RootAuthFooterV1,
3021 crypto_header: &CryptoHeaderFixed,
3022) -> Result<[u8; 32], FormatError> {
3023 let descriptor_digest = root_auth_descriptor_digest(
3024 footer.authenticator_id,
3025 footer.signer_identity_type,
3026 &footer.signer_identity_bytes,
3027 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3028 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3029 })?,
3030 footer.footer_length()?,
3031 )?;
3032 let signer_digest =
3033 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3034 if signer_digest != footer.signer_identity_digest {
3035 return Err(FormatError::InvalidArchive(
3036 "public no-key signer identity digest mismatch",
3037 ));
3038 }
3039 Ok(archive_root(ArchiveRootInputs {
3040 archive_uuid: footer.archive_uuid,
3041 session_id: footer.session_id,
3042 format_version: FORMAT_VERSION,
3043 volume_format_rev: VOLUME_FORMAT_REV,
3044 compression_algo: crypto_header.compression_algo,
3045 aead_algo: crypto_header.aead_algo,
3046 fec_algo: crypto_header.fec_algo,
3047 kdf_algo: crypto_header.kdf_algo,
3048 critical_metadata_digest: footer.critical_metadata_digest,
3049 index_digest: footer.index_digest,
3050 fec_layout_digest: footer.fec_layout_digest,
3051 total_data_block_count: footer.total_data_block_count,
3052 data_block_merkle_root: footer.data_block_merkle_root,
3053 root_auth_descriptor_digest: descriptor_digest,
3054 signer_identity_digest: signer_digest,
3055 }))
3056}
3057
3058fn parse_valid_manifest_footer(
3059 volume_header: &VolumeHeader,
3060 subkeys: &Subkeys,
3061 manifest_bytes: &[u8],
3062 block_size: u32,
3063) -> Result<ManifestFooter, FormatError> {
3064 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3065 validate_manifest_footer(volume_header, &manifest_footer, subkeys, manifest_bytes)?;
3066 manifest_footer.validate_index_root_extent(block_size)?;
3067 Ok(manifest_footer)
3068}
3069
3070fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3071 matches!(
3072 error,
3073 FormatError::BadMagic {
3074 structure: "ManifestFooter",
3075 } | FormatError::NonZeroReserved {
3076 structure: "ManifestFooter",
3077 } | FormatError::InvalidAuthoritativeFlag(_)
3078 | FormatError::HmacMismatch {
3079 structure: "ManifestFooter",
3080 }
3081 )
3082}
3083
3084fn validate_seekable_supported_volume(
3085 volume_header: &VolumeHeader,
3086 crypto_header: &CryptoHeaderFixed,
3087 extensions: &[ExtensionTlv<'_>],
3088) -> Result<(), FormatError> {
3089 reject_unsupported_raw_stream_profile(extensions)?;
3090 if crypto_header.stripe_width != volume_header.stripe_width {
3091 return Err(FormatError::InvalidArchive(
3092 "VolumeHeader and CryptoHeader stripe_width differ",
3093 ));
3094 }
3095 Ok(())
3096}
3097
3098pub(crate) fn validate_crypto_class_parity_exactness(
3099 crypto_header: &CryptoHeaderFixed,
3100) -> Result<(), FormatError> {
3101 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3102 if crypto_header.fec_parity_shards as u32 != fec {
3103 return Err(FormatError::InvalidArchive(
3104 "fec_parity_shards does not match v41 compute_parity",
3105 ));
3106 }
3107 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3108 if crypto_header.index_fec_parity_shards as u32 != index {
3109 return Err(FormatError::InvalidArchive(
3110 "index_fec_parity_shards does not match v41 compute_parity",
3111 ));
3112 }
3113 let index_root = required_object_parity(
3114 crypto_header.index_root_fec_data_shards as u64,
3115 crypto_header,
3116 )?;
3117 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3118 return Err(FormatError::InvalidArchive(
3119 "index_root_fec_parity_shards does not match v41 compute_parity",
3120 ));
3121 }
3122 Ok(())
3123}
3124
3125fn validate_volume_set_member(
3126 first: &ParsedSeekableVolume,
3127 candidate: &ParsedSeekableVolume,
3128) -> Result<(), FormatError> {
3129 validate_volume_set_member_metadata(
3130 &first.volume_header,
3131 &first.crypto_header,
3132 &first.crypto_header_bytes,
3133 &candidate.volume_header,
3134 &candidate.crypto_header,
3135 &candidate.crypto_header_bytes,
3136 )
3137}
3138
3139fn validate_volume_set_member_metadata(
3140 first_volume_header: &VolumeHeader,
3141 first_crypto_header: &CryptoHeaderFixed,
3142 first_crypto_header_bytes: &[u8],
3143 candidate_volume_header: &VolumeHeader,
3144 candidate_crypto_header: &CryptoHeaderFixed,
3145 candidate_crypto_header_bytes: &[u8],
3146) -> Result<(), FormatError> {
3147 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3148 || candidate_volume_header.session_id != first_volume_header.session_id
3149 {
3150 return Err(FormatError::InvalidArchive(
3151 "mixed archive or session IDs in volume set",
3152 ));
3153 }
3154 if candidate_crypto_header_bytes != first_crypto_header_bytes
3155 || candidate_crypto_header != first_crypto_header
3156 {
3157 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3158 }
3159 Ok(())
3160}
3161
3162pub(crate) fn manifest_bootstrap_fields_match(
3163 left: &ManifestFooter,
3164 right: &ManifestFooter,
3165) -> bool {
3166 left.archive_uuid == right.archive_uuid
3167 && left.session_id == right.session_id
3168 && left.is_authoritative == right.is_authoritative
3169 && left.total_volumes == right.total_volumes
3170 && left.index_root_first_block == right.index_root_first_block
3171 && left.index_root_data_block_count == right.index_root_data_block_count
3172 && left.index_root_parity_block_count == right.index_root_parity_block_count
3173 && left.index_root_encrypted_size == right.index_root_encrypted_size
3174 && left.index_root_decompressed_size == right.index_root_decompressed_size
3175}
3176
3177fn validate_complete_global_block_coverage(
3178 blocks: &BTreeMap<u64, BlockRecord>,
3179 erased_block_indices: &BTreeSet<u64>,
3180) -> Result<(), FormatError> {
3181 let mut expected = 0u64;
3182 let mut block_iter = blocks.keys().copied().peekable();
3183 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
3184
3185 loop {
3186 let next_block = block_iter.peek().copied();
3187 let next_erasure = erasure_iter.peek().copied();
3188 let next = match (next_block, next_erasure) {
3189 (Some(block), Some(erasure)) if block == erasure => {
3190 return Err(FormatError::InvalidArchive(
3191 "BlockRecord index is both present and erased",
3192 ));
3193 }
3194 (Some(block), Some(erasure)) => block.min(erasure),
3195 (Some(block), None) => block,
3196 (None, Some(erasure)) => erasure,
3197 (None, None) => return Ok(()),
3198 };
3199
3200 if next != expected {
3201 return Err(FormatError::InvalidArchive(
3202 "complete volume set has missing global blocks",
3203 ));
3204 }
3205 if next_block == Some(next) {
3206 block_iter.next();
3207 }
3208 if next_erasure == Some(next) {
3209 erasure_iter.next();
3210 }
3211 expected = expected
3212 .checked_add(1)
3213 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
3214 }
3215}
3216
3217#[derive(Debug)]
3218struct V41Terminal {
3219 image: CriticalMetadataImage,
3220 manifest_footer_bytes: Vec<u8>,
3221 root_auth_footer_bytes: Option<Vec<u8>>,
3222 root_auth_footer: Option<RootAuthFooterV1>,
3223 volume_trailer: VolumeTrailer,
3224}
3225
3226pub(crate) struct SequentialTerminalMaterial {
3227 pub(crate) manifest_footer: ManifestFooter,
3228 pub(crate) volume_trailer: VolumeTrailer,
3229 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
3230}
3231
3232#[derive(Debug)]
3233struct V41PublicTerminal {
3234 image: CriticalMetadataImage,
3235 root_auth_footer_bytes: Vec<u8>,
3236 root_auth_footer: RootAuthFooterV1,
3237}
3238
3239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3240struct CmraDecoderTuple {
3241 shard_size: u32,
3242 data_shard_count: u16,
3243 parity_shard_count: u16,
3244 image_length: u32,
3245 image_sha256: [u8; 32],
3246}
3247
3248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3249struct CmraIdentityHints {
3250 archive_uuid: [u8; 16],
3251 session_id: [u8; 16],
3252 volume_index: u32,
3253}
3254
3255impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
3256 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3257 Self {
3258 shard_size: header.shard_size,
3259 data_shard_count: header.data_shard_count,
3260 parity_shard_count: header.parity_shard_count,
3261 image_length: header.image_length,
3262 image_sha256: header.image_sha256,
3263 }
3264 }
3265}
3266
3267impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
3268 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3269 Self {
3270 archive_uuid: header.archive_uuid_hint,
3271 session_id: header.session_id_hint,
3272 volume_index: header.volume_index_hint,
3273 }
3274 }
3275}
3276
3277impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
3278 fn from(locator: CriticalRecoveryLocator) -> Self {
3279 Self {
3280 shard_size: locator.cmra_shard_size,
3281 data_shard_count: locator.cmra_data_shard_count,
3282 parity_shard_count: locator.cmra_parity_shard_count,
3283 image_length: locator.cmra_image_length,
3284 image_sha256: locator.cmra_image_sha256,
3285 }
3286 }
3287}
3288
3289impl From<CriticalRecoveryLocator> for CmraIdentityHints {
3290 fn from(locator: CriticalRecoveryLocator) -> Self {
3291 Self {
3292 archive_uuid: locator.archive_uuid_hint,
3293 session_id: locator.session_id_hint,
3294 volume_index: locator.volume_index_hint,
3295 }
3296 }
3297}
3298
3299#[derive(Debug)]
3300struct RecoveredCmra {
3301 image: CriticalMetadataImage,
3302 tuple: CmraDecoderTuple,
3303 header_hints: Option<CmraIdentityHints>,
3304 cmra_length: u64,
3305}
3306
3307#[derive(Debug)]
3308struct TerminalCandidate {
3309 terminal: V41Terminal,
3310 anchor: usize,
3311 locator_sequence: Option<u32>,
3312 cmra_offset: u64,
3313 cmra_length: u64,
3314}
3315
3316#[derive(Debug)]
3317struct PublicTerminalCandidate {
3318 terminal: V41PublicTerminal,
3319 anchor: usize,
3320 cmra_offset: u64,
3321 cmra_length: u64,
3322}
3323
3324#[derive(Debug, Clone, Copy)]
3325enum CmraRecoveryMode {
3326 KeyHolding,
3327 PublicNoKey,
3328}
3329
3330#[derive(Clone, Copy)]
3331pub(crate) struct KeyHoldingTerminalContext<'a> {
3332 pub(crate) subkeys: &'a Subkeys,
3333 pub(crate) volume_header: &'a VolumeHeader,
3334 pub(crate) crypto_header: &'a CryptoHeaderFixed,
3335 pub(crate) crypto_header_bytes: &'a [u8],
3336}
3337
3338fn locate_v41_terminal(
3339 bytes: &[u8],
3340 context: KeyHoldingTerminalContext<'_>,
3341 options: ReaderOptions,
3342) -> Result<V41Terminal, FormatError> {
3343 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
3344}
3345
3346fn locate_v41_terminal_read_at(
3347 reader: &dyn ArchiveReadAt,
3348 len: u64,
3349 context: KeyHoldingTerminalContext<'_>,
3350 options: ReaderOptions,
3351) -> Result<V41Terminal, FormatError> {
3352 let mut candidates = Vec::new();
3353 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
3354 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
3355 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
3356 }
3357 if len >= LOCATOR_PAIR_LEN as u64 {
3358 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
3359 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
3360 }
3361
3362 if candidates.is_empty() {
3363 let scan = max_critical_recovery_scan(options)? as u64;
3364 let scan_start = len.saturating_sub(scan);
3365 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
3366 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
3367 let mut offset = tail.len().saturating_sub(4);
3368 while offset < tail.len() {
3369 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
3370 if tail.get(offset..offset + 4) == Some(b"TZCL") {
3371 collect_v41_locator_candidate_read_at(
3372 reader,
3373 absolute_offset,
3374 2,
3375 context,
3376 &mut candidates,
3377 );
3378 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
3379 if let Ok(candidate) =
3380 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
3381 {
3382 candidates.push(candidate);
3383 }
3384 }
3385 if offset == 0 {
3386 break;
3387 }
3388 offset -= 1;
3389 }
3390 }
3391
3392 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
3393}
3394
3395fn locate_v41_terminal_candidate(
3396 bytes: &[u8],
3397 context: KeyHoldingTerminalContext<'_>,
3398 options: ReaderOptions,
3399) -> Result<TerminalCandidate, FormatError> {
3400 let mut candidates = Vec::new();
3401 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
3402 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
3403 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
3404 }
3405 if bytes.len() >= LOCATOR_PAIR_LEN {
3406 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
3407 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
3408 }
3409
3410 if candidates.is_empty() {
3411 let scan = max_critical_recovery_scan(options)?;
3412 let scan_start = bytes.len().saturating_sub(scan);
3413 let mut offset = bytes.len().saturating_sub(4);
3414 while offset >= scan_start {
3415 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
3416 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
3417 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
3418 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
3419 candidates.push(candidate);
3420 }
3421 }
3422 if offset == 0 {
3423 break;
3424 }
3425 offset -= 1;
3426 }
3427 }
3428
3429 choose_v41_terminal_candidate(candidates)
3430}
3431
3432fn locate_v41_public_terminal(
3433 bytes: &[u8],
3434 volume_header: &VolumeHeader,
3435 crypto_header: &CryptoHeaderFixed,
3436 options: ReaderOptions,
3437) -> Result<V41PublicTerminal, FormatError> {
3438 let mut candidates = Vec::new();
3439 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
3440 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
3441 collect_v41_public_locator_candidate(
3442 bytes,
3443 final_offset,
3444 0,
3445 volume_header,
3446 crypto_header,
3447 &mut candidates,
3448 );
3449 }
3450 if bytes.len() >= LOCATOR_PAIR_LEN {
3451 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
3452 collect_v41_public_locator_candidate(
3453 bytes,
3454 mirror_offset,
3455 1,
3456 volume_header,
3457 crypto_header,
3458 &mut candidates,
3459 );
3460 }
3461
3462 if candidates.is_empty() {
3463 let scan = max_critical_recovery_scan(options)?;
3464 let scan_start = bytes.len().saturating_sub(scan);
3465 let mut offset = bytes.len().saturating_sub(4);
3466 while offset >= scan_start {
3467 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
3468 collect_v41_public_locator_candidate(
3469 bytes,
3470 offset,
3471 2,
3472 volume_header,
3473 crypto_header,
3474 &mut candidates,
3475 );
3476 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
3477 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
3478 bytes,
3479 offset,
3480 volume_header,
3481 crypto_header,
3482 ) {
3483 candidates.push(candidate);
3484 }
3485 }
3486 if offset == 0 {
3487 break;
3488 }
3489 offset -= 1;
3490 }
3491 }
3492
3493 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
3494}
3495
3496fn collect_v41_locator_candidate(
3497 bytes: &[u8],
3498 offset: usize,
3499 expected_sequence: u32,
3500 context: KeyHoldingTerminalContext<'_>,
3501 candidates: &mut Vec<TerminalCandidate>,
3502) {
3503 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
3504 return;
3505 };
3506 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
3507 return;
3508 };
3509 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
3510 return;
3511 }
3512 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
3513 candidates.push(candidate);
3514 }
3515}
3516
3517fn collect_v41_locator_candidate_read_at(
3518 reader: &dyn ArchiveReadAt,
3519 offset: u64,
3520 expected_sequence: u32,
3521 context: KeyHoldingTerminalContext<'_>,
3522 candidates: &mut Vec<TerminalCandidate>,
3523) {
3524 let Ok(raw) = read_at_vec(
3525 reader,
3526 offset,
3527 CRITICAL_RECOVERY_LOCATOR_LEN,
3528 "CriticalRecoveryLocator",
3529 ) else {
3530 return;
3531 };
3532 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
3533 return;
3534 };
3535 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
3536 return;
3537 }
3538 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
3539 candidates.push(candidate);
3540 }
3541}
3542
3543fn collect_v41_public_locator_candidate(
3544 bytes: &[u8],
3545 offset: usize,
3546 expected_sequence: u32,
3547 volume_header: &VolumeHeader,
3548 crypto_header: &CryptoHeaderFixed,
3549 candidates: &mut Vec<PublicTerminalCandidate>,
3550) {
3551 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
3552 return;
3553 };
3554 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
3555 return;
3556 };
3557 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
3558 return;
3559 }
3560 if let Ok(candidate) =
3561 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
3562 {
3563 candidates.push(candidate);
3564 }
3565}
3566
3567fn choose_v41_terminal_candidate(
3568 mut candidates: Vec<TerminalCandidate>,
3569) -> Result<TerminalCandidate, FormatError> {
3570 candidates.sort_by_key(|candidate| candidate.anchor);
3571 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
3572 "no valid v41 CMRA candidate found",
3573 ))?;
3574 if let Some(previous) = candidates.last() {
3575 if previous.anchor == winner.anchor
3576 && (previous.cmra_offset != winner.cmra_offset
3577 || previous.cmra_length != winner.cmra_length)
3578 {
3579 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
3580 }
3581 }
3582 Ok(winner)
3583}
3584
3585fn choose_v41_public_terminal_candidate(
3586 mut candidates: Vec<PublicTerminalCandidate>,
3587) -> Result<PublicTerminalCandidate, FormatError> {
3588 candidates.sort_by_key(|candidate| candidate.anchor);
3589 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
3590 "no valid v41 public CMRA candidate found",
3591 ))?;
3592 if let Some(previous) = candidates.last() {
3593 if previous.anchor == winner.anchor
3594 && (previous.cmra_offset != winner.cmra_offset
3595 || previous.cmra_length != winner.cmra_length)
3596 {
3597 return Err(FormatError::InvalidArchive(
3598 "ambiguous v41 public CMRA candidates",
3599 ));
3600 }
3601 }
3602 Ok(winner)
3603}
3604
3605fn parse_locator_cmra_candidate(
3606 bytes: &[u8],
3607 locator_offset: usize,
3608 locator: CriticalRecoveryLocator,
3609 context: KeyHoldingTerminalContext<'_>,
3610) -> Result<TerminalCandidate, FormatError> {
3611 let tuple = CmraDecoderTuple::from(locator);
3612 validate_cmra_decoder_tuple(tuple)?;
3613 let expected_cmra_length = cmra_serialized_length(tuple)?;
3614 if locator.cmra_length as u64 != expected_cmra_length {
3615 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
3616 }
3617 validate_locator_position(locator_offset, locator)?;
3618 let recovered = recover_cmra(
3619 bytes,
3620 locator.cmra_offset,
3621 Some(tuple),
3622 CmraRecoveryMode::KeyHolding,
3623 )?;
3624 if recovered.tuple != tuple {
3625 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
3626 }
3627 if expected_cmra_length != recovered.cmra_length {
3628 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
3629 }
3630 validate_locator_image_boundary(locator, &recovered.image)?;
3631 validate_cmra_identity_hints(
3632 recovered.header_hints,
3633 Some(CmraIdentityHints::from(locator)),
3634 &recovered.image,
3635 )?;
3636 let terminal = validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context)?;
3637 Ok(TerminalCandidate {
3638 terminal,
3639 anchor: locator_offset
3640 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
3641 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
3642 locator_sequence: Some(locator.locator_sequence),
3643 cmra_offset: locator.cmra_offset,
3644 cmra_length: recovered.cmra_length,
3645 })
3646}
3647
3648fn parse_locator_cmra_candidate_read_at(
3649 reader: &dyn ArchiveReadAt,
3650 locator_offset: u64,
3651 locator: CriticalRecoveryLocator,
3652 context: KeyHoldingTerminalContext<'_>,
3653) -> Result<TerminalCandidate, FormatError> {
3654 let tuple = CmraDecoderTuple::from(locator);
3655 validate_cmra_decoder_tuple(tuple)?;
3656 let expected_cmra_length = cmra_serialized_length(tuple)?;
3657 if locator.cmra_length as u64 != expected_cmra_length {
3658 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
3659 }
3660 validate_locator_position(
3661 to_usize(locator_offset, "CriticalRecoveryLocator")?,
3662 locator,
3663 )?;
3664 let recovered = recover_cmra_read_at(
3665 reader,
3666 locator.cmra_offset,
3667 Some(tuple),
3668 CmraRecoveryMode::KeyHolding,
3669 )?;
3670 if recovered.tuple != tuple {
3671 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
3672 }
3673 if expected_cmra_length != recovered.cmra_length {
3674 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
3675 }
3676 validate_locator_image_boundary(locator, &recovered.image)?;
3677 validate_cmra_identity_hints(
3678 recovered.header_hints,
3679 Some(CmraIdentityHints::from(locator)),
3680 &recovered.image,
3681 )?;
3682 let terminal =
3683 validate_recovered_terminal_read_at(recovered.image, recovered.tuple, reader, context)?;
3684 Ok(TerminalCandidate {
3685 terminal,
3686 anchor: to_usize(
3687 checked_u64_add(
3688 locator_offset,
3689 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
3690 "locator anchor overflow",
3691 )?,
3692 "locator anchor overflow",
3693 )?,
3694 locator_sequence: Some(locator.locator_sequence),
3695 cmra_offset: locator.cmra_offset,
3696 cmra_length: recovered.cmra_length,
3697 })
3698}
3699
3700fn parse_public_locator_cmra_candidate(
3701 bytes: &[u8],
3702 locator_offset: usize,
3703 locator: CriticalRecoveryLocator,
3704 volume_header: &VolumeHeader,
3705 crypto_header: &CryptoHeaderFixed,
3706) -> Result<PublicTerminalCandidate, FormatError> {
3707 let tuple = CmraDecoderTuple::from(locator);
3708 validate_cmra_decoder_tuple(tuple)?;
3709 let expected_cmra_length = cmra_serialized_length(tuple)?;
3710 if locator.cmra_length as u64 != expected_cmra_length {
3711 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
3712 }
3713 validate_locator_position(locator_offset, locator)?;
3714 let recovered = recover_cmra(
3715 bytes,
3716 locator.cmra_offset,
3717 Some(tuple),
3718 CmraRecoveryMode::PublicNoKey,
3719 )?;
3720 if recovered.tuple != tuple {
3721 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
3722 }
3723 if expected_cmra_length != recovered.cmra_length {
3724 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
3725 }
3726 validate_locator_image_boundary(locator, &recovered.image)?;
3727 validate_cmra_identity_hints(
3728 recovered.header_hints,
3729 Some(CmraIdentityHints::from(locator)),
3730 &recovered.image,
3731 )?;
3732 let terminal =
3733 validate_recovered_public_terminal(recovered.image, bytes, volume_header, crypto_header)?;
3734 Ok(PublicTerminalCandidate {
3735 terminal,
3736 anchor: locator_offset
3737 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
3738 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
3739 cmra_offset: locator.cmra_offset,
3740 cmra_length: recovered.cmra_length,
3741 })
3742}
3743
3744fn parse_locatorless_cmra_candidate(
3745 bytes: &[u8],
3746 cmra_offset: usize,
3747 context: KeyHoldingTerminalContext<'_>,
3748) -> Result<TerminalCandidate, FormatError> {
3749 let recovered = recover_cmra(
3750 bytes,
3751 cmra_offset as u64,
3752 None,
3753 CmraRecoveryMode::KeyHolding,
3754 )?;
3755 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
3756 return Err(FormatError::InvalidArchive(
3757 "locatorless CMRA boundary mismatch",
3758 ));
3759 }
3760 if recovered
3761 .image
3762 .volume_trailer_offset
3763 .checked_add(VOLUME_TRAILER_LEN as u64)
3764 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
3765 != cmra_offset as u64
3766 {
3767 return Err(FormatError::InvalidArchive(
3768 "locatorless trailer boundary mismatch",
3769 ));
3770 }
3771 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
3772 let terminal = validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context)?;
3773 Ok(TerminalCandidate {
3774 terminal,
3775 anchor: cmra_offset
3776 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
3777 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
3778 locator_sequence: None,
3779 cmra_offset: cmra_offset as u64,
3780 cmra_length: recovered.cmra_length,
3781 })
3782}
3783
3784fn parse_locatorless_cmra_candidate_read_at(
3785 reader: &dyn ArchiveReadAt,
3786 cmra_offset: u64,
3787 context: KeyHoldingTerminalContext<'_>,
3788) -> Result<TerminalCandidate, FormatError> {
3789 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
3790 if recovered.image.body_bytes_before_cmra != cmra_offset {
3791 return Err(FormatError::InvalidArchive(
3792 "locatorless CMRA boundary mismatch",
3793 ));
3794 }
3795 if recovered
3796 .image
3797 .volume_trailer_offset
3798 .checked_add(VOLUME_TRAILER_LEN as u64)
3799 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
3800 != cmra_offset
3801 {
3802 return Err(FormatError::InvalidArchive(
3803 "locatorless trailer boundary mismatch",
3804 ));
3805 }
3806 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
3807 let terminal =
3808 validate_recovered_terminal_read_at(recovered.image, recovered.tuple, reader, context)?;
3809 Ok(TerminalCandidate {
3810 terminal,
3811 anchor: to_usize(
3812 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
3813 "CMRA anchor overflow",
3814 )?,
3815 locator_sequence: None,
3816 cmra_offset,
3817 cmra_length: recovered.cmra_length,
3818 })
3819}
3820
3821fn parse_public_locatorless_cmra_candidate(
3822 bytes: &[u8],
3823 cmra_offset: usize,
3824 volume_header: &VolumeHeader,
3825 crypto_header: &CryptoHeaderFixed,
3826) -> Result<PublicTerminalCandidate, FormatError> {
3827 let recovered = recover_cmra(
3828 bytes,
3829 cmra_offset as u64,
3830 None,
3831 CmraRecoveryMode::PublicNoKey,
3832 )?;
3833 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
3834 return Err(FormatError::InvalidArchive(
3835 "locatorless CMRA boundary mismatch",
3836 ));
3837 }
3838 if recovered
3839 .image
3840 .volume_trailer_offset
3841 .checked_add(VOLUME_TRAILER_LEN as u64)
3842 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
3843 != cmra_offset as u64
3844 {
3845 return Err(FormatError::InvalidArchive(
3846 "locatorless trailer boundary mismatch",
3847 ));
3848 }
3849 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
3850 let terminal =
3851 validate_recovered_public_terminal(recovered.image, bytes, volume_header, crypto_header)?;
3852 Ok(PublicTerminalCandidate {
3853 terminal,
3854 anchor: cmra_offset
3855 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
3856 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
3857 cmra_offset: cmra_offset as u64,
3858 cmra_length: recovered.cmra_length,
3859 })
3860}
3861
3862fn validate_locator_position(
3863 locator_offset: usize,
3864 locator: CriticalRecoveryLocator,
3865) -> Result<(), FormatError> {
3866 if locator.cmra_offset != locator.body_bytes_before_cmra {
3867 return Err(FormatError::InvalidArchive(
3868 "locator CMRA boundary mismatch",
3869 ));
3870 }
3871 if locator
3872 .volume_trailer_offset
3873 .checked_add(VOLUME_TRAILER_LEN as u64)
3874 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
3875 != locator.cmra_offset
3876 {
3877 return Err(FormatError::InvalidArchive(
3878 "locator trailer boundary mismatch",
3879 ));
3880 }
3881 let expected_offset = match locator.locator_sequence {
3882 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
3883 0 => locator
3884 .cmra_offset
3885 .checked_add(locator.cmra_length as u64)
3886 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
3887 _ => None,
3888 }
3889 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
3890 if expected_offset != locator_offset as u64 {
3891 return Err(FormatError::InvalidArchive(
3892 "locator position does not match sequence",
3893 ));
3894 }
3895 Ok(())
3896}
3897
3898fn validate_locator_image_boundary(
3899 locator: CriticalRecoveryLocator,
3900 image: &CriticalMetadataImage,
3901) -> Result<(), FormatError> {
3902 if locator.volume_trailer_offset != image.volume_trailer_offset
3903 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
3904 || image
3905 .volume_trailer_offset
3906 .checked_add(VOLUME_TRAILER_LEN as u64)
3907 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
3908 != locator.cmra_offset
3909 {
3910 return Err(FormatError::InvalidArchive(
3911 "locator and CMRA image boundaries differ",
3912 ));
3913 }
3914 Ok(())
3915}
3916
3917fn validate_cmra_identity_hints(
3918 header_hints: Option<CmraIdentityHints>,
3919 locator_hints: Option<CmraIdentityHints>,
3920 image: &CriticalMetadataImage,
3921) -> Result<(), FormatError> {
3922 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
3923 if header != locator {
3924 return Err(FormatError::InvalidArchive(
3925 "CMRA header and locator identity hints differ",
3926 ));
3927 }
3928 }
3929 for hints in [header_hints, locator_hints].into_iter().flatten() {
3930 if hints.archive_uuid != image.archive_uuid
3931 || hints.session_id != image.session_id
3932 || hints.volume_index != image.volume_index
3933 {
3934 return Err(FormatError::InvalidArchive(
3935 "CMRA identity hints do not match recovered image",
3936 ));
3937 }
3938 }
3939 Ok(())
3940}
3941
3942fn recover_cmra(
3943 bytes: &[u8],
3944 cmra_offset: u64,
3945 locator_tuple: Option<CmraDecoderTuple>,
3946 mode: CmraRecoveryMode,
3947) -> Result<RecoveredCmra, FormatError> {
3948 let offset = to_usize(cmra_offset, "CMRA")?;
3949 let header_bytes = slice(
3950 bytes,
3951 offset,
3952 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
3953 "CriticalMetadataRecoveryHeader",
3954 )?;
3955 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
3956 validate_cmra_decoder_tuple(tuple)?;
3957 let cmra_length = cmra_serialized_length(tuple)?;
3958 let cmra_len = to_usize(cmra_length, "CMRA")?;
3959 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
3960 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
3961}
3962
3963fn recover_cmra_read_at(
3964 reader: &dyn ArchiveReadAt,
3965 cmra_offset: u64,
3966 locator_tuple: Option<CmraDecoderTuple>,
3967 mode: CmraRecoveryMode,
3968) -> Result<RecoveredCmra, FormatError> {
3969 let header_bytes = read_at_vec(
3970 reader,
3971 cmra_offset,
3972 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
3973 "CriticalMetadataRecoveryHeader",
3974 )?;
3975 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
3976 validate_cmra_decoder_tuple(tuple)?;
3977 let cmra_length = cmra_serialized_length(tuple)?;
3978 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
3979 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
3980}
3981
3982fn recover_cmra_header_tuple(
3983 header_bytes: &[u8],
3984 locator_tuple: Option<CmraDecoderTuple>,
3985) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
3986 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
3987 Ok(match (parsed_header, locator_tuple) {
3988 (Ok(header), Some(locator_tuple)) => {
3989 let header_tuple = CmraDecoderTuple::from(header);
3990 if header_tuple != locator_tuple {
3991 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
3992 }
3993 (locator_tuple, Some(CmraIdentityHints::from(header)))
3994 }
3995 (Ok(header), None) => (
3996 CmraDecoderTuple::from(header),
3997 Some(CmraIdentityHints::from(header)),
3998 ),
3999 (
4000 Err(FormatError::BadCrc {
4001 structure: "CriticalMetadataRecoveryHeader",
4002 }),
4003 Some(tuple),
4004 ) => (tuple, None),
4005 (Err(err), _) => return Err(err),
4006 })
4007}
4008
4009fn recover_cmra_from_bytes(
4010 cmra_bytes: &[u8],
4011 tuple: CmraDecoderTuple,
4012 header_hints: Option<CmraIdentityHints>,
4013 cmra_length: u64,
4014 mode: CmraRecoveryMode,
4015) -> Result<RecoveredCmra, FormatError> {
4016 let shard_size = tuple.shard_size as usize;
4017 let mut data_shards = vec![None; tuple.data_shard_count as usize];
4018 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
4019 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
4020 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
4021 let raw = slice(
4022 cmra_bytes,
4023 cursor,
4024 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
4025 "CriticalMetadataRecoveryShard",
4026 )?;
4027 let shard = match CriticalMetadataRecoveryShard::parse(raw, shard_size) {
4028 Ok(shard) => Some(shard),
4029 Err(FormatError::BadCrc {
4030 structure: "CriticalMetadataRecoveryShard",
4031 }) => None,
4032 Err(err) => return Err(err),
4033 };
4034 if let Some(shard) = shard {
4035 validate_cmra_shard(&shard, idx, tuple)?;
4036 if shard.shard_role == 0 {
4037 let data_slot = data_shards
4038 .get_mut(idx)
4039 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
4040 *data_slot = Some(shard.payload);
4041 } else {
4042 let parity_idx = idx - tuple.data_shard_count as usize;
4043 let parity_slot =
4044 parity_shards
4045 .get_mut(parity_idx)
4046 .ok_or(FormatError::InvalidArchive(
4047 "CMRA parity shard out of range",
4048 ))?;
4049 *parity_slot = Some(shard.payload);
4050 }
4051 }
4052 cursor = checked_add(
4053 cursor,
4054 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
4055 "CriticalMetadataRecoveryShard",
4056 )?;
4057 }
4058 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
4059 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
4060 for shard in repaired {
4061 image_bytes.extend_from_slice(&shard);
4062 }
4063 image_bytes.truncate(tuple.image_length as usize);
4064 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
4065 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
4066 }
4067 let image = CriticalMetadataImage::parse(&image_bytes)?;
4068 validate_critical_metadata_image(&image, mode)?;
4069 Ok(RecoveredCmra {
4070 image,
4071 tuple,
4072 header_hints,
4073 cmra_length,
4074 })
4075}
4076
4077fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
4078 let shard_size = tuple.shard_size as u64;
4079 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
4080 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
4081 }
4082 let image_length = tuple.image_length as u64;
4083 let min = critical_image_min();
4084 let cap = critical_image_cap()?;
4085 if image_length < min || image_length > cap {
4086 return Err(FormatError::InvalidArchive(
4087 "CMRA image_length is outside bounds",
4088 ));
4089 }
4090 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
4091 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
4092 return Err(FormatError::InvalidArchive(
4093 "CMRA data_shard_count does not match image length",
4094 ));
4095 }
4096 let max_parity = 2u64.max(ceil_div_u64(
4097 checked_u64_mul(
4098 expected_data_shards,
4099 READER_MAX_CMRA_PARITY_PCT as u64,
4100 "CMRA parity overflow",
4101 )?,
4102 100,
4103 )?);
4104 if tuple.parity_shard_count as u64 > max_parity {
4105 return Err(FormatError::ReaderResourceLimitExceeded {
4106 field: "CMRA parity shard count",
4107 cap: max_parity,
4108 actual: tuple.parity_shard_count as u64,
4109 });
4110 }
4111 let total = expected_data_shards
4112 .checked_add(tuple.parity_shard_count as u64)
4113 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
4114 if total > 65_535 {
4115 return Err(FormatError::FecTooManyShards(total as usize));
4116 }
4117 Ok(())
4118}
4119
4120fn validate_cmra_writer_parity_lower_bound(
4121 tuple: CmraDecoderTuple,
4122 bit_rot_buffer_pct: u8,
4123) -> Result<(), FormatError> {
4124 let min_parity = 2u64.max(ceil_div_u64(
4125 checked_u64_mul(
4126 tuple.data_shard_count as u64,
4127 bit_rot_buffer_pct as u64,
4128 "CMRA parity lower-bound overflow",
4129 )?,
4130 100,
4131 )?);
4132 if (tuple.parity_shard_count as u64) < min_parity {
4133 return Err(FormatError::InvalidArchive(
4134 "CMRA parity shard count is below authenticated bit-rot lower bound",
4135 ));
4136 }
4137 Ok(())
4138}
4139
4140fn validate_cmra_shard(
4141 shard: &CriticalMetadataRecoveryShard,
4142 serialized_idx: usize,
4143 tuple: CmraDecoderTuple,
4144) -> Result<(), FormatError> {
4145 if shard.shard_index as usize != serialized_idx {
4146 return Err(FormatError::InvalidArchive(
4147 "CMRA shards are not in canonical order",
4148 ));
4149 }
4150 let data_count = tuple.data_shard_count as usize;
4151 let shard_size = tuple.shard_size as usize;
4152 if serialized_idx < data_count {
4153 if shard.shard_role != 0 {
4154 return Err(FormatError::InvalidArchive(
4155 "CMRA data shard has wrong role",
4156 ));
4157 }
4158 let expected_len = if serialized_idx + 1 == data_count {
4159 let used = tuple.image_length as usize - serialized_idx * shard_size;
4160 if used == 0 {
4161 shard_size
4162 } else {
4163 used
4164 }
4165 } else {
4166 shard_size
4167 };
4168 if shard.shard_payload_length as usize != expected_len {
4169 return Err(FormatError::InvalidArchive(
4170 "CMRA data shard payload length is non-canonical",
4171 ));
4172 }
4173 if serialized_idx + 1 == data_count
4174 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
4175 {
4176 return Err(FormatError::InvalidArchive(
4177 "CMRA final data shard padding is non-zero",
4178 ));
4179 }
4180 } else {
4181 if shard.shard_role != 1 {
4182 return Err(FormatError::InvalidArchive(
4183 "CMRA parity shard has wrong role",
4184 ));
4185 }
4186 if shard.shard_payload_length as usize != shard_size {
4187 return Err(FormatError::InvalidArchive(
4188 "CMRA parity shard payload length is non-canonical",
4189 ));
4190 }
4191 }
4192 Ok(())
4193}
4194
4195fn validate_critical_metadata_image(
4196 image: &CriticalMetadataImage,
4197 mode: CmraRecoveryMode,
4198) -> Result<(), FormatError> {
4199 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
4200 if image.volume_header_offset != 0
4201 || image.volume_header_length != VOLUME_HEADER_LEN as u32
4202 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
4203 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
4204 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
4205 || image.body_bytes_before_cmra
4206 != image
4207 .volume_trailer_offset
4208 .checked_add(VOLUME_TRAILER_LEN as u64)
4209 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
4210 {
4211 return Err(FormatError::InvalidArchive(
4212 "CriticalMetadataImage fixed layout is invalid",
4213 ));
4214 }
4215 if root_auth_present {
4216 if image.root_auth_footer_offset == 0
4217 || image.root_auth_footer_length == 0
4218 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
4219 {
4220 return Err(FormatError::InvalidArchive(
4221 "CriticalMetadataImage root-auth range is invalid",
4222 ));
4223 }
4224 } else if image.root_auth_footer_offset != 0
4225 || image.root_auth_footer_length != 0
4226 || image.root_auth_footer_sha256 != [0u8; 32]
4227 {
4228 return Err(FormatError::InvalidArchive(
4229 "CriticalMetadataImage root-auth fields must be zero when absent",
4230 ));
4231 }
4232 let block_record_len = image_block_record_len_from_region(image)?;
4233 let block_record_len_u64 = u64::try_from(block_record_len)
4234 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
4235 match mode {
4236 CmraRecoveryMode::KeyHolding => {
4237 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
4238 FormatError::InvalidArchive("BlockRecord region length overflow"),
4239 )?;
4240 if image.block_records_length != expected_len {
4241 return Err(FormatError::InvalidArchive(
4242 "CriticalMetadataImage terminal equations are invalid",
4243 ));
4244 }
4245 }
4246 CmraRecoveryMode::PublicNoKey => {
4247 if image.block_records_length % block_record_len_u64 != 0 {
4248 return Err(FormatError::InvalidArchive(
4249 "CriticalMetadataImage BlockRecord region is not aligned",
4250 ));
4251 }
4252 }
4253 }
4254 if image.block_records_offset
4255 != image
4256 .crypto_header_offset
4257 .checked_add(image.crypto_header_length as u64)
4258 .ok_or(FormatError::InvalidArchive(
4259 "CryptoHeader boundary overflow",
4260 ))?
4261 || image.manifest_footer_offset
4262 != image
4263 .block_records_offset
4264 .checked_add(image.block_records_length)
4265 .ok_or(FormatError::InvalidArchive(
4266 "ManifestFooter boundary overflow",
4267 ))?
4268 {
4269 return Err(FormatError::InvalidArchive(
4270 "CriticalMetadataImage terminal equations are invalid",
4271 ));
4272 }
4273 let manifest_end = image
4274 .manifest_footer_offset
4275 .checked_add(MANIFEST_FOOTER_LEN as u64)
4276 .ok_or(FormatError::InvalidArchive(
4277 "RootAuthFooter boundary overflow",
4278 ))?;
4279 if root_auth_present {
4280 if image.root_auth_footer_offset != manifest_end
4281 || image
4282 .root_auth_footer_offset
4283 .checked_add(image.root_auth_footer_length as u64)
4284 .ok_or(FormatError::InvalidArchive(
4285 "VolumeTrailer boundary overflow",
4286 ))?
4287 != image.volume_trailer_offset
4288 {
4289 return Err(FormatError::InvalidArchive(
4290 "CriticalMetadataImage root-auth terminal equations are invalid",
4291 ));
4292 }
4293 } else if image.volume_trailer_offset != manifest_end {
4294 return Err(FormatError::InvalidArchive(
4295 "CriticalMetadataImage unsigned terminal equations are invalid",
4296 ));
4297 }
4298 let expected_types: &[u16] = if root_auth_present {
4299 &[1, 2, 3, 4, 5]
4300 } else {
4301 &[1, 2, 3, 5]
4302 };
4303 if image.regions.len() != expected_types.len()
4304 || image
4305 .regions
4306 .iter()
4307 .map(|region| region.region_type)
4308 .ne(expected_types.iter().copied())
4309 {
4310 return Err(FormatError::InvalidArchive(
4311 "CriticalMetadataImage regions are not canonical",
4312 ));
4313 }
4314 validate_image_region(
4315 image,
4316 1,
4317 image.volume_header_offset,
4318 image.volume_header_length,
4319 )?;
4320 validate_image_region(
4321 image,
4322 2,
4323 image.crypto_header_offset,
4324 image.crypto_header_length,
4325 )?;
4326 validate_image_region(
4327 image,
4328 3,
4329 image.manifest_footer_offset,
4330 image.manifest_footer_length,
4331 )?;
4332 if root_auth_present {
4333 validate_image_region(
4334 image,
4335 4,
4336 image.root_auth_footer_offset,
4337 image.root_auth_footer_length,
4338 )?;
4339 }
4340 validate_image_region(
4341 image,
4342 5,
4343 image.volume_trailer_offset,
4344 image.volume_trailer_length,
4345 )?;
4346 if sha256_region(image, 1)? != image.volume_header_sha256
4347 || sha256_region(image, 2)? != image.crypto_header_sha256
4348 || sha256_region(image, 3)? != image.manifest_footer_sha256
4349 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
4350 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
4351 || sha256_region(image, 5)? != image.volume_trailer_sha256
4352 {
4353 return Err(FormatError::InvalidArchive(
4354 "CriticalMetadataImage region digest mismatch",
4355 ));
4356 }
4357 Ok(())
4358}
4359
4360fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
4361 let crypto_region = image
4362 .region(2)
4363 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4364 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4365 crypto.fixed.validate_supported_profile()?;
4366 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
4367}
4368
4369fn validate_image_region(
4370 image: &CriticalMetadataImage,
4371 region_type: u16,
4372 offset: u64,
4373 length: u32,
4374) -> Result<(), FormatError> {
4375 let region = image
4376 .region(region_type)
4377 .ok_or(FormatError::InvalidArchive(
4378 "missing CriticalMetadataImage region",
4379 ))?;
4380 if region.offset != offset || region.bytes.len() != length as usize {
4381 return Err(FormatError::InvalidArchive(
4382 "CriticalMetadataImage region range mismatch",
4383 ));
4384 }
4385 Ok(())
4386}
4387
4388fn validate_image_identity(
4389 image: &CriticalMetadataImage,
4390 volume_header: &VolumeHeader,
4391 crypto_header: &CryptoHeaderFixed,
4392) -> Result<(), FormatError> {
4393 if image.archive_uuid != volume_header.archive_uuid
4394 || image.session_id != volume_header.session_id
4395 || image.volume_index != volume_header.volume_index
4396 || image.stripe_width != volume_header.stripe_width
4397 || image.stripe_width != crypto_header.stripe_width
4398 {
4399 return Err(FormatError::InvalidArchive(
4400 "CriticalMetadataImage identity does not match selected volume",
4401 ));
4402 }
4403 Ok(())
4404}
4405
4406fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
4407 Ok(sha256_bytes(
4408 &image
4409 .region(region_type)
4410 .ok_or(FormatError::InvalidArchive(
4411 "missing CriticalMetadataImage region",
4412 ))?
4413 .bytes,
4414 ))
4415}
4416
4417fn validate_recovered_terminal(
4418 image: CriticalMetadataImage,
4419 tuple: CmraDecoderTuple,
4420 bytes: &[u8],
4421 context: KeyHoldingTerminalContext<'_>,
4422) -> Result<V41Terminal, FormatError> {
4423 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
4424 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
4425 validate_recovered_terminal_inner(image, tuple, cmra_boundary_magic_ok, context)
4426}
4427
4428fn validate_recovered_terminal_read_at(
4429 image: CriticalMetadataImage,
4430 tuple: CmraDecoderTuple,
4431 reader: &dyn ArchiveReadAt,
4432 context: KeyHoldingTerminalContext<'_>,
4433) -> Result<V41Terminal, FormatError> {
4434 let mut magic = [0u8; 4];
4435 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
4436 validate_recovered_terminal_inner(image, tuple, magic == *b"TZCR", context)
4437}
4438
4439fn validate_recovered_terminal_inner(
4440 image: CriticalMetadataImage,
4441 tuple: CmraDecoderTuple,
4442 cmra_boundary_magic_ok: bool,
4443 context: KeyHoldingTerminalContext<'_>,
4444) -> Result<V41Terminal, FormatError> {
4445 let subkeys = context.subkeys;
4446 let volume_header = context.volume_header;
4447 let crypto_header = context.crypto_header;
4448 let volume_header_region = image
4449 .region(1)
4450 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
4451 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
4452 if &recovered_volume_header != volume_header {
4453 return Err(FormatError::InvalidArchive(
4454 "CMRA VolumeHeader differs from parsed VolumeHeader",
4455 ));
4456 }
4457 validate_image_identity(&image, volume_header, crypto_header)?;
4458 let crypto_region = image
4459 .region(2)
4460 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4461 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4462 if recovered_crypto.fixed != *crypto_header {
4463 return Err(FormatError::InvalidArchive(
4464 "CMRA CryptoHeader differs from parsed CryptoHeader",
4465 ));
4466 }
4467 let recovered_pre_hmac_len = crypto_region
4468 .bytes
4469 .len()
4470 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
4471 .ok_or(FormatError::InvalidArchive(
4472 "CMRA CryptoHeader is too short",
4473 ))?;
4474 let parsed_pre_hmac_len = context
4475 .crypto_header_bytes
4476 .len()
4477 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
4478 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
4479 if recovered_pre_hmac_len != parsed_pre_hmac_len
4480 || crypto_region.bytes[..recovered_pre_hmac_len]
4481 != context.crypto_header_bytes[..parsed_pre_hmac_len]
4482 {
4483 return Err(FormatError::InvalidArchive(
4484 "CMRA CryptoHeader differs from parsed CryptoHeader",
4485 ));
4486 }
4487 verify_hmac(
4488 HmacDomain::CryptoHeader,
4489 &subkeys.mac_key,
4490 &volume_header.archive_uuid,
4491 &volume_header.session_id,
4492 recovered_crypto.hmac_covered_bytes,
4493 &recovered_crypto.header_hmac,
4494 )?;
4495 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
4496 recovered_crypto.validate_extension_semantics()?;
4497
4498 let manifest_region = image
4499 .region(3)
4500 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
4501 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
4502 validate_manifest_footer(
4503 volume_header,
4504 &manifest_footer,
4505 subkeys,
4506 &manifest_region.bytes,
4507 )?;
4508 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
4509
4510 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
4511 let root_auth_region = image
4512 .region(4)
4513 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
4514 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
4515 if footer.archive_uuid != volume_header.archive_uuid
4516 || footer.session_id != volume_header.session_id
4517 || footer.footer_length()? != image.root_auth_footer_length
4518 {
4519 return Err(FormatError::InvalidArchive(
4520 "RootAuthFooter identity or length does not match terminal image",
4521 ));
4522 }
4523 Some(footer)
4524 } else {
4525 None
4526 };
4527
4528 let trailer_region = image
4529 .region(5)
4530 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
4531 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
4532 verify_hmac(
4533 HmacDomain::VolumeTrailer,
4534 &subkeys.mac_key,
4535 &volume_header.archive_uuid,
4536 &volume_header.session_id,
4537 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
4538 &trailer.trailer_hmac,
4539 )?;
4540 validate_trailer_identity(volume_header, &trailer)?;
4541 validate_v41_trailer_equations(&image, &trailer)?;
4542
4543 if !cmra_boundary_magic_ok {
4544 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
4545 }
4546
4547 let manifest_footer_bytes = manifest_region.bytes.clone();
4548 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
4549 Ok(V41Terminal {
4550 image,
4551 manifest_footer_bytes,
4552 root_auth_footer_bytes,
4553 root_auth_footer,
4554 volume_trailer: trailer,
4555 })
4556}
4557
4558fn validate_recovered_public_terminal(
4559 image: CriticalMetadataImage,
4560 bytes: &[u8],
4561 volume_header: &VolumeHeader,
4562 crypto_header: &CryptoHeaderFixed,
4563) -> Result<V41PublicTerminal, FormatError> {
4564 if image.layout_flags & 0x0000_0001 == 0 {
4565 return Err(FormatError::ReaderUnsupported(
4566 "public no-key verification requires root-auth",
4567 ));
4568 }
4569 let volume_header_region = image
4570 .region(1)
4571 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
4572 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
4573 if &recovered_volume_header != volume_header {
4574 return Err(FormatError::InvalidArchive(
4575 "CMRA VolumeHeader differs from parsed VolumeHeader",
4576 ));
4577 }
4578 validate_image_identity(&image, volume_header, crypto_header)?;
4579 let crypto_region = image
4580 .region(2)
4581 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4582 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4583 if recovered_crypto.fixed != *crypto_header {
4584 return Err(FormatError::InvalidArchive(
4585 "CMRA CryptoHeader differs from parsed CryptoHeader",
4586 ));
4587 }
4588 recovered_crypto.validate_extension_semantics()?;
4589
4590 image
4591 .region(3)
4592 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
4593
4594 let root_auth_region = image
4595 .region(4)
4596 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
4597 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
4598 if root_auth_footer.archive_uuid != volume_header.archive_uuid
4599 || root_auth_footer.session_id != volume_header.session_id
4600 || root_auth_footer.footer_length()? != image.root_auth_footer_length
4601 {
4602 return Err(FormatError::InvalidArchive(
4603 "public RootAuthFooter identity or length does not match terminal image",
4604 ));
4605 }
4606
4607 let trailer_region = image
4608 .region(5)
4609 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
4610 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
4611 validate_trailer_identity(volume_header, &trailer)?;
4612 validate_v41_public_trailer_profile(&image, &trailer)?;
4613
4614 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
4615 if bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
4616 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
4617 }
4618
4619 let root_auth_footer_bytes = root_auth_region.bytes.clone();
4620 Ok(V41PublicTerminal {
4621 image,
4622 root_auth_footer_bytes,
4623 root_auth_footer,
4624 })
4625}
4626
4627fn validate_v41_trailer_equations(
4628 image: &CriticalMetadataImage,
4629 trailer: &VolumeTrailer,
4630) -> Result<(), FormatError> {
4631 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
4632 if trailer.bytes_written != image.volume_trailer_offset
4633 || trailer.manifest_footer_offset != image.manifest_footer_offset
4634 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
4635 || trailer.block_count != image.block_count
4636 {
4637 return Err(FormatError::InvalidArchive(
4638 "VolumeTrailer does not match v41 terminal layout",
4639 ));
4640 }
4641 if root_auth_present {
4642 if trailer.root_auth_flags != 0x0000_0001
4643 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
4644 || trailer.root_auth_footer_length != image.root_auth_footer_length
4645 || image.root_auth_footer_offset
4646 != image
4647 .manifest_footer_offset
4648 .checked_add(MANIFEST_FOOTER_LEN as u64)
4649 .ok_or(FormatError::InvalidArchive(
4650 "RootAuthFooter trailer boundary overflow",
4651 ))?
4652 || image
4653 .root_auth_footer_offset
4654 .checked_add(image.root_auth_footer_length as u64)
4655 .ok_or(FormatError::InvalidArchive(
4656 "RootAuthFooter trailer boundary overflow",
4657 ))?
4658 != image.volume_trailer_offset
4659 {
4660 return Err(FormatError::InvalidArchive(
4661 "VolumeTrailer root-auth fields do not match v41 terminal layout",
4662 ));
4663 }
4664 } else if trailer.root_auth_footer_offset != 0
4665 || trailer.root_auth_footer_length != 0
4666 || trailer.root_auth_flags != 0
4667 {
4668 return Err(FormatError::InvalidArchive(
4669 "VolumeTrailer root-auth fields must be zero when absent",
4670 ));
4671 }
4672 Ok(())
4673}
4674
4675fn validate_v41_public_trailer_profile(
4676 image: &CriticalMetadataImage,
4677 trailer: &VolumeTrailer,
4678) -> Result<(), FormatError> {
4679 if trailer.bytes_written != image.volume_trailer_offset
4680 || trailer.manifest_footer_offset != image.manifest_footer_offset
4681 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
4682 {
4683 return Err(FormatError::InvalidArchive(
4684 "VolumeTrailer does not match v41 public terminal layout",
4685 ));
4686 }
4687 if trailer.root_auth_flags != 0x0000_0001
4688 || trailer.root_auth_footer_offset == 0
4689 || trailer.root_auth_footer_length == 0
4690 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
4691 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
4692 || trailer.root_auth_footer_length != image.root_auth_footer_length
4693 || image.root_auth_footer_offset
4694 != image
4695 .manifest_footer_offset
4696 .checked_add(MANIFEST_FOOTER_LEN as u64)
4697 .ok_or(FormatError::InvalidArchive(
4698 "RootAuthFooter trailer boundary overflow",
4699 ))?
4700 || image
4701 .root_auth_footer_offset
4702 .checked_add(image.root_auth_footer_length as u64)
4703 .ok_or(FormatError::InvalidArchive(
4704 "RootAuthFooter trailer boundary overflow",
4705 ))?
4706 != image.volume_trailer_offset
4707 {
4708 return Err(FormatError::InvalidArchive(
4709 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
4710 ));
4711 }
4712 Ok(())
4713}
4714
4715fn critical_image_min() -> u64 {
4716 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
4717 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
4718 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
4719 + VOLUME_HEADER_LEN as u64
4720 + MIN_CRYPTO_HEADER_LEN
4721 + MANIFEST_FOOTER_LEN as u64
4722 + VOLUME_TRAILER_LEN as u64
4723 + IMAGE_CRC_LEN as u64
4724}
4725
4726fn critical_image_cap() -> Result<u64, FormatError> {
4727 [
4728 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
4729 5 * SERIALIZED_REGION_HEADER_LEN as u64,
4730 VOLUME_HEADER_LEN as u64,
4731 READER_MAX_CRYPTO_HEADER_LEN as u64,
4732 MANIFEST_FOOTER_LEN as u64,
4733 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
4734 VOLUME_TRAILER_LEN as u64,
4735 IMAGE_CRC_LEN as u64,
4736 ]
4737 .into_iter()
4738 .try_fold(0u64, |total, value| {
4739 total
4740 .checked_add(value)
4741 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
4742 })
4743}
4744
4745fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
4746 let shard_total = (tuple.data_shard_count as u64)
4747 .checked_add(tuple.parity_shard_count as u64)
4748 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
4749 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
4750 .checked_add(tuple.shard_size as u64)
4751 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
4752 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
4753 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
4754 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
4755}
4756
4757fn cmra_worst_case_cap() -> Result<u64, FormatError> {
4758 let cap = critical_image_cap()?;
4759 let mut worst = 0u64;
4760 let mut shard_size = 512u64;
4761 while shard_size <= 4096 {
4762 let data = ceil_div_u64(cap, shard_size)?;
4763 let parity = 2u64.max(ceil_div_u64(
4764 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
4765 100,
4766 )?);
4767 let tuple = CmraDecoderTuple {
4768 shard_size: shard_size as u32,
4769 data_shard_count: u16::try_from(data)
4770 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
4771 parity_shard_count: u16::try_from(parity)
4772 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
4773 image_length: u32::try_from(cap)
4774 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
4775 image_sha256: [0u8; 32],
4776 };
4777 worst = worst.max(cmra_serialized_length(tuple)?);
4778 shard_size += 2;
4779 }
4780 Ok(worst)
4781}
4782
4783pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
4784 let total = [
4785 MANIFEST_FOOTER_LEN as u64,
4786 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
4787 VOLUME_TRAILER_LEN as u64,
4788 cmra_worst_case_cap()?,
4789 LOCATOR_PAIR_LEN as u64,
4790 ]
4791 .into_iter()
4792 .try_fold(0u64, |sum, value| {
4793 sum.checked_add(value)
4794 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
4795 })?;
4796 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
4797}
4798
4799fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
4800 let worst = cmra_worst_case_cap()?;
4801 let total = options
4802 .max_trailing_garbage_scan
4803 .try_into()
4804 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
4805 .and_then(|scan: u64| {
4806 scan.checked_add(worst)
4807 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
4808 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
4809 })?;
4810 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
4811}
4812
4813fn validate_bootstrap_single_volume_input(
4814 volume_header: &VolumeHeader,
4815 crypto_header: &CryptoHeaderFixed,
4816) -> Result<(), FormatError> {
4817 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
4818 return Err(FormatError::ReaderUnsupported(
4819 "bootstrap sidecar reader supports only single-volume archive input",
4820 ));
4821 }
4822 if crypto_header.stripe_width != volume_header.stripe_width {
4823 return Err(FormatError::InvalidArchive(
4824 "VolumeHeader and CryptoHeader stripe_width differ",
4825 ));
4826 }
4827 Ok(())
4828}
4829
4830#[derive(Debug)]
4831struct ParsedBootstrapSidecar {
4832 manifest_footer: Option<ManifestFooter>,
4833 index_root_records_section: Option<(u64, u64)>,
4834 dictionary_records_section: Option<(u64, u64)>,
4835}
4836
4837pub(crate) struct NonSeekableBootstrapMaterial {
4838 pub(crate) manifest_footer: ManifestFooter,
4839 pub(crate) payload_dictionary: Option<Vec<u8>>,
4840}
4841
4842#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4843enum BootstrapSidecarUse {
4844 SeekableAssist,
4845 NonSeekableRandomAccess,
4846}
4847
4848impl ParsedBootstrapSidecar {
4849 fn require_sections_for(
4850 &self,
4851 sidecar_use: BootstrapSidecarUse,
4852 crypto_header: &CryptoHeaderFixed,
4853 ) -> Result<(), FormatError> {
4854 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
4855 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
4856 return Err(FormatError::ReaderUnsupported(
4857 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
4858 ));
4859 }
4860 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
4861 return Err(FormatError::ReaderUnsupported(
4862 "dictionary bootstrap required",
4863 ));
4864 }
4865 }
4866 Ok(())
4867 }
4868}
4869
4870pub(crate) fn parse_non_seekable_bootstrap_material(
4871 bootstrap_sidecar: &[u8],
4872 volume_header: &VolumeHeader,
4873 crypto_header: &CryptoHeaderFixed,
4874 subkeys: &Subkeys,
4875) -> Result<NonSeekableBootstrapMaterial, FormatError> {
4876 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
4877 let sidecar =
4878 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
4879 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
4880 let manifest_footer = sidecar
4881 .manifest_footer
4882 .clone()
4883 .ok_or(FormatError::ReaderUnsupported(
4884 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
4885 ))?;
4886
4887 let mut blocks = BTreeMap::new();
4888 let (offset, length) =
4889 sidecar
4890 .index_root_records_section
4891 .ok_or(FormatError::ReaderUnsupported(
4892 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
4893 ))?;
4894 let index_root_records = parse_sidecar_block_records(
4895 bootstrap_sidecar,
4896 crypto_header.block_size as usize,
4897 SidecarBlockRecordsSection {
4898 offset,
4899 length,
4900 extent: index_root_extent_from_manifest(&manifest_footer),
4901 data_kind: BlockKind::IndexRootData,
4902 parity_kind: BlockKind::IndexRootParity,
4903 structure: "IndexRoot",
4904 },
4905 )?;
4906 insert_sidecar_records(&mut blocks, index_root_records)?;
4907
4908 let limits = metadata_limits(crypto_header);
4909 let index_root_plaintext = load_metadata_object_from_parts(
4910 &blocks,
4911 ObjectLoadContext::index_root(
4912 volume_header,
4913 crypto_header,
4914 subkeys,
4915 index_root_extent_from_manifest(&manifest_footer),
4916 ),
4917 manifest_footer.index_root_decompressed_size,
4918 )?;
4919 let index_root = IndexRoot::parse(
4920 &index_root_plaintext,
4921 crypto_header.has_dictionary != 0,
4922 limits,
4923 )?;
4924
4925 if crypto_header.has_dictionary != 0 {
4926 let (offset, length) =
4927 sidecar
4928 .dictionary_records_section
4929 .ok_or(FormatError::ReaderUnsupported(
4930 "dictionary bootstrap required",
4931 ))?;
4932 let dictionary_records = parse_sidecar_block_records(
4933 bootstrap_sidecar,
4934 crypto_header.block_size as usize,
4935 SidecarBlockRecordsSection {
4936 offset,
4937 length,
4938 extent: dictionary_extent_from_index_root(&index_root)?,
4939 data_kind: BlockKind::DictionaryData,
4940 parity_kind: BlockKind::DictionaryParity,
4941 structure: "dictionary",
4942 },
4943 )?;
4944 insert_sidecar_records(&mut blocks, dictionary_records)?;
4945 }
4946 let payload_dictionary =
4947 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
4948
4949 Ok(NonSeekableBootstrapMaterial {
4950 manifest_footer,
4951 payload_dictionary,
4952 })
4953}
4954
4955fn parse_bootstrap_sidecar(
4956 bytes: &[u8],
4957 volume_header: &VolumeHeader,
4958 crypto_header: &CryptoHeaderFixed,
4959 subkeys: &Subkeys,
4960) -> Result<ParsedBootstrapSidecar, FormatError> {
4961 let header_bytes = slice(
4962 bytes,
4963 0,
4964 BOOTSTRAP_SIDECAR_HEADER_LEN,
4965 "BootstrapSidecarHeader",
4966 )?;
4967 let header = BootstrapSidecarHeader::parse(header_bytes)?;
4968 if header.archive_uuid != volume_header.archive_uuid
4969 || header.session_id != volume_header.session_id
4970 {
4971 return Err(FormatError::InvalidArchive(
4972 "bootstrap sidecar identity does not match VolumeHeader",
4973 ));
4974 }
4975 verify_hmac(
4976 HmacDomain::BootstrapSidecar,
4977 &subkeys.mac_key,
4978 &volume_header.archive_uuid,
4979 &volume_header.session_id,
4980 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
4981 &header.sidecar_hmac,
4982 )?;
4983 header.validate_packed_layout(bytes.len() as u64)?;
4984 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
4985
4986 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
4987 return Err(FormatError::InvalidArchive(
4988 "bootstrap sidecar has dictionary records while has_dictionary is false",
4989 ));
4990 }
4991
4992 let manifest_footer = if header.has_manifest_footer() {
4993 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
4994 let manifest_bytes = slice(
4995 bytes,
4996 manifest_offset,
4997 MANIFEST_FOOTER_LEN,
4998 "ManifestFooter",
4999 )?;
5000 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5001 validate_sidecar_manifest_footer(
5002 volume_header,
5003 crypto_header,
5004 &manifest_footer,
5005 subkeys,
5006 manifest_bytes,
5007 )?;
5008 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5009 Some(manifest_footer)
5010 } else {
5011 None
5012 };
5013
5014 Ok(ParsedBootstrapSidecar {
5015 manifest_footer,
5016 index_root_records_section: header.has_index_root_records().then_some((
5017 header.index_root_records_offset,
5018 header.index_root_records_length,
5019 )),
5020 dictionary_records_section: header.has_dictionary_records().then_some((
5021 header.dictionary_records_offset,
5022 header.dictionary_records_length,
5023 )),
5024 })
5025}
5026
5027fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
5028 ObjectExtent {
5029 first_block_index: manifest_footer.index_root_first_block,
5030 data_block_count: manifest_footer.index_root_data_block_count,
5031 parity_block_count: manifest_footer.index_root_parity_block_count,
5032 encrypted_size: manifest_footer.index_root_encrypted_size,
5033 }
5034}
5035
5036fn insert_sidecar_records(
5037 blocks: &mut BTreeMap<u64, BlockRecord>,
5038 records: Vec<BlockRecord>,
5039) -> Result<(), FormatError> {
5040 for record in records {
5041 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
5042 if existing != record {
5043 return Err(FormatError::InvalidArchive(
5044 "bootstrap sidecar conflicts with volume BlockRecord",
5045 ));
5046 }
5047 }
5048 }
5049 Ok(())
5050}
5051
5052fn validate_sidecar_manifest_footer(
5053 volume_header: &VolumeHeader,
5054 crypto_header: &CryptoHeaderFixed,
5055 footer: &ManifestFooter,
5056 subkeys: &Subkeys,
5057 raw: &[u8],
5058) -> Result<(), FormatError> {
5059 if footer.archive_uuid != volume_header.archive_uuid
5060 || footer.session_id != volume_header.session_id
5061 {
5062 return Err(FormatError::InvalidArchive(
5063 "sidecar ManifestFooter identity does not match VolumeHeader",
5064 ));
5065 }
5066 if footer.volume_index != 0 {
5067 return Err(FormatError::InvalidArchive(
5068 "sidecar ManifestFooter volume_index must be zero",
5069 ));
5070 }
5071 if footer.total_volumes != crypto_header.stripe_width {
5072 return Err(FormatError::InvalidArchive(
5073 "sidecar ManifestFooter total_volumes does not match stripe_width",
5074 ));
5075 }
5076 if footer.is_authoritative != 1 {
5077 return Err(FormatError::InvalidArchive(
5078 "sidecar ManifestFooter is not authoritative",
5079 ));
5080 }
5081 verify_hmac(
5082 HmacDomain::ManifestFooter,
5083 &subkeys.mac_key,
5084 &volume_header.archive_uuid,
5085 &volume_header.session_id,
5086 &raw[..MANIFEST_HMAC_COVERED_LEN],
5087 &footer.manifest_hmac,
5088 )
5089}
5090
5091fn validate_sidecar_size_cap(
5092 header: &BootstrapSidecarHeader,
5093 crypto_header: &CryptoHeaderFixed,
5094 file_size: u64,
5095) -> Result<(), FormatError> {
5096 let record_len = checked_u64_add(
5097 crypto_header.block_size as u64,
5098 BLOCK_RECORD_FRAMING_LEN as u64,
5099 "bootstrap sidecar cap overflow",
5100 )?;
5101 let max_index_records = crypto_header.index_root_fec_data_shards as u64
5102 + crypto_header.index_root_fec_parity_shards as u64;
5103 let max_record_section_bytes = checked_u64_mul(
5104 max_index_records,
5105 record_len,
5106 "bootstrap sidecar cap overflow",
5107 )?;
5108 if header.index_root_records_length % record_len != 0 {
5109 return Err(FormatError::InvalidArchive(
5110 "bootstrap sidecar IndexRoot records length is not aligned",
5111 ));
5112 }
5113 if header.index_root_records_length / record_len > max_index_records {
5114 return Err(FormatError::InvalidArchive(
5115 "bootstrap sidecar IndexRoot records exceed resource cap",
5116 ));
5117 }
5118 if header.dictionary_records_length % record_len != 0 {
5119 return Err(FormatError::InvalidArchive(
5120 "bootstrap sidecar dictionary records length is not aligned",
5121 ));
5122 }
5123 if header.dictionary_records_length / record_len > max_index_records {
5124 return Err(FormatError::InvalidArchive(
5125 "bootstrap sidecar dictionary records exceed resource cap",
5126 ));
5127 }
5128
5129 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
5130 if header.has_manifest_footer() {
5131 cap = cap
5132 .checked_add(MANIFEST_FOOTER_LEN as u64)
5133 .ok_or(FormatError::InvalidArchive(
5134 "bootstrap sidecar cap overflow",
5135 ))?;
5136 }
5137 if header.has_index_root_records() {
5138 cap = checked_u64_add(
5139 cap,
5140 max_record_section_bytes,
5141 "bootstrap sidecar cap overflow",
5142 )?;
5143 }
5144 if header.has_dictionary_records() {
5145 cap = checked_u64_add(
5146 cap,
5147 max_record_section_bytes,
5148 "bootstrap sidecar cap overflow",
5149 )?;
5150 }
5151 if file_size > cap {
5152 return Err(FormatError::InvalidArchive(
5153 "bootstrap sidecar exceeds resource cap",
5154 ));
5155 }
5156 Ok(())
5157}
5158
5159#[derive(Debug, Clone, Copy)]
5160struct SidecarBlockRecordsSection {
5161 offset: u64,
5162 length: u64,
5163 extent: ObjectExtent,
5164 data_kind: BlockKind,
5165 parity_kind: BlockKind,
5166 structure: &'static str,
5167}
5168
5169fn parse_sidecar_block_records(
5170 sidecar_bytes: &[u8],
5171 block_size: usize,
5172 section: SidecarBlockRecordsSection,
5173) -> Result<Vec<BlockRecord>, FormatError> {
5174 let record_len = block_size
5175 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5176 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5177 if section.length % record_len as u64 != 0 {
5178 return Err(FormatError::InvalidArchive(
5179 "sidecar BlockRecord section is not aligned",
5180 ));
5181 }
5182 let expected_count =
5183 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
5184 let actual_count = usize::try_from(section.length / record_len as u64)
5185 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
5186 if actual_count != expected_count {
5187 return Err(FormatError::InvalidArchive(
5188 "sidecar BlockRecord section does not match declared extent",
5189 ));
5190 }
5191 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
5192 let raw = slice(
5193 sidecar_bytes,
5194 start,
5195 to_usize(section.length, "BootstrapSidecarHeader")?,
5196 "BootstrapSidecarHeader",
5197 )?;
5198 let mut records = Vec::with_capacity(expected_count);
5199
5200 for idx in 0..expected_count {
5201 let record = BlockRecord::parse(
5202 slice(raw, idx * record_len, record_len, "BlockRecord")?,
5203 block_size,
5204 )?;
5205 let expected_block_index = checked_u64_add(
5206 section.extent.first_block_index,
5207 idx as u64,
5208 section.structure,
5209 )?;
5210 if record.block_index != expected_block_index {
5211 return Err(FormatError::InvalidArchive(
5212 "sidecar BlockRecord section has missing or duplicate blocks",
5213 ));
5214 }
5215 let expected_kind = if idx < section.extent.data_block_count as usize {
5216 section.data_kind
5217 } else {
5218 section.parity_kind
5219 };
5220 if record.kind != expected_kind {
5221 return Err(FormatError::InvalidArchive(
5222 "sidecar BlockRecord section has wrong kind",
5223 ));
5224 }
5225 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
5226 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
5227 {
5228 return Err(FormatError::InvalidArchive(
5229 "sidecar BlockRecord section has wrong last-data flag",
5230 ));
5231 }
5232 records.push(record);
5233 }
5234
5235 Ok(records)
5236}
5237
5238fn validate_trailer_identity(
5239 volume_header: &VolumeHeader,
5240 trailer: &VolumeTrailer,
5241) -> Result<(), FormatError> {
5242 if trailer.archive_uuid != volume_header.archive_uuid
5243 || trailer.session_id != volume_header.session_id
5244 || trailer.volume_index != volume_header.volume_index
5245 {
5246 return Err(FormatError::InvalidArchive(
5247 "VolumeTrailer identity does not match VolumeHeader",
5248 ));
5249 }
5250 Ok(())
5251}
5252
5253fn validate_manifest_footer(
5254 volume_header: &VolumeHeader,
5255 footer: &ManifestFooter,
5256 subkeys: &Subkeys,
5257 raw: &[u8],
5258) -> Result<(), FormatError> {
5259 if footer.archive_uuid != volume_header.archive_uuid
5260 || footer.session_id != volume_header.session_id
5261 || footer.volume_index != volume_header.volume_index
5262 {
5263 return Err(FormatError::InvalidArchive(
5264 "ManifestFooter identity does not match VolumeHeader",
5265 ));
5266 }
5267 if footer.total_volumes != volume_header.stripe_width {
5268 return Err(FormatError::InvalidArchive(
5269 "ManifestFooter total_volumes does not match stripe_width",
5270 ));
5271 }
5272 if footer.is_authoritative != 1 {
5273 return Err(FormatError::InvalidArchive(
5274 "ManifestFooter is not authoritative",
5275 ));
5276 }
5277 verify_hmac(
5278 HmacDomain::ManifestFooter,
5279 &subkeys.mac_key,
5280 &volume_header.archive_uuid,
5281 &volume_header.session_id,
5282 &raw[..MANIFEST_HMAC_COVERED_LEN],
5283 &footer.manifest_hmac,
5284 )
5285}
5286
5287#[derive(Debug)]
5288struct ParsedBlockRegion {
5289 blocks: BTreeMap<u64, BlockRecord>,
5290 erased_block_indices: BTreeSet<u64>,
5291}
5292
5293fn parse_block_region(
5294 bytes: &[u8],
5295 start: usize,
5296 end: usize,
5297 block_size: usize,
5298 volume_header: &VolumeHeader,
5299 trailer: &VolumeTrailer,
5300) -> Result<ParsedBlockRegion, FormatError> {
5301 if end < start {
5302 return Err(FormatError::InvalidArchive(
5303 "ManifestFooter starts before BlockRecord region",
5304 ));
5305 }
5306 let record_len = block_size
5307 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5308 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5309 let region_len = end - start;
5310 if region_len % record_len != 0 {
5311 return Err(FormatError::InvalidArchive(
5312 "BlockRecord region length is not aligned",
5313 ));
5314 }
5315 let observed_count = region_len / record_len;
5316 if observed_count as u64 != trailer.block_count {
5317 return Err(FormatError::InvalidArchive(
5318 "VolumeTrailer block_count does not match BlockRecord region",
5319 ));
5320 }
5321
5322 let mut blocks = BTreeMap::new();
5323 let mut erased_block_indices = BTreeSet::new();
5324 for idx in 0..observed_count {
5325 let offset = start + idx * record_len;
5326 let expected_block_index = checked_u64_add(
5327 volume_header.volume_index as u64,
5328 checked_u64_mul(
5329 idx as u64,
5330 volume_header.stripe_width as u64,
5331 "BlockRecord index overflow",
5332 )?,
5333 "BlockRecord index overflow",
5334 )?;
5335 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5336 match BlockRecord::parse(raw, block_size) {
5337 Ok(record) => {
5338 if record.block_index != expected_block_index {
5339 return Err(FormatError::InvalidArchive(
5340 "BlockRecord index does not match volume position",
5341 ));
5342 }
5343 if blocks.insert(record.block_index, record).is_some() {
5344 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5345 }
5346 }
5347 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
5348 if !erased_block_indices.insert(expected_block_index) {
5349 return Err(FormatError::InvalidArchive(
5350 "duplicate erased BlockRecord index",
5351 ));
5352 }
5353 }
5354 Err(err) => return Err(err),
5355 }
5356 }
5357
5358 Ok(ParsedBlockRegion {
5359 blocks,
5360 erased_block_indices,
5361 })
5362}
5363
5364fn validate_seekable_block_region_layout(
5365 start: u64,
5366 end: u64,
5367 block_size: usize,
5368 trailer: &VolumeTrailer,
5369) -> Result<(), FormatError> {
5370 if end < start {
5371 return Err(FormatError::InvalidArchive(
5372 "ManifestFooter starts before BlockRecord region",
5373 ));
5374 }
5375 let record_len = block_record_len(block_size)?;
5376 let region_len = end - start;
5377 if region_len % record_len != 0 {
5378 return Err(FormatError::InvalidArchive(
5379 "BlockRecord region length is not aligned",
5380 ));
5381 }
5382 let observed_count = region_len / record_len;
5383 if observed_count != trailer.block_count {
5384 return Err(FormatError::InvalidArchive(
5385 "VolumeTrailer block_count does not match BlockRecord region",
5386 ));
5387 }
5388 Ok(())
5389}
5390
5391fn parse_public_block_observation(
5392 bytes: &[u8],
5393 start: usize,
5394 image: &CriticalMetadataImage,
5395 block_size: usize,
5396 volume_header: &VolumeHeader,
5397) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
5398 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
5399 if start != image_start {
5400 return Err(FormatError::InvalidArchive(
5401 "public BlockRecord observation start mismatch",
5402 ));
5403 }
5404 let scan_limit_u64 = image
5405 .block_records_offset
5406 .checked_add(image.block_records_length)
5407 .ok_or(FormatError::InvalidArchive(
5408 "public BlockRecord observation limit overflow",
5409 ))?;
5410 if scan_limit_u64 != image.manifest_footer_offset {
5411 return Err(FormatError::InvalidArchive(
5412 "public BlockRecord observation limit mismatch",
5413 ));
5414 }
5415 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
5416 if scan_limit < start {
5417 return Err(FormatError::InvalidArchive(
5418 "public BlockRecord observation limit before start",
5419 ));
5420 }
5421 let record_len = block_size
5422 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5423 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5424 let region_len = scan_limit - start;
5425 if region_len % record_len != 0 {
5426 return Err(FormatError::InvalidArchive(
5427 "public BlockRecord observation window is not aligned",
5428 ));
5429 }
5430
5431 let mut blocks = BTreeMap::new();
5432 let mut offset = start;
5433 let mut observed_slot = 0u64;
5434 while offset < scan_limit {
5435 let magic_end = checked_add(offset, 4, "BlockRecord")?;
5436 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
5437 break;
5438 }
5439 let record_end = checked_add(offset, record_len, "BlockRecord")?;
5440 if record_end > scan_limit {
5441 return Err(FormatError::InvalidArchive(
5442 "public BlockRecord observation slot is incomplete",
5443 ));
5444 }
5445 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5446 let record = BlockRecord::parse(raw, block_size)?;
5447 let expected_block_index = checked_u64_add(
5448 volume_header.volume_index as u64,
5449 checked_u64_mul(
5450 observed_slot,
5451 volume_header.stripe_width as u64,
5452 "BlockRecord index overflow",
5453 )?,
5454 "BlockRecord index overflow",
5455 )?;
5456 if record.block_index != expected_block_index {
5457 return Err(FormatError::InvalidArchive(
5458 "public BlockRecord index does not match volume position",
5459 ));
5460 }
5461 if blocks.insert(record.block_index, record).is_some() {
5462 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5463 }
5464 offset = record_end;
5465 observed_slot = observed_slot
5466 .checked_add(1)
5467 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
5468 }
5469
5470 let mut scan = if offset < scan_limit {
5471 checked_add(offset, record_len, "BlockRecord")?
5472 } else {
5473 scan_limit
5474 };
5475 while scan < scan_limit {
5476 let magic_end = checked_add(scan, 4, "BlockRecord")?;
5477 let record_end = checked_add(scan, record_len, "BlockRecord")?;
5478 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
5479 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
5480 if BlockRecord::parse(raw, block_size).is_ok() {
5481 return Err(FormatError::InvalidArchive(
5482 "public observation has ambiguous extra BlockRecord",
5483 ));
5484 }
5485 }
5486 scan = record_end;
5487 }
5488
5489 Ok(blocks)
5490}
5491
5492pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
5493 matches!(
5494 error,
5495 FormatError::BadCrc {
5496 structure: "BlockRecord",
5497 }
5498 )
5499}
5500
5501fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
5502 let len = block_size
5503 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5504 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5505 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
5506}
5507
5508fn expected_striped_volume_block_count(
5509 total_block_count: u64,
5510 stripe_width: u32,
5511 volume_index: u32,
5512) -> Result<u64, FormatError> {
5513 if stripe_width == 0 {
5514 return Err(FormatError::ZeroStripeWidth);
5515 }
5516 if volume_index >= stripe_width {
5517 return Err(FormatError::VolumeIndexOutOfRange {
5518 volume_index,
5519 stripe_width,
5520 });
5521 }
5522 let volume_index = volume_index as u64;
5523 if total_block_count <= volume_index {
5524 return Ok(0);
5525 }
5526 Ok((total_block_count - 1 - volume_index) / stripe_width as u64 + 1)
5527}
5528
5529fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
5530 lhs.checked_mul(rhs)
5531 .ok_or(FormatError::InvalidArchive(reason))
5532}
5533
5534fn parse_stream_block_prefix(
5535 bytes: &[u8],
5536 start: usize,
5537 block_size: usize,
5538 volume_header: &VolumeHeader,
5539) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
5540 let record_len = block_size
5541 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5542 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5543 let mut blocks = BTreeMap::new();
5544 let mut offset = start;
5545 let mut observed_block_count = 0u64;
5546
5547 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
5548 let expected_block_index =
5549 expected_stream_block_index(volume_header, observed_block_count)?;
5550 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5551 match BlockRecord::parse(raw, block_size) {
5552 Ok(record) => {
5553 if record.block_index != expected_block_index {
5554 return Err(FormatError::InvalidArchive(
5555 "BlockRecord index does not match stream position",
5556 ));
5557 }
5558 if blocks.insert(record.block_index, record).is_some() {
5559 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5560 }
5561 }
5562 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
5563 Err(err) => return Err(err),
5564 }
5565 offset = checked_add(offset, record_len, "BlockRecord")?;
5566 observed_block_count = observed_block_count
5567 .checked_add(1)
5568 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
5569 }
5570
5571 Ok((blocks, offset, observed_block_count))
5572}
5573
5574pub(crate) fn expected_stream_block_index(
5575 volume_header: &VolumeHeader,
5576 observed_block_count: u64,
5577) -> Result<u64, FormatError> {
5578 checked_u64_add(
5579 volume_header.volume_index as u64,
5580 checked_u64_mul(
5581 observed_block_count,
5582 volume_header.stripe_width as u64,
5583 "BlockRecord index overflow",
5584 )?,
5585 "BlockRecord index overflow",
5586 )
5587}
5588
5589fn parse_sequential_block_or_erasure(
5590 bytes: &[u8],
5591 offset: usize,
5592 record_len: usize,
5593 block_size: usize,
5594 volume_header: &VolumeHeader,
5595 observed_block_count: u64,
5596) -> Result<Option<BlockRecord>, FormatError> {
5597 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
5598 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5599 match BlockRecord::parse(raw, block_size) {
5600 Ok(record) => {
5601 if record.block_index != expected_block_index {
5602 return Err(FormatError::InvalidArchive(
5603 "BlockRecord index does not match stream position",
5604 ));
5605 }
5606 Ok(Some(record))
5607 }
5608 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
5609 Err(err) => Err(err),
5610 }
5611}
5612
5613fn parse_terminal_material(
5614 bytes: &[u8],
5615 manifest_offset: usize,
5616 observed_block_count: u64,
5617 context: KeyHoldingTerminalContext<'_>,
5618 options: ReaderOptions,
5619) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
5620 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
5621 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
5622 return Err(FormatError::InvalidArchive(
5623 "sequential terminal does not end at EOF",
5624 ));
5625 }
5626 let terminal = candidate.terminal;
5627 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
5628 return Err(FormatError::InvalidArchive(
5629 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
5630 ));
5631 }
5632 if terminal.volume_trailer.block_count != observed_block_count {
5633 return Err(FormatError::InvalidArchive(
5634 "VolumeTrailer block_count does not match observed stream",
5635 ));
5636 }
5637 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
5638 Ok((
5639 manifest_footer,
5640 terminal.volume_trailer,
5641 terminal.root_auth_footer,
5642 ))
5643}
5644
5645pub(crate) fn parse_terminal_material_read_at(
5646 reader: &dyn ArchiveReadAt,
5647 input_len: u64,
5648 manifest_offset: u64,
5649 observed_block_count: u64,
5650 context: KeyHoldingTerminalContext<'_>,
5651) -> Result<SequentialTerminalMaterial, FormatError> {
5652 let mut candidates = Vec::new();
5653 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5654 collect_v41_locator_candidate_read_at(
5655 reader,
5656 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
5657 0,
5658 context,
5659 &mut candidates,
5660 );
5661 }
5662 if input_len >= LOCATOR_PAIR_LEN as u64 {
5663 collect_v41_locator_candidate_read_at(
5664 reader,
5665 input_len - LOCATOR_PAIR_LEN as u64,
5666 1,
5667 context,
5668 &mut candidates,
5669 );
5670 }
5671
5672 let candidate = choose_v41_terminal_candidate(candidates)?;
5673 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
5674 return Err(FormatError::InvalidArchive(
5675 "sequential terminal does not end at EOF",
5676 ));
5677 }
5678 let terminal = candidate.terminal;
5679 if terminal.image.manifest_footer_offset != manifest_offset {
5680 return Err(FormatError::InvalidArchive(
5681 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
5682 ));
5683 }
5684 if terminal.volume_trailer.block_count != observed_block_count {
5685 return Err(FormatError::InvalidArchive(
5686 "VolumeTrailer block_count does not match observed stream",
5687 ));
5688 }
5689 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
5690 Ok(SequentialTerminalMaterial {
5691 manifest_footer,
5692 volume_trailer: terminal.volume_trailer,
5693 root_auth_footer: terminal.root_auth_footer,
5694 })
5695}
5696
5697fn terminal_candidate_reaches_eof(
5698 candidate: &TerminalCandidate,
5699 input_len: usize,
5700) -> Result<bool, FormatError> {
5701 let expected_end =
5702 match candidate.locator_sequence {
5703 Some(0) => candidate.anchor,
5704 Some(1) => candidate
5705 .anchor
5706 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
5707 .ok_or(FormatError::InvalidArchive(
5708 "terminal EOF boundary overflow",
5709 ))?,
5710 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
5711 FormatError::InvalidArchive("terminal EOF boundary overflow"),
5712 )?,
5713 Some(_) => {
5714 return Err(FormatError::InvalidArchive(
5715 "invalid terminal locator sequence",
5716 ))
5717 }
5718 };
5719 Ok(expected_end == input_len)
5720}
5721
5722#[derive(Debug, Default)]
5723struct PendingSequentialEnvelope {
5724 data_shards: Vec<Option<Vec<u8>>>,
5725 parity_shards: Vec<Option<Vec<u8>>>,
5726 saw_last_data: bool,
5727 awaiting_tentative_parity: bool,
5728}
5729
5730impl PendingSequentialEnvelope {
5731 fn is_empty(&self) -> bool {
5732 self.data_shards.is_empty() && self.parity_shards.is_empty()
5733 }
5734}
5735
5736fn handle_sequential_payload_erasure(
5737 pending: &mut PendingSequentialEnvelope,
5738 crypto_header: &CryptoHeaderFixed,
5739 metadata_seen: bool,
5740) -> Result<(), FormatError> {
5741 if metadata_seen || pending.saw_last_data {
5742 return Err(FormatError::BadCrc {
5743 structure: "BlockRecord",
5744 });
5745 }
5746 if !sequential_payload_parity_is_guaranteed(crypto_header) {
5747 return Err(FormatError::BadCrc {
5748 structure: "BlockRecord",
5749 });
5750 }
5751 pending.data_shards.push(None);
5752 pending.awaiting_tentative_parity = true;
5753 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
5754 return Err(FormatError::InvalidArchive(
5755 "sequential payload envelope exceeds data-shard cap",
5756 ));
5757 }
5758 Ok(())
5759}
5760
5761fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
5762 crypto_header.fec_parity_shards > 0
5763 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
5764}
5765
5766fn sequential_extract_tar_stream_with_options(
5767 bytes: &[u8],
5768 master_key: &MasterKey,
5769 options: ReaderOptions,
5770) -> Result<Vec<u8>, FormatError> {
5771 if bytes.len() < VOLUME_HEADER_LEN {
5772 return Err(FormatError::InvalidLength {
5773 structure: "archive",
5774 expected: VOLUME_HEADER_LEN,
5775 actual: bytes.len(),
5776 });
5777 }
5778
5779 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
5780 let crypto_start = volume_header.crypto_header_offset as usize;
5781 let crypto_len = volume_header.crypto_header_length as usize;
5782 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
5783 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
5784 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
5785 let subkeys = Subkeys::derive(
5786 master_key,
5787 &volume_header.archive_uuid,
5788 &volume_header.session_id,
5789 )?;
5790 verify_hmac(
5791 HmacDomain::CryptoHeader,
5792 &subkeys.mac_key,
5793 &volume_header.archive_uuid,
5794 &volume_header.session_id,
5795 parsed_crypto.hmac_covered_bytes,
5796 &parsed_crypto.header_hmac,
5797 )?;
5798 parsed_crypto.validate_extension_semantics()?;
5799 validate_sequential_supported_volume(
5800 &volume_header,
5801 &parsed_crypto.fixed,
5802 &parsed_crypto.extensions,
5803 )?;
5804 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5805
5806 let block_size = parsed_crypto.fixed.block_size as usize;
5807 let record_len = block_size
5808 .checked_add(BLOCK_RECORD_FRAMING_LEN)
5809 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5810 let mut offset = crypto_end;
5811 let mut observed_block_count = 0u64;
5812 let mut metadata_seen = false;
5813 let mut pending = PendingSequentialEnvelope::default();
5814 let mut next_envelope_index = 0u64;
5815 let mut tar_stream = Vec::new();
5816 let max_tar_stream_size = options.max_verify_tar_size;
5817 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
5818 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
5819 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
5820 parsed_crypto.fixed.max_path_length,
5821 total_extraction_cap,
5822 );
5823
5824 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
5825 let record = parse_sequential_block_or_erasure(
5826 bytes,
5827 offset,
5828 record_len,
5829 block_size,
5830 &volume_header,
5831 observed_block_count,
5832 )?;
5833 observed_block_count = observed_block_count
5834 .checked_add(1)
5835 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
5836 let Some(record) = record else {
5837 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
5838 offset = checked_add(offset, record_len, "BlockRecord")?;
5839 continue;
5840 };
5841
5842 match record.kind {
5843 BlockKind::PayloadData => {
5844 if metadata_seen {
5845 return Err(FormatError::InvalidArchive(
5846 "payload BlockRecord appears after metadata",
5847 ));
5848 }
5849 if pending.awaiting_tentative_parity {
5850 return Err(FormatError::InvalidArchive(
5851 "sequential payload envelope boundary is ambiguous after CRC erasure",
5852 ));
5853 }
5854 if pending.saw_last_data {
5855 finalize_sequential_envelope(
5856 &mut pending,
5857 SequentialEnvelopeDecodeContext {
5858 crypto_header: &parsed_crypto.fixed,
5859 subkeys: &subkeys,
5860 volume_header: &volume_header,
5861 next_envelope_index: &mut next_envelope_index,
5862 tar_stream: &mut tar_stream,
5863 max_tar_stream_size,
5864 tar_stream_total_validator: &mut tar_stream_total_validator,
5865 },
5866 )?;
5867 }
5868 let is_last_data = record.is_last_data();
5869 pending.data_shards.push(Some(record.payload));
5870 if is_last_data {
5871 pending.saw_last_data = true;
5872 }
5873 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
5874 return Err(FormatError::InvalidArchive(
5875 "sequential payload envelope exceeds data-shard cap",
5876 ));
5877 }
5878 }
5879 BlockKind::PayloadParity => {
5880 if metadata_seen {
5881 return Err(FormatError::InvalidArchive(
5882 "payload parity BlockRecord appears after metadata",
5883 ));
5884 }
5885 if pending.awaiting_tentative_parity {
5886 pending.awaiting_tentative_parity = false;
5887 pending.saw_last_data = true;
5888 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
5889 return Err(FormatError::InvalidArchive(
5890 "payload parity appears before envelope data is complete",
5891 ));
5892 }
5893 pending.parity_shards.push(Some(record.payload));
5894 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
5895 return Err(FormatError::InvalidArchive(
5896 "sequential payload envelope exceeds parity-shard cap",
5897 ));
5898 }
5899 }
5900 _ => {
5901 if !pending.is_empty() {
5902 finalize_sequential_envelope(
5903 &mut pending,
5904 SequentialEnvelopeDecodeContext {
5905 crypto_header: &parsed_crypto.fixed,
5906 subkeys: &subkeys,
5907 volume_header: &volume_header,
5908 next_envelope_index: &mut next_envelope_index,
5909 tar_stream: &mut tar_stream,
5910 max_tar_stream_size,
5911 tar_stream_total_validator: &mut tar_stream_total_validator,
5912 },
5913 )?;
5914 }
5915 metadata_seen = true;
5916 }
5917 }
5918
5919 offset = checked_add(offset, record_len, "BlockRecord")?;
5920 }
5921
5922 if !pending.is_empty() {
5923 finalize_sequential_envelope(
5924 &mut pending,
5925 SequentialEnvelopeDecodeContext {
5926 crypto_header: &parsed_crypto.fixed,
5927 subkeys: &subkeys,
5928 volume_header: &volume_header,
5929 next_envelope_index: &mut next_envelope_index,
5930 tar_stream: &mut tar_stream,
5931 max_tar_stream_size,
5932 tar_stream_total_validator: &mut tar_stream_total_validator,
5933 },
5934 )?;
5935 }
5936
5937 parse_terminal_material(
5938 bytes,
5939 offset,
5940 observed_block_count,
5941 KeyHoldingTerminalContext {
5942 subkeys: &subkeys,
5943 volume_header: &volume_header,
5944 crypto_header: &parsed_crypto.fixed,
5945 crypto_header_bytes: crypto_bytes,
5946 },
5947 options,
5948 )?;
5949 validate_tar_stream_total_extraction_size(
5952 &tar_stream,
5953 parsed_crypto.fixed.max_path_length,
5954 total_extraction_cap,
5955 )?;
5956 Ok(tar_stream)
5957}
5958
5959fn validate_sequential_supported_volume(
5960 volume_header: &VolumeHeader,
5961 crypto_header: &CryptoHeaderFixed,
5962 extensions: &[ExtensionTlv<'_>],
5963) -> Result<(), FormatError> {
5964 reject_unsupported_raw_stream_profile(extensions)?;
5965 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
5966 return Err(FormatError::ReaderUnsupported(
5967 "sequential reader supports only single-volume archive input",
5968 ));
5969 }
5970 if crypto_header.stripe_width != volume_header.stripe_width {
5971 return Err(FormatError::InvalidArchive(
5972 "VolumeHeader and CryptoHeader stripe_width differ",
5973 ));
5974 }
5975 if crypto_header.has_dictionary != 0 {
5976 return Err(FormatError::ReaderUnsupported(
5977 "dictionary bootstrap required for non-seekable sequential extraction",
5978 ));
5979 }
5980 Ok(())
5981}
5982
5983struct SequentialEnvelopeDecodeContext<'a> {
5984 crypto_header: &'a CryptoHeaderFixed,
5985 subkeys: &'a Subkeys,
5986 volume_header: &'a VolumeHeader,
5987 next_envelope_index: &'a mut u64,
5988 tar_stream: &'a mut Vec<u8>,
5989 max_tar_stream_size: usize,
5990 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
5991}
5992
5993fn finalize_sequential_envelope(
5994 pending: &mut PendingSequentialEnvelope,
5995 context: SequentialEnvelopeDecodeContext<'_>,
5996) -> Result<(), FormatError> {
5997 if !pending.saw_last_data {
5998 return Err(FormatError::InvalidArchive(
5999 "sequential payload envelope is missing last-data flag",
6000 ));
6001 }
6002 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
6003 return Err(FormatError::InvalidArchive(
6004 "sequential payload envelope exceeds data-shard cap",
6005 ));
6006 }
6007 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
6008 return Err(FormatError::InvalidArchive(
6009 "sequential payload envelope exceeds parity-shard cap",
6010 ));
6011 }
6012 let required_parity =
6013 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
6014 if pending.parity_shards.len() < required_parity as usize {
6015 return Err(FormatError::InvalidArchive(
6016 "sequential payload envelope has insufficient parity for recovery settings",
6017 ));
6018 }
6019
6020 let repaired = repair_data_gf16(
6021 &pending.data_shards,
6022 &pending.parity_shards,
6023 context.crypto_header.block_size as usize,
6024 )?;
6025 let mut encrypted =
6026 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
6027 for shard in repaired {
6028 encrypted.extend_from_slice(&shard);
6029 }
6030 let plaintext = decrypt_padded_aead_object(
6031 AeadObjectContext {
6032 algo: context.crypto_header.aead_algo,
6033 key: &context.subkeys.enc_key,
6034 nonce_seed: &context.subkeys.nonce_seed,
6035 domain: b"envelope",
6036 archive_uuid: &context.volume_header.archive_uuid,
6037 session_id: &context.volume_header.session_id,
6038 counter: *context.next_envelope_index,
6039 },
6040 &encrypted,
6041 )?;
6042 decode_concatenated_zstd_frames_with_cap(
6043 &plaintext,
6044 None,
6045 context.tar_stream,
6046 context.max_tar_stream_size,
6047 Some(context.tar_stream_total_validator),
6048 )?;
6049 *context.next_envelope_index = (*context.next_envelope_index)
6050 .checked_add(1)
6051 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
6052 *pending = PendingSequentialEnvelope::default();
6053 Ok(())
6054}
6055
6056fn decode_concatenated_zstd_frames_with_cap(
6057 plaintext: &[u8],
6058 dictionary: Option<&[u8]>,
6059 output: &mut Vec<u8>,
6060 max_output_len: usize,
6061 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
6062) -> Result<(), FormatError> {
6063 let mut cursor = 0usize;
6064 while cursor < plaintext.len() {
6065 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
6066 .map_err(|_| FormatError::InvalidZstdFrame)?;
6067 if frame_len == 0 {
6068 return Err(FormatError::InvalidZstdFrame);
6069 }
6070 let end = checked_add(cursor, frame_len, "zstd frame")?;
6071 validate_exact_zstd_frame(&plaintext[cursor..end])?;
6072 if let Some(dictionary) = dictionary {
6073 let mut decoder =
6074 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
6075 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6076 read_zstd_frame_to_capped_output(
6077 &mut decoder,
6078 output,
6079 max_output_len,
6080 tar_stream_total_validator.as_deref_mut(),
6081 )?;
6082 } else {
6083 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
6084 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6085 read_zstd_frame_to_capped_output(
6086 &mut decoder,
6087 output,
6088 max_output_len,
6089 tar_stream_total_validator.as_deref_mut(),
6090 )?;
6091 }
6092 cursor = end;
6093 }
6094 Ok(())
6095}
6096
6097fn read_zstd_frame_to_capped_output<R: Read>(
6098 decoder: &mut R,
6099 output: &mut Vec<u8>,
6100 max_output_len: usize,
6101 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
6102) -> Result<(), FormatError> {
6103 let mut buf = [0u8; 64 * 1024];
6104 loop {
6105 let read = decoder
6106 .read(&mut buf)
6107 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6108 if read == 0 {
6109 return Ok(());
6110 }
6111 let next_len = output
6112 .len()
6113 .checked_add(read)
6114 .ok_or(FormatError::ReaderUnsupported(
6115 "sequential tar stream exceeds configured verification cap",
6116 ))?;
6117 if next_len > max_output_len {
6118 return Err(FormatError::ReaderUnsupported(
6119 "sequential tar stream exceeds configured verification cap",
6120 ));
6121 }
6122 output.extend_from_slice(&buf[..read]);
6123 if let Some(validator) = tar_stream_total_validator.as_mut() {
6124 validator.observe(output)?;
6125 }
6126 }
6127}
6128
6129fn load_archive_dictionary(
6130 blocks: &impl BlockProvider,
6131 subkeys: &Subkeys,
6132 volume_header: &VolumeHeader,
6133 crypto_header: &CryptoHeaderFixed,
6134 index_root: &IndexRoot,
6135) -> Result<Option<Vec<u8>>, FormatError> {
6136 if crypto_header.has_dictionary == 0 {
6137 return Ok(None);
6138 }
6139 let plaintext = load_metadata_object_from_parts(
6140 blocks,
6141 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
6142 index_root.header.dictionary_decompressed_size,
6143 )?;
6144 Ok(Some(plaintext))
6145}
6146
6147#[derive(Clone, Copy)]
6148struct ObjectLoadContext<'a> {
6149 volume_header: &'a VolumeHeader,
6150 crypto_header: &'a CryptoHeaderFixed,
6151 extent: ObjectExtent,
6152 data_kind: BlockKind,
6153 parity_kind: BlockKind,
6154 key: &'a [u8; 32],
6155 nonce_seed: &'a [u8; 32],
6156 domain: &'a [u8],
6157 counter: u64,
6158 class_data_shard_max: u16,
6159 class_parity_shard_max: u16,
6160}
6161
6162impl<'a> ObjectLoadContext<'a> {
6163 fn index_root(
6164 volume_header: &'a VolumeHeader,
6165 crypto_header: &'a CryptoHeaderFixed,
6166 subkeys: &'a Subkeys,
6167 extent: ObjectExtent,
6168 ) -> Self {
6169 Self {
6170 volume_header,
6171 crypto_header,
6172 extent,
6173 data_kind: BlockKind::IndexRootData,
6174 parity_kind: BlockKind::IndexRootParity,
6175 key: &subkeys.index_root_key,
6176 nonce_seed: &subkeys.index_nonce_seed,
6177 domain: b"idxroot",
6178 counter: 0,
6179 class_data_shard_max: crypto_header.index_root_fec_data_shards,
6180 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
6181 }
6182 }
6183
6184 fn index_shard(
6185 volume_header: &'a VolumeHeader,
6186 crypto_header: &'a CryptoHeaderFixed,
6187 subkeys: &'a Subkeys,
6188 entry: &ShardEntry,
6189 ) -> Self {
6190 Self {
6191 volume_header,
6192 crypto_header,
6193 extent: ObjectExtent {
6194 first_block_index: entry.first_block_index,
6195 data_block_count: entry.data_block_count,
6196 parity_block_count: entry.parity_block_count,
6197 encrypted_size: entry.encrypted_size,
6198 },
6199 data_kind: BlockKind::IndexShardData,
6200 parity_kind: BlockKind::IndexShardParity,
6201 key: &subkeys.index_shard_key,
6202 nonce_seed: &subkeys.index_nonce_seed,
6203 domain: b"idxshard",
6204 counter: entry.shard_index,
6205 class_data_shard_max: crypto_header.index_fec_data_shards,
6206 class_parity_shard_max: crypto_header.index_fec_parity_shards,
6207 }
6208 }
6209
6210 fn directory_hint(
6211 volume_header: &'a VolumeHeader,
6212 crypto_header: &'a CryptoHeaderFixed,
6213 subkeys: &'a Subkeys,
6214 entry: &DirectoryHintShardEntry,
6215 ) -> Self {
6216 Self {
6217 volume_header,
6218 crypto_header,
6219 extent: ObjectExtent {
6220 first_block_index: entry.first_block_index,
6221 data_block_count: entry.data_block_count,
6222 parity_block_count: entry.parity_block_count,
6223 encrypted_size: entry.encrypted_size,
6224 },
6225 data_kind: BlockKind::DirectoryHintData,
6226 parity_kind: BlockKind::DirectoryHintParity,
6227 key: &subkeys.dir_hint_key,
6228 nonce_seed: &subkeys.index_nonce_seed,
6229 domain: b"dirhint",
6230 counter: entry.hint_shard_index,
6231 class_data_shard_max: crypto_header.index_fec_data_shards,
6232 class_parity_shard_max: crypto_header.index_fec_parity_shards,
6233 }
6234 }
6235
6236 fn dictionary(
6237 volume_header: &'a VolumeHeader,
6238 crypto_header: &'a CryptoHeaderFixed,
6239 subkeys: &'a Subkeys,
6240 index_root: &IndexRoot,
6241 ) -> Result<Self, FormatError> {
6242 Ok(Self {
6243 volume_header,
6244 crypto_header,
6245 extent: dictionary_extent_from_index_root(index_root)?,
6246 data_kind: BlockKind::DictionaryData,
6247 parity_kind: BlockKind::DictionaryParity,
6248 key: &subkeys.dictionary_key,
6249 nonce_seed: &subkeys.index_nonce_seed,
6250 domain: b"dict",
6251 counter: 0,
6252 class_data_shard_max: crypto_header.index_root_fec_data_shards,
6253 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
6254 })
6255 }
6256
6257 fn payload(
6258 volume_header: &'a VolumeHeader,
6259 crypto_header: &'a CryptoHeaderFixed,
6260 subkeys: &'a Subkeys,
6261 envelope: &EnvelopeEntry,
6262 ) -> Self {
6263 Self {
6264 volume_header,
6265 crypto_header,
6266 extent: ObjectExtent {
6267 first_block_index: envelope.first_block_index,
6268 data_block_count: envelope.data_block_count,
6269 parity_block_count: envelope.parity_block_count,
6270 encrypted_size: envelope.encrypted_size,
6271 },
6272 data_kind: BlockKind::PayloadData,
6273 parity_kind: BlockKind::PayloadParity,
6274 key: &subkeys.enc_key,
6275 nonce_seed: &subkeys.nonce_seed,
6276 domain: b"envelope",
6277 counter: envelope.envelope_index,
6278 class_data_shard_max: crypto_header.fec_data_shards,
6279 class_parity_shard_max: crypto_header.fec_parity_shards,
6280 }
6281 }
6282}
6283
6284fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
6285 if index_root.header.dictionary_data_block_count == 0
6286 || index_root.header.dictionary_encrypted_size == 0
6287 || index_root.header.dictionary_decompressed_size == 0
6288 {
6289 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
6290 }
6291 Ok(ObjectExtent {
6292 first_block_index: index_root.header.dictionary_first_block,
6293 data_block_count: index_root.header.dictionary_data_block_count,
6294 parity_block_count: index_root.header.dictionary_parity_block_count,
6295 encrypted_size: index_root.header.dictionary_encrypted_size,
6296 })
6297}
6298
6299fn load_metadata_object_from_parts(
6300 blocks: &impl BlockProvider,
6301 context: ObjectLoadContext<'_>,
6302 decompressed_size: u32,
6303) -> Result<Vec<u8>, FormatError> {
6304 let compressed = load_decrypted_object_from_parts(blocks, context)?;
6305 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
6306}
6307
6308fn load_decrypted_object_from_parts(
6309 blocks: &impl BlockProvider,
6310 context: ObjectLoadContext<'_>,
6311) -> Result<Vec<u8>, FormatError> {
6312 let repaired = load_repaired_object_data_shards_from_parts(
6313 blocks,
6314 context.crypto_header,
6315 context.extent,
6316 context.data_kind,
6317 context.parity_kind,
6318 context.class_data_shard_max,
6319 context.class_parity_shard_max,
6320 )?;
6321 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
6322 for shard in repaired {
6323 encrypted.extend_from_slice(&shard);
6324 }
6325 if encrypted.len() != context.extent.encrypted_size as usize {
6326 return Err(FormatError::InvalidArchive(
6327 "object encrypted size does not match repaired shards",
6328 ));
6329 }
6330
6331 decrypt_padded_aead_object(
6332 AeadObjectContext {
6333 algo: context.crypto_header.aead_algo,
6334 key: context.key,
6335 nonce_seed: context.nonce_seed,
6336 domain: context.domain,
6337 archive_uuid: &context.volume_header.archive_uuid,
6338 session_id: &context.volume_header.session_id,
6339 counter: context.counter,
6340 },
6341 &encrypted,
6342 )
6343}
6344
6345fn load_repaired_object_data_shards_from_parts(
6346 blocks: &impl BlockProvider,
6347 crypto_header: &CryptoHeaderFixed,
6348 extent: ObjectExtent,
6349 data_kind: BlockKind,
6350 parity_kind: BlockKind,
6351 class_data_shard_max: u16,
6352 class_parity_shard_max: u16,
6353) -> Result<Vec<Vec<u8>>, FormatError> {
6354 validate_object_extent(
6355 extent,
6356 crypto_header,
6357 class_data_shard_max,
6358 class_parity_shard_max,
6359 )?;
6360 let block_size = crypto_header.block_size as usize;
6361 let data_count = extent.data_block_count as usize;
6362 let parity_count = extent.parity_block_count as usize;
6363 let mut data_shards = Vec::with_capacity(data_count);
6364 let mut parity_shards = Vec::with_capacity(parity_count);
6365
6366 for offset in 0..data_count {
6367 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
6368 if let Some(record) = blocks.block(block_index)? {
6369 if record.kind != data_kind {
6370 return Err(FormatError::InvalidArchive(
6371 "object data block has unexpected kind",
6372 ));
6373 }
6374 let should_be_last = offset + 1 == data_count;
6375 if record.is_last_data() != should_be_last {
6376 return Err(FormatError::InvalidArchive(
6377 "object last-data flag is not on the final data block",
6378 ));
6379 }
6380 data_shards.push(Some(record.payload.clone()));
6381 } else {
6382 data_shards.push(None);
6383 }
6384 }
6385
6386 for offset in 0..parity_count {
6387 let block_index = checked_u64_add(
6388 extent.first_block_index,
6389 data_count as u64 + offset as u64,
6390 "object",
6391 )?;
6392 if let Some(record) = blocks.block(block_index)? {
6393 if record.kind != parity_kind {
6394 return Err(FormatError::InvalidArchive(
6395 "object parity block has unexpected kind",
6396 ));
6397 }
6398 if record.is_last_data() {
6399 return Err(FormatError::InvalidArchive(
6400 "object parity block has last-data flag",
6401 ));
6402 }
6403 parity_shards.push(Some(record.payload.clone()));
6404 } else {
6405 parity_shards.push(None);
6406 }
6407 }
6408
6409 repair_data_gf16(&data_shards, &parity_shards, block_size)
6410}
6411
6412fn validate_object_extent(
6413 extent: ObjectExtent,
6414 crypto_header: &CryptoHeaderFixed,
6415 class_data_shard_max: u16,
6416 class_parity_shard_max: u16,
6417) -> Result<(), FormatError> {
6418 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
6419 return Err(FormatError::InvalidArchive(
6420 "encrypted object has zero data blocks or size",
6421 ));
6422 }
6423 if extent.data_block_count > class_data_shard_max as u32 {
6424 return Err(FormatError::InvalidArchive(
6425 "encrypted object exceeds its class data-shard maximum",
6426 ));
6427 }
6428 if extent.parity_block_count > class_parity_shard_max as u32 {
6429 return Err(FormatError::InvalidArchive(
6430 "encrypted object exceeds its class parity-shard maximum",
6431 ));
6432 }
6433 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
6434 if extent.parity_block_count != required_parity {
6435 return Err(FormatError::InvalidArchive(
6436 "encrypted object parity does not match v41 compute_parity",
6437 ));
6438 }
6439 let total = checked_u64_add(
6440 extent.data_block_count as u64,
6441 extent.parity_block_count as u64,
6442 "encrypted object shard count overflow",
6443 )?;
6444 if total > 65_535 {
6445 return Err(FormatError::FecTooManyShards(total as usize));
6446 }
6447 let expected = checked_u64_mul(
6448 extent.data_block_count as u64,
6449 crypto_header.block_size as u64,
6450 "encrypted object size overflow",
6451 )?;
6452 if expected != extent.encrypted_size as u64 {
6453 return Err(FormatError::InvalidArchive(
6454 "encrypted object size is not data_block_count * block_size",
6455 ));
6456 }
6457 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
6458 return Err(FormatError::InvalidArchive(
6459 "encrypted object is too small for AEAD tag",
6460 ));
6461 }
6462 Ok(())
6463}
6464
6465pub(crate) fn required_object_parity(
6466 data_block_count: u64,
6467 crypto_header: &CryptoHeaderFixed,
6468) -> Result<u32, FormatError> {
6469 let min_parity =
6470 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
6471 1
6472 } else {
6473 0
6474 };
6475 let mut parity = 0u64;
6476 for _ in 0..100 {
6477 let total = data_block_count
6478 .checked_add(parity)
6479 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
6480 let by_volume = checked_u64_mul(
6481 crypto_header.volume_loss_tolerance as u64,
6482 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
6483 "volume-loss parity overflow",
6484 )?;
6485 let by_bitrot = ceil_div_u64(
6486 checked_u64_mul(
6487 total,
6488 crypto_header.bit_rot_buffer_pct as u64,
6489 "bit-rot parity overflow",
6490 )?,
6491 100,
6492 )?;
6493 let next = by_volume
6494 .checked_add(by_bitrot)
6495 .ok_or(FormatError::InvalidArchive("parity overflow"))?
6496 .max(min_parity);
6497 if next == parity {
6498 return u32::try_from(next)
6499 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
6500 }
6501 parity = next;
6502 }
6503 Err(FormatError::InvalidArchive(
6504 "parity calculation did not converge",
6505 ))
6506}
6507
6508fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
6509 if denominator == 0 {
6510 return Err(FormatError::InvalidArchive("division by zero"));
6511 }
6512 numerator
6513 .checked_add(denominator - 1)
6514 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
6515 .map(|value| value / denominator)
6516}
6517
6518fn frame_range_for_file<'b>(
6519 shard: &'b IndexShard,
6520 file: &FileEntry,
6521) -> Result<Vec<&'b FrameEntry>, FormatError> {
6522 let mut frames = Vec::with_capacity(file.frame_count as usize);
6523 for offset in 0..file.frame_count as u64 {
6524 let frame_index =
6525 file.first_frame_index
6526 .checked_add(offset)
6527 .ok_or(FormatError::InvalidArchive(
6528 "FileEntry frame range overflow",
6529 ))?;
6530 let frame = shard
6531 .frames
6532 .iter()
6533 .find(|entry| entry.frame_index == frame_index)
6534 .ok_or(FormatError::InvalidArchive(
6535 "FileEntry references missing FrameEntry",
6536 ))?;
6537 frames.push(frame);
6538 }
6539 Ok(frames)
6540}
6541
6542fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
6543 MetadataLimits {
6544 block_size: crypto_header.block_size,
6545 max_path_length: crypto_header.max_path_length,
6546 max_payload_data_shards: crypto_header.fec_data_shards,
6547 max_payload_parity_shards: crypto_header.fec_parity_shards,
6548 max_index_data_shards: crypto_header.index_fec_data_shards,
6549 max_index_parity_shards: crypto_header.index_fec_parity_shards,
6550 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
6551 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
6552 ..MetadataLimits::default()
6553 }
6554}
6555
6556fn verify_dense_keys<T>(
6557 entries: &BTreeMap<u64, T>,
6558 expected_count: u64,
6559 structure: &'static str,
6560) -> Result<(), FormatError> {
6561 if entries.len() as u64 != expected_count {
6562 return Err(FormatError::InvalidArchive(
6563 "decoded table count does not match IndexRoot",
6564 ));
6565 }
6566 for expected in 0..expected_count {
6567 if !entries.contains_key(&expected) {
6568 return Err(FormatError::InvalidMetadata {
6569 structure,
6570 reason: "global index coverage has a gap",
6571 });
6572 }
6573 }
6574 Ok(())
6575}
6576
6577fn validate_envelope_frame_coverage(
6578 frames: &BTreeMap<u64, FrameEntry>,
6579 envelopes: &BTreeMap<u64, EnvelopeEntry>,
6580) -> Result<(), FormatError> {
6581 let mut accounted_frames = BTreeSet::new();
6582 for envelope in envelopes.values() {
6583 let first = envelope.first_frame_index;
6584 let end =
6585 first
6586 .checked_add(envelope.frame_count as u64)
6587 .ok_or(FormatError::InvalidArchive(
6588 "EnvelopeEntry frame range overflow",
6589 ))?;
6590 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
6591 for frame_index in first..end {
6592 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
6593 "EnvelopeEntry references missing FrameEntry",
6594 ))?;
6595 if frame.envelope_index != envelope.envelope_index {
6596 return Err(FormatError::InvalidArchive(
6597 "FrameEntry envelope_index does not match containing EnvelopeEntry",
6598 ));
6599 }
6600 if !accounted_frames.insert(frame_index) {
6601 return Err(FormatError::InvalidArchive(
6602 "FrameEntry is covered by multiple EnvelopeEntries",
6603 ));
6604 }
6605 let start = frame.offset_in_envelope as usize;
6606 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
6607 if end > envelope.plaintext_size as usize {
6608 return Err(FormatError::InvalidArchive(
6609 "FrameEntry exceeds EnvelopeEntry plaintext_size",
6610 ));
6611 }
6612 ranges.push((start, end));
6613 }
6614 validate_exact_coverage_ranges(
6615 &mut ranges,
6616 envelope.plaintext_size as usize,
6617 "EnvelopeEntry frame coverage has a gap or overlap",
6618 )?;
6619 }
6620
6621 for frame_index in frames.keys() {
6622 if !accounted_frames.contains(frame_index) {
6623 return Err(FormatError::InvalidArchive(
6624 "FrameEntry is not covered by any EnvelopeEntry",
6625 ));
6626 }
6627 }
6628 Ok(())
6629}
6630
6631fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
6632 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
6633 for shard in shards {
6634 for (idx, file) in shard.files.iter().enumerate() {
6635 let path = shard
6636 .file_path(idx)
6637 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
6638 .to_vec();
6639 let start = shard
6640 .tar_member_group_start(idx)
6641 .ok_or(FormatError::InvalidArchive(
6642 "FileEntry tar member start is missing",
6643 ))?;
6644 let key = (file.path_hash, path, start);
6645 validate_global_file_table_key_step(previous.as_ref(), &key)?;
6646 previous = Some(key);
6647 }
6648 }
6649 Ok(())
6650}
6651
6652fn validate_global_file_table_key_step(
6653 previous: Option<&([u8; 8], Vec<u8>, u64)>,
6654 current: &([u8; 8], Vec<u8>, u64),
6655) -> Result<(), FormatError> {
6656 if let Some(previous) = previous {
6657 if previous >= current {
6658 return Err(FormatError::InvalidArchive(
6659 "global FileEntry rows are not sorted and unique",
6660 ));
6661 }
6662 }
6663 Ok(())
6664}
6665
6666fn validate_file_extent_coverage_ranges(
6667 extents: &[(u64, u64)],
6668 tar_len: u64,
6669) -> Result<(), FormatError> {
6670 let mut ranges = Vec::with_capacity(extents.len());
6671 for (start, len) in extents {
6672 let end = checked_u64_add(*start, *len, "FileEntry")?;
6673 if end > tar_len {
6674 return Err(FormatError::InvalidArchive(
6675 "FileEntry extent exceeds IndexRoot tar_total_size",
6676 ));
6677 }
6678 ranges.push((*start, end));
6679 }
6680 validate_exact_coverage_ranges_u64(
6681 &mut ranges,
6682 tar_len,
6683 "FileEntry extents do not cover tar stream exactly",
6684 )
6685}
6686
6687fn add_expected_directory_hint_rows(
6688 map: &mut DirectoryHintMap,
6689 shard_row_index: u32,
6690 path: &[u8],
6691 kind: TarEntryKind,
6692) {
6693 map.entry(Vec::new()).or_default().insert(shard_row_index);
6694 for (idx, byte) in path.iter().enumerate() {
6695 if *byte == b'/' {
6696 map.entry(path[..idx].to_vec())
6697 .or_default()
6698 .insert(shard_row_index);
6699 }
6700 }
6701 if kind == TarEntryKind::Directory {
6702 map.entry(path.to_vec())
6703 .or_default()
6704 .insert(shard_row_index);
6705 }
6706}
6707
6708fn validate_directory_hint_tables_against_expected(
6709 tables: &[DirectoryHintTable],
6710 expected: &DirectoryHintMap,
6711) -> Result<(), FormatError> {
6712 let mut actual = Vec::new();
6713 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
6714
6715 for table in tables {
6716 for entry_index in 0..table.entries.len() {
6717 let path = table
6718 .entry_path(entry_index)
6719 .ok_or(FormatError::InvalidArchive(
6720 "DirectoryHintEntry path is missing",
6721 ))?;
6722 let key = (hash_prefix(path), path.to_vec());
6723 if let Some(previous) = &previous_key {
6724 if previous >= &key {
6725 return Err(FormatError::InvalidArchive(
6726 "DirectoryHintEntry rows are not globally sorted",
6727 ));
6728 }
6729 }
6730 previous_key = Some(key);
6731
6732 let rows =
6733 table
6734 .shard_rows_for_entry(entry_index)
6735 .ok_or(FormatError::InvalidArchive(
6736 "DirectoryHintEntry shard rows are missing",
6737 ))?;
6738 actual.push((path.to_vec(), rows.to_vec()));
6739 }
6740 }
6741
6742 if actual != sorted_directory_hint_rows(expected) {
6743 return Err(FormatError::InvalidArchive(
6744 "directory hint map does not match decoded files",
6745 ));
6746 }
6747 Ok(())
6748}
6749
6750fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
6751 let mut rows = map
6752 .iter()
6753 .map(|(path, shard_rows)| {
6754 (
6755 path.clone(),
6756 shard_rows.iter().copied().collect::<Vec<u32>>(),
6757 )
6758 })
6759 .collect::<Vec<_>>();
6760 rows.sort_by(|(left_path, _), (right_path, _)| {
6761 hash_prefix(left_path)
6762 .cmp(&hash_prefix(right_path))
6763 .then_with(|| left_path.cmp(right_path))
6764 });
6765 rows
6766}
6767
6768fn validate_exact_coverage_ranges(
6769 ranges: &mut [(usize, usize)],
6770 expected_end: usize,
6771 reason: &'static str,
6772) -> Result<(), FormatError> {
6773 ranges.sort_unstable();
6774 let mut cursor = 0usize;
6775 for (start, end) in ranges.iter().copied() {
6776 if start != cursor || end < start {
6777 return Err(FormatError::InvalidArchive(reason));
6778 }
6779 cursor = end;
6780 }
6781 if cursor != expected_end {
6782 return Err(FormatError::InvalidArchive(reason));
6783 }
6784 Ok(())
6785}
6786
6787fn validate_exact_coverage_ranges_u64(
6788 ranges: &mut [(u64, u64)],
6789 expected_end: u64,
6790 reason: &'static str,
6791) -> Result<(), FormatError> {
6792 ranges.sort_unstable();
6793 let mut cursor = 0u64;
6794 for (start, end) in ranges.iter().copied() {
6795 if start != cursor || end < start {
6796 return Err(FormatError::InvalidArchive(reason));
6797 }
6798 cursor = end;
6799 }
6800 if cursor != expected_end {
6801 return Err(FormatError::InvalidArchive(reason));
6802 }
6803 Ok(())
6804}
6805
6806fn object_block_range(
6807 first_block_index: u64,
6808 data_block_count: u32,
6809 parity_block_count: u32,
6810 structure: &'static str,
6811) -> Result<(u64, u64), FormatError> {
6812 let total = data_block_count as u64 + parity_block_count as u64;
6813 if total == 0 {
6814 return Err(FormatError::InvalidArchive(structure));
6815 }
6816 let end = checked_u64_add(first_block_index, total, structure)?;
6817 Ok((first_block_index, end))
6818}
6819
6820fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
6821 ranges.sort_unstable();
6822 for pair in ranges.windows(2) {
6823 if pair[0].1 > pair[1].0 {
6824 return Err(FormatError::InvalidArchive(
6825 "encrypted object block ranges overlap",
6826 ));
6827 }
6828 }
6829 Ok(())
6830}
6831
6832pub(crate) fn observed_archive_size(
6833 sizes: impl IntoIterator<Item = u64>,
6834) -> Result<u64, FormatError> {
6835 sizes.into_iter().try_fold(0u64, |sum, size| {
6836 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
6837 "observed archive size overflow",
6838 ))
6839 })
6840}
6841
6842pub(crate) fn total_extraction_size_cap(
6843 options: ReaderOptions,
6844 observed_archive_bytes: u64,
6845) -> u64 {
6846 options
6847 .max_total_extraction_size
6848 .min(observed_archive_bytes.saturating_mul(10))
6849}
6850
6851fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
6852 std::str::from_utf8(bytes)
6853 .map(|path| path.to_owned())
6854 .map_err(|_| FormatError::UnsafeArchivePath)
6855}
6856
6857fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
6858 let mut bytes = [0u8; 104];
6859 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
6860 bytes[36..40].fill(0);
6861 bytes
6862}
6863
6864fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
6865 let digest = Sha256::digest(bytes);
6866 let mut out = [0u8; 32];
6867 out.copy_from_slice(&digest);
6868 out
6869}
6870
6871fn slice<'b>(
6872 bytes: &'b [u8],
6873 offset: usize,
6874 len: usize,
6875 structure: &'static str,
6876) -> Result<&'b [u8], FormatError> {
6877 let end = checked_add(offset, len, structure)?;
6878 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
6879 structure,
6880 expected: end,
6881 actual: bytes.len(),
6882 })
6883}
6884
6885fn read_at_vec(
6886 reader: &dyn ArchiveReadAt,
6887 offset: u64,
6888 len: usize,
6889 structure: &'static str,
6890) -> Result<Vec<u8>, FormatError> {
6891 let expected_end = offset
6892 .checked_add(len as u64)
6893 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
6894 let observed_len = reader.len()?;
6895 if expected_end > observed_len {
6896 return Err(FormatError::InvalidLength {
6897 structure,
6898 expected: to_usize(expected_end, structure)?,
6899 actual: to_usize(observed_len, structure)?,
6900 });
6901 }
6902 let mut out = vec![0u8; len];
6903 reader.read_exact_at(offset, &mut out)?;
6904 Ok(out)
6905}
6906
6907fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
6908 lhs.checked_add(rhs)
6909 .ok_or(FormatError::InvalidArchive(structure))
6910}
6911
6912fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
6913 lhs.checked_add(rhs)
6914 .ok_or(FormatError::InvalidArchive(structure))
6915}
6916
6917fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
6918 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
6919}
6920
6921#[cfg(test)]
6922mod tests {
6923 use std::fs;
6924
6925 use super::*;
6926 use crate::compression::compress_zstd_frame;
6927 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
6928 use crate::fec::encode_parity_gf16;
6929 use crate::format::{
6930 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
6931 VOLUME_FORMAT_REV,
6932 };
6933 use crate::metadata::{
6934 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
6935 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
6936 };
6937 use crate::non_seekable_reader::{
6938 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
6939 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
6940 NonSeekableReaderOptions, SequentialRootAuthStatus,
6941 };
6942 use crate::writer::{
6943 write_archive, write_archive_with_dictionary, write_archive_with_kdf,
6944 write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
6945 WriterOptions,
6946 };
6947
6948 fn master_key() -> MasterKey {
6949 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
6950 }
6951
6952 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
6953 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
6954
6955 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
6956 RootAuthWriterConfig {
6957 authenticator_id: TEST_ROOT_AUTH_ID,
6958 signer_identity_type: 0,
6959 signer_identity: &[],
6960 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
6961 }
6962 }
6963
6964 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
6965 request.archive_root.to_vec()
6966 }
6967
6968 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
6969 footer.authenticator_id == TEST_ROOT_AUTH_ID
6970 && footer.signer_identity_type == 0
6971 && footer.signer_identity_bytes.is_empty()
6972 && footer.authenticator_value.as_slice() == archive_root
6973 }
6974
6975 fn dictionary() -> &'static [u8] {
6976 b"dir/dict.txt common words common words common words dictionary payload"
6977 }
6978
6979 #[derive(Clone)]
6980 struct CountingReadAt {
6981 bytes: std::sync::Arc<Vec<u8>>,
6982 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
6983 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
6984 }
6985
6986 impl CountingReadAt {
6987 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
6988 Self {
6989 bytes: std::sync::Arc::new(bytes),
6990 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
6991 denied_ranges: std::sync::Arc::new(denied_ranges),
6992 }
6993 }
6994
6995 fn reads(&self) -> Vec<(u64, u64)> {
6996 self.reads.lock().unwrap().clone()
6997 }
6998 }
6999
7000 impl ArchiveReadAt for CountingReadAt {
7001 fn len(&self) -> Result<u64, FormatError> {
7002 u64::try_from(self.bytes.as_ref().len())
7003 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
7004 }
7005
7006 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
7007 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
7008 self.reads.lock().unwrap().push((offset, end));
7009 if self
7010 .denied_ranges
7011 .iter()
7012 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
7013 {
7014 return Err(FormatError::InvalidArchive("denied test read"));
7015 }
7016 let start = to_usize(offset, "archive")?;
7017 let end_usize = checked_add(start, buf.len(), "archive")?;
7018 let source = self
7019 .bytes
7020 .get(start..end_usize)
7021 .ok_or(FormatError::InvalidLength {
7022 structure: "archive",
7023 expected: end_usize,
7024 actual: self.bytes.as_ref().len(),
7025 })?;
7026 buf.copy_from_slice(source);
7027 Ok(())
7028 }
7029 }
7030
7031 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
7032 left_start < right_end && right_start < left_end
7033 }
7034
7035 fn single_stream_options() -> WriterOptions {
7036 WriterOptions {
7037 stripe_width: 1,
7038 volume_loss_tolerance: 0,
7039 ..WriterOptions::default()
7040 }
7041 }
7042
7043 struct ChunkedReader {
7044 bytes: Vec<u8>,
7045 cursor: usize,
7046 max_chunk: usize,
7047 }
7048
7049 impl ChunkedReader {
7050 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
7051 Self {
7052 bytes,
7053 cursor: 0,
7054 max_chunk,
7055 }
7056 }
7057 }
7058
7059 impl std::io::Read for ChunkedReader {
7060 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
7061 if self.cursor >= self.bytes.len() {
7062 return Ok(0);
7063 }
7064 let available = self.bytes.len() - self.cursor;
7065 let len = available.min(buf.len()).min(self.max_chunk);
7066 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
7067 self.cursor += len;
7068 Ok(len)
7069 }
7070 }
7071
7072 #[test]
7073 fn global_file_table_key_step_rejects_distinct_path_regression() {
7074 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
7075 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
7076
7077 assert_eq!(
7078 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
7079 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7080 );
7081 }
7082
7083 #[test]
7084 fn global_file_table_key_step_rejects_duplicate_full_key() {
7085 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
7086 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
7087
7088 assert_eq!(
7089 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
7090 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7091 );
7092 }
7093
7094 fn small_block_recovery_options() -> WriterOptions {
7095 WriterOptions {
7096 block_size: 4096,
7097 chunk_size: 32 * 1024,
7098 envelope_target_size: 32 * 1024,
7099 stripe_width: 1,
7100 volume_loss_tolerance: 0,
7101 bit_rot_buffer_pct: 1,
7102 fec_data_shards: 16,
7103 fec_parity_shards: 1,
7104 index_fec_data_shards: 4,
7105 index_fec_parity_shards: 1,
7106 index_root_fec_data_shards: 16,
7107 index_root_fec_parity_shards: 1,
7108 ..WriterOptions::default()
7109 }
7110 }
7111
7112 fn parity_rich_recovery_options() -> WriterOptions {
7113 WriterOptions {
7114 block_size: 4096,
7115 chunk_size: 32 * 1024,
7116 envelope_target_size: 32 * 1024,
7117 stripe_width: 1,
7118 volume_loss_tolerance: 0,
7119 bit_rot_buffer_pct: 40,
7120 fec_data_shards: 16,
7121 fec_parity_shards: 16,
7122 index_fec_data_shards: 4,
7123 index_fec_parity_shards: 4,
7124 index_root_fec_data_shards: 16,
7125 index_root_fec_parity_shards: 16,
7126 ..WriterOptions::default()
7127 }
7128 }
7129
7130 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
7131 let mut state = 0x1234_5678u32;
7132 (0..len)
7133 .map(|_| {
7134 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
7135 (state >> 24) as u8
7136 })
7137 .collect()
7138 }
7139
7140 #[test]
7141 fn opens_lists_verifies_and_extracts_one_file_archive() {
7142 let archive = write_archive(
7143 &[RegularFile::new("dir/hello.txt", b"hello m7")],
7144 &master_key(),
7145 single_stream_options(),
7146 )
7147 .unwrap();
7148 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7149
7150 assert_eq!(
7151 opened.list_files().unwrap(),
7152 vec![ArchiveEntry {
7153 path: "dir/hello.txt".to_string(),
7154 file_data_size: 8,
7155 kind: TarEntryKind::Regular,
7156 mode: 0o644,
7157 mtime: 0,
7158 diagnostics: Vec::new(),
7159 }]
7160 );
7161 opened.verify().unwrap();
7162 assert_eq!(
7163 opened.extract_file("dir/hello.txt").unwrap(),
7164 Some(b"hello m7".to_vec())
7165 );
7166 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
7167 }
7168
7169 #[test]
7170 fn root_auth_archive_round_trips_and_verifies_with_callback() {
7171 let archive = write_archive_with_root_auth(
7172 &[RegularFile::new("signed.txt", b"root-auth payload")],
7173 &master_key(),
7174 single_stream_options(),
7175 RootAuthWriterConfig {
7176 authenticator_id: 0x7777,
7177 signer_identity_type: 1,
7178 signer_identity: b"test signer",
7179 authenticator_value_length: 32,
7180 },
7181 |request| Ok(request.archive_root.to_vec()),
7182 )
7183 .unwrap();
7184
7185 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7186 opened.verify().unwrap();
7187 let verified = opened
7188 .verify_root_auth_with(|footer, archive_root| {
7189 Ok(footer.authenticator_value == archive_root.as_slice())
7190 })
7191 .unwrap();
7192
7193 assert_eq!(verified.authenticator_id, 0x7777);
7194 assert_eq!(verified.signer_identity_type, 1);
7195 assert_eq!(verified.signer_identity_bytes, b"test signer");
7196 assert_eq!(
7197 verified.archive_root,
7198 opened.root_auth_footer.as_ref().unwrap().archive_root
7199 );
7200 assert_eq!(
7201 verified.diagnostics,
7202 vec![
7203 RootAuthDiagnostic::RootAuthContentVerified,
7204 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
7205 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
7206 RootAuthDiagnostic::RecoveryMarginUnchecked,
7207 ]
7208 );
7209 }
7210
7211 #[test]
7212 fn root_auth_verification_requires_authenticator_success() {
7213 let archive = write_archive_with_root_auth(
7214 &[RegularFile::new("signed.txt", b"root-auth payload")],
7215 &master_key(),
7216 single_stream_options(),
7217 RootAuthWriterConfig {
7218 authenticator_id: 9,
7219 signer_identity_type: 1,
7220 signer_identity: b"test signer",
7221 authenticator_value_length: 32,
7222 },
7223 |request| Ok(request.archive_root.to_vec()),
7224 )
7225 .unwrap();
7226 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7227
7228 assert_eq!(
7229 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
7230 FormatError::InvalidArchive("root-auth authenticator verification failed")
7231 );
7232 }
7233
7234 #[test]
7235 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
7236 let archive = write_archive_with_root_auth(
7237 &[RegularFile::new("public.txt", b"public commitment")],
7238 &master_key(),
7239 single_stream_options(),
7240 RootAuthWriterConfig {
7241 authenticator_id: 0x2222,
7242 signer_identity_type: 1,
7243 signer_identity: b"public verifier",
7244 authenticator_value_length: 32,
7245 },
7246 |request| Ok(request.archive_root.to_vec()),
7247 )
7248 .unwrap();
7249
7250 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
7251 Ok(footer.authenticator_value == archive_root.as_slice())
7252 })
7253 .unwrap();
7254
7255 assert_eq!(verified.authenticator_id, 0x2222);
7256 assert_eq!(verified.signer_identity_bytes, b"public verifier");
7257 assert!(verified.total_data_block_count > 0);
7258 }
7259
7260 #[test]
7261 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
7262 let archive = write_archive_with_root_auth(
7263 &[RegularFile::new(
7264 "public-fields.txt",
7265 b"public source authority",
7266 )],
7267 &master_key(),
7268 single_stream_options(),
7269 RootAuthWriterConfig {
7270 authenticator_id: 0x2222,
7271 signer_identity_type: 1,
7272 signer_identity: b"public verifier",
7273 authenticator_value_length: 32,
7274 },
7275 |request| Ok(request.archive_root.to_vec()),
7276 )
7277 .unwrap();
7278 let mut bytes = archive.bytes.clone();
7279
7280 rewrite_public_cmra_image(&mut bytes, |image| {
7281 let manifest_region = image
7282 .regions
7283 .iter_mut()
7284 .find(|region| region.region_type == 3)
7285 .unwrap();
7286 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
7287
7288 let trailer_region = image
7289 .regions
7290 .iter_mut()
7291 .find(|region| region.region_type == 5)
7292 .unwrap();
7293 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
7294 trailer.block_count += 7;
7295 trailer_region.bytes = trailer.to_bytes().to_vec();
7296 });
7297
7298 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
7299 Ok(footer.authenticator_value == archive_root.as_slice())
7300 })
7301 .unwrap();
7302 }
7303
7304 #[test]
7305 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
7306 let archive = write_archive_with_root_auth(
7307 &[RegularFile::new(
7308 "public-crypto.txt",
7309 b"cross-volume public profile",
7310 )],
7311 &master_key(),
7312 WriterOptions {
7313 stripe_width: 2,
7314 volume_loss_tolerance: 0,
7315 ..WriterOptions::default()
7316 },
7317 RootAuthWriterConfig {
7318 authenticator_id: 0x3333,
7319 signer_identity_type: 1,
7320 signer_identity: b"public verifier",
7321 authenticator_value_length: 32,
7322 },
7323 |request| Ok(request.archive_root.to_vec()),
7324 )
7325 .unwrap();
7326 let mut volumes = archive.volumes.clone();
7327 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
7328 let crypto_offset = volume_header.crypto_header_offset as usize;
7329 let expected_volume_size = 123_456_789u64;
7330 volumes[1][crypto_offset + 52..crypto_offset + 60]
7331 .copy_from_slice(&expected_volume_size.to_le_bytes());
7332 rewrite_public_cmra_image(&mut volumes[1], |image| {
7333 let crypto_region = image
7334 .regions
7335 .iter_mut()
7336 .find(|region| region.region_type == 2)
7337 .unwrap();
7338 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
7339 });
7340
7341 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
7342 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
7343 Ok(footer.authenticator_value == archive_root.as_slice())
7344 })
7345 .unwrap();
7346 }
7347
7348 #[test]
7349 fn locator_based_cmra_recovery_only_ignores_header_crc_failures() {
7350 let archive = write_archive_with_root_auth(
7351 &[RegularFile::new("cmra-header.txt", b"header fallback")],
7352 &master_key(),
7353 single_stream_options(),
7354 RootAuthWriterConfig {
7355 authenticator_id: 0x4444,
7356 signer_identity_type: 1,
7357 signer_identity: b"public verifier",
7358 authenticator_value_length: 32,
7359 },
7360 |request| Ok(request.archive_root.to_vec()),
7361 )
7362 .unwrap();
7363 let final_locator = final_recovery_locator(&archive.bytes);
7364
7365 let mut bad_crc = archive.bytes.clone();
7366 let crc_offset =
7367 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
7368 bad_crc[crc_offset] ^= 0x55;
7369 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
7370 Ok(footer.authenticator_value == archive_root.as_slice())
7371 })
7372 .unwrap();
7373
7374 let mut bad_magic = archive.bytes.clone();
7375 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
7376 assert_eq!(
7377 public_no_key_verify_archive_with(&bad_magic, |_, _| Ok(true)).unwrap_err(),
7378 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7379 );
7380
7381 let mut bad_hint = archive.bytes.clone();
7382 bad_hint[crc_offset] ^= 0xAA;
7383 for offset in [
7384 bad_hint.len() - LOCATOR_PAIR_LEN,
7385 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
7386 ] {
7387 let mut locator = CriticalRecoveryLocator::parse(
7388 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
7389 )
7390 .unwrap();
7391 locator.volume_index_hint += 1;
7392 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
7393 .copy_from_slice(&locator.to_bytes());
7394 }
7395 assert_eq!(
7396 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
7397 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7398 );
7399 }
7400
7401 #[test]
7402 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
7403 let archive = write_archive(
7404 &[RegularFile::new(
7405 "cmra-floor.txt",
7406 b"authenticated CMRA floor",
7407 )],
7408 &master_key(),
7409 single_stream_options(),
7410 )
7411 .unwrap();
7412 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
7413 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
7414 let locator = CriticalRecoveryLocator::parse(
7415 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
7416 )
7417 .unwrap();
7418 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
7419 let crypto_start = volume_header.crypto_header_offset as usize;
7420 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
7421 let crypto_header = CryptoHeader::parse(
7422 &malformed[crypto_start..crypto_end],
7423 volume_header.crypto_header_length,
7424 )
7425 .unwrap();
7426 let subkeys = Subkeys::derive(
7427 &master_key(),
7428 &volume_header.archive_uuid,
7429 &volume_header.session_id,
7430 )
7431 .unwrap();
7432
7433 assert_eq!(
7434 parse_locator_cmra_candidate(
7435 &malformed,
7436 final_offset,
7437 locator,
7438 KeyHoldingTerminalContext {
7439 subkeys: &subkeys,
7440 volume_header: &volume_header,
7441 crypto_header: &crypto_header.fixed,
7442 crypto_header_bytes: &malformed[crypto_start..crypto_end],
7443 },
7444 )
7445 .unwrap_err(),
7446 FormatError::InvalidArchive(
7447 "CMRA parity shard count is below authenticated bit-rot lower bound"
7448 )
7449 );
7450 assert!(open_archive(&malformed, &master_key()).is_err());
7451 }
7452
7453 #[test]
7454 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
7455 let archive = write_archive(
7456 &[RegularFile::new(
7457 "locator-order.txt",
7458 b"locator tuple first",
7459 )],
7460 &master_key(),
7461 single_stream_options(),
7462 )
7463 .unwrap();
7464 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
7465 let mut locator = final_recovery_locator(&archive.bytes);
7466 locator.cmra_shard_size = 513;
7467 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
7468 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
7469 let crypto_start = volume_header.crypto_header_offset as usize;
7470 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
7471 let crypto_header = CryptoHeader::parse(
7472 &archive.bytes[crypto_start..crypto_end],
7473 volume_header.crypto_header_length,
7474 )
7475 .unwrap();
7476 let subkeys = Subkeys::derive(
7477 &master_key(),
7478 &volume_header.archive_uuid,
7479 &volume_header.session_id,
7480 )
7481 .unwrap();
7482
7483 assert_eq!(
7484 parse_locator_cmra_candidate(
7485 &archive.bytes,
7486 final_offset,
7487 locator,
7488 KeyHoldingTerminalContext {
7489 subkeys: &subkeys,
7490 volume_header: &volume_header,
7491 crypto_header: &crypto_header.fixed,
7492 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
7493 },
7494 )
7495 .unwrap_err(),
7496 FormatError::InvalidArchive("CMRA shard_size is invalid")
7497 );
7498 }
7499
7500 #[test]
7501 fn sequential_extract_rejects_bytes_after_terminal_locator() {
7502 let archive = write_archive(
7503 &[RegularFile::new("seq.txt", b"sequential EOF")],
7504 &master_key(),
7505 single_stream_options(),
7506 )
7507 .unwrap();
7508 let mut appended = archive.bytes.clone();
7509 appended.extend_from_slice(&[0xAA; 32]);
7510
7511 assert_eq!(
7512 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
7513 FormatError::InvalidArchive("sequential terminal does not end at EOF")
7514 );
7515 }
7516
7517 #[test]
7518 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
7519 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
7520 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
7521
7522 assert_eq!(
7523 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
7524 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7525 );
7526 }
7527
7528 #[test]
7529 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
7530 let archive = write_archive_with_root_auth(
7531 &[RegularFile::new("signed.txt", b"ed25519 payload")],
7532 &master_key(),
7533 single_stream_options(),
7534 test_root_auth_config(),
7535 |request| Ok(test_root_auth_value(request)),
7536 )
7537 .unwrap();
7538
7539 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7540 let root_auth = opened
7541 .verify_root_auth_with(|footer, archive_root| {
7542 Ok(test_root_auth_verifies(footer, archive_root))
7543 })
7544 .unwrap();
7545 assert_eq!(
7546 root_auth.archive_root,
7547 opened.root_auth_footer.as_ref().unwrap().archive_root
7548 );
7549
7550 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
7551 Ok(test_root_auth_verifies(footer, archive_root))
7552 })
7553 .unwrap();
7554 assert_eq!(public.archive_root, root_auth.archive_root);
7555 assert_eq!(
7556 public.diagnostics,
7557 vec![
7558 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
7559 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
7560 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
7561 ]
7562 );
7563 }
7564
7565 #[test]
7566 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
7567 let options = WriterOptions {
7568 block_size: 4096,
7569 chunk_size: 16 * 1024,
7570 envelope_target_size: 16 * 1024,
7571 stripe_width: 2,
7572 volume_loss_tolerance: 1,
7573 bit_rot_buffer_pct: 0,
7574 fec_data_shards: 16,
7575 fec_parity_shards: 1,
7576 index_fec_data_shards: 4,
7577 index_fec_parity_shards: 1,
7578 index_root_fec_data_shards: 16,
7579 index_root_fec_parity_shards: 1,
7580 ..WriterOptions::default()
7581 };
7582 let archive = write_archive_with_root_auth(
7583 &[RegularFile::new("missing-volume.txt", b"recover me")],
7584 &master_key(),
7585 options,
7586 test_root_auth_config(),
7587 |request| Ok(test_root_auth_value(request)),
7588 )
7589 .unwrap();
7590
7591 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
7592 let root_auth = opened
7593 .verify_root_auth_with(|footer, archive_root| {
7594 Ok(test_root_auth_verifies(footer, archive_root))
7595 })
7596 .unwrap();
7597 assert!(root_auth
7598 .diagnostics
7599 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
7600 }
7601
7602 #[test]
7603 fn public_no_key_rejects_unsigned_archives() {
7604 let archive = write_archive(
7605 &[RegularFile::new("plain.txt", b"unsigned")],
7606 &master_key(),
7607 single_stream_options(),
7608 )
7609 .unwrap();
7610
7611 assert_eq!(
7612 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
7613 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7614 );
7615 }
7616
7617 #[test]
7618 fn unsigned_archive_reports_root_auth_absent() {
7619 let archive = write_archive(
7620 &[RegularFile::new("plain.txt", b"unsigned")],
7621 &master_key(),
7622 single_stream_options(),
7623 )
7624 .unwrap();
7625 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7626
7627 assert_eq!(
7628 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
7629 FormatError::ReaderUnsupported("root-auth footer is absent")
7630 );
7631 }
7632
7633 #[test]
7634 fn safe_extract_writes_regular_file_under_root() {
7635 let archive = write_archive(
7636 &[RegularFile::new("dir/hello.txt", b"safe m8")],
7637 &master_key(),
7638 single_stream_options(),
7639 )
7640 .unwrap();
7641 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7642 let tmp = tempfile::tempdir().unwrap();
7643
7644 opened
7645 .extract_file_to(
7646 "dir/hello.txt",
7647 tmp.path(),
7648 SafeExtractionOptions::default(),
7649 )
7650 .unwrap()
7651 .unwrap();
7652
7653 assert_eq!(
7654 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
7655 b"safe m8"
7656 );
7657 }
7658
7659 #[test]
7660 fn safe_extract_rejects_overwriting_existing_file_by_default() {
7661 let archive = write_archive(
7662 &[RegularFile::new("hello.txt", b"new")],
7663 &master_key(),
7664 single_stream_options(),
7665 )
7666 .unwrap();
7667 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7668 let tmp = tempfile::tempdir().unwrap();
7669 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
7670
7671 assert_eq!(
7672 opened
7673 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
7674 .unwrap_err(),
7675 FormatError::UnsafeOverwrite
7676 );
7677 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
7678 }
7679
7680 #[test]
7681 fn opens_and_verifies_empty_archive() {
7682 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
7683 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7684
7685 assert!(opened.list_files().unwrap().is_empty());
7686 opened.verify().unwrap();
7687 }
7688
7689 #[test]
7690 fn default_reader_options_allow_v36_trailing_garbage_scan() {
7691 let archive = write_archive(
7692 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
7693 &master_key(),
7694 single_stream_options(),
7695 )
7696 .unwrap();
7697 let mut with_trailing_garbage = archive.bytes.clone();
7698 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
7699
7700 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
7701 assert_eq!(
7702 opened.extract_file("garbage-tolerant.txt").unwrap(),
7703 Some(b"still intact".to_vec())
7704 );
7705 }
7706
7707 #[test]
7708 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
7709 assert_eq!(
7710 open_archive(
7711 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
7712 &master_key()
7713 )
7714 .unwrap_err(),
7715 FormatError::InvalidLength {
7716 structure: "archive",
7717 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
7718 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
7719 }
7720 );
7721
7722 let mut header = test_volume_header();
7723 header.crypto_header_length = 512;
7724 let mut unavailable_crypto = header.to_bytes().to_vec();
7725 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
7726
7727 assert_eq!(
7728 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
7729 FormatError::InvalidLength {
7730 structure: "CryptoHeader",
7731 expected: VOLUME_HEADER_LEN + 512,
7732 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
7733 }
7734 );
7735 }
7736
7737 #[test]
7738 fn seekable_open_rejects_in_bounds_noncanonical_crypto_header_offset() {
7739 let archive = write_archive(
7740 &[RegularFile::new("offset.txt", b"offset")],
7741 &master_key(),
7742 single_stream_options(),
7743 )
7744 .unwrap();
7745 let mut mutated = archive.bytes;
7746 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
7747 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
7748 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
7749
7750 assert_eq!(
7751 open_archive(&mutated, &master_key()).unwrap_err(),
7752 FormatError::NonCanonicalCryptoHeaderOffset(VOLUME_HEADER_LEN as u32 + 1)
7753 );
7754 }
7755
7756 #[test]
7757 fn rejects_wrong_key_before_metadata_release() {
7758 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
7759 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
7760
7761 assert_eq!(
7762 open_archive(&archive.bytes, &wrong).unwrap_err(),
7763 FormatError::HmacMismatch {
7764 structure: "CryptoHeader"
7765 }
7766 );
7767 }
7768
7769 #[test]
7770 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
7771 let mut archive = write_archive(
7772 &[RegularFile::new("file.txt", b"authenticated")],
7773 &master_key(),
7774 single_stream_options(),
7775 )
7776 .unwrap()
7777 .bytes;
7778 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
7779 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
7780 let crypto = CryptoHeader::parse(
7781 &archive[VOLUME_HEADER_LEN..crypto_end],
7782 volume.crypto_header_length,
7783 )
7784 .unwrap();
7785 let block_size = crypto.fixed.block_size as usize;
7786 archive[crypto_end + 16] ^= 1;
7787 let crc_offset = crypto_end + 16 + block_size;
7788 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
7789 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
7790
7791 let opened = open_archive(&archive, &master_key()).unwrap();
7792 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
7793 }
7794
7795 #[test]
7796 fn list_and_extract_use_final_view_for_duplicate_paths() {
7797 let archive = write_archive(
7798 &[
7799 RegularFile::new("same.txt", b"old"),
7800 RegularFile::new("same.txt", b"newer"),
7801 ],
7802 &master_key(),
7803 single_stream_options(),
7804 )
7805 .unwrap();
7806 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7807
7808 assert_eq!(
7809 opened.list_index_entries().unwrap(),
7810 vec![ArchiveIndexEntry {
7811 path: "same.txt".to_string(),
7812 file_data_size: 5,
7813 }]
7814 );
7815 assert_eq!(
7816 opened.lookup_index_entry("same.txt").unwrap(),
7817 Some(ArchiveIndexEntry {
7818 path: "same.txt".to_string(),
7819 file_data_size: 5,
7820 })
7821 );
7822 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
7823 assert_eq!(
7824 opened.list_files().unwrap(),
7825 vec![ArchiveEntry {
7826 path: "same.txt".to_string(),
7827 file_data_size: 5,
7828 kind: TarEntryKind::Regular,
7829 mode: 0o644,
7830 mtime: 0,
7831 diagnostics: Vec::new(),
7832 }]
7833 );
7834 assert_eq!(
7835 opened.extract_file("same.txt").unwrap(),
7836 Some(b"newer".to_vec())
7837 );
7838 opened.verify().unwrap();
7839 }
7840
7841 #[test]
7842 fn index_entries_do_not_decrypt_payload_envelopes() {
7843 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
7844 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
7845
7846 assert_eq!(
7847 opened.list_index_entries().unwrap(),
7848 vec![
7849 ArchiveIndexEntry {
7850 path: "broken.txt".to_string(),
7851 file_data_size: b"broken payload\n".len() as u64,
7852 },
7853 ArchiveIndexEntry {
7854 path: "healthy.txt".to_string(),
7855 file_data_size: b"healthy payload\n".len() as u64,
7856 },
7857 ]
7858 );
7859 assert_eq!(
7860 opened.lookup_index_entry("broken.txt").unwrap(),
7861 Some(ArchiveIndexEntry {
7862 path: "broken.txt".to_string(),
7863 file_data_size: b"broken payload\n".len() as u64,
7864 })
7865 );
7866 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
7867 }
7868
7869 #[test]
7870 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
7871 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
7874 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
7875
7876 assert_eq!(
7877 opened.extract_file("healthy.txt").unwrap(),
7878 Some(b"healthy payload\n".to_vec())
7879 );
7880 assert_eq!(
7881 opened.extract_file("broken.txt").unwrap_err(),
7882 FormatError::AeadFailure
7883 );
7884 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
7885 }
7886
7887 #[test]
7888 fn seekable_extract_does_not_read_unselected_payload_envelope() {
7889 let healthy = pseudo_random_bytes(64 * 1024);
7890 let broken = pseudo_random_bytes(64 * 1024);
7891 let options = WriterOptions {
7892 block_size: 4096,
7893 chunk_size: 4096,
7894 envelope_target_size: 8192,
7895 stripe_width: 1,
7896 volume_loss_tolerance: 0,
7897 bit_rot_buffer_pct: 0,
7898 fec_data_shards: 4,
7899 fec_parity_shards: 0,
7900 index_fec_data_shards: 4,
7901 index_fec_parity_shards: 0,
7902 index_root_fec_data_shards: 4,
7903 index_root_fec_parity_shards: 0,
7904 ..WriterOptions::default()
7905 };
7906 let archive = write_archive(
7907 &[
7908 RegularFile::new("healthy.bin", &healthy),
7909 RegularFile::new("broken.bin", &broken),
7910 ],
7911 &master_key(),
7912 options,
7913 )
7914 .unwrap();
7915 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
7916 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
7917 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
7918 let denied_block_indices = broken_envelopes
7919 .iter()
7920 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
7921 .flat_map(|envelope| {
7922 let block_count =
7923 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
7924 envelope.first_block_index..envelope.first_block_index + block_count
7925 })
7926 .collect::<BTreeSet<_>>();
7927 assert!(
7928 !denied_block_indices.is_empty(),
7929 "fixture must place broken.bin in at least one unshared envelope"
7930 );
7931 let denied_ranges = block_record_slots(&archive.bytes)
7932 .into_iter()
7933 .filter_map(|(offset, len, record)| {
7934 denied_block_indices
7935 .contains(&record.block_index)
7936 .then_some((offset as u64, (offset + len) as u64))
7937 })
7938 .collect::<Vec<_>>();
7939 assert!(!denied_ranges.is_empty());
7940
7941 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
7942 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
7943
7944 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
7945 for (read_start, read_end) in reader.reads() {
7946 assert!(
7947 denied_ranges
7948 .iter()
7949 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
7950 "targeted extract read an unrelated payload BlockRecord range"
7951 );
7952 }
7953 assert_eq!(
7954 opened.extract_file("broken.bin").unwrap_err(),
7955 FormatError::InvalidArchive("denied test read")
7956 );
7957 }
7958
7959 #[test]
7960 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
7961 struct FailOnFirstWrite;
7962
7963 impl std::io::Write for FailOnFirstWrite {
7964 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
7965 Err(std::io::Error::other("sink stopped"))
7966 }
7967
7968 fn flush(&mut self) -> std::io::Result<()> {
7969 Ok(())
7970 }
7971 }
7972
7973 let payload = pseudo_random_bytes(128 * 1024);
7974 let options = WriterOptions {
7975 block_size: 4096,
7976 chunk_size: 4096,
7977 envelope_target_size: 8192,
7978 stripe_width: 1,
7979 volume_loss_tolerance: 0,
7980 bit_rot_buffer_pct: 0,
7981 fec_data_shards: 4,
7982 fec_parity_shards: 0,
7983 index_fec_data_shards: 4,
7984 index_fec_parity_shards: 0,
7985 index_root_fec_data_shards: 4,
7986 index_root_fec_parity_shards: 0,
7987 ..WriterOptions::default()
7988 };
7989 let archive = write_archive(
7990 &[RegularFile::new("large.bin", &payload)],
7991 &master_key(),
7992 options,
7993 )
7994 .unwrap();
7995 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
7996 let envelopes = envelope_entries_for_path(&eager, "large.bin");
7997 let first_envelope = envelopes
7998 .first()
7999 .expect("large fixture should have at least one envelope")
8000 .envelope_index;
8001 let later_envelope_blocks = envelopes
8002 .iter()
8003 .filter(|entry| entry.envelope_index != first_envelope)
8004 .flat_map(|entry| {
8005 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
8006 entry.first_block_index..entry.first_block_index + block_count
8007 })
8008 .collect::<BTreeSet<_>>();
8009 assert!(
8010 !later_envelope_blocks.is_empty(),
8011 "fixture must span more than one payload envelope"
8012 );
8013 let denied_ranges = block_record_slots(&archive.bytes)
8014 .into_iter()
8015 .filter_map(|(offset, len, record)| {
8016 later_envelope_blocks
8017 .contains(&record.block_index)
8018 .then_some((offset as u64, (offset + len) as u64))
8019 })
8020 .collect::<Vec<_>>();
8021 assert!(!denied_ranges.is_empty());
8022
8023 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
8024 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
8025 let mut writer = FailOnFirstWrite;
8026
8027 let err = opened
8028 .extract_file_to_writer("large.bin", &mut writer)
8029 .unwrap_err();
8030 assert_eq!(err.to_string(), "extraction output write failed");
8031 for (read_start, read_end) in reader.reads() {
8032 assert!(
8033 denied_ranges
8034 .iter()
8035 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
8036 "streaming writer read a later payload envelope before surfacing writer failure"
8037 );
8038 }
8039 }
8040
8041 #[test]
8042 fn extract_file_to_writer_writes_bounded_chunks() {
8043 struct ChunkRecorder {
8044 total: usize,
8045 max_write: usize,
8046 writes: usize,
8047 }
8048
8049 impl std::io::Write for ChunkRecorder {
8050 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
8051 self.total += buf.len();
8052 self.max_write = self.max_write.max(buf.len());
8053 self.writes += 1;
8054 Ok(buf.len())
8055 }
8056
8057 fn flush(&mut self) -> std::io::Result<()> {
8058 Ok(())
8059 }
8060 }
8061
8062 let payload = pseudo_random_bytes(128 * 1024);
8063 let options = WriterOptions {
8064 block_size: 4096,
8065 chunk_size: 4096,
8066 envelope_target_size: 8192,
8067 stripe_width: 1,
8068 volume_loss_tolerance: 0,
8069 bit_rot_buffer_pct: 0,
8070 fec_data_shards: 4,
8071 fec_parity_shards: 0,
8072 index_fec_data_shards: 4,
8073 index_fec_parity_shards: 0,
8074 index_root_fec_data_shards: 4,
8075 index_root_fec_parity_shards: 0,
8076 ..WriterOptions::default()
8077 };
8078 let archive = write_archive(
8079 &[RegularFile::new("large.bin", &payload)],
8080 &master_key(),
8081 options,
8082 )
8083 .unwrap();
8084 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8085 let mut writer = ChunkRecorder {
8086 total: 0,
8087 max_write: 0,
8088 writes: 0,
8089 };
8090
8091 opened
8092 .extract_file_to_writer("large.bin", &mut writer)
8093 .unwrap()
8094 .unwrap();
8095
8096 assert_eq!(writer.total, payload.len());
8097 assert!(writer.writes > 1);
8098 assert!(
8099 writer.max_write <= options.chunk_size as usize,
8100 "writer saw a {} byte chunk, larger than the {} byte frame target",
8101 writer.max_write,
8102 options.chunk_size
8103 );
8104 }
8105
8106 #[test]
8107 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
8108 let payload = pseudo_random_bytes(128 * 1024);
8109 let options = WriterOptions {
8110 block_size: 4096,
8111 chunk_size: 4096,
8112 envelope_target_size: 8192,
8113 stripe_width: 1,
8114 volume_loss_tolerance: 0,
8115 bit_rot_buffer_pct: 0,
8116 fec_data_shards: 4,
8117 fec_parity_shards: 0,
8118 index_fec_data_shards: 4,
8119 index_fec_parity_shards: 0,
8120 index_root_fec_data_shards: 4,
8121 index_root_fec_parity_shards: 0,
8122 ..WriterOptions::default()
8123 };
8124 let archive = write_archive(
8125 &[RegularFile::new("large.bin", &payload)],
8126 &master_key(),
8127 options,
8128 )
8129 .unwrap();
8130 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8131 let envelopes = envelope_entries_for_path(&eager, "large.bin");
8132 let last_envelope = envelopes
8133 .last()
8134 .expect("large fixture should have at least one envelope");
8135 assert_ne!(
8136 envelopes.first().unwrap().envelope_index,
8137 last_envelope.envelope_index,
8138 "fixture must span more than one payload envelope"
8139 );
8140 let corrupt_slot = block_record_slots(&archive.bytes)
8141 .into_iter()
8142 .enumerate()
8143 .find_map(|(slot, (_, _, record))| {
8144 (record.block_index == last_envelope.first_block_index).then_some(slot)
8145 })
8146 .unwrap();
8147 let mut corrupted = archive.bytes;
8148 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
8149 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8150 let tmp = tempfile::tempdir().unwrap();
8151
8152 assert!(matches!(
8153 opened
8154 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
8155 .unwrap_err(),
8156 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
8157 ));
8158 assert!(!tmp.path().join("large.bin").exists());
8159 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
8160 }
8161
8162 #[test]
8163 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
8164 let archive = write_archive(
8165 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
8166 &master_key(),
8167 single_stream_options(),
8168 )
8169 .unwrap();
8170 let opened = open_archive_with_bootstrap_sidecar(
8171 &archive.bytes,
8172 &archive.bootstrap_sidecar,
8173 &master_key(),
8174 )
8175 .unwrap();
8176
8177 assert_eq!(
8178 opened.list_files().unwrap(),
8179 vec![ArchiveEntry {
8180 path: "dir/sidecar.txt".to_string(),
8181 file_data_size: 13,
8182 kind: TarEntryKind::Regular,
8183 mode: 0o644,
8184 mtime: 0,
8185 diagnostics: Vec::new(),
8186 }]
8187 );
8188 assert_eq!(
8189 opened.extract_file("dir/sidecar.txt").unwrap(),
8190 Some(b"hello sidecar".to_vec())
8191 );
8192 opened.verify().unwrap();
8193 }
8194
8195 #[test]
8196 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
8197 let archive = write_archive_with_dictionary(
8198 &[RegularFile::new(
8199 "dir/dict.txt",
8200 b"common words common words dictionary payload",
8201 )],
8202 &master_key(),
8203 single_stream_options(),
8204 dictionary(),
8205 )
8206 .unwrap();
8207 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8208
8209 assert_eq!(opened.crypto_header.has_dictionary, 1);
8210 assert!(opened.index_root.header.dictionary_data_block_count > 0);
8211 assert_eq!(
8212 opened.list_files().unwrap(),
8213 vec![ArchiveEntry {
8214 path: "dir/dict.txt".to_string(),
8215 file_data_size: 44,
8216 kind: TarEntryKind::Regular,
8217 mode: 0o644,
8218 mtime: 0,
8219 diagnostics: Vec::new(),
8220 }]
8221 );
8222 assert_eq!(
8223 opened.extract_file("dir/dict.txt").unwrap(),
8224 Some(b"common words common words dictionary payload".to_vec())
8225 );
8226 opened.verify().unwrap();
8227 }
8228
8229 #[test]
8230 fn dictionary_object_tamper_fails_before_payload_decompression() {
8231 let archive = write_archive_with_dictionary(
8232 &[RegularFile::new(
8233 "dir/dict.txt",
8234 b"common words common words dictionary payload",
8235 )],
8236 &master_key(),
8237 single_stream_options(),
8238 dictionary(),
8239 )
8240 .unwrap();
8241 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8242 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8243 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
8244 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
8245 let dictionary_offset =
8246 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
8247
8248 let mut tampered = archive.bytes.clone();
8249 tampered[dictionary_offset + 16] ^= 0x01;
8250 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
8251 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
8252 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
8253
8254 assert_eq!(
8255 open_archive(&tampered, &master_key()).unwrap_err(),
8256 FormatError::AeadFailure
8257 );
8258 }
8259
8260 #[test]
8261 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
8262 let archive = write_archive_with_dictionary(
8263 &[RegularFile::new(
8264 "dict-sidecar.txt",
8265 b"common words common words sidecar payload",
8266 )],
8267 &master_key(),
8268 single_stream_options(),
8269 dictionary(),
8270 )
8271 .unwrap();
8272 let opened = open_non_seekable_archive(
8273 &archive.bytes,
8274 &master_key(),
8275 Some(&archive.bootstrap_sidecar),
8276 )
8277 .unwrap();
8278
8279 assert_eq!(
8280 opened.extract_file("dict-sidecar.txt").unwrap(),
8281 Some(b"common words common words sidecar payload".to_vec())
8282 );
8283 opened.verify().unwrap();
8284 }
8285
8286 #[test]
8287 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
8288 let archive = write_archive(
8289 &[RegularFile::new(
8290 "sidecar-terminal.txt",
8291 b"sidecar authority",
8292 )],
8293 &master_key(),
8294 single_stream_options(),
8295 )
8296 .unwrap();
8297 let mut corrupted = archive.bytes.clone();
8298 corrupt_v41_terminal_recovery(&mut corrupted);
8299 assert!(open_archive(&corrupted, &master_key()).is_err());
8300
8301 let opened =
8302 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
8303 .unwrap();
8304
8305 assert!(opened.volume_trailer.is_none());
8306 assert_eq!(
8307 opened.extract_file("sidecar-terminal.txt").unwrap(),
8308 Some(b"sidecar authority".to_vec())
8309 );
8310 opened.verify().unwrap();
8311 }
8312
8313 #[test]
8314 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
8315 let archive = write_archive_with_dictionary(
8316 &[RegularFile::new(
8317 "dict-no-terminal.txt",
8318 b"common words common words without terminal",
8319 )],
8320 &master_key(),
8321 single_stream_options(),
8322 dictionary(),
8323 )
8324 .unwrap();
8325 let terminal_offset = terminal_material_offset(&archive.bytes);
8326 let truncated = archive.bytes[..terminal_offset].to_vec();
8327 assert!(open_archive(&truncated, &master_key()).is_err());
8328
8329 let opened =
8330 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
8331 .unwrap();
8332
8333 assert!(opened.volume_trailer.is_none());
8334 assert_eq!(
8335 opened.extract_file("dict-no-terminal.txt").unwrap(),
8336 Some(b"common words common words without terminal".to_vec())
8337 );
8338 opened.verify().unwrap();
8339 }
8340
8341 #[test]
8342 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
8343 let archive = write_archive(
8344 &[RegularFile::new(
8345 "sidecar-erasure.txt",
8346 b"repair through sidecar",
8347 )],
8348 &master_key(),
8349 single_stream_options(),
8350 )
8351 .unwrap();
8352 let mut corrupted = archive.bytes.clone();
8353 corrupt_first_block_record_payload(&mut corrupted);
8354
8355 let opened = open_archive_with_bootstrap_sidecar(
8356 &corrupted,
8357 &archive.bootstrap_sidecar,
8358 &master_key(),
8359 )
8360 .unwrap();
8361 assert_eq!(
8362 opened.extract_file("sidecar-erasure.txt").unwrap(),
8363 Some(b"repair through sidecar".to_vec())
8364 );
8365 }
8366
8367 #[test]
8368 fn extraction_rejects_logical_payload_above_total_size_cap() {
8369 let archive = write_archive(
8370 &[RegularFile::new("cap.txt", b"payload")],
8371 &master_key(),
8372 single_stream_options(),
8373 )
8374 .unwrap();
8375 let options = ReaderOptions {
8376 max_total_extraction_size: 3,
8377 ..ReaderOptions::default()
8378 };
8379 let opened =
8380 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
8381
8382 assert_eq!(
8383 opened.extract_file("cap.txt").unwrap_err(),
8384 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
8385 );
8386 }
8387
8388 #[test]
8389 fn verify_does_not_apply_extraction_payload_cap() {
8390 let archive = write_archive(
8391 &[RegularFile::new("verify-cap.txt", b"payload")],
8392 &master_key(),
8393 single_stream_options(),
8394 )
8395 .unwrap();
8396 let options = ReaderOptions {
8397 max_total_extraction_size: 3,
8398 ..ReaderOptions::default()
8399 };
8400 let opened =
8401 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
8402
8403 opened.verify().unwrap();
8404 assert_eq!(
8405 opened.extract_file("verify-cap.txt").unwrap_err(),
8406 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
8407 );
8408 }
8409
8410 #[test]
8411 fn verify_streams_past_legacy_in_memory_tar_cap() {
8412 let data = vec![0x5a; 4096];
8413 let archive = write_archive(
8414 &[RegularFile::new("verify-large.txt", &data)],
8415 &master_key(),
8416 single_stream_options(),
8417 )
8418 .unwrap();
8419 let options = ReaderOptions {
8420 max_verify_tar_size: 1,
8421 ..ReaderOptions::default()
8422 };
8423 let opened =
8424 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
8425
8426 opened.verify().unwrap();
8427 }
8428
8429 #[test]
8430 fn dictionary_sidecar_requires_dictionary_record_section() {
8431 let archive = write_archive_with_dictionary(
8432 &[RegularFile::new("dict-missing.txt", b"common words")],
8433 &master_key(),
8434 single_stream_options(),
8435 dictionary(),
8436 )
8437 .unwrap();
8438 let header = BootstrapSidecarHeader::parse(
8439 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
8440 )
8441 .unwrap();
8442 let mut missing_dictionary =
8443 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
8444 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
8445 header.flags &= !0x04;
8446 header.dictionary_records_offset = 0;
8447 header.dictionary_records_length = 0;
8448 });
8449
8450 assert_eq!(
8451 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
8452 .unwrap_err(),
8453 FormatError::ReaderUnsupported("dictionary bootstrap required")
8454 );
8455 }
8456
8457 #[test]
8458 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
8459 let archive = write_archive_with_dictionary(
8460 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
8461 &master_key(),
8462 single_stream_options(),
8463 dictionary(),
8464 )
8465 .unwrap();
8466
8467 let mut wrong_kind = archive.bootstrap_sidecar.clone();
8468 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
8469 record.kind = BlockKind::IndexRootData;
8470 });
8471 assert_eq!(
8472 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
8473 .unwrap_err(),
8474 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
8475 );
8476
8477 let mut wrong_last = archive.bootstrap_sidecar.clone();
8478 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
8479 record.flags = 0;
8480 });
8481 assert_eq!(
8482 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
8483 .unwrap_err(),
8484 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
8485 );
8486 }
8487
8488 #[test]
8489 fn non_seekable_random_access_requires_sidecar() {
8490 let archive = write_archive(
8491 &[RegularFile::new("file.txt", b"payload")],
8492 &master_key(),
8493 single_stream_options(),
8494 )
8495 .unwrap();
8496
8497 assert_eq!(
8498 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
8499 FormatError::ReaderUnsupported(
8500 "non-seekable random access requires a bootstrap sidecar"
8501 )
8502 );
8503 assert!(open_non_seekable_archive(
8504 &archive.bytes,
8505 &master_key(),
8506 Some(&archive.bootstrap_sidecar)
8507 )
8508 .is_ok());
8509 }
8510
8511 #[test]
8512 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
8513 let archive = write_archive(
8514 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
8515 &master_key(),
8516 single_stream_options(),
8517 )
8518 .unwrap();
8519 let index_root_only = sparse_bootstrap_sidecar(
8520 &archive.bootstrap_sidecar,
8521 &master_key(),
8522 false,
8523 true,
8524 false,
8525 );
8526
8527 assert_eq!(
8528 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
8529 .unwrap_err(),
8530 FormatError::ReaderUnsupported(
8531 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
8532 )
8533 );
8534 }
8535
8536 #[test]
8537 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
8538 let archive = write_archive(
8539 &[RegularFile::new("sparse-index.txt", b"recover index root")],
8540 &master_key(),
8541 single_stream_options(),
8542 )
8543 .unwrap();
8544 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8545 let mut corrupted = archive.bytes.clone();
8546 corrupt_object_extent_records(
8547 &mut corrupted,
8548 index_root_extent_from_manifest(&opened.manifest_footer),
8549 );
8550 assert!(open_archive(&corrupted, &master_key()).is_err());
8551
8552 let index_root_only = sparse_bootstrap_sidecar(
8553 &archive.bootstrap_sidecar,
8554 &master_key(),
8555 false,
8556 true,
8557 false,
8558 );
8559 let recovered =
8560 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
8561 .unwrap();
8562
8563 assert_eq!(
8564 recovered.extract_file("sparse-index.txt").unwrap(),
8565 Some(b"recover index root".to_vec())
8566 );
8567 recovered.verify().unwrap();
8568 }
8569
8570 #[test]
8571 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
8572 let archive = write_archive_with_dictionary(
8573 &[RegularFile::new(
8574 "sparse-dict.txt",
8575 b"common words common words sparse dictionary",
8576 )],
8577 &master_key(),
8578 single_stream_options(),
8579 dictionary(),
8580 )
8581 .unwrap();
8582 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8583 let mut corrupted = archive.bytes.clone();
8584 corrupt_object_extent_records(
8585 &mut corrupted,
8586 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
8587 );
8588 assert!(open_archive(&corrupted, &master_key()).is_err());
8589
8590 let dictionary_only = sparse_bootstrap_sidecar(
8591 &archive.bootstrap_sidecar,
8592 &master_key(),
8593 false,
8594 false,
8595 true,
8596 );
8597 assert_eq!(
8598 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
8599 .unwrap_err(),
8600 FormatError::ReaderUnsupported(
8601 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
8602 )
8603 );
8604
8605 let recovered =
8606 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
8607 .unwrap();
8608 assert_eq!(
8609 recovered.extract_file("sparse-dict.txt").unwrap(),
8610 Some(b"common words common words sparse dictionary".to_vec())
8611 );
8612 recovered.verify().unwrap();
8613 }
8614
8615 #[test]
8616 fn sequential_extracts_dictionary_free_tar_stream() {
8617 let archive = write_archive(
8618 &[RegularFile::new("seq.txt", b"streaming")],
8619 &master_key(),
8620 single_stream_options(),
8621 )
8622 .unwrap();
8623
8624 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
8625 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
8626 assert_eq!(member.path, b"seq.txt");
8627 assert_eq!(member.data, b"streaming");
8628 }
8629
8630 #[test]
8631 fn sequential_rejects_logical_payload_above_total_size_cap() {
8632 let archive = write_archive(
8633 &[RegularFile::new("seq-cap.txt", b"payload")],
8634 &master_key(),
8635 single_stream_options(),
8636 )
8637 .unwrap();
8638 let options = ReaderOptions {
8639 max_total_extraction_size: 3,
8640 ..ReaderOptions::default()
8641 };
8642
8643 assert_eq!(
8644 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
8645 .unwrap_err(),
8646 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
8647 );
8648 }
8649
8650 #[test]
8651 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
8652 let archive = write_archive(
8653 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
8654 &master_key(),
8655 single_stream_options(),
8656 )
8657 .unwrap();
8658 let options = ReaderOptions {
8659 max_verify_tar_size: 512,
8660 ..ReaderOptions::default()
8661 };
8662
8663 assert_eq!(
8664 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
8665 .unwrap_err(),
8666 FormatError::ReaderUnsupported(
8667 "sequential tar stream exceeds configured verification cap"
8668 )
8669 );
8670 }
8671
8672 #[test]
8673 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
8674 let archive = write_archive(
8675 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
8676 &master_key(),
8677 single_stream_options(),
8678 )
8679 .unwrap();
8680 let mut corrupted = archive.bytes;
8681 corrupt_first_block_record_payload(&mut corrupted);
8682
8683 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
8684 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
8685 assert_eq!(member.path, b"seq-erasure.txt");
8686 assert_eq!(member.data, b"stream repair");
8687 }
8688
8689 #[test]
8690 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
8691 let archive = write_archive(
8692 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
8693 &master_key(),
8694 WriterOptions {
8695 bit_rot_buffer_pct: 0,
8696 fec_parity_shards: 0,
8697 index_fec_parity_shards: 0,
8698 index_root_fec_parity_shards: 0,
8699 ..single_stream_options()
8700 },
8701 )
8702 .unwrap();
8703 let mut corrupted = archive.bytes;
8704 corrupt_first_block_record_payload(&mut corrupted);
8705
8706 assert_eq!(
8707 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
8708 FormatError::BadCrc {
8709 structure: "BlockRecord"
8710 }
8711 );
8712 }
8713
8714 #[test]
8715 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
8716 let archive = write_archive(
8717 &[RegularFile::new(
8718 "seq.txt",
8719 b"payload must not be returned after terminal auth failure",
8720 )],
8721 &master_key(),
8722 single_stream_options(),
8723 )
8724 .unwrap();
8725 let mut corrupted = archive.bytes;
8726 corrupt_v41_terminal_recovery(&mut corrupted);
8727
8728 match sequential_extract_tar_stream(&corrupted, &master_key()) {
8729 Ok(bytes) => panic!(
8730 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
8731 bytes.len()
8732 ),
8733 Err(err) => assert_eq!(
8734 err,
8735 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
8736 ),
8737 }
8738 }
8739
8740 #[test]
8741 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
8742 let archive = write_archive_with_dictionary(
8743 &[RegularFile::new(
8744 "seq-dict.txt",
8745 b"common words common words dictionary payload",
8746 )],
8747 &master_key(),
8748 single_stream_options(),
8749 b"common words dictionary",
8750 )
8751 .unwrap();
8752
8753 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
8754 Ok(bytes) => panic!(
8755 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
8756 bytes.len()
8757 ),
8758 Err(err) => assert_eq!(
8759 err,
8760 FormatError::ReaderUnsupported(
8761 "dictionary bootstrap required for non-seekable sequential extraction"
8762 )
8763 ),
8764 }
8765 }
8766
8767 #[test]
8768 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
8769 let archive = write_archive_with_dictionary(
8770 &[RegularFile::new(
8771 "seq-dict-open.txt",
8772 b"common words common words bootstrap required",
8773 )],
8774 &master_key(),
8775 single_stream_options(),
8776 b"common words bootstrap",
8777 )
8778 .unwrap();
8779
8780 assert_eq!(
8781 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
8782 FormatError::ReaderUnsupported(
8783 "non-seekable random access requires a bootstrap sidecar"
8784 )
8785 );
8786 }
8787
8788 #[test]
8789 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
8790 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
8791 let mut output = Vec::new();
8792
8793 assert_eq!(
8794 decode_concatenated_zstd_frames_with_cap(
8795 &skippable,
8796 None,
8797 &mut output,
8798 usize::MAX,
8799 None,
8800 )
8801 .unwrap_err(),
8802 FormatError::NotStandardZstdFrame
8803 );
8804 assert!(output.is_empty());
8805 }
8806
8807 #[test]
8808 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
8809 let archive = write_archive(
8810 &[RegularFile::new("live.txt", b"stream verify")],
8811 &master_key(),
8812 single_stream_options(),
8813 )
8814 .unwrap();
8815
8816 let report =
8817 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
8818
8819 assert_eq!(report.file_count, 1);
8820 assert_eq!(report.total_volumes, 1);
8821 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
8822 assert!(report.payload_block_count > 0);
8823 }
8824
8825 #[test]
8826 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
8827 let archive = write_archive(
8828 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
8829 &master_key(),
8830 single_stream_options(),
8831 )
8832 .unwrap();
8833
8834 let report =
8835 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
8836 .unwrap();
8837
8838 assert_eq!(report.file_count, 1);
8839 assert_eq!(report.tar_total_size % 512, 0);
8840 }
8841
8842 #[test]
8843 fn live_non_seekable_verify_stream_accepts_empty_archive() {
8844 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
8845
8846 let report =
8847 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
8848
8849 assert_eq!(report.file_count, 0);
8850 assert_eq!(report.payload_block_count, 0);
8851 assert_eq!(report.tar_total_size, 0);
8852 }
8853
8854 #[test]
8855 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
8856 let archive = write_archive_with_dictionary(
8857 &[RegularFile::new(
8858 "live-dict.txt",
8859 b"common words common words dictionary payload",
8860 )],
8861 &master_key(),
8862 single_stream_options(),
8863 b"common words dictionary",
8864 )
8865 .unwrap();
8866
8867 assert_eq!(
8868 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
8869 .unwrap_err(),
8870 FormatError::ReaderUnsupported(
8871 "dictionary bootstrap required for non-seekable sequential verification"
8872 )
8873 );
8874 }
8875
8876 #[test]
8877 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
8878 let archive = write_archive_with_dictionary(
8879 &[RegularFile::new(
8880 "live-dict-sidecar.txt",
8881 b"common words common words dictionary payload",
8882 )],
8883 &master_key(),
8884 single_stream_options(),
8885 b"common words dictionary",
8886 )
8887 .unwrap();
8888
8889 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
8890 std::io::Cursor::new(archive.bytes),
8891 &archive.bootstrap_sidecar,
8892 &master_key(),
8893 NonSeekableReaderOptions::default(),
8894 )
8895 .unwrap();
8896
8897 assert_eq!(report.file_count, 1);
8898 assert_eq!(report.total_volumes, 1);
8899 }
8900
8901 #[test]
8902 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
8903 let archive = write_archive(
8904 &[RegularFile::new("tail-cap.txt", b"payload")],
8905 &master_key(),
8906 single_stream_options(),
8907 )
8908 .unwrap();
8909 let options = NonSeekableReaderOptions {
8910 max_terminal_tail_size: 8,
8911 ..NonSeekableReaderOptions::default()
8912 };
8913
8914 assert_eq!(
8915 verify_non_seekable_stream_with_options(
8916 std::io::Cursor::new(archive.bytes),
8917 &master_key(),
8918 options
8919 )
8920 .unwrap_err(),
8921 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
8922 );
8923 }
8924
8925 #[test]
8926 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
8927 let archive = write_archive(
8928 &[RegularFile::new("metadata-cap.txt", b"payload")],
8929 &master_key(),
8930 single_stream_options(),
8931 )
8932 .unwrap();
8933 let options = NonSeekableReaderOptions {
8934 max_retained_metadata_bytes: 1,
8935 ..NonSeekableReaderOptions::default()
8936 };
8937
8938 assert_eq!(
8939 verify_non_seekable_stream_with_options(
8940 std::io::Cursor::new(archive.bytes),
8941 &master_key(),
8942 options
8943 )
8944 .unwrap_err(),
8945 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
8946 );
8947 }
8948
8949 #[test]
8950 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
8951 let archive = write_archive(
8952 &[RegularFile::new("metadata-erasure.txt", b"payload")],
8953 &master_key(),
8954 single_stream_options(),
8955 )
8956 .unwrap();
8957 let mut corrupted = archive.bytes;
8958 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
8959 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
8960
8961 let report =
8962 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
8963
8964 assert_eq!(report.file_count, 1);
8965 }
8966
8967 #[test]
8968 fn live_non_seekable_verify_rejects_member_count_above_cap() {
8969 let archive = write_archive(
8970 &[RegularFile::new("member-cap.txt", b"payload")],
8971 &master_key(),
8972 single_stream_options(),
8973 )
8974 .unwrap();
8975 let options = NonSeekableReaderOptions {
8976 max_streamed_member_count: 0,
8977 ..NonSeekableReaderOptions::default()
8978 };
8979
8980 assert_eq!(
8981 verify_non_seekable_stream_with_options(
8982 std::io::Cursor::new(archive.bytes),
8983 &master_key(),
8984 options
8985 )
8986 .unwrap_err(),
8987 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
8988 );
8989 }
8990
8991 #[test]
8992 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
8993 let archive = write_archive(
8994 &[RegularFile::new("live-total-cap.txt", b"payload")],
8995 &master_key(),
8996 single_stream_options(),
8997 )
8998 .unwrap();
8999 let mut options = NonSeekableReaderOptions::default();
9000 options.reader.max_total_extraction_size = 3;
9001
9002 assert_eq!(
9003 verify_non_seekable_stream_with_options(
9004 std::io::Cursor::new(archive.bytes),
9005 &master_key(),
9006 options
9007 )
9008 .unwrap_err(),
9009 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9010 );
9011 }
9012
9013 #[test]
9014 fn live_non_seekable_verify_reports_root_auth_wire_only() {
9015 let archive = write_archive_with_root_auth(
9016 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
9017 &master_key(),
9018 single_stream_options(),
9019 test_root_auth_config(),
9020 |request| Ok(test_root_auth_value(request)),
9021 )
9022 .unwrap();
9023
9024 let report =
9025 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9026
9027 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
9028 }
9029
9030 #[test]
9031 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
9032 let archive = write_archive(
9033 &[
9034 RegularFile::new("alpha.txt", b"alpha"),
9035 RegularFile::new("nested/beta.txt", b"beta"),
9036 ],
9037 &master_key(),
9038 single_stream_options(),
9039 )
9040 .unwrap();
9041 let tmp = tempfile::tempdir().unwrap();
9042 let out = tmp.path().join("out");
9043
9044 let report = extract_non_seekable_stream_to_dir(
9045 std::io::Cursor::new(archive.bytes),
9046 &master_key(),
9047 &out,
9048 NonSeekableReaderOptions::default(),
9049 SafeExtractionOptions::default(),
9050 )
9051 .unwrap();
9052
9053 assert_eq!(report.verification.file_count, 2);
9054 assert_eq!(report.extracted_member_count, 2);
9055 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
9056 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
9057 }
9058
9059 #[test]
9060 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
9061 let archive = write_archive(
9062 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
9063 &master_key(),
9064 single_stream_options(),
9065 )
9066 .unwrap();
9067 let tmp = tempfile::tempdir().unwrap();
9068 let out = tmp.path().join("out");
9069
9070 extract_non_seekable_stream_to_dir(
9071 ChunkedReader::new(archive.bytes, 1),
9072 &master_key(),
9073 &out,
9074 NonSeekableReaderOptions::default(),
9075 SafeExtractionOptions::default(),
9076 )
9077 .unwrap();
9078
9079 assert_eq!(
9080 fs::read(out.join("tiny-extract.txt")).unwrap(),
9081 b"chunked extraction"
9082 );
9083 }
9084
9085 #[test]
9086 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
9087 let archive = write_archive(
9088 &[RegularFile::new("late-fail.txt", b"must remain staged")],
9089 &master_key(),
9090 single_stream_options(),
9091 )
9092 .unwrap();
9093 let mut corrupted = archive.bytes;
9094 corrupt_v41_terminal_recovery(&mut corrupted);
9095 let tmp = tempfile::tempdir().unwrap();
9096 let out = tmp.path().join("out");
9097
9098 match extract_non_seekable_stream_to_dir(
9099 std::io::Cursor::new(corrupted),
9100 &master_key(),
9101 &out,
9102 NonSeekableReaderOptions::default(),
9103 SafeExtractionOptions::default(),
9104 )
9105 .unwrap_err()
9106 {
9107 ExtractError::Format(err) => assert_eq!(
9108 err,
9109 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
9110 ),
9111 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
9112 }
9113 assert!(!out.exists());
9114 }
9115
9116 #[test]
9117 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
9118 let archive = write_archive(
9119 &[RegularFile::new("same.txt", b"new")],
9120 &master_key(),
9121 single_stream_options(),
9122 )
9123 .unwrap();
9124 let tmp = tempfile::tempdir().unwrap();
9125 let out = tmp.path().join("out");
9126 fs::create_dir(&out).unwrap();
9127 fs::write(out.join("same.txt"), b"old").unwrap();
9128
9129 match extract_non_seekable_stream_to_dir(
9130 std::io::Cursor::new(archive.bytes.clone()),
9131 &master_key(),
9132 &out,
9133 NonSeekableReaderOptions::default(),
9134 SafeExtractionOptions::default(),
9135 )
9136 .unwrap_err()
9137 {
9138 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
9139 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
9140 }
9141 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
9142
9143 extract_non_seekable_stream_to_dir(
9144 std::io::Cursor::new(archive.bytes),
9145 &master_key(),
9146 &out,
9147 NonSeekableReaderOptions::default(),
9148 SafeExtractionOptions {
9149 overwrite_existing: true,
9150 },
9151 )
9152 .unwrap();
9153 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
9154 }
9155
9156 #[test]
9157 fn live_non_seekable_list_stream_matches_seekable_final_view() {
9158 let archive = write_archive(
9159 &[
9160 RegularFile::new("a.txt", b"a"),
9161 RegularFile::new("b.txt", b"bb"),
9162 ],
9163 &master_key(),
9164 single_stream_options(),
9165 )
9166 .unwrap();
9167 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
9168 let expected = seekable.list_files().unwrap();
9169
9170 let report = list_non_seekable_stream(
9171 std::io::Cursor::new(archive.bytes),
9172 &master_key(),
9173 NonSeekableReaderOptions::default(),
9174 )
9175 .unwrap();
9176
9177 assert_eq!(report.verification.file_count, 2);
9178 assert_eq!(report.entries, expected);
9179 }
9180
9181 #[test]
9182 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
9183 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9184 let mut bad_flags = archive.bootstrap_sidecar.clone();
9185 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
9186 header.flags |= 0x08;
9187 });
9188 assert_eq!(
9189 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
9190 .unwrap_err(),
9191 FormatError::UnknownBootstrapSidecarFlags(0x0b)
9192 );
9193
9194 let mut trailing = archive.bootstrap_sidecar.clone();
9195 trailing.push(0);
9196 assert_eq!(
9197 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
9198 .unwrap_err(),
9199 FormatError::NonCanonicalBootstrapSidecarLayout
9200 );
9201 }
9202
9203 #[test]
9204 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
9205 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9206 let mut wrong_volume = archive.bootstrap_sidecar.clone();
9207 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
9208 footer.volume_index = 1;
9209 });
9210 assert_eq!(
9211 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
9212 .unwrap_err(),
9213 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
9214 );
9215
9216 let mut non_authoritative = archive.bootstrap_sidecar.clone();
9217 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
9218 footer.is_authoritative = 0;
9219 });
9220 assert_eq!(
9221 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
9222 .unwrap_err(),
9223 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
9224 );
9225 }
9226
9227 #[test]
9228 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
9229 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9230 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9231 let crypto_start = volume_header.crypto_header_offset as usize;
9232 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9233 let crypto_header = CryptoHeader::parse(
9234 &archive.bytes[crypto_start..crypto_end],
9235 volume_header.crypto_header_length,
9236 )
9237 .unwrap();
9238 let subkeys = Subkeys::derive(
9239 &master_key(),
9240 &volume_header.archive_uuid,
9241 &volume_header.session_id,
9242 )
9243 .unwrap();
9244 let mut opened_header = volume_header;
9245 opened_header.volume_index = 1;
9246
9247 let parsed = parse_bootstrap_sidecar(
9248 &archive.bootstrap_sidecar,
9249 &opened_header,
9250 &crypto_header.fixed,
9251 &subkeys,
9252 )
9253 .unwrap();
9254
9255 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
9256 }
9257
9258 #[test]
9259 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
9260 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9261 let mut conflicting = archive.bootstrap_sidecar.clone();
9262 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
9263 footer.index_root_first_block += 1;
9264 });
9265
9266 assert_eq!(
9267 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
9268 .unwrap_err(),
9269 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
9270 );
9271 }
9272
9273 #[test]
9274 fn sidecar_size_cap_counts_only_present_sparse_sections() {
9275 let mut crypto_header = test_crypto_header();
9276 crypto_header.has_dictionary = 1;
9277 crypto_header.index_root_fec_data_shards = 1;
9278 crypto_header.index_root_fec_parity_shards = 0;
9279 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9280 let header = BootstrapSidecarHeader {
9281 archive_uuid: [0x31; 16],
9282 session_id: [0x42; 16],
9283 flags: 0x04,
9284 manifest_footer_offset: 0,
9285 manifest_footer_length: 0,
9286 index_root_records_offset: 0,
9287 index_root_records_length: 0,
9288 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9289 dictionary_records_length: record_len,
9290 sidecar_hmac: [0u8; 32],
9291 header_crc32c: 0,
9292 };
9293
9294 validate_sidecar_size_cap(
9295 &header,
9296 &crypto_header,
9297 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
9298 )
9299 .unwrap();
9300 assert_eq!(
9301 validate_sidecar_size_cap(
9302 &header,
9303 &crypto_header,
9304 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
9305 )
9306 .unwrap_err(),
9307 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
9308 );
9309 }
9310
9311 #[test]
9312 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
9313 let mut crypto_header = test_crypto_header();
9314 crypto_header.index_root_fec_data_shards = 1;
9315 crypto_header.index_root_fec_parity_shards = 0;
9316 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9317 let header = BootstrapSidecarHeader {
9318 archive_uuid: [0x31; 16],
9319 session_id: [0x42; 16],
9320 flags: 0x02,
9321 manifest_footer_offset: 0,
9322 manifest_footer_length: 0,
9323 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9324 index_root_records_length: record_len * 2,
9325 dictionary_records_offset: 0,
9326 dictionary_records_length: 0,
9327 sidecar_hmac: [0u8; 32],
9328 header_crc32c: 0,
9329 };
9330
9331 assert_eq!(
9332 validate_sidecar_size_cap(
9333 &header,
9334 &crypto_header,
9335 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
9336 )
9337 .unwrap_err(),
9338 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
9339 );
9340 }
9341
9342 #[test]
9343 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
9344 let mut crypto_header = test_crypto_header();
9345 crypto_header.block_size = u32::MAX;
9346 crypto_header.index_root_fec_data_shards = u16::MAX;
9347 crypto_header.index_root_fec_parity_shards = u16::MAX;
9348 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9349 let max_records = crypto_header.index_root_fec_data_shards as u64
9350 + crypto_header.index_root_fec_parity_shards as u64;
9351 let max_section_len = max_records * record_len;
9352 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
9353 + MANIFEST_FOOTER_LEN as u64
9354 + max_section_len
9355 + max_section_len;
9356 let header = BootstrapSidecarHeader {
9357 archive_uuid: [0x31; 16],
9358 session_id: [0x42; 16],
9359 flags: 0x01 | 0x02 | 0x04,
9360 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9361 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
9362 index_root_records_offset: 0,
9363 index_root_records_length: max_section_len,
9364 dictionary_records_offset: 0,
9365 dictionary_records_length: max_section_len,
9366 sidecar_hmac: [0u8; 32],
9367 header_crc32c: 0,
9368 };
9369
9370 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
9371 assert_eq!(
9372 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
9373 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
9374 );
9375 }
9376
9377 #[test]
9378 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
9379 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9380 let mut with_dictionary = archive.bootstrap_sidecar.clone();
9381 let header =
9382 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
9383 .unwrap();
9384 let record_len = sidecar_record_len(&with_dictionary);
9385 let first_record = header.index_root_records_offset as usize;
9386 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
9387 let dictionary_offset = with_dictionary.len() as u64;
9388 with_dictionary.extend_from_slice(&copied_record);
9389 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
9390 header.flags |= 0x04;
9391 header.dictionary_records_offset = dictionary_offset;
9392 header.dictionary_records_length = record_len as u64;
9393 });
9394
9395 assert_eq!(
9396 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
9397 .unwrap_err(),
9398 FormatError::InvalidArchive(
9399 "bootstrap sidecar has dictionary records while has_dictionary is false"
9400 )
9401 );
9402 }
9403
9404 #[test]
9405 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
9406 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9407 let mut missing = archive.bootstrap_sidecar.clone();
9408 let record_len = sidecar_record_len(&missing);
9409 let new_len = missing.len() - record_len;
9410 missing.truncate(new_len);
9411 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
9412 header.index_root_records_length -= record_len as u64;
9413 });
9414 assert_eq!(
9415 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
9416 .unwrap_err(),
9417 FormatError::InvalidArchive(
9418 "sidecar BlockRecord section does not match declared extent"
9419 )
9420 );
9421
9422 let mut duplicate = archive.bootstrap_sidecar.clone();
9423 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
9424 record.block_index -= 1;
9425 });
9426 assert_eq!(
9427 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
9428 .unwrap_err(),
9429 FormatError::InvalidArchive(
9430 "sidecar BlockRecord section has missing or duplicate blocks"
9431 )
9432 );
9433
9434 let mut misordered = archive.bootstrap_sidecar.clone();
9435 swap_sidecar_index_records(&mut misordered, 0, 1);
9436 assert_eq!(
9437 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
9438 .unwrap_err(),
9439 FormatError::InvalidArchive(
9440 "sidecar BlockRecord section has missing or duplicate blocks"
9441 )
9442 );
9443
9444 let mut wrong_kind = archive.bootstrap_sidecar.clone();
9445 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
9446 record.kind = BlockKind::PayloadData;
9447 });
9448 assert_eq!(
9449 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
9450 .unwrap_err(),
9451 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
9452 );
9453
9454 let mut wrong_last = archive.bootstrap_sidecar.clone();
9455 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
9456 record.flags = 0;
9457 });
9458 assert_eq!(
9459 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
9460 .unwrap_err(),
9461 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
9462 );
9463 }
9464
9465 #[test]
9466 fn verify_helper_rejects_envelope_frame_coverage_gap() {
9467 let frames = BTreeMap::from([(
9468 0,
9469 FrameEntry {
9470 frame_index: 0,
9471 envelope_index: 0,
9472 offset_in_envelope: 0,
9473 compressed_size: 10,
9474 decompressed_size: 512,
9475 flags: 0,
9476 tar_stream_offset: 0,
9477 },
9478 )]);
9479 let envelopes = BTreeMap::from([(
9480 0,
9481 EnvelopeEntry {
9482 envelope_index: 0,
9483 first_block_index: 0,
9484 data_block_count: 1,
9485 parity_block_count: 1,
9486 encrypted_size: 4096,
9487 plaintext_size: 11,
9488 first_frame_index: 0,
9489 frame_count: 1,
9490 },
9491 )]);
9492
9493 assert_eq!(
9494 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
9495 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
9496 );
9497 }
9498
9499 #[test]
9500 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
9501 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
9502 assert_eq!(
9503 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
9504 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
9505 );
9506 assert_eq!(
9507 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
9508 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
9509 );
9510 }
9511
9512 #[test]
9513 fn verify_rejects_authenticated_content_hash_mismatch() {
9514 let options = WriterOptions {
9515 index_root_fec_parity_shards: 0,
9516 ..single_stream_options()
9517 };
9518 let archive = write_archive(
9519 &[RegularFile::new("content-hash.txt", b"hash covered")],
9520 &master_key(),
9521 options,
9522 )
9523 .unwrap();
9524 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9525
9526 let mut root = opened.index_root.clone();
9527 root.header.content_sha256 = [0xa5; 32];
9528 let root_plaintext = root.to_bytes();
9529 IndexRoot::parse(
9530 &root_plaintext,
9531 false,
9532 metadata_limits(&opened.crypto_header),
9533 )
9534 .unwrap();
9535 assert_eq!(
9536 root_plaintext.len() as u32,
9537 opened.manifest_footer.index_root_decompressed_size
9538 );
9539
9540 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
9541 let mut next_block_index = opened.manifest_footer.index_root_first_block;
9542 let replacement = encrypt_test_object(
9543 &compressed_root,
9544 &opened.subkeys.index_root_key,
9545 &opened.subkeys.index_nonce_seed,
9546 b"idxroot",
9547 0,
9548 BlockKind::IndexRootData,
9549 &mut next_block_index,
9550 &opened.crypto_header,
9551 &opened.volume_header,
9552 );
9553 assert_eq!(
9554 replacement.extent.data_block_count,
9555 opened.manifest_footer.index_root_data_block_count
9556 );
9557 assert_eq!(
9558 replacement.extent.encrypted_size,
9559 opened.manifest_footer.index_root_encrypted_size
9560 );
9561
9562 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9563 let crypto_end = volume_header.crypto_header_offset as usize
9564 + volume_header.crypto_header_length as usize;
9565 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
9566 let mut malformed = archive.bytes.clone();
9567 for record in replacement.records {
9568 let offset = crypto_end + record.block_index as usize * record_len;
9569 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
9570 }
9571
9572 let reopened = open_archive(&malformed, &master_key()).unwrap();
9573 assert_eq!(
9574 reopened.verify().unwrap_err(),
9575 FormatError::InvalidArchive(
9576 "IndexRoot content_sha256 does not match decoded tar stream"
9577 )
9578 );
9579 }
9580
9581 #[test]
9582 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
9583 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
9584 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
9585 path[0] = b'x';
9586 });
9587 assert_eq!(
9588 path_mismatch.verify().unwrap_err(),
9589 FormatError::InvalidArchive("tar member path does not match FileEntry path")
9590 );
9591
9592 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
9593 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
9594 file.file_data_size += 1;
9595 });
9596 assert_eq!(
9597 size_mismatch.verify().unwrap_err(),
9598 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
9599 );
9600 }
9601
9602 #[test]
9603 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
9604 let (mut opened, _) = multi_envelope_reader_fixture();
9605 let locating = opened.index_root.shards[0].clone();
9606 let mut duplicate = opened.load_index_shard(&locating).unwrap();
9607 duplicate.header.shard_index = 1;
9608 duplicate.frames[0].flags ^= 0x0000_0001;
9609 let duplicate_plaintext = duplicate.to_bytes();
9610 let mut next_block_index = opened
9611 .blocks
9612 .keys()
9613 .last()
9614 .copied()
9615 .map(|index| index + 1)
9616 .unwrap_or(0);
9617 let duplicate_object = encrypt_test_object(
9618 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
9619 &opened.subkeys.index_shard_key,
9620 &opened.subkeys.index_nonce_seed,
9621 b"idxshard",
9622 1,
9623 BlockKind::IndexShardData,
9624 &mut next_block_index,
9625 &opened.crypto_header,
9626 &opened.volume_header,
9627 );
9628 insert_records(&mut opened.blocks, &duplicate_object.records);
9629 opened.index_root.shards.push(ShardEntry {
9630 shard_index: 1,
9631 first_block_index: duplicate_object.extent.first_block_index,
9632 data_block_count: duplicate_object.extent.data_block_count,
9633 parity_block_count: 0,
9634 encrypted_size: duplicate_object.extent.encrypted_size,
9635 decompressed_size: duplicate_plaintext.len() as u32,
9636 file_count: locating.file_count,
9637 first_path_hash: locating.first_path_hash,
9638 last_path_hash: locating.last_path_hash,
9639 });
9640 opened.index_root.header.file_count += locating.file_count as u64;
9641
9642 assert_eq!(
9643 opened.verify().unwrap_err(),
9644 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
9645 );
9646 }
9647
9648 #[test]
9649 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
9650 let (mut opened, _) = multi_envelope_reader_fixture();
9651 let locating = opened.index_root.shards[0].clone();
9652 let mut duplicate = opened.load_index_shard(&locating).unwrap();
9653 duplicate.header.shard_index = 1;
9654 duplicate.envelopes[0].first_block_index += 1;
9655 let duplicate_plaintext = duplicate.to_bytes();
9656 let mut next_block_index = opened
9657 .blocks
9658 .keys()
9659 .last()
9660 .copied()
9661 .map(|index| index + 1)
9662 .unwrap_or(0);
9663 let duplicate_object = encrypt_test_object(
9664 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
9665 &opened.subkeys.index_shard_key,
9666 &opened.subkeys.index_nonce_seed,
9667 b"idxshard",
9668 1,
9669 BlockKind::IndexShardData,
9670 &mut next_block_index,
9671 &opened.crypto_header,
9672 &opened.volume_header,
9673 );
9674 insert_records(&mut opened.blocks, &duplicate_object.records);
9675 opened.index_root.shards.push(ShardEntry {
9676 shard_index: 1,
9677 first_block_index: duplicate_object.extent.first_block_index,
9678 data_block_count: duplicate_object.extent.data_block_count,
9679 parity_block_count: 0,
9680 encrypted_size: duplicate_object.extent.encrypted_size,
9681 decompressed_size: duplicate_plaintext.len() as u32,
9682 file_count: locating.file_count,
9683 first_path_hash: locating.first_path_hash,
9684 last_path_hash: locating.last_path_hash,
9685 });
9686 opened.index_root.header.file_count += locating.file_count as u64;
9687
9688 assert_eq!(
9689 opened.verify().unwrap_err(),
9690 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
9691 );
9692 }
9693
9694 #[test]
9695 fn verify_rejects_non_contiguous_global_envelope_indexes() {
9696 let (mut opened, _) = multi_envelope_reader_fixture();
9697 replace_first_index_shard(&mut opened, |shard| {
9698 let frame = shard
9699 .frames
9700 .iter_mut()
9701 .find(|entry| entry.frame_index == 1)
9702 .unwrap();
9703 frame.envelope_index = 2;
9704
9705 let envelope = shard
9706 .envelopes
9707 .iter_mut()
9708 .find(|entry| entry.envelope_index == 1)
9709 .unwrap();
9710 envelope.envelope_index = 2;
9711 });
9712
9713 assert_eq!(
9714 opened.verify().unwrap_err(),
9715 FormatError::InvalidMetadata {
9716 structure: "EnvelopeEntry",
9717 reason: "global index coverage has a gap",
9718 }
9719 );
9720 }
9721
9722 #[test]
9723 fn verify_rejects_payload_object_extent_overlap() {
9724 let (mut opened, _) = multi_envelope_reader_fixture();
9725 replace_first_index_shard(&mut opened, |shard| {
9726 let first_block_index = shard.envelopes[0].first_block_index;
9727 shard.envelopes[1].first_block_index = first_block_index;
9728 });
9729
9730 assert_eq!(
9731 opened.verify().unwrap_err(),
9732 FormatError::InvalidArchive("encrypted object block ranges overlap")
9733 );
9734 }
9735
9736 #[test]
9737 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
9738 let volume_header = test_volume_header();
9739 let crypto_header = test_crypto_header();
9740 let subkeys = Subkeys::derive(
9741 &master_key(),
9742 &volume_header.archive_uuid,
9743 &volume_header.session_id,
9744 )
9745 .unwrap();
9746 let mut next_block_index = 0u64;
9747 let mut blocks = BTreeMap::new();
9748
9749 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
9750 let beta = test_member(b"beta.txt", b"beta cross shard\n");
9751 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
9752 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
9753 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
9754 let envelope_plaintext =
9755 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
9756 let payload = encrypt_test_object(
9757 &envelope_plaintext,
9758 &subkeys.enc_key,
9759 &subkeys.nonce_seed,
9760 b"envelope",
9761 0,
9762 BlockKind::PayloadData,
9763 &mut next_block_index,
9764 &crypto_header,
9765 &volume_header,
9766 );
9767 insert_records(&mut blocks, &payload.records);
9768
9769 let envelope = EnvelopeEntry {
9770 envelope_index: 0,
9771 first_block_index: payload.extent.first_block_index,
9772 data_block_count: payload.extent.data_block_count,
9773 parity_block_count: 0,
9774 encrypted_size: payload.extent.encrypted_size,
9775 plaintext_size: envelope_plaintext.len() as u32,
9776 first_frame_index: 0,
9777 frame_count: 2,
9778 };
9779 let frame0 = FrameEntry {
9780 frame_index: 0,
9781 envelope_index: 0,
9782 offset_in_envelope: 0,
9783 compressed_size: frame0_plaintext.len() as u32,
9784 decompressed_size: alpha.len() as u32,
9785 flags: 0x0000_0003,
9786 tar_stream_offset: 0,
9787 };
9788 let frame1 = FrameEntry {
9789 frame_index: 1,
9790 envelope_index: 0,
9791 offset_in_envelope: frame0_plaintext.len() as u32,
9792 compressed_size: frame1_plaintext.len() as u32,
9793 decompressed_size: beta.len() as u32,
9794 flags: 0x0000_0003,
9795 tar_stream_offset: alpha.len() as u64,
9796 };
9797
9798 let (shard0_plaintext, first0, last0) = build_test_index_shard(
9799 &[TestFileMeta {
9800 path: b"alpha.txt".to_vec(),
9801 frame_index: 0,
9802 tar_stream_offset: 0,
9803 member_group_size: alpha.len() as u64,
9804 file_data_size: b"alpha cross shard\n".len() as u64,
9805 }],
9806 &[frame0],
9807 std::slice::from_ref(&envelope),
9808 );
9809 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
9810 &[TestFileMeta {
9811 path: b"beta.txt".to_vec(),
9812 frame_index: 1,
9813 tar_stream_offset: alpha.len() as u64,
9814 member_group_size: beta.len() as u64,
9815 file_data_size: b"beta cross shard\n".len() as u64,
9816 }],
9817 &[frame1],
9818 std::slice::from_ref(&envelope),
9819 );
9820 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
9821
9822 let shard0 = encrypt_test_object(
9823 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
9824 &subkeys.index_shard_key,
9825 &subkeys.index_nonce_seed,
9826 b"idxshard",
9827 0,
9828 BlockKind::IndexShardData,
9829 &mut next_block_index,
9830 &crypto_header,
9831 &volume_header,
9832 );
9833 let shard1 = encrypt_test_object(
9834 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
9835 &subkeys.index_shard_key,
9836 &subkeys.index_nonce_seed,
9837 b"idxshard",
9838 1,
9839 BlockKind::IndexShardData,
9840 &mut next_block_index,
9841 &crypto_header,
9842 &volume_header,
9843 );
9844 insert_records(&mut blocks, &shard0.records);
9845 insert_records(&mut blocks, &shard1.records);
9846
9847 let index_root = IndexRoot {
9848 header: IndexRootHeader {
9849 frame_count: 2,
9850 envelope_count: 1,
9851 file_count: 2,
9852 payload_block_count: payload.extent.data_block_count as u64,
9853 tar_total_size: tar_stream.len() as u64,
9854 content_sha256: sha256_bytes(&tar_stream),
9855 ..IndexRootHeader::empty()
9856 },
9857 shards: vec![
9858 ShardEntry {
9859 shard_index: 0,
9860 first_block_index: shard0.extent.first_block_index,
9861 data_block_count: shard0.extent.data_block_count,
9862 parity_block_count: 0,
9863 encrypted_size: shard0.extent.encrypted_size,
9864 decompressed_size: shard0_plaintext.len() as u32,
9865 file_count: 1,
9866 first_path_hash: first0,
9867 last_path_hash: last0,
9868 },
9869 ShardEntry {
9870 shard_index: 1,
9871 first_block_index: shard1.extent.first_block_index,
9872 data_block_count: shard1.extent.data_block_count,
9873 parity_block_count: 0,
9874 encrypted_size: shard1.extent.encrypted_size,
9875 decompressed_size: shard1_plaintext.len() as u32,
9876 file_count: 1,
9877 first_path_hash: first1,
9878 last_path_hash: last1,
9879 },
9880 ],
9881 directory_hint_shards: Vec::new(),
9882 };
9883
9884 let index_root_plaintext = index_root.to_bytes();
9885 let index_root_object = encrypt_test_object(
9886 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
9887 &subkeys.index_root_key,
9888 &subkeys.index_nonce_seed,
9889 b"idxroot",
9890 0,
9891 BlockKind::IndexRootData,
9892 &mut next_block_index,
9893 &crypto_header,
9894 &volume_header,
9895 );
9896 insert_records(&mut blocks, &index_root_object.records);
9897
9898 let archive_uuid = volume_header.archive_uuid;
9899 let session_id = volume_header.session_id;
9900 let opened = OpenedArchive {
9901 options: ReaderOptions::default(),
9902 observed_archive_bytes: 1_000_000,
9903 observed_volume_count: 1,
9904 subkeys,
9905 blocks,
9906 lazy_blocks: None,
9907 crypto_header_bytes: Vec::new(),
9908 volume_header,
9909 crypto_header,
9910 manifest_footer: ManifestFooter {
9911 archive_uuid,
9912 session_id,
9913 volume_index: 0,
9914 is_authoritative: 1,
9915 total_volumes: 1,
9916 index_root_first_block: index_root_object.extent.first_block_index,
9917 index_root_data_block_count: index_root_object.extent.data_block_count,
9918 index_root_parity_block_count: 0,
9919 index_root_encrypted_size: index_root_object.extent.encrypted_size,
9920 index_root_decompressed_size: index_root_plaintext.len() as u32,
9921 manifest_hmac: [0u8; 32],
9922 },
9923 volume_trailer: Some(VolumeTrailer {
9924 archive_uuid,
9925 session_id,
9926 volume_index: 0,
9927 block_count: next_block_index,
9928 bytes_written: 0,
9929 manifest_footer_offset: 0,
9930 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
9931 closed_at_ns: 0,
9932 root_auth_footer_offset: 0,
9933 root_auth_footer_length: 0,
9934 root_auth_flags: 0,
9935 trailer_hmac: [0u8; 32],
9936 }),
9937 root_auth_footer: None,
9938 index_root,
9939 payload_dictionary: None,
9940 };
9941
9942 opened.verify().unwrap();
9943 }
9944
9945 #[test]
9946 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
9947 let options = WriterOptions {
9948 index_root_fec_parity_shards: 0,
9949 ..single_stream_options()
9950 };
9951 let archive = write_archive(
9952 &[RegularFile::new("only.txt", b"only payload")],
9953 &master_key(),
9954 options,
9955 )
9956 .unwrap();
9957 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9958 assert!(opened.index_root.directory_hint_shards.is_empty());
9959
9960 let mut root = opened.index_root.clone();
9961 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
9962 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
9963 let root_plaintext = root.to_bytes();
9964 IndexRoot::parse(
9965 &root_plaintext,
9966 false,
9967 metadata_limits(&opened.crypto_header),
9968 )
9969 .unwrap();
9970 assert_eq!(
9971 root_plaintext.len() as u32,
9972 opened.manifest_footer.index_root_decompressed_size
9973 );
9974
9975 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
9976 let mut next_block_index = opened.manifest_footer.index_root_first_block;
9977 let replacement = encrypt_test_object(
9978 &compressed_root,
9979 &opened.subkeys.index_root_key,
9980 &opened.subkeys.index_nonce_seed,
9981 b"idxroot",
9982 0,
9983 BlockKind::IndexRootData,
9984 &mut next_block_index,
9985 &opened.crypto_header,
9986 &opened.volume_header,
9987 );
9988 assert_eq!(
9989 replacement.extent.first_block_index,
9990 opened.manifest_footer.index_root_first_block
9991 );
9992 assert_eq!(
9993 replacement.extent.data_block_count,
9994 opened.manifest_footer.index_root_data_block_count
9995 );
9996 assert_eq!(
9997 replacement.extent.encrypted_size,
9998 opened.manifest_footer.index_root_encrypted_size
9999 );
10000
10001 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10002 let crypto_end = volume_header.crypto_header_offset as usize
10003 + volume_header.crypto_header_length as usize;
10004 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
10005 let mut malformed = archive.bytes.clone();
10006 for record in replacement.records {
10007 let offset = crypto_end + record.block_index as usize * record_len;
10008 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
10009 }
10010
10011 let reopened = open_archive(&malformed, &master_key()).unwrap();
10012 assert_eq!(
10013 reopened.index_root.header.file_count,
10014 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
10015 );
10016 assert!(reopened.index_root.directory_hint_shards.is_empty());
10017
10018 assert_eq!(
10019 reopened.verify().unwrap_err(),
10020 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
10021 );
10022 }
10023
10024 #[test]
10025 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
10026 let mut map = DirectoryHintMap::new();
10027 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
10028 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
10029
10030 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
10031 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
10032 assert_eq!(
10033 map.get(b"foo/bar".as_slice()),
10034 Some(&BTreeSet::from([2, 4]))
10035 );
10036 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
10037 assert!(!map.contains_key(b"foobar".as_slice()));
10038 }
10039
10040 #[test]
10041 fn directory_hint_validation_requires_exact_global_map() {
10042 let mut expected = DirectoryHintMap::new();
10043 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
10044 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
10045 let rows = sorted_directory_hint_rows(&expected);
10046 let table = directory_hint_table_from_rows(7, &rows, 2);
10047
10048 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
10049 .unwrap();
10050
10051 let mut missing_root = expected.clone();
10052 missing_root.remove(&Vec::new());
10053 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
10054 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
10055 assert_eq!(
10056 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
10057 .unwrap_err(),
10058 FormatError::InvalidArchive("directory hint map does not match decoded files")
10059 );
10060
10061 let mut expected_missing_directory_entry = expected.clone();
10062 expected_missing_directory_entry
10063 .get_mut(b"foo".as_slice())
10064 .unwrap()
10065 .remove(&1);
10066 assert_eq!(
10067 validate_directory_hint_tables_against_expected(
10068 std::slice::from_ref(&table),
10069 &expected_missing_directory_entry,
10070 )
10071 .unwrap_err(),
10072 FormatError::InvalidArchive("directory hint map does not match decoded files")
10073 );
10074
10075 let mut extra = expected.clone();
10076 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
10077 let extra_rows = sorted_directory_hint_rows(&extra);
10078 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
10079 assert_eq!(
10080 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
10081 FormatError::InvalidArchive("directory hint map does not match decoded files")
10082 );
10083 }
10084
10085 #[test]
10086 fn directory_hint_validation_rejects_global_order_mismatch() {
10087 let mut expected = DirectoryHintMap::new();
10088 expected.insert(Vec::new(), BTreeSet::from([0]));
10089 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
10090 let rows = sorted_directory_hint_rows(&expected);
10091 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
10092 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
10093
10094 assert_eq!(
10095 validate_directory_hint_tables_against_expected(&[second, first], &expected)
10096 .unwrap_err(),
10097 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
10098 );
10099 }
10100
10101 #[test]
10102 fn object_extent_rejects_parity_above_class_cap() {
10103 let crypto_header = CryptoHeaderFixed {
10104 length: 0,
10105 compression_algo: CompressionAlgo::ZstdFramed,
10106 aead_algo: AeadAlgo::AesGcmSiv256,
10107 fec_algo: FecAlgo::ReedSolomonGF16,
10108 kdf_algo: KdfAlgo::Raw,
10109 chunk_size: 1024,
10110 envelope_target_size: 4096,
10111 block_size: 4096,
10112 fec_data_shards: 1,
10113 fec_parity_shards: 1,
10114 index_fec_data_shards: 1,
10115 index_fec_parity_shards: 1,
10116 index_root_fec_data_shards: 1,
10117 index_root_fec_parity_shards: 1,
10118 stripe_width: 1,
10119 volume_loss_tolerance: 0,
10120 bit_rot_buffer_pct: 0,
10121 has_dictionary: 0,
10122 max_path_length: 4096,
10123 expected_volume_size: 0,
10124 };
10125 let extent = ObjectExtent {
10126 first_block_index: 0,
10127 data_block_count: 1,
10128 parity_block_count: 2,
10129 encrypted_size: 4096,
10130 };
10131
10132 assert_eq!(
10133 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
10134 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
10135 );
10136 }
10137
10138 #[test]
10139 fn object_extent_rejects_parity_below_recoverability_requirement() {
10140 let crypto_header = CryptoHeaderFixed {
10141 length: 0,
10142 compression_algo: CompressionAlgo::ZstdFramed,
10143 aead_algo: AeadAlgo::AesGcmSiv256,
10144 fec_algo: FecAlgo::ReedSolomonGF16,
10145 kdf_algo: KdfAlgo::Raw,
10146 chunk_size: 1024,
10147 envelope_target_size: 4096,
10148 block_size: 4096,
10149 fec_data_shards: 1,
10150 fec_parity_shards: 1,
10151 index_fec_data_shards: 1,
10152 index_fec_parity_shards: 1,
10153 index_root_fec_data_shards: 1,
10154 index_root_fec_parity_shards: 1,
10155 stripe_width: 2,
10156 volume_loss_tolerance: 1,
10157 bit_rot_buffer_pct: 0,
10158 has_dictionary: 0,
10159 max_path_length: 4096,
10160 expected_volume_size: 0,
10161 };
10162 let extent = ObjectExtent {
10163 first_block_index: 0,
10164 data_block_count: 1,
10165 parity_block_count: 0,
10166 encrypted_size: 4096,
10167 };
10168
10169 assert_eq!(
10170 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
10171 FormatError::InvalidArchive(
10172 "encrypted object parity does not match v41 compute_parity"
10173 )
10174 );
10175 }
10176
10177 #[test]
10178 fn encrypted_object_extent_matrix_rejects_overlaps() {
10179 let (opened, _) = multi_envelope_reader_fixture();
10180 let loaded_shard = opened
10181 .load_index_shard(&opened.index_root.shards[0])
10182 .unwrap();
10183 let base_envelopes = loaded_shard
10184 .envelopes
10185 .iter()
10186 .map(|entry| (entry.envelope_index, entry.clone()))
10187 .collect::<BTreeMap<_, _>>();
10188 let payload_start = loaded_shard.envelopes[0].first_block_index;
10189 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
10190
10191 let mut payload_overlap = base_envelopes.clone();
10192 payload_overlap
10193 .get_mut(&loaded_shard.envelopes[1].envelope_index)
10194 .unwrap()
10195 .first_block_index = payload_start;
10196 assert_eq!(
10197 opened
10198 .validate_encrypted_object_block_ranges(&payload_overlap)
10199 .unwrap_err(),
10200 overlap
10201 );
10202
10203 let mut shard_overlap = opened.clone();
10204 let shard = shard_overlap.index_root.shards[0].clone();
10205 shard_overlap.index_root.shards.push(ShardEntry {
10206 shard_index: 1,
10207 ..shard
10208 });
10209 assert_eq!(
10210 shard_overlap
10211 .validate_encrypted_object_block_ranges(&base_envelopes)
10212 .unwrap_err(),
10213 overlap
10214 );
10215
10216 let mut dictionary_overlap = opened.clone();
10217 dictionary_overlap.crypto_header.has_dictionary = 1;
10218 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
10219 dictionary_overlap
10220 .index_root
10221 .header
10222 .dictionary_data_block_count = 1;
10223 dictionary_overlap
10224 .index_root
10225 .header
10226 .dictionary_parity_block_count = 0;
10227 dictionary_overlap
10228 .index_root
10229 .header
10230 .dictionary_encrypted_size = 4096;
10231 dictionary_overlap
10232 .index_root
10233 .header
10234 .dictionary_decompressed_size = 128;
10235 assert_eq!(
10236 dictionary_overlap
10237 .validate_encrypted_object_block_ranges(&base_envelopes)
10238 .unwrap_err(),
10239 overlap
10240 );
10241
10242 let mut hint_overlap = opened.clone();
10243 hint_overlap
10244 .index_root
10245 .directory_hint_shards
10246 .push(DirectoryHintShardEntry {
10247 hint_shard_index: 0,
10248 first_dir_hash: [0; 8],
10249 last_dir_hash: [0; 8],
10250 first_block_index: payload_start,
10251 data_block_count: 1,
10252 parity_block_count: 0,
10253 encrypted_size: 4096,
10254 decompressed_size: 128,
10255 entry_count: 1,
10256 });
10257 assert_eq!(
10258 hint_overlap
10259 .validate_encrypted_object_block_ranges(&base_envelopes)
10260 .unwrap_err(),
10261 overlap
10262 );
10263 }
10264
10265 #[test]
10266 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
10267 let volume_header = test_volume_header();
10268 let crypto_header = test_crypto_header();
10269 let subkeys = Subkeys::derive(
10270 &master_key(),
10271 &volume_header.archive_uuid,
10272 &volume_header.session_id,
10273 )
10274 .unwrap();
10275 let mut next_block_index = 0u64;
10276
10277 let index_root_payload = b"index root metadata object";
10278 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
10279 assert_metadata_object_from_compressed(
10280 &{
10281 let mut bytes = index_root_compressed.clone();
10282 bytes.push(0);
10283 bytes
10284 },
10285 index_root_payload.len(),
10286 &subkeys,
10287 &volume_header,
10288 &crypto_header,
10289 &subkeys.index_root_key,
10290 &subkeys.index_nonce_seed,
10291 b"idxroot",
10292 0,
10293 BlockKind::IndexRootData,
10294 BlockKind::IndexRootParity,
10295 crypto_header.index_root_fec_data_shards,
10296 crypto_header.index_root_fec_parity_shards,
10297 &mut next_block_index,
10298 FormatError::TrailingBytesAfterZstdFrame,
10299 );
10300 assert_metadata_object_from_compressed(
10301 &index_root_compressed,
10302 index_root_payload.len() + 1,
10303 &subkeys,
10304 &volume_header,
10305 &crypto_header,
10306 &subkeys.index_root_key,
10307 &subkeys.index_nonce_seed,
10308 b"idxroot",
10309 0,
10310 BlockKind::IndexRootData,
10311 BlockKind::IndexRootParity,
10312 crypto_header.index_root_fec_data_shards,
10313 crypto_header.index_root_fec_parity_shards,
10314 &mut next_block_index,
10315 FormatError::ZstdDecompressedSizeMismatch {
10316 expected: index_root_payload.len() + 1,
10317 actual: index_root_payload.len(),
10318 },
10319 );
10320
10321 let index_shard_payload = b"index shard metadata object";
10322 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
10323 assert_metadata_object_from_compressed(
10324 &{
10325 let mut bytes = index_shard_compressed.clone();
10326 bytes.push(0);
10327 bytes
10328 },
10329 index_shard_payload.len(),
10330 &subkeys,
10331 &volume_header,
10332 &crypto_header,
10333 &subkeys.index_shard_key,
10334 &subkeys.index_nonce_seed,
10335 b"idxshard",
10336 1,
10337 BlockKind::IndexShardData,
10338 BlockKind::IndexShardParity,
10339 crypto_header.index_fec_data_shards,
10340 crypto_header.index_fec_parity_shards,
10341 &mut next_block_index,
10342 FormatError::TrailingBytesAfterZstdFrame,
10343 );
10344 assert_metadata_object_from_compressed(
10345 &index_shard_compressed,
10346 index_shard_payload.len() + 1,
10347 &subkeys,
10348 &volume_header,
10349 &crypto_header,
10350 &subkeys.index_shard_key,
10351 &subkeys.index_nonce_seed,
10352 b"idxshard",
10353 1,
10354 BlockKind::IndexShardData,
10355 BlockKind::IndexShardParity,
10356 crypto_header.index_fec_data_shards,
10357 crypto_header.index_fec_parity_shards,
10358 &mut next_block_index,
10359 FormatError::ZstdDecompressedSizeMismatch {
10360 expected: index_shard_payload.len() + 1,
10361 actual: index_shard_payload.len(),
10362 },
10363 );
10364
10365 let directory_hint_payload = b"directory hint metadata object";
10366 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
10367 assert_metadata_object_from_compressed(
10368 &{
10369 let mut bytes = directory_hint_compressed.clone();
10370 bytes.push(0);
10371 bytes
10372 },
10373 directory_hint_payload.len(),
10374 &subkeys,
10375 &volume_header,
10376 &crypto_header,
10377 &subkeys.dir_hint_key,
10378 &subkeys.index_nonce_seed,
10379 b"dirhint",
10380 0,
10381 BlockKind::DirectoryHintData,
10382 BlockKind::DirectoryHintParity,
10383 crypto_header.index_fec_data_shards,
10384 crypto_header.index_fec_parity_shards,
10385 &mut next_block_index,
10386 FormatError::TrailingBytesAfterZstdFrame,
10387 );
10388 assert_metadata_object_from_compressed(
10389 &directory_hint_compressed,
10390 directory_hint_payload.len() + 1,
10391 &subkeys,
10392 &volume_header,
10393 &crypto_header,
10394 &subkeys.dir_hint_key,
10395 &subkeys.index_nonce_seed,
10396 b"dirhint",
10397 0,
10398 BlockKind::DirectoryHintData,
10399 BlockKind::DirectoryHintParity,
10400 crypto_header.index_fec_data_shards,
10401 crypto_header.index_fec_parity_shards,
10402 &mut next_block_index,
10403 FormatError::ZstdDecompressedSizeMismatch {
10404 expected: directory_hint_payload.len() + 1,
10405 actual: directory_hint_payload.len(),
10406 },
10407 );
10408
10409 let dictionary_payload = b"dictionary metadata object";
10410 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
10411 assert_metadata_object_from_compressed(
10412 &{
10413 let mut bytes = dictionary_compressed.clone();
10414 bytes.push(0);
10415 bytes
10416 },
10417 dictionary_payload.len(),
10418 &subkeys,
10419 &volume_header,
10420 &crypto_header,
10421 &subkeys.dictionary_key,
10422 &subkeys.index_nonce_seed,
10423 b"dict",
10424 0,
10425 BlockKind::DictionaryData,
10426 BlockKind::DictionaryParity,
10427 crypto_header.index_root_fec_data_shards,
10428 crypto_header.index_root_fec_parity_shards,
10429 &mut next_block_index,
10430 FormatError::TrailingBytesAfterZstdFrame,
10431 );
10432 assert_metadata_object_from_compressed(
10433 &dictionary_compressed,
10434 dictionary_payload.len() + 1,
10435 &subkeys,
10436 &volume_header,
10437 &crypto_header,
10438 &subkeys.dictionary_key,
10439 &subkeys.index_nonce_seed,
10440 b"dict",
10441 0,
10442 BlockKind::DictionaryData,
10443 BlockKind::DictionaryParity,
10444 crypto_header.index_root_fec_data_shards,
10445 crypto_header.index_root_fec_parity_shards,
10446 &mut next_block_index,
10447 FormatError::ZstdDecompressedSizeMismatch {
10448 expected: dictionary_payload.len() + 1,
10449 actual: dictionary_payload.len(),
10450 },
10451 );
10452 }
10453
10454 #[test]
10455 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
10456 let volume_header = test_volume_header();
10457 let crypto_header = test_crypto_header();
10458 let subkeys = Subkeys::derive(
10459 &master_key(),
10460 &volume_header.archive_uuid,
10461 &volume_header.session_id,
10462 )
10463 .unwrap();
10464 let mut next_block_index = 0u64;
10465
10466 let index_root_payload = b"index root metadata object";
10467 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
10468 index_root_payload,
10469 &subkeys,
10470 &volume_header,
10471 &crypto_header,
10472 &subkeys.index_root_key,
10473 &subkeys.index_nonce_seed,
10474 b"idxroot",
10475 0,
10476 BlockKind::IndexRootData,
10477 &mut next_block_index,
10478 );
10479 let mut index_root_extent = index_root_extent;
10480 index_root_extent.encrypted_size = index_root_extent
10481 .encrypted_size
10482 .saturating_add(crypto_header.block_size);
10483 assert_eq!(
10484 load_metadata_object_from_parts(
10485 &index_root_records,
10486 ObjectLoadContext::index_root(
10487 &volume_header,
10488 &crypto_header,
10489 &subkeys,
10490 index_root_extent,
10491 ),
10492 index_root_payload.len() as u32,
10493 )
10494 .unwrap_err(),
10495 FormatError::InvalidArchive(
10496 "encrypted object size is not data_block_count * block_size"
10497 )
10498 );
10499
10500 let index_shard_payload = b"index shard metadata object";
10501 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
10502 index_shard_payload,
10503 &subkeys,
10504 &volume_header,
10505 &crypto_header,
10506 &subkeys.index_shard_key,
10507 &subkeys.index_nonce_seed,
10508 b"idxshard",
10509 1,
10510 BlockKind::IndexShardData,
10511 &mut next_block_index,
10512 );
10513 let mut index_shard_extent = index_shard_extent;
10514 index_shard_extent.encrypted_size = index_shard_extent
10515 .encrypted_size
10516 .saturating_add(crypto_header.block_size);
10517 assert_eq!(
10518 load_metadata_object_from_parts(
10519 &index_shard_records,
10520 ObjectLoadContext {
10521 volume_header: &volume_header,
10522 crypto_header: &crypto_header,
10523 extent: index_shard_extent,
10524 data_kind: BlockKind::IndexShardData,
10525 parity_kind: BlockKind::IndexShardParity,
10526 key: &subkeys.index_shard_key,
10527 nonce_seed: &subkeys.index_nonce_seed,
10528 domain: b"idxshard",
10529 counter: 1,
10530 class_data_shard_max: crypto_header.index_fec_data_shards,
10531 class_parity_shard_max: crypto_header.index_fec_parity_shards,
10532 },
10533 index_shard_payload.len() as u32,
10534 )
10535 .unwrap_err(),
10536 FormatError::InvalidArchive(
10537 "encrypted object size is not data_block_count * block_size"
10538 )
10539 );
10540
10541 let directory_hint_payload = b"directory hint metadata object";
10542 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
10543 directory_hint_payload,
10544 &subkeys,
10545 &volume_header,
10546 &crypto_header,
10547 &subkeys.dir_hint_key,
10548 &subkeys.index_nonce_seed,
10549 b"dirhint",
10550 0,
10551 BlockKind::DirectoryHintData,
10552 &mut next_block_index,
10553 );
10554 let mut directory_hint_extent = directory_hint_extent;
10555 directory_hint_extent.encrypted_size = directory_hint_extent
10556 .encrypted_size
10557 .saturating_add(crypto_header.block_size);
10558 assert_eq!(
10559 load_metadata_object_from_parts(
10560 &directory_hint_records,
10561 ObjectLoadContext {
10562 volume_header: &volume_header,
10563 crypto_header: &crypto_header,
10564 extent: directory_hint_extent,
10565 data_kind: BlockKind::DirectoryHintData,
10566 parity_kind: BlockKind::DirectoryHintParity,
10567 key: &subkeys.dir_hint_key,
10568 nonce_seed: &subkeys.index_nonce_seed,
10569 domain: b"dirhint",
10570 counter: 0,
10571 class_data_shard_max: crypto_header.index_fec_data_shards,
10572 class_parity_shard_max: crypto_header.index_fec_parity_shards,
10573 },
10574 directory_hint_payload.len() as u32,
10575 )
10576 .unwrap_err(),
10577 FormatError::InvalidArchive(
10578 "encrypted object size is not data_block_count * block_size"
10579 )
10580 );
10581
10582 let dictionary_payload = b"dictionary metadata object";
10583 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
10584 dictionary_payload,
10585 &subkeys,
10586 &volume_header,
10587 &crypto_header,
10588 &subkeys.dictionary_key,
10589 &subkeys.index_nonce_seed,
10590 b"dict",
10591 0,
10592 BlockKind::DictionaryData,
10593 &mut next_block_index,
10594 );
10595 let mut dictionary_extent = dictionary_extent;
10596 dictionary_extent.encrypted_size = dictionary_extent
10597 .encrypted_size
10598 .saturating_add(crypto_header.block_size);
10599 assert_eq!(
10600 load_metadata_object_from_parts(
10601 &dictionary_records,
10602 ObjectLoadContext {
10603 volume_header: &volume_header,
10604 crypto_header: &crypto_header,
10605 extent: dictionary_extent,
10606 data_kind: BlockKind::DictionaryData,
10607 parity_kind: BlockKind::DictionaryParity,
10608 key: &subkeys.dictionary_key,
10609 nonce_seed: &subkeys.index_nonce_seed,
10610 domain: b"dict",
10611 counter: 0,
10612 class_data_shard_max: crypto_header.index_root_fec_data_shards,
10613 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
10614 },
10615 dictionary_payload.len() as u32,
10616 )
10617 .unwrap_err(),
10618 FormatError::InvalidArchive(
10619 "encrypted object size is not data_block_count * block_size"
10620 )
10621 );
10622 }
10623
10624 #[test]
10625 fn opens_complete_multi_volume_archive() {
10626 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
10627 let archive = write_archive(
10628 &files,
10629 &master_key(),
10630 WriterOptions {
10631 stripe_width: 2,
10632 volume_loss_tolerance: 1,
10633 ..single_stream_options()
10634 },
10635 )
10636 .unwrap();
10637 assert_eq!(archive.volumes.len(), 2);
10638
10639 let volume_refs = archive
10640 .volumes
10641 .iter()
10642 .map(Vec::as_slice)
10643 .collect::<Vec<_>>();
10644 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
10645
10646 assert_eq!(opened.volume_header.stripe_width, 2);
10647 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
10648 assert_eq!(
10649 opened.extract_file("alpha.txt").unwrap(),
10650 Some(b"hello from volume stripes".to_vec())
10651 );
10652 opened.verify().unwrap();
10653 }
10654
10655 #[test]
10656 fn recovers_from_one_missing_volume_when_parity_allows() {
10657 let files = [RegularFile::new("alpha.txt", b"recover me")];
10658 let archive = write_archive(
10659 &files,
10660 &master_key(),
10661 WriterOptions {
10662 stripe_width: 2,
10663 volume_loss_tolerance: 1,
10664 ..single_stream_options()
10665 },
10666 )
10667 .unwrap();
10668
10669 let recovered =
10670 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
10671 assert_eq!(
10672 recovered.extract_file("alpha.txt").unwrap(),
10673 Some(b"recover me".to_vec())
10674 );
10675 recovered.verify().unwrap();
10676 }
10677
10678 #[test]
10679 fn recovers_from_crc_corrupted_block_when_parity_allows() {
10680 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
10681 let archive = write_archive(
10682 &files,
10683 &master_key(),
10684 WriterOptions {
10685 stripe_width: 2,
10686 volume_loss_tolerance: 1,
10687 ..single_stream_options()
10688 },
10689 )
10690 .unwrap();
10691 let mut volumes = archive.volumes.clone();
10692 corrupt_first_block_record_payload(&mut volumes[0]);
10693
10694 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
10695 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
10696
10697 assert_eq!(
10698 recovered.extract_file("alpha.txt").unwrap(),
10699 Some(b"repair corrupt block".to_vec())
10700 );
10701 recovered.verify().unwrap();
10702 }
10703
10704 #[test]
10705 fn rejects_multi_volume_count_mismatch_without_tolerance() {
10706 let files = [RegularFile::new("alpha.txt", b"count check")];
10707 let archive = write_archive(
10708 &files,
10709 &master_key(),
10710 WriterOptions {
10711 stripe_width: 3,
10712 volume_loss_tolerance: 0,
10713 ..single_stream_options()
10714 },
10715 )
10716 .unwrap();
10717
10718 assert_eq!(
10719 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
10720 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
10721 );
10722 }
10723
10724 #[test]
10725 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
10726 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
10727 let archive = write_archive(
10728 &files,
10729 &master_key(),
10730 WriterOptions {
10731 stripe_width: 2,
10732 volume_loss_tolerance: 1,
10733 ..single_stream_options()
10734 },
10735 )
10736 .unwrap();
10737
10738 let mut bad_first = archive.volumes[0].clone();
10739 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
10740 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
10741 });
10742
10743 open_archive_volumes(
10744 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
10745 &master_key(),
10746 )
10747 .unwrap();
10748 }
10749
10750 #[test]
10751 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
10752 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
10753 let archive = write_archive(
10754 &files,
10755 &master_key(),
10756 WriterOptions {
10757 stripe_width: 2,
10758 volume_loss_tolerance: 1,
10759 ..single_stream_options()
10760 },
10761 )
10762 .unwrap();
10763 let mut volumes = archive.volumes.clone();
10764
10765 let mut corrupted = false;
10766 for volume in &mut volumes {
10767 if let Some(slot) =
10768 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
10769 {
10770 corrupt_block_record_payload_at_slot(volume, *slot);
10771 corrupted = true;
10772 break;
10773 }
10774 }
10775 assert!(corrupted, "expected an IndexRootData record");
10776
10777 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
10778 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
10779 assert_eq!(
10780 opened.extract_file("alpha.txt").unwrap(),
10781 Some(b"repair meta root".to_vec())
10782 );
10783 opened.verify().unwrap();
10784 }
10785
10786 #[test]
10787 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
10788 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
10789 let archive = write_archive(
10790 &files,
10791 &master_key(),
10792 WriterOptions {
10793 stripe_width: 2,
10794 volume_loss_tolerance: 1,
10795 ..single_stream_options()
10796 },
10797 )
10798 .unwrap();
10799 let mut volumes = archive.volumes.clone();
10800
10801 let mut corrupted = false;
10802 for volume in &mut volumes {
10803 if let Some(slot) =
10804 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
10805 {
10806 corrupt_block_record_payload_at_slot(volume, *slot);
10807 corrupted = true;
10808 break;
10809 }
10810 }
10811 assert!(corrupted, "expected an IndexShardData record");
10812
10813 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
10814 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
10815 assert_eq!(
10816 opened.extract_file("alpha.txt").unwrap(),
10817 Some(b"repair meta shard".to_vec())
10818 );
10819 opened.verify().unwrap();
10820 }
10821
10822 #[test]
10823 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
10824 let files = [RegularFile::new(
10825 "alpha.txt",
10826 b"bitrot parity is not volume loss",
10827 )];
10828 let archive = write_archive(
10829 &files,
10830 &master_key(),
10831 WriterOptions {
10832 stripe_width: 2,
10833 volume_loss_tolerance: 0,
10834 bit_rot_buffer_pct: 1,
10835 ..single_stream_options()
10836 },
10837 )
10838 .unwrap();
10839
10840 assert_eq!(
10841 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
10842 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
10843 );
10844 }
10845
10846 #[test]
10847 fn repairs_crc_erasure_only_within_parity_budget() {
10848 let payload = pseudo_random_bytes(12_000);
10849 let archive = write_archive(
10850 &[RegularFile::new("rot.bin", &payload)],
10851 &master_key(),
10852 small_block_recovery_options(),
10853 )
10854 .unwrap();
10855 let payload_slots = first_payload_data_run_slots(&archive.bytes);
10856 assert!(
10857 payload_slots.len() >= 2,
10858 "fixture must contain a multi-block payload object"
10859 );
10860
10861 let mut one_erasure = archive.bytes.clone();
10862 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
10863 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
10864 assert_eq!(
10865 repaired.extract_file("rot.bin").unwrap(),
10866 Some(payload.clone())
10867 );
10868
10869 let mut two_erasures = archive.bytes.clone();
10870 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
10871 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
10872 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
10873 assert_eq!(
10874 unrepaired.extract_file("rot.bin").unwrap_err(),
10875 FormatError::FecTooFewAvailableShards
10876 );
10877 }
10878
10879 #[test]
10880 fn verify_rejects_missing_required_object_block_extent() {
10881 let (mut opened, missing_block) = multi_envelope_reader_fixture();
10882 assert!(opened.blocks.remove(&missing_block).is_some());
10883
10884 assert_eq!(
10885 opened.verify().unwrap_err(),
10886 FormatError::FecTooFewAvailableShards
10887 );
10888 }
10889
10890 #[test]
10891 fn parity_crc_erasure_does_not_hide_authenticated_data() {
10892 let payload = pseudo_random_bytes(12_000);
10893 let archive = write_archive(
10894 &[RegularFile::new("parity-erasure.bin", &payload)],
10895 &master_key(),
10896 parity_rich_recovery_options(),
10897 )
10898 .unwrap();
10899 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
10900 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
10901 assert!(
10902 parity_slots.len() >= 2,
10903 "fixture must contain redundant parity shards"
10904 );
10905 let mut corrupted = archive.bytes;
10906 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
10907 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
10908
10909 let opened = open_archive(&corrupted, &master_key()).unwrap();
10910 assert_eq!(
10911 opened.extract_file("parity-erasure.bin").unwrap(),
10912 Some(payload)
10913 );
10914 opened.verify().unwrap();
10915 }
10916
10917 #[test]
10918 fn rejects_odd_block_size_before_fec_repair() {
10919 let archive = write_archive(
10920 &[RegularFile::new("odd-block.txt", b"payload")],
10921 &master_key(),
10922 small_block_recovery_options(),
10923 )
10924 .unwrap();
10925 let mut malformed = archive.bytes;
10926 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
10927 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
10928 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
10929
10930 assert_eq!(
10931 open_archive(&malformed, &master_key()).unwrap_err(),
10932 FormatError::OddBlockSize(4097)
10933 );
10934 }
10935
10936 #[test]
10937 fn rejects_structurally_malformed_block_records_instead_of_repairing() {
10938 let archive = write_archive(
10939 &[RegularFile::new("structural-block.txt", b"payload")],
10940 &master_key(),
10941 small_block_recovery_options(),
10942 )
10943 .unwrap();
10944 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
10945
10946 let mut bad_magic = archive.bytes.clone();
10947 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
10948 assert_eq!(
10949 open_archive(&bad_magic, &master_key()).unwrap_err(),
10950 FormatError::BadMagic {
10951 structure: "BlockRecord"
10952 }
10953 );
10954
10955 let mut bad_reserved = archive.bytes;
10956 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
10957 assert_eq!(
10958 open_archive(&bad_reserved, &master_key()).unwrap_err(),
10959 FormatError::NonZeroReserved {
10960 structure: "BlockRecord"
10961 }
10962 );
10963 }
10964
10965 #[test]
10966 fn rejects_parity_block_with_last_data_flag() {
10967 let archive = write_archive(
10968 &[RegularFile::new("parity-flag.txt", b"payload")],
10969 &master_key(),
10970 small_block_recovery_options(),
10971 )
10972 .unwrap();
10973 let parity_slot =
10974 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
10975 let mut malformed = archive.bytes;
10976 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
10977 record.flags = 0x01;
10978 });
10979
10980 assert_eq!(
10981 open_archive(&malformed, &master_key()).unwrap_err(),
10982 FormatError::ParityBlockHasLastDataFlag
10983 );
10984 }
10985
10986 #[test]
10987 fn rejects_missing_and_duplicate_payload_last_data_flags() {
10988 let payload = pseudo_random_bytes(12_000);
10989 let archive = write_archive(
10990 &[RegularFile::new("flags.bin", &payload)],
10991 &master_key(),
10992 small_block_recovery_options(),
10993 )
10994 .unwrap();
10995 let payload_slots = first_payload_data_run_slots(&archive.bytes);
10996 assert!(
10997 payload_slots.len() >= 2,
10998 "fixture must contain a multi-block payload object"
10999 );
11000
11001 let mut duplicate_last = archive.bytes.clone();
11002 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
11003 record.flags = 0x01;
11004 });
11005 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
11006 assert_eq!(
11007 opened.extract_file("flags.bin").unwrap_err(),
11008 FormatError::InvalidArchive("object last-data flag is not on the final data block")
11009 );
11010
11011 let mut missing_last = archive.bytes;
11012 mutate_block_record_at_slot(
11013 &mut missing_last,
11014 *payload_slots.last().unwrap(),
11015 |record| {
11016 record.flags = 0;
11017 },
11018 );
11019 let opened = open_archive(&missing_last, &master_key()).unwrap();
11020 assert_eq!(
11021 opened.extract_file("flags.bin").unwrap_err(),
11022 FormatError::InvalidArchive("object last-data flag is not on the final data block")
11023 );
11024 }
11025
11026 #[test]
11027 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
11028 let files = [RegularFile::new(
11029 "footer-copy.txt",
11030 b"survives one bad footer",
11031 )];
11032 let archive = write_archive(
11033 &files,
11034 &master_key(),
11035 WriterOptions {
11036 stripe_width: 2,
11037 volume_loss_tolerance: 1,
11038 ..single_stream_options()
11039 },
11040 )
11041 .unwrap();
11042 let mut volumes = archive.volumes.clone();
11043 corrupt_manifest_footer_hmac(&mut volumes[0]);
11044
11045 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11046 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11047 assert_eq!(opened.manifest_footer.volume_index, 0);
11048 assert_eq!(opened.volume_header.volume_index, 0);
11049 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
11050 assert_eq!(
11051 opened.extract_file("footer-copy.txt").unwrap(),
11052 Some(b"survives one bad footer".to_vec())
11053 );
11054 opened.verify().unwrap();
11055 }
11056
11057 #[test]
11058 fn manifest_footer_corruption_requires_trusted_sidecar() {
11059 let archive = write_archive(
11060 &[RegularFile::new("footer.txt", b"sidecar authority")],
11061 &master_key(),
11062 single_stream_options(),
11063 )
11064 .unwrap();
11065 let manifest_offset = terminal_material_offset(&archive.bytes);
11066 let mut corrupted = archive.bytes.clone();
11067 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
11068 corrupt_v41_terminal_recovery(&mut corrupted);
11069
11070 assert!(open_archive(&corrupted, &master_key()).is_err());
11071
11072 let opened =
11073 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
11074 .unwrap();
11075 assert!(opened.volume_trailer.is_none());
11076 assert_eq!(
11077 opened.extract_file("footer.txt").unwrap(),
11078 Some(b"sidecar authority".to_vec())
11079 );
11080 opened.verify().unwrap();
11081 }
11082
11083 #[test]
11084 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
11085 let archive = write_archive(
11086 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
11087 &master_key(),
11088 single_stream_options(),
11089 )
11090 .unwrap();
11091 let strict_options = ReaderOptions {
11092 max_trailing_garbage_scan: 0,
11093 ..ReaderOptions::default()
11094 };
11095
11096 let manifest_offset = terminal_material_offset(&archive.bytes);
11097 for offset in [
11098 manifest_offset + 71,
11099 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
11100 ] {
11101 let mut corrupted = archive.bytes.clone();
11102 corrupted[offset] ^= 0x01;
11103 open_archive(&corrupted, &master_key()).unwrap();
11104 }
11105
11106 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
11107 for offset in [
11108 trailer_offset + 75,
11109 trailer_offset + TRAILER_HMAC_COVERED_LEN,
11110 ] {
11111 let mut corrupted = archive.bytes.clone();
11112 corrupted[offset] ^= 0x01;
11113 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
11114 }
11115
11116 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
11117 let mut header =
11118 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11119 .unwrap();
11120 header.manifest_footer_offset += 1;
11121 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
11122 assert_eq!(
11123 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
11124 .unwrap_err(),
11125 FormatError::HmacMismatch {
11126 structure: "BootstrapSidecarHeader"
11127 }
11128 );
11129
11130 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
11131 let mut header =
11132 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
11133 header.sidecar_hmac[0] ^= 1;
11134 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
11135 assert_eq!(
11136 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
11137 .unwrap_err(),
11138 FormatError::HmacMismatch {
11139 structure: "BootstrapSidecarHeader"
11140 }
11141 );
11142
11143 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
11144 let header =
11145 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11146 .unwrap();
11147 let mut header_bytes = header.to_bytes();
11148 header_bytes[124] ^= 0x01;
11149 let crc = crc32c::crc32c(&header_bytes[..124]);
11150 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
11151 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
11152 let opened = open_archive_with_bootstrap_sidecar(
11153 &archive.bytes,
11154 &non_covered_sidecar,
11155 &master_key(),
11156 )
11157 .unwrap();
11158 assert_eq!(
11159 opened.extract_file("hmac-boundary.txt").unwrap(),
11160 Some(b"boundary bytes".to_vec())
11161 );
11162 }
11163
11164 #[test]
11165 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
11166 let archive = write_archive(
11167 &[RegularFile::new("volume-index.txt", b"identity")],
11168 &master_key(),
11169 single_stream_options(),
11170 )
11171 .unwrap();
11172
11173 let mut bad_trailer = archive.bytes.clone();
11174 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
11175 trailer.volume_index = 1;
11176 });
11177 open_archive(&bad_trailer, &master_key()).unwrap();
11178
11179 let mut bad_manifest = archive.bytes;
11180 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
11181 footer.volume_index = 1;
11182 });
11183 open_archive(&bad_manifest, &master_key()).unwrap();
11184 }
11185
11186 #[test]
11187 fn rejects_same_key_header_terminal_material_splice() {
11188 let first = write_archive(
11189 &[RegularFile::new("splice.txt", b"same shape")],
11190 &master_key(),
11191 single_stream_options(),
11192 )
11193 .unwrap();
11194 let second = write_archive(
11195 &[RegularFile::new("splice.txt", b"same shape")],
11196 &master_key(),
11197 single_stream_options(),
11198 )
11199 .unwrap();
11200 assert_ne!(first.archive_uuid, second.archive_uuid);
11201 assert_eq!(
11202 terminal_material_offset(&first.bytes),
11203 terminal_material_offset(&second.bytes)
11204 );
11205 assert_eq!(first.bytes.len(), second.bytes.len());
11206
11207 let terminal_offset = terminal_material_offset(&first.bytes);
11208 let mut spliced = first.bytes.clone();
11209 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
11210
11211 assert_eq!(
11212 open_archive(&spliced, &master_key()).unwrap_err(),
11213 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
11214 );
11215 }
11216
11217 #[test]
11218 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
11219 let kdf_params = crate::crypto::KdfParams::Argon2id {
11220 t_cost: 1,
11221 m_cost_kib: 8,
11222 parallelism: 1,
11223 salt: b"0123456789abcdef".to_vec(),
11224 };
11225 let archive = write_archive_with_kdf(
11226 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
11227 &master_key(),
11228 single_stream_options(),
11229 &kdf_params,
11230 )
11231 .unwrap();
11232 let mut mutated = archive.bytes.clone();
11233 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11234 let subkeys = Subkeys::derive(
11235 &master_key(),
11236 &volume_header.archive_uuid,
11237 &volume_header.session_id,
11238 )
11239 .unwrap();
11240
11241 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11242 let crypto_region = image
11243 .regions
11244 .iter_mut()
11245 .find(|region| region.region_type == 2)
11246 .unwrap();
11247 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
11248 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
11249 crypto_region.bytes[salt_start] ^= 0x01;
11250 let hmac = compute_hmac(
11251 HmacDomain::CryptoHeader,
11252 &subkeys.mac_key,
11253 &volume_header.archive_uuid,
11254 &volume_header.session_id,
11255 &crypto_region.bytes[..hmac_offset],
11256 );
11257 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
11258 });
11259
11260 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11261 let locator = final_recovery_locator(&mutated);
11262 let crypto_start = volume_header.crypto_header_offset as usize;
11263 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11264 let parsed_crypto = CryptoHeader::parse(
11265 &mutated[crypto_start..crypto_end],
11266 volume_header.crypto_header_length,
11267 )
11268 .unwrap();
11269 assert_eq!(
11270 parse_locator_cmra_candidate(
11271 &mutated,
11272 final_offset,
11273 locator,
11274 KeyHoldingTerminalContext {
11275 subkeys: &subkeys,
11276 volume_header: &volume_header,
11277 crypto_header: &parsed_crypto.fixed,
11278 crypto_header_bytes: &mutated[crypto_start..crypto_end],
11279 },
11280 )
11281 .unwrap_err(),
11282 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
11283 );
11284 assert!(open_archive(&mutated, &master_key()).is_err());
11285 }
11286
11287 #[test]
11288 fn rejects_same_key_crypto_header_splice_with_session_mismatch() {
11289 let base = WriterOptions {
11290 archive_uuid: Some([0x11; 16]),
11291 session_id: Some([0x22; 16]),
11292 ..single_stream_options()
11293 };
11294 let same_archive = WriterOptions {
11295 archive_uuid: Some([0x11; 16]),
11296 session_id: Some([0x33; 16]),
11297 ..single_stream_options()
11298 };
11299
11300 let first = write_archive(
11301 &[RegularFile::new("splice.txt", b"same shape")],
11302 &master_key(),
11303 base,
11304 )
11305 .unwrap();
11306 let second = write_archive(
11307 &[RegularFile::new("splice.txt", b"same shape")],
11308 &master_key(),
11309 same_archive,
11310 )
11311 .unwrap();
11312
11313 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
11314 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
11315 let crypto_start = volume_header.crypto_header_offset as usize;
11316 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11317 let second_crypto_end = second_volume_header.crypto_header_offset as usize
11318 + second_volume_header.crypto_header_length as usize;
11319 assert_eq!(crypto_end, second_crypto_end);
11320
11321 let mut spliced = first.bytes.clone();
11322 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
11323
11324 assert_eq!(
11325 open_archive(&spliced, &master_key()).unwrap_err(),
11326 FormatError::HmacMismatch {
11327 structure: "CryptoHeader"
11328 }
11329 );
11330 }
11331
11332 #[test]
11333 fn rejects_same_key_object_splice_with_session_mismatch() {
11334 let first = write_archive(
11335 &[RegularFile::new("splice.txt", b"same shape")],
11336 &master_key(),
11337 WriterOptions {
11338 archive_uuid: Some([0x11; 16]),
11339 session_id: Some([0x22; 16]),
11340 ..single_stream_options()
11341 },
11342 )
11343 .unwrap();
11344 let second = write_archive(
11345 &[RegularFile::new("splice.txt", b"same shape")],
11346 &master_key(),
11347 WriterOptions {
11348 archive_uuid: Some([0x11; 16]),
11349 session_id: Some([0x33; 16]),
11350 ..single_stream_options()
11351 },
11352 )
11353 .unwrap();
11354
11355 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
11356 let crypto_end = volume_header.crypto_header_offset as usize
11357 + volume_header.crypto_header_length as usize;
11358 let terminal_offset = terminal_material_offset(&first.bytes);
11359 let second_terminal_offset = terminal_material_offset(&second.bytes);
11360 assert_eq!(terminal_offset, second_terminal_offset);
11361
11362 let mut spliced = first.bytes.clone();
11363 spliced[crypto_end..terminal_offset]
11364 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
11365
11366 assert_eq!(
11367 open_archive(&spliced, &master_key()).unwrap_err(),
11368 FormatError::AeadFailure
11369 );
11370 }
11371
11372 #[test]
11373 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
11374 let archive = write_archive(
11375 &[RegularFile::new(
11376 "trailer-range.txt",
11377 b"authenticated ranges",
11378 )],
11379 &master_key(),
11380 single_stream_options(),
11381 )
11382 .unwrap();
11383 let strict_options = ReaderOptions {
11384 max_trailing_garbage_scan: 0,
11385 ..ReaderOptions::default()
11386 };
11387 let bytes = archive.bytes;
11388 let manifest_offset = terminal_material_offset(&bytes);
11389 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
11390
11391 let mut wrong_footer_length = bytes.clone();
11392 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
11393 trailer.manifest_footer_length = 42;
11394 });
11395 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
11396 .unwrap();
11397
11398 for (label, offset) in [
11399 (
11400 "offset before trailer by 1",
11401 manifest_offset.saturating_sub(1),
11402 ),
11403 ("offset after trailer", manifest_offset + 1),
11404 ("offset at stream start", 0),
11405 ("offset at trailer", trailer_offset),
11406 ("offset beyond trailer", trailer_offset + 4),
11407 ] {
11408 let mut wrong_footer_offset = bytes.clone();
11409 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
11410 trailer.manifest_footer_offset = offset as u64;
11411 });
11412 open_archive(&wrong_footer_offset, &master_key())
11413 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
11414 }
11415
11416 let mut wrong_bytes_written = bytes.clone();
11417 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
11418 trailer.bytes_written += 1;
11419 });
11420 open_archive(&wrong_bytes_written, &master_key()).unwrap();
11421
11422 let mut wrong_block_count = bytes.clone();
11423 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
11424 trailer.block_count += 1;
11425 });
11426 open_archive(&wrong_block_count, &master_key()).unwrap();
11427
11428 let mut wrong_footer_offset = bytes.clone();
11429 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
11430 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
11431 });
11432 open_archive(&wrong_footer_offset, &master_key()).unwrap();
11433 }
11434
11435 #[test]
11436 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
11437 let archive = write_archive(
11438 &[RegularFile::new(
11439 "trailer-trailing-scan.txt",
11440 b"trailer scan boundaries",
11441 )],
11442 &master_key(),
11443 single_stream_options(),
11444 )
11445 .unwrap();
11446 let options = ReaderOptions {
11447 max_trailing_garbage_scan: 8,
11448 ..ReaderOptions::default()
11449 };
11450
11451 let mut within_scan = archive.bytes.clone();
11452 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
11453 let opened =
11454 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
11455 assert_eq!(
11456 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
11457 Some(b"trailer scan boundaries".to_vec())
11458 );
11459
11460 let mut beyond_scan = archive.bytes.clone();
11461 beyond_scan.resize(
11462 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
11463 0xAA,
11464 );
11465 assert_eq!(
11466 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
11467 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
11468 );
11469 }
11470
11471 #[test]
11472 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
11473 let archive = write_archive(
11474 &[RegularFile::new("index-root-size.txt", b"extent size")],
11475 &master_key(),
11476 single_stream_options(),
11477 )
11478 .unwrap();
11479 let mut malformed = archive.bytes;
11480 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
11481 .expect("archive should contain IndexRootData");
11482 mutate_block_record_at_slot(&mut malformed, slot, |record| {
11483 record.payload[0] ^= 0x55;
11484 });
11485
11486 assert_eq!(
11487 open_archive(&malformed, &master_key()).unwrap_err(),
11488 FormatError::AeadFailure
11489 );
11490 }
11491
11492 #[test]
11493 fn rejects_block_record_at_wrong_stripe_position() {
11494 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
11495 let archive = write_archive(
11496 &files,
11497 &master_key(),
11498 WriterOptions {
11499 stripe_width: 2,
11500 volume_loss_tolerance: 1,
11501 ..single_stream_options()
11502 },
11503 )
11504 .unwrap();
11505 let mut volumes = archive.volumes.clone();
11506 mutate_first_block_record(&mut volumes[0], |record| {
11507 record.block_index += 2;
11508 });
11509
11510 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11511 assert_eq!(
11512 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
11513 FormatError::InvalidArchive("BlockRecord index does not match volume position")
11514 );
11515 }
11516
11517 #[test]
11518 fn rejects_decreasing_block_record_index_in_required_region() {
11519 let archive = write_archive(
11520 &[RegularFile::new("alpha.txt", b"decreasing block index")],
11521 &master_key(),
11522 single_stream_options(),
11523 )
11524 .unwrap();
11525 assert!(block_record_slots(&archive.bytes).len() >= 2);
11526
11527 let mut malformed = archive.bytes;
11528 mutate_block_record_at_slot(&mut malformed, 1, |record| {
11529 record.block_index = 0;
11530 });
11531
11532 assert_eq!(
11533 open_archive(&malformed, &master_key()).unwrap_err(),
11534 FormatError::InvalidArchive("BlockRecord index does not match volume position")
11535 );
11536 }
11537
11538 #[test]
11539 fn rejects_duplicate_authenticated_volume_indexes() {
11540 let files = [RegularFile::new("alpha.txt", b"duplicates")];
11541 let archive = write_archive(
11542 &files,
11543 &master_key(),
11544 WriterOptions {
11545 stripe_width: 2,
11546 volume_loss_tolerance: 1,
11547 ..single_stream_options()
11548 },
11549 )
11550 .unwrap();
11551
11552 assert_eq!(
11553 open_archive_volumes(
11554 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
11555 &master_key()
11556 )
11557 .unwrap_err(),
11558 FormatError::InvalidArchive("duplicate authenticated volume index")
11559 );
11560 }
11561
11562 #[test]
11563 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
11564 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
11565 let archive = write_archive(
11566 &files,
11567 &master_key(),
11568 WriterOptions {
11569 stripe_width: 2,
11570 volume_loss_tolerance: 1,
11571 ..single_stream_options()
11572 },
11573 )
11574 .unwrap();
11575 let mut conflicting = archive.volumes[0].clone();
11576 corrupt_first_block_record_payload(&mut conflicting);
11577
11578 assert_eq!(
11579 open_archive_volumes(
11580 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
11581 &master_key()
11582 )
11583 .unwrap_err(),
11584 FormatError::InvalidArchive("duplicate authenticated volume index")
11585 );
11586 }
11587
11588 fn directory_hint_table_from_rows(
11589 hint_shard_index: u64,
11590 rows: &[(Vec<u8>, Vec<u32>)],
11591 shard_count: u32,
11592 ) -> DirectoryHintTable {
11593 let mut entries = Vec::new();
11594 let mut shard_row_indexes = Vec::new();
11595 let mut string_pool = Vec::new();
11596
11597 for (path, rows) in rows {
11598 let path_offset = if path.is_empty() {
11599 0
11600 } else {
11601 let offset = string_pool.len() as u64;
11602 string_pool.extend_from_slice(path);
11603 offset
11604 };
11605 let shard_list_start_index = shard_row_indexes.len() as u32;
11606 shard_row_indexes.extend_from_slice(rows);
11607 entries.push(DirectoryHintEntry {
11608 dir_hash: hash_prefix(path),
11609 path_offset,
11610 path_length: path.len() as u32,
11611 shard_list_start_index,
11612 shard_count: rows.len() as u32,
11613 });
11614 }
11615
11616 let table_bytes =
11617 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
11618 let locating = DirectoryHintShardEntry {
11619 hint_shard_index,
11620 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
11621 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
11622 first_block_index: 0,
11623 data_block_count: 1,
11624 parity_block_count: 0,
11625 encrypted_size: 4096,
11626 decompressed_size: table_bytes.len() as u32,
11627 entry_count: rows.len() as u64,
11628 };
11629 DirectoryHintTable::parse(
11630 &table_bytes,
11631 &locating,
11632 shard_count,
11633 MetadataLimits::default(),
11634 )
11635 .unwrap()
11636 }
11637
11638 fn directory_hint_table_bytes(
11639 hint_shard_index: u64,
11640 entries: Vec<DirectoryHintEntry>,
11641 shard_row_indexes: Vec<u32>,
11642 string_pool: Vec<u8>,
11643 ) -> Vec<u8> {
11644 let header_len = DirectoryHintTableHeader {
11645 version: 1,
11646 hint_shard_index,
11647 entry_count: 0,
11648 entry_table_offset: 0,
11649 shard_list_offset: 0,
11650 string_pool_offset: 0,
11651 string_pool_size: 0,
11652 }
11653 .to_bytes()
11654 .len();
11655 let entry_len = entries
11656 .first()
11657 .map(|entry| entry.to_bytes().len())
11658 .unwrap_or(0);
11659 let shard_list_offset = if entries.is_empty() {
11660 0
11661 } else {
11662 header_len + entries.len() * entry_len
11663 };
11664 let string_pool_offset = if string_pool.is_empty() {
11665 0
11666 } else {
11667 shard_list_offset + shard_row_indexes.len() * 4
11668 };
11669
11670 let header = DirectoryHintTableHeader {
11671 version: 1,
11672 hint_shard_index,
11673 entry_count: entries.len() as u64,
11674 entry_table_offset: if entries.is_empty() {
11675 0
11676 } else {
11677 header_len as u64
11678 },
11679 shard_list_offset: shard_list_offset as u64,
11680 string_pool_offset: string_pool_offset as u64,
11681 string_pool_size: string_pool.len() as u64,
11682 };
11683
11684 let mut out = Vec::new();
11685 out.extend_from_slice(&header.to_bytes());
11686 for entry in entries {
11687 out.extend_from_slice(&entry.to_bytes());
11688 }
11689 for row in shard_row_indexes {
11690 out.extend_from_slice(&row.to_le_bytes());
11691 }
11692 out.extend_from_slice(&string_pool);
11693 out
11694 }
11695
11696 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
11697 let (record_offset, _) = first_block_record(volume);
11698 volume[record_offset + 16] ^= 0x55;
11699 }
11700
11701 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
11702 let (record_offset, _) = block_record_at_slot(volume, slot);
11703 volume[record_offset + 16] ^= 0x55;
11704 }
11705
11706 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
11707 let (record_offset, _) = block_record_at_slot(volume, slot);
11708 volume[record_offset] ^= 0x55;
11709 }
11710
11711 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
11712 let (record_offset, _) = block_record_at_slot(volume, slot);
11713 volume[record_offset + 14] = 0x01;
11714 }
11715
11716 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
11717 let manifest_offset = terminal_material_offset(volume);
11718 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
11719 }
11720
11721 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
11722 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11723 CriticalRecoveryLocator::parse(
11724 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11725 )
11726 .unwrap()
11727 }
11728
11729 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
11730 let locator = final_recovery_locator(volume);
11731 let tuple = CmraDecoderTuple::from(locator);
11732 assert!(parity_shard_count < tuple.parity_shard_count);
11733 let cmra_offset = locator.cmra_offset as usize;
11734 let shard_size = tuple.shard_size as usize;
11735 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
11736 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
11737 let mut header = CriticalMetadataRecoveryHeader::parse(
11738 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
11739 )
11740 .unwrap();
11741 header.parity_shard_count = parity_shard_count;
11742
11743 let mut cmra =
11744 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
11745 cmra.extend_from_slice(&header.to_bytes());
11746 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11747 for row in 0..kept_rows {
11748 let start = rows_start + row * row_len;
11749 cmra.extend_from_slice(&volume[start..start + row_len]);
11750 }
11751
11752 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
11753 out.extend_from_slice(&volume[..cmra_offset]);
11754 out.extend_from_slice(&cmra);
11755 let mut mirror = locator;
11756 mirror.locator_sequence = 1;
11757 mirror.cmra_length = cmra.len() as u32;
11758 mirror.cmra_parity_shard_count = parity_shard_count;
11759 out.extend_from_slice(&mirror.to_bytes());
11760 let final_locator = CriticalRecoveryLocator {
11761 locator_sequence: 0,
11762 ..mirror
11763 };
11764 out.extend_from_slice(&final_locator.to_bytes());
11765 out
11766 }
11767
11768 fn rewrite_public_cmra_image(
11769 volume: &mut [u8],
11770 mutate: impl FnOnce(&mut CriticalMetadataImage),
11771 ) {
11772 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
11773 }
11774
11775 fn rewrite_cmra_image(
11776 volume: &mut [u8],
11777 mode: CmraRecoveryMode,
11778 mutate: impl FnOnce(&mut CriticalMetadataImage),
11779 ) {
11780 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11781 let locator = final_recovery_locator(volume);
11782 let tuple = CmraDecoderTuple::from(locator);
11783 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
11784 let mut image = recovered.image;
11785 mutate(&mut image);
11786 refresh_critical_image_region_digests(&mut image);
11787 let image_bytes = image.to_bytes().unwrap();
11788 assert_eq!(image_bytes.len(), tuple.image_length as usize);
11789
11790 let shard_size = tuple.shard_size as usize;
11791 let data_shard_count = tuple.data_shard_count as usize;
11792 let parity_shard_count = tuple.parity_shard_count as usize;
11793 assert!(image_bytes.len() <= data_shard_count * shard_size);
11794
11795 let mut data_shards = Vec::with_capacity(data_shard_count);
11796 for idx in 0..data_shard_count {
11797 let start = idx * shard_size;
11798 let end = (start + shard_size).min(image_bytes.len());
11799 let mut payload = vec![0u8; shard_size];
11800 if start < image_bytes.len() {
11801 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
11802 }
11803 data_shards.push(payload);
11804 }
11805 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
11806 let image_sha256 = sha256_bytes(&image_bytes);
11807
11808 let header = CriticalMetadataRecoveryHeader {
11809 shard_size: tuple.shard_size,
11810 data_shard_count: tuple.data_shard_count,
11811 parity_shard_count: tuple.parity_shard_count,
11812 image_length: tuple.image_length,
11813 archive_uuid_hint: locator.archive_uuid_hint,
11814 session_id_hint: locator.session_id_hint,
11815 volume_index_hint: locator.volume_index_hint,
11816 image_sha256,
11817 header_crc32c: 0,
11818 };
11819 let mut cmra = Vec::new();
11820 cmra.extend_from_slice(&header.to_bytes());
11821 for (idx, payload) in data_shards.into_iter().enumerate() {
11822 let payload_len = if idx + 1 == data_shard_count {
11823 image_bytes.len() - idx * shard_size
11824 } else {
11825 shard_size
11826 };
11827 cmra.extend_from_slice(
11828 &CriticalMetadataRecoveryShard {
11829 shard_index: idx as u16,
11830 shard_role: 0,
11831 shard_payload_length: payload_len as u32,
11832 payload,
11833 shard_crc32c: 0,
11834 }
11835 .to_bytes(shard_size)
11836 .unwrap(),
11837 );
11838 }
11839 for (idx, payload) in parity_shards.into_iter().enumerate() {
11840 cmra.extend_from_slice(
11841 &CriticalMetadataRecoveryShard {
11842 shard_index: (data_shard_count + idx) as u16,
11843 shard_role: 1,
11844 shard_payload_length: shard_size as u32,
11845 payload,
11846 shard_crc32c: 0,
11847 }
11848 .to_bytes(shard_size)
11849 .unwrap(),
11850 );
11851 }
11852 assert_eq!(cmra.len() as u64, recovered.cmra_length);
11853 let cmra_offset = locator.cmra_offset as usize;
11854 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
11855
11856 rewrite_locator_image_sha(volume, final_offset, image_sha256);
11857 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
11858 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
11859 }
11860
11861 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
11862 image.volume_header_sha256 = sha256_bytes(
11863 &image
11864 .regions
11865 .iter()
11866 .find(|region| region.region_type == 1)
11867 .unwrap()
11868 .bytes,
11869 );
11870 image.crypto_header_sha256 = sha256_bytes(
11871 &image
11872 .regions
11873 .iter()
11874 .find(|region| region.region_type == 2)
11875 .unwrap()
11876 .bytes,
11877 );
11878 image.manifest_footer_sha256 = sha256_bytes(
11879 &image
11880 .regions
11881 .iter()
11882 .find(|region| region.region_type == 3)
11883 .unwrap()
11884 .bytes,
11885 );
11886 image.root_auth_footer_sha256 = image
11887 .regions
11888 .iter()
11889 .find(|region| region.region_type == 4)
11890 .map(|region| sha256_bytes(®ion.bytes))
11891 .unwrap_or([0u8; 32]);
11892 image.volume_trailer_sha256 = sha256_bytes(
11893 &image
11894 .regions
11895 .iter()
11896 .find(|region| region.region_type == 5)
11897 .unwrap()
11898 .bytes,
11899 );
11900 }
11901
11902 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
11903 let mut locator =
11904 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
11905 .unwrap();
11906 locator.cmra_image_sha256 = image_sha256;
11907 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
11908 }
11909
11910 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
11911 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11912 let final_locator = CriticalRecoveryLocator::parse(
11913 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11914 )
11915 .unwrap();
11916 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
11917 volume[final_locator.cmra_offset as usize] ^= 0x55;
11918 volume[mirror_offset] ^= 0x55;
11919 volume[final_offset] ^= 0x55;
11920 }
11921
11922 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
11923 let (record_offset, record_len) = first_block_record(volume);
11924 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
11925 let mut record = BlockRecord::parse(
11926 &volume[record_offset..record_offset + record_len],
11927 block_size,
11928 )
11929 .unwrap();
11930 mutate(&mut record);
11931 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
11932 }
11933
11934 fn mutate_block_record_at_slot(
11935 volume: &mut [u8],
11936 slot: usize,
11937 mutate: impl FnOnce(&mut BlockRecord),
11938 ) {
11939 let (record_offset, record_len) = block_record_at_slot(volume, slot);
11940 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
11941 let mut record = BlockRecord::parse(
11942 &volume[record_offset..record_offset + record_len],
11943 block_size,
11944 )
11945 .unwrap();
11946 mutate(&mut record);
11947 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
11948 }
11949
11950 fn first_block_record(volume: &[u8]) -> (usize, usize) {
11951 block_record_at_slot(volume, 0)
11952 }
11953
11954 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
11955 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
11956 let crypto_start = volume_header.crypto_header_offset as usize;
11957 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11958 let crypto_header = CryptoHeader::parse(
11959 &volume[crypto_start..crypto_end],
11960 volume_header.crypto_header_length,
11961 )
11962 .unwrap();
11963 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11964 let record_offset = crypto_end + slot * record_len;
11965 assert!(volume.len() >= record_offset + record_len);
11966 (record_offset, record_len)
11967 }
11968
11969 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
11970 block_record_slots(volume)
11971 .into_iter()
11972 .enumerate()
11973 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
11974 }
11975
11976 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
11977 block_record_slots(volume)
11978 .into_iter()
11979 .enumerate()
11980 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
11981 .collect()
11982 }
11983
11984 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
11985 let mut slots = Vec::new();
11986 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
11987 if record.kind == BlockKind::PayloadData {
11988 slots.push(slot);
11989 } else if !slots.is_empty() {
11990 break;
11991 }
11992 }
11993 slots
11994 }
11995
11996 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
11997 envelope_entries_for_path(opened, path)
11998 .into_iter()
11999 .map(|entry| entry.envelope_index)
12000 .collect()
12001 }
12002
12003 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
12004 let normalized =
12005 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
12006 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
12007 let file = &located.shard.files[located.file_index];
12008 frame_range_for_file(&located.shard, file)
12009 .unwrap()
12010 .into_iter()
12011 .map(|frame| {
12012 located
12013 .shard
12014 .envelopes
12015 .iter()
12016 .find(|entry| entry.envelope_index == frame.envelope_index)
12017 .unwrap()
12018 .clone()
12019 })
12020 .collect()
12021 }
12022
12023 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
12024 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12025 let crypto_start = volume_header.crypto_header_offset as usize;
12026 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12027 let crypto_header = CryptoHeader::parse(
12028 &volume[crypto_start..crypto_end],
12029 volume_header.crypto_header_length,
12030 )
12031 .unwrap();
12032 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12033 let manifest_offset = terminal_material_offset(volume);
12034 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
12035 let record_count = (manifest_offset - crypto_end) / record_len;
12036 (0..record_count)
12037 .map(|slot| {
12038 let offset = crypto_end + slot * record_len;
12039 let record = BlockRecord::parse(
12040 &volume[offset..offset + record_len],
12041 record_len - BLOCK_RECORD_FRAMING_LEN,
12042 )
12043 .unwrap();
12044 (offset, record_len, record)
12045 })
12046 .collect()
12047 }
12048
12049 fn rewrite_manifest_footer(
12050 volume: &mut [u8],
12051 master_key: &MasterKey,
12052 mutate: impl FnOnce(&mut ManifestFooter),
12053 ) {
12054 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12055 let offset = terminal_material_offset(volume);
12056 let mut footer =
12057 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
12058 mutate(&mut footer);
12059 footer.manifest_hmac = [0u8; 32];
12060 let mut footer_bytes = footer.to_bytes();
12061 let subkeys = Subkeys::derive(
12062 master_key,
12063 &volume_header.archive_uuid,
12064 &volume_header.session_id,
12065 )
12066 .unwrap();
12067 footer.manifest_hmac = compute_hmac(
12068 HmacDomain::ManifestFooter,
12069 &subkeys.mac_key,
12070 &volume_header.archive_uuid,
12071 &volume_header.session_id,
12072 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
12073 );
12074 footer_bytes = footer.to_bytes();
12075 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
12076 }
12077
12078 fn rewrite_volume_trailer(
12079 volume: &mut [u8],
12080 master_key: &MasterKey,
12081 mutate: impl FnOnce(&mut VolumeTrailer),
12082 ) {
12083 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12084 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
12085 let mut trailer =
12086 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
12087 mutate(&mut trailer);
12088 trailer.trailer_hmac = [0u8; 32];
12089 let mut trailer_bytes = trailer.to_bytes();
12090 let subkeys = Subkeys::derive(
12091 master_key,
12092 &volume_header.archive_uuid,
12093 &volume_header.session_id,
12094 )
12095 .unwrap();
12096 trailer.trailer_hmac = compute_hmac(
12097 HmacDomain::VolumeTrailer,
12098 &subkeys.mac_key,
12099 &volume_header.archive_uuid,
12100 &volume_header.session_id,
12101 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
12102 );
12103 trailer_bytes = trailer.to_bytes();
12104 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
12105 }
12106
12107 fn rewrite_sidecar_header(
12108 sidecar: &mut [u8],
12109 master_key: &MasterKey,
12110 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
12111 ) {
12112 let mut header =
12113 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12114 mutate(&mut header);
12115 write_signed_sidecar_header(sidecar, master_key, &mut header);
12116 }
12117
12118 fn write_signed_sidecar_header(
12119 sidecar: &mut [u8],
12120 master_key: &MasterKey,
12121 header: &mut BootstrapSidecarHeader,
12122 ) {
12123 header.sidecar_hmac = [0u8; 32];
12124 let mut header_bytes = header.to_bytes();
12125 let subkeys =
12126 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
12127 header.sidecar_hmac = compute_hmac(
12128 HmacDomain::BootstrapSidecar,
12129 &subkeys.mac_key,
12130 &header.archive_uuid,
12131 &header.session_id,
12132 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
12133 );
12134 header_bytes = header.to_bytes();
12135 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12136 }
12137
12138 fn sparse_bootstrap_sidecar(
12139 source: &[u8],
12140 master_key: &MasterKey,
12141 include_manifest: bool,
12142 include_index_root: bool,
12143 include_dictionary: bool,
12144 ) -> Vec<u8> {
12145 let source_header =
12146 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12147 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
12148 let mut header = BootstrapSidecarHeader {
12149 archive_uuid: source_header.archive_uuid,
12150 session_id: source_header.session_id,
12151 flags: 0,
12152 manifest_footer_offset: 0,
12153 manifest_footer_length: 0,
12154 index_root_records_offset: 0,
12155 index_root_records_length: 0,
12156 dictionary_records_offset: 0,
12157 dictionary_records_length: 0,
12158 sidecar_hmac: [0u8; 32],
12159 header_crc32c: 0,
12160 };
12161
12162 if include_manifest {
12163 assert!(source_header.has_manifest_footer());
12164 let (offset, length) = append_sidecar_section(
12165 source,
12166 &mut sidecar,
12167 source_header.manifest_footer_offset,
12168 source_header.manifest_footer_length as u64,
12169 );
12170 header.flags |= 0x01;
12171 header.manifest_footer_offset = offset;
12172 header.manifest_footer_length = length as u32;
12173 }
12174 if include_index_root {
12175 assert!(source_header.has_index_root_records());
12176 let (offset, length) = append_sidecar_section(
12177 source,
12178 &mut sidecar,
12179 source_header.index_root_records_offset,
12180 source_header.index_root_records_length,
12181 );
12182 header.flags |= 0x02;
12183 header.index_root_records_offset = offset;
12184 header.index_root_records_length = length;
12185 }
12186 if include_dictionary {
12187 assert!(source_header.has_dictionary_records());
12188 let (offset, length) = append_sidecar_section(
12189 source,
12190 &mut sidecar,
12191 source_header.dictionary_records_offset,
12192 source_header.dictionary_records_length,
12193 );
12194 header.flags |= 0x04;
12195 header.dictionary_records_offset = offset;
12196 header.dictionary_records_length = length;
12197 }
12198
12199 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
12200 sidecar
12201 }
12202
12203 fn append_sidecar_section(
12204 source: &[u8],
12205 sidecar: &mut Vec<u8>,
12206 source_offset: u64,
12207 length: u64,
12208 ) -> (u64, u64) {
12209 let source_offset = source_offset as usize;
12210 let length = length as usize;
12211 let offset = sidecar.len() as u64;
12212 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
12213 (offset, length as u64)
12214 }
12215
12216 fn mutate_sidecar_manifest(
12217 sidecar: &mut [u8],
12218 master_key: &MasterKey,
12219 mutate: impl FnOnce(&mut ManifestFooter),
12220 ) {
12221 let header =
12222 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12223 let offset = header.manifest_footer_offset as usize;
12224 let mut footer =
12225 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
12226 mutate(&mut footer);
12227 footer.manifest_hmac = [0u8; 32];
12228 let mut footer_bytes = footer.to_bytes();
12229 let subkeys =
12230 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
12231 footer.manifest_hmac = compute_hmac(
12232 HmacDomain::ManifestFooter,
12233 &subkeys.mac_key,
12234 &footer.archive_uuid,
12235 &footer.session_id,
12236 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
12237 );
12238 footer_bytes = footer.to_bytes();
12239 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
12240 }
12241
12242 fn mutate_sidecar_index_record(
12243 sidecar: &mut [u8],
12244 record_index: usize,
12245 mutate: impl FnOnce(&mut BlockRecord),
12246 ) {
12247 let header =
12248 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12249 let record_len = sidecar_record_len(sidecar);
12250 let offset = header.index_root_records_offset as usize + record_index * record_len;
12251 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12252 let mut record =
12253 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
12254 mutate(&mut record);
12255 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
12256 }
12257
12258 fn mutate_sidecar_dictionary_record(
12259 sidecar: &mut [u8],
12260 record_index: usize,
12261 mutate: impl FnOnce(&mut BlockRecord),
12262 ) {
12263 let header =
12264 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12265 let record_len = sidecar_record_len(sidecar);
12266 let offset = header.dictionary_records_offset as usize + record_index * record_len;
12267 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12268 let mut record =
12269 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
12270 mutate(&mut record);
12271 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
12272 }
12273
12274 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
12275 let header =
12276 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12277 let record_len = sidecar_record_len(sidecar);
12278 let left_offset = header.index_root_records_offset as usize + left * record_len;
12279 let right_offset = header.index_root_records_offset as usize + right * record_len;
12280 for idx in 0..record_len {
12281 sidecar.swap(left_offset + idx, right_offset + idx);
12282 }
12283 }
12284
12285 fn sidecar_record_len(sidecar: &[u8]) -> usize {
12286 let header =
12287 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12288 let footer_offset = header.manifest_footer_offset as usize;
12289 let footer =
12290 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
12291 .unwrap();
12292 let index_record_count = footer.index_root_data_block_count as usize
12293 + footer.index_root_parity_block_count as usize;
12294 header.index_root_records_length as usize / index_record_count
12295 }
12296
12297 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
12298 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12299 assert_eq!(volume_header.volume_index, 0);
12300 assert_eq!(volume_header.stripe_width, 1);
12301 let crypto_start = volume_header.crypto_header_offset as usize;
12302 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12303 let crypto_header = CryptoHeader::parse(
12304 &volume[crypto_start..crypto_end],
12305 volume_header.crypto_header_length,
12306 )
12307 .unwrap();
12308 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12309 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
12310 for offset in 0..record_count {
12311 let block_index = extent.first_block_index + offset;
12312 let record_offset = crypto_end + block_index as usize * record_len;
12313 volume[record_offset + 16] ^= 0x55;
12314 }
12315 }
12316
12317 fn terminal_material_offset(volume: &[u8]) -> usize {
12318 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12319 let crypto_start = volume_header.crypto_header_offset as usize;
12320 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12321 let crypto_header = CryptoHeader::parse(
12322 &volume[crypto_start..crypto_end],
12323 volume_header.crypto_header_length,
12324 )
12325 .unwrap();
12326 let (_, offset, _) = parse_stream_block_prefix(
12327 volume,
12328 crypto_end,
12329 crypto_header.fixed.block_size as usize,
12330 &volume_header,
12331 )
12332 .unwrap();
12333 offset
12334 }
12335
12336 #[derive(Debug)]
12337 struct TestObject {
12338 extent: ObjectExtent,
12339 records: Vec<BlockRecord>,
12340 }
12341
12342 #[derive(Debug)]
12343 struct TestFileMeta {
12344 path: Vec<u8>,
12345 frame_index: u64,
12346 tar_stream_offset: u64,
12347 member_group_size: u64,
12348 file_data_size: u64,
12349 }
12350
12351 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
12352 let volume_header = test_volume_header();
12353 let crypto_header = test_crypto_header();
12354 let subkeys = Subkeys::derive(
12355 &master_key(),
12356 &volume_header.archive_uuid,
12357 &volume_header.session_id,
12358 )
12359 .unwrap();
12360 let mut next_block_index = 0u64;
12361 let mut blocks = BTreeMap::new();
12362
12363 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
12364 let broken = test_member(b"broken.txt", b"broken payload\n");
12365 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
12366
12367 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
12368 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
12369
12370 let healthy_payload = encrypt_test_object(
12371 &healthy_frame,
12372 &subkeys.enc_key,
12373 &subkeys.nonce_seed,
12374 b"envelope",
12375 0,
12376 BlockKind::PayloadData,
12377 &mut next_block_index,
12378 &crypto_header,
12379 &volume_header,
12380 );
12381 let broken_payload = encrypt_test_object(
12382 &broken_frame,
12383 &subkeys.enc_key,
12384 &subkeys.nonce_seed,
12385 b"envelope",
12386 1,
12387 BlockKind::PayloadData,
12388 &mut next_block_index,
12389 &crypto_header,
12390 &volume_header,
12391 );
12392 let broken_payload_block = broken_payload.extent.first_block_index;
12393 insert_records(&mut blocks, &healthy_payload.records);
12394 insert_records(&mut blocks, &broken_payload.records);
12395
12396 let frames = vec![
12397 FrameEntry {
12398 frame_index: 0,
12399 envelope_index: 0,
12400 offset_in_envelope: 0,
12401 compressed_size: healthy_frame.len() as u32,
12402 decompressed_size: healthy.len() as u32,
12403 flags: 0x0000_0003,
12404 tar_stream_offset: 0,
12405 },
12406 FrameEntry {
12407 frame_index: 1,
12408 envelope_index: 1,
12409 offset_in_envelope: 0,
12410 compressed_size: broken_frame.len() as u32,
12411 decompressed_size: broken.len() as u32,
12412 flags: 0x0000_0003,
12413 tar_stream_offset: healthy.len() as u64,
12414 },
12415 ];
12416 let envelopes = vec![
12417 EnvelopeEntry {
12418 envelope_index: 0,
12419 first_block_index: healthy_payload.extent.first_block_index,
12420 data_block_count: healthy_payload.extent.data_block_count,
12421 parity_block_count: 0,
12422 encrypted_size: healthy_payload.extent.encrypted_size,
12423 plaintext_size: healthy_frame.len() as u32,
12424 first_frame_index: 0,
12425 frame_count: 1,
12426 },
12427 EnvelopeEntry {
12428 envelope_index: 1,
12429 first_block_index: broken_payload.extent.first_block_index,
12430 data_block_count: broken_payload.extent.data_block_count,
12431 parity_block_count: 0,
12432 encrypted_size: broken_payload.extent.encrypted_size,
12433 plaintext_size: broken_frame.len() as u32,
12434 first_frame_index: 1,
12435 frame_count: 1,
12436 },
12437 ];
12438 let files = vec![
12439 TestFileMeta {
12440 path: b"healthy.txt".to_vec(),
12441 frame_index: 0,
12442 tar_stream_offset: 0,
12443 member_group_size: healthy.len() as u64,
12444 file_data_size: b"healthy payload\n".len() as u64,
12445 },
12446 TestFileMeta {
12447 path: b"broken.txt".to_vec(),
12448 frame_index: 1,
12449 tar_stream_offset: healthy.len() as u64,
12450 member_group_size: broken.len() as u64,
12451 file_data_size: b"broken payload\n".len() as u64,
12452 },
12453 ];
12454
12455 let (index_shard_plaintext, first_path_hash, last_path_hash) =
12456 build_test_index_shard(&files, &frames, &envelopes);
12457 let index_shard = encrypt_test_object(
12458 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
12459 &subkeys.index_shard_key,
12460 &subkeys.index_nonce_seed,
12461 b"idxshard",
12462 0,
12463 BlockKind::IndexShardData,
12464 &mut next_block_index,
12465 &crypto_header,
12466 &volume_header,
12467 );
12468 insert_records(&mut blocks, &index_shard.records);
12469
12470 let shard_entry = ShardEntry {
12471 shard_index: 0,
12472 first_block_index: index_shard.extent.first_block_index,
12473 data_block_count: index_shard.extent.data_block_count,
12474 parity_block_count: 0,
12475 encrypted_size: index_shard.extent.encrypted_size,
12476 decompressed_size: index_shard_plaintext.len() as u32,
12477 file_count: files.len() as u32,
12478 first_path_hash,
12479 last_path_hash,
12480 };
12481 let mut root_header = IndexRootHeader::empty();
12482 root_header.frame_count = frames.len() as u64;
12483 root_header.envelope_count = envelopes.len() as u64;
12484 root_header.file_count = files.len() as u64;
12485 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
12486 + broken_payload.extent.data_block_count as u64;
12487 root_header.tar_total_size = tar_stream.len() as u64;
12488 root_header.content_sha256 = sha256_bytes(&tar_stream);
12489 let index_root = IndexRoot {
12490 header: root_header,
12491 shards: vec![shard_entry],
12492 directory_hint_shards: Vec::new(),
12493 };
12494
12495 let index_root_plaintext = index_root.to_bytes();
12496 let index_root_object = encrypt_test_object(
12497 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
12498 &subkeys.index_root_key,
12499 &subkeys.index_nonce_seed,
12500 b"idxroot",
12501 0,
12502 BlockKind::IndexRootData,
12503 &mut next_block_index,
12504 &crypto_header,
12505 &volume_header,
12506 );
12507 insert_records(&mut blocks, &index_root_object.records);
12508
12509 let archive_uuid = volume_header.archive_uuid;
12510 let session_id = volume_header.session_id;
12511 let opened = OpenedArchive {
12512 options: ReaderOptions::default(),
12513 observed_archive_bytes: 1_000_000,
12514 observed_volume_count: 1,
12515 subkeys,
12516 blocks,
12517 lazy_blocks: None,
12518 crypto_header_bytes: Vec::new(),
12519 volume_header,
12520 crypto_header,
12521 manifest_footer: ManifestFooter {
12522 archive_uuid,
12523 session_id,
12524 volume_index: 0,
12525 is_authoritative: 1,
12526 total_volumes: 1,
12527 index_root_first_block: index_root_object.extent.first_block_index,
12528 index_root_data_block_count: index_root_object.extent.data_block_count,
12529 index_root_parity_block_count: 0,
12530 index_root_encrypted_size: index_root_object.extent.encrypted_size,
12531 index_root_decompressed_size: index_root_plaintext.len() as u32,
12532 manifest_hmac: [0u8; 32],
12533 },
12534 volume_trailer: Some(VolumeTrailer {
12535 archive_uuid,
12536 session_id,
12537 volume_index: 0,
12538 block_count: next_block_index,
12539 bytes_written: 0,
12540 manifest_footer_offset: 0,
12541 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
12542 closed_at_ns: 0,
12543 root_auth_footer_offset: 0,
12544 root_auth_footer_length: 0,
12545 root_auth_flags: 0,
12546 trailer_hmac: [0u8; 32],
12547 }),
12548 root_auth_footer: None,
12549 index_root,
12550 payload_dictionary: None,
12551 };
12552 (opened, broken_payload_block)
12553 }
12554
12555 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
12556 let locating = opened.index_root.shards[0].clone();
12557 let mut shard = opened.load_index_shard(&locating).unwrap();
12558 mutate(&mut shard);
12559 let plaintext = shard.to_bytes();
12560 let mut next_block_index = opened
12561 .blocks
12562 .keys()
12563 .last()
12564 .copied()
12565 .map(|index| index + 1)
12566 .unwrap_or(0);
12567 let replacement = encrypt_test_object(
12568 &compress_zstd_frame(&plaintext, 1).unwrap(),
12569 &opened.subkeys.index_shard_key,
12570 &opened.subkeys.index_nonce_seed,
12571 b"idxshard",
12572 locating.shard_index,
12573 BlockKind::IndexShardData,
12574 &mut next_block_index,
12575 &opened.crypto_header,
12576 &opened.volume_header,
12577 );
12578 insert_records(&mut opened.blocks, &replacement.records);
12579 opened.index_root.shards[0] = ShardEntry {
12580 shard_index: locating.shard_index,
12581 first_block_index: replacement.extent.first_block_index,
12582 data_block_count: replacement.extent.data_block_count,
12583 parity_block_count: 0,
12584 encrypted_size: replacement.extent.encrypted_size,
12585 decompressed_size: plaintext.len() as u32,
12586 file_count: shard.files.len() as u32,
12587 first_path_hash: shard.files.first().unwrap().path_hash,
12588 last_path_hash: shard.files.last().unwrap().path_hash,
12589 };
12590 }
12591
12592 fn rewrite_as_single_healthy_file(
12593 opened: &mut OpenedArchive,
12594 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
12595 ) {
12596 let healthy_path = b"healthy.txt";
12597 let healthy_payload = b"healthy payload\n";
12598 let healthy_member = test_member(healthy_path, healthy_payload);
12599 replace_first_index_shard(opened, |shard| {
12600 let file_index = (0..shard.files.len())
12601 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
12602 .unwrap();
12603 let mut file = shard.files[file_index].clone();
12604 let frame = shard
12605 .frames
12606 .iter()
12607 .find(|entry| entry.frame_index == 0)
12608 .unwrap()
12609 .clone();
12610 let envelope = shard
12611 .envelopes
12612 .iter()
12613 .find(|entry| entry.envelope_index == 0)
12614 .unwrap()
12615 .clone();
12616 let mut path = healthy_path.to_vec();
12617
12618 file.path_offset = 0;
12619 file.path_length = path.len() as u32;
12620 file.first_frame_index = 0;
12621 file.frame_count = 1;
12622 file.offset_in_first_frame_plaintext = 0;
12623 file.tar_member_group_size = healthy_member.len() as u64;
12624 file.file_data_size = healthy_payload.len() as u64;
12625 file.flags = 0;
12626 mutate(&mut file, &mut path);
12627 file.path_offset = 0;
12628 file.path_length = path.len() as u32;
12629 file.path_hash = hash_prefix(&path);
12630
12631 shard.files = vec![file];
12632 shard.frames = vec![frame];
12633 shard.envelopes = vec![envelope];
12634 shard.string_pool = path;
12635 });
12636
12637 opened.index_root.header.file_count = 1;
12638 opened.index_root.header.frame_count = 1;
12639 opened.index_root.header.envelope_count = 1;
12640 opened.index_root.header.payload_block_count = 1;
12641 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
12642 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
12643 }
12644
12645 fn test_volume_header() -> VolumeHeader {
12646 VolumeHeader {
12647 format_version: FORMAT_VERSION,
12648 volume_format_rev: VOLUME_FORMAT_REV,
12649 volume_index: 0,
12650 stripe_width: 1,
12651 archive_uuid: [0x31; 16],
12652 session_id: [0x42; 16],
12653 crypto_header_offset: VOLUME_HEADER_LEN as u32,
12654 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
12655 header_crc32c: 0,
12656 }
12657 }
12658
12659 fn test_crypto_header() -> CryptoHeaderFixed {
12660 CryptoHeaderFixed {
12661 length: CRYPTO_HEADER_FIXED_LEN as u32,
12662 compression_algo: CompressionAlgo::ZstdFramed,
12663 aead_algo: AeadAlgo::AesGcmSiv256,
12664 fec_algo: FecAlgo::ReedSolomonGF16,
12665 kdf_algo: KdfAlgo::Raw,
12666 chunk_size: 4096,
12667 envelope_target_size: 8192,
12668 block_size: 4096,
12669 fec_data_shards: 4,
12670 fec_parity_shards: 0,
12671 index_fec_data_shards: 4,
12672 index_fec_parity_shards: 0,
12673 index_root_fec_data_shards: 4,
12674 index_root_fec_parity_shards: 0,
12675 stripe_width: 1,
12676 volume_loss_tolerance: 0,
12677 bit_rot_buffer_pct: 0,
12678 has_dictionary: 0,
12679 max_path_length: 4096,
12680 expected_volume_size: 0,
12681 }
12682 }
12683
12684 #[allow(clippy::too_many_arguments)]
12685 fn encrypt_test_object(
12686 plaintext: &[u8],
12687 key: &[u8; 32],
12688 nonce_seed: &[u8; 32],
12689 domain: &[u8],
12690 counter: u64,
12691 data_kind: BlockKind,
12692 next_block_index: &mut u64,
12693 crypto_header: &CryptoHeaderFixed,
12694 volume_header: &VolumeHeader,
12695 ) -> TestObject {
12696 let block_size = crypto_header.block_size as usize;
12697 let encrypted = encrypt_padded_aead_object(
12698 AeadObjectContext {
12699 algo: crypto_header.aead_algo,
12700 key,
12701 nonce_seed,
12702 domain,
12703 archive_uuid: &volume_header.archive_uuid,
12704 session_id: &volume_header.session_id,
12705 counter,
12706 },
12707 block_size,
12708 plaintext,
12709 )
12710 .unwrap();
12711 assert_eq!(encrypted.len() % block_size, 0);
12712
12713 let first_block_index = *next_block_index;
12714 let data_block_count = encrypted.len() / block_size;
12715 let records = encrypted
12716 .chunks(block_size)
12717 .enumerate()
12718 .map(|(index, payload)| BlockRecord {
12719 block_index: first_block_index + index as u64,
12720 kind: data_kind,
12721 flags: if index + 1 == data_block_count {
12722 0x01
12723 } else {
12724 0
12725 },
12726 payload: payload.to_vec(),
12727 record_crc32c: 0,
12728 })
12729 .collect::<Vec<_>>();
12730 *next_block_index += data_block_count as u64;
12731
12732 TestObject {
12733 extent: ObjectExtent {
12734 first_block_index,
12735 data_block_count: data_block_count as u32,
12736 parity_block_count: 0,
12737 encrypted_size: encrypted.len() as u32,
12738 },
12739 records,
12740 }
12741 }
12742
12743 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
12744 for record in records {
12745 assert!(blocks.insert(record.block_index, record.clone()).is_none());
12746 }
12747 }
12748
12749 #[allow(clippy::too_many_arguments)]
12750 fn build_metadata_object_from_payload(
12751 payload: &[u8],
12752 _subkeys: &Subkeys,
12753 volume_header: &VolumeHeader,
12754 crypto_header: &CryptoHeaderFixed,
12755 key: &[u8; 32],
12756 nonce_seed: &[u8; 32],
12757 domain: &[u8],
12758 counter: u64,
12759 data_kind: BlockKind,
12760 next_block_index: &mut u64,
12761 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
12762 let compressed = compress_zstd_frame(payload, 1).unwrap();
12763 build_metadata_object_from_compressed(
12764 &compressed,
12765 key,
12766 nonce_seed,
12767 domain,
12768 counter,
12769 data_kind,
12770 next_block_index,
12771 crypto_header,
12772 volume_header,
12773 )
12774 }
12775
12776 #[allow(clippy::too_many_arguments)]
12777 fn build_metadata_object_from_compressed(
12778 compressed: &[u8],
12779 key: &[u8; 32],
12780 nonce_seed: &[u8; 32],
12781 domain: &[u8],
12782 counter: u64,
12783 data_kind: BlockKind,
12784 next_block_index: &mut u64,
12785 crypto_header: &CryptoHeaderFixed,
12786 volume_header: &VolumeHeader,
12787 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
12788 let object = encrypt_test_object(
12789 compressed,
12790 key,
12791 nonce_seed,
12792 domain,
12793 counter,
12794 data_kind,
12795 next_block_index,
12796 crypto_header,
12797 volume_header,
12798 );
12799
12800 let mut blocks = BTreeMap::new();
12801 for record in object.records {
12802 blocks.insert(record.block_index, record);
12803 }
12804 (object.extent, blocks)
12805 }
12806
12807 #[allow(clippy::too_many_arguments)]
12808 fn assert_metadata_object_from_compressed(
12809 compressed: &[u8],
12810 decompressed_size: usize,
12811 _subkeys: &Subkeys,
12812 volume_header: &VolumeHeader,
12813 crypto_header: &CryptoHeaderFixed,
12814 key: &[u8; 32],
12815 nonce_seed: &[u8; 32],
12816 domain: &[u8],
12817 counter: u64,
12818 data_kind: BlockKind,
12819 parity_kind: BlockKind,
12820 class_data_shards: u16,
12821 class_parity_shards: u16,
12822 next_block_index: &mut u64,
12823 expected: FormatError,
12824 ) {
12825 let (extent, blocks) = build_metadata_object_from_compressed(
12826 compressed,
12827 key,
12828 nonce_seed,
12829 domain,
12830 counter,
12831 data_kind,
12832 next_block_index,
12833 crypto_header,
12834 volume_header,
12835 );
12836 let error = load_metadata_object_from_parts(
12837 &blocks,
12838 ObjectLoadContext {
12839 volume_header,
12840 crypto_header,
12841 extent,
12842 data_kind,
12843 parity_kind,
12844 key,
12845 nonce_seed,
12846 domain,
12847 counter,
12848 class_data_shard_max: class_data_shards,
12849 class_parity_shard_max: class_parity_shards,
12850 },
12851 decompressed_size as u32,
12852 )
12853 .unwrap_err();
12854 assert_eq!(error, expected);
12855 }
12856
12857 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
12858 let record = blocks.get_mut(&block_index).unwrap();
12859 assert_eq!(record.kind, BlockKind::PayloadData);
12860 record.payload[0] ^= 0x55;
12861 }
12862
12863 fn build_test_index_shard(
12864 files: &[TestFileMeta],
12865 frames: &[FrameEntry],
12866 envelopes: &[EnvelopeEntry],
12867 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
12868 let mut sorted = files
12869 .iter()
12870 .map(|file| (hash_prefix(&file.path), file))
12871 .collect::<Vec<_>>();
12872 sorted.sort_by(|left, right| {
12873 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
12874 right.0,
12875 right.1.path.as_slice(),
12876 right.1.tar_stream_offset,
12877 ))
12878 });
12879
12880 let mut string_pool = Vec::new();
12881 let mut file_entries = Vec::with_capacity(sorted.len());
12882 for (path_hash, file) in &sorted {
12883 let path_offset = string_pool.len() as u32;
12884 string_pool.extend_from_slice(&file.path);
12885 file_entries.push(FileEntry {
12886 path_hash: *path_hash,
12887 path_offset,
12888 path_length: file.path.len() as u32,
12889 first_frame_index: file.frame_index,
12890 frame_count: 1,
12891 offset_in_first_frame_plaintext: 0,
12892 tar_member_group_size: file.member_group_size,
12893 file_data_size: file.file_data_size,
12894 flags: 0,
12895 });
12896 }
12897
12898 let header = IndexShardHeader {
12899 version: 1,
12900 shard_index: 0,
12901 file_count: file_entries.len() as u32,
12902 frame_count: frames.len() as u32,
12903 envelope_count: envelopes.len() as u32,
12904 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
12905 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
12906 as u32,
12907 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
12908 + file_entries.len() * FILE_ENTRY_LEN
12909 + frames.len() * FRAME_ENTRY_LEN) as u32,
12910 string_pool_offset: (INDEX_SHARD_HEADER_LEN
12911 + file_entries.len() * FILE_ENTRY_LEN
12912 + frames.len() * FRAME_ENTRY_LEN
12913 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
12914 string_pool_size: string_pool.len() as u32,
12915 };
12916
12917 let mut bytes = Vec::new();
12918 bytes.extend_from_slice(&header.to_bytes());
12919 for entry in &file_entries {
12920 bytes.extend_from_slice(&entry.to_bytes());
12921 }
12922 for entry in frames {
12923 bytes.extend_from_slice(&entry.to_bytes());
12924 }
12925 for entry in envelopes {
12926 bytes.extend_from_slice(&entry.to_bytes());
12927 }
12928 bytes.extend_from_slice(&string_pool);
12929
12930 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
12931 }
12932
12933 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
12934 let mut out = Vec::new();
12935 out.extend_from_slice(&test_tar_header(path, data.len() as u64));
12936 out.extend_from_slice(data);
12937 out.resize(out.len() + padding_to_512(data.len()), 0);
12938 out
12939 }
12940
12941 fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
12942 let mut header = [0u8; 512];
12943 header[..path.len()].copy_from_slice(path);
12944 write_test_tar_octal(&mut header[100..108], 0o644);
12945 write_test_tar_octal(&mut header[108..116], 0);
12946 write_test_tar_octal(&mut header[116..124], 0);
12947 write_test_tar_octal(&mut header[124..136], size);
12948 write_test_tar_octal(&mut header[136..148], 0);
12949 header[148..156].fill(b' ');
12950 header[156] = b'0';
12951 header[257..263].copy_from_slice(b"ustar\0");
12952 header[263..265].copy_from_slice(b"00");
12953 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
12954 write_test_tar_checksum(&mut header[148..156], checksum);
12955 header
12956 }
12957
12958 fn write_test_tar_octal(field: &mut [u8], value: u64) {
12959 let digits = format!("{value:o}");
12960 field.fill(0);
12961 let start = field.len() - 1 - digits.len();
12962 field[..start].fill(b'0');
12963 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
12964 }
12965
12966 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
12967 let digits = format!("{value:06o}");
12968 field[0..6].copy_from_slice(digits.as_bytes());
12969 field[6] = 0;
12970 field[7] = b' ';
12971 }
12972
12973 fn padding_to_512(len: usize) -> usize {
12974 let remainder = len % 512;
12975 if remainder == 0 {
12976 0
12977 } else {
12978 512 - remainder
12979 }
12980 }
12981}