Skip to main content

tzap_core/
reader.rs

1use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
2use std::fs::File;
3use std::io::{Read, Write};
4use std::sync::Arc;
5use std::thread;
6
7use sha2::{Digest, Sha256};
8
9use crate::compression::{decompress_exact_zstd_frame, validate_exact_zstd_frame};
10use crate::crypto::{
11    decrypt_padded_aead_object, verify_integrity_tag, AeadObjectContext, HmacDomain, MasterKey,
12    Subkeys,
13};
14use crate::fec::{encode_parity_gf16, repair_data_gf16};
15use crate::format::{
16    AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, BLOCK_RECORD_FRAMING_LEN,
17    BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
18    CRITICAL_METADATA_RECOVERY_HEADER_LEN, CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN,
19    CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_HEADER_HMAC_LEN, FORMAT_VERSION, IMAGE_CRC_LEN,
20    LOCATOR_PAIR_LEN, MANIFEST_FOOTER_LEN, READER_MAX_CMRA_PARITY_PCT,
21    READER_MAX_CRYPTO_HEADER_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN,
22    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::{
29    StreamedEnvelopeSummary, StreamedFrameSummary, StreamedPayloadSummary,
30};
31use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
32use crate::root_auth::{
33    archive_root, critical_metadata_digest, data_block_merkle_root, fec_layout_digest,
34    index_digest, root_auth_descriptor_digest, signer_identity_digest, ArchiveRootInputs,
35    CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
36};
37use crate::tar_model::{
38    parse_tar_member_group, restore_streaming_tar_member_group,
39    stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
40    MetadataDiagnostic, NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
41    TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
42    TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
43};
44use crate::wire::{
45    BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage, CriticalMetadataRecoveryHeader,
46    CriticalMetadataRecoveryShard, CriticalRecoveryLocator, CryptoHeader, CryptoHeaderFixed,
47    ExtensionTlv, ManifestFooter, RootAuthFooterV1, VolumeHeader, VolumeTrailer,
48};
49
50const TRAILER_HMAC_COVERED_LEN: usize = 96;
51const MANIFEST_HMAC_COVERED_LEN: usize = 104;
52const SIDECAR_HMAC_COVERED_LEN: usize = 92;
53const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
54const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
55const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
56const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
57
58fn default_jobs() -> usize {
59    std::thread::available_parallelism()
60        .map(|jobs| jobs.get())
61        .unwrap_or(1)
62}
63
64pub trait ArchiveReadAt: Send + Sync + 'static {
65    fn len(&self) -> Result<u64, FormatError>;
66    fn is_empty(&self) -> Result<bool, FormatError> {
67        Ok(self.len()? == 0)
68    }
69    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
70}
71
72impl ArchiveReadAt for File {
73    fn len(&self) -> Result<u64, FormatError> {
74        self.metadata()
75            .map(|metadata| metadata.len())
76            .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
77    }
78
79    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
80        file_read_exact_at(self, offset, buf)
81    }
82}
83
84#[cfg(unix)]
85fn file_read_exact_at(file: &File, mut offset: u64, mut buf: &mut [u8]) -> Result<(), FormatError> {
86    use std::os::unix::fs::FileExt;
87
88    while !buf.is_empty() {
89        let read = file
90            .read_at(buf, offset)
91            .map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
92        if read == 0 {
93            return Err(FormatError::InvalidArchive("archive read failed"));
94        }
95        offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
96        let rest = std::mem::take(&mut buf).split_at_mut(read).1;
97        buf = rest;
98    }
99    Ok(())
100}
101
102#[cfg(windows)]
103fn file_read_exact_at(file: &File, mut offset: u64, mut buf: &mut [u8]) -> Result<(), FormatError> {
104    use std::os::windows::fs::FileExt;
105
106    while !buf.is_empty() {
107        let read = file
108            .seek_read(buf, offset)
109            .map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
110        if read == 0 {
111            return Err(FormatError::InvalidArchive("archive read failed"));
112        }
113        offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
114        let rest = std::mem::take(&mut buf).split_at_mut(read).1;
115        buf = rest;
116    }
117    Ok(())
118}
119
120#[cfg(not(any(unix, windows)))]
121fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
122    let mut file = file
123        .try_clone()
124        .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
125    std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
126        .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
127    file.read_exact(buf)
128        .map_err(|_| FormatError::InvalidArchive("archive read failed"))
129}
130
131impl ArchiveReadAt for Vec<u8> {
132    fn len(&self) -> Result<u64, FormatError> {
133        u64::try_from(self.len())
134            .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
135    }
136
137    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
138        let offset = to_usize(offset, "archive")?;
139        let end = checked_add(offset, buf.len(), "archive")?;
140        let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
141            structure: "archive",
142            expected: end,
143            actual: self.len(),
144        })?;
145        buf.copy_from_slice(source);
146        Ok(())
147    }
148}
149
150impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
151    fn len(&self) -> Result<u64, FormatError> {
152        (**self).len()
153    }
154
155    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
156        (**self).read_exact_at(offset, buf)
157    }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub struct ReaderOptions {
162    pub max_trailing_garbage_scan: usize,
163    pub max_verify_tar_size: usize,
164    pub max_total_extraction_size: u64,
165    pub jobs: usize,
166}
167
168impl Default for ReaderOptions {
169    fn default() -> Self {
170        Self {
171            max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
172            max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
173            max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
174            jobs: default_jobs(),
175        }
176    }
177}
178
179pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
180    if options.jobs == 0 {
181        return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
182    }
183    Ok(())
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct ArchiveEntry {
188    pub path: String,
189    pub file_data_size: u64,
190    pub kind: TarEntryKind,
191    pub mode: u32,
192    pub mtime: u64,
193    pub diagnostics: Vec<MetadataDiagnostic>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct ArchiveIndexEntry {
198    pub path: String,
199    pub file_data_size: u64,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct ExtractedArchiveMember {
204    pub path: String,
205    pub kind: TarEntryKind,
206    pub data: Vec<u8>,
207    pub link_target: Option<String>,
208    pub diagnostics: Vec<MetadataDiagnostic>,
209}
210
211#[derive(Debug, Clone)]
212pub struct OpenedArchive {
213    options: ReaderOptions,
214    observed_archive_bytes: u64,
215    observed_volume_count: u32,
216    subkeys: Subkeys,
217    blocks: BTreeMap<u64, BlockRecord>,
218    lazy_blocks: Option<Arc<SeekableBlockSource>>,
219    crypto_header_bytes: Vec<u8>,
220    pub volume_header: VolumeHeader,
221    pub crypto_header: CryptoHeaderFixed,
222    pub manifest_footer: ManifestFooter,
223    pub volume_trailer: Option<VolumeTrailer>,
224    pub root_auth_footer: Option<RootAuthFooterV1>,
225    pub index_root: IndexRoot,
226    payload_dictionary: Option<Vec<u8>>,
227}
228
229#[derive(Debug)]
230pub struct ArchiveContentVerification<'a> {
231    archive: &'a OpenedArchive,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct ArchiveRepairPatch {
236    pub volume_index: u32,
237    pub block_index: u64,
238    pub record_offset: u64,
239    pub record_bytes: Vec<u8>,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum RootAuthDiagnostic {
244    RootAuthContentVerified,
245    RootAuthDeferredFullArchiveScanRequired,
246    AuthenticatedMetadataNotRootSigned,
247    RecoveryMarginNotRootAuthenticated,
248    ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
249    RecoveryMarginChecked,
250    RecoveryMarginFailed,
251    RecoveryMarginUnchecked,
252}
253
254impl RootAuthDiagnostic {
255    pub const fn label(self) -> &'static str {
256        match self {
257            Self::RootAuthContentVerified => "root_auth_content_verified",
258            Self::RootAuthDeferredFullArchiveScanRequired => {
259                "root_auth_deferred_full_archive_scan_required"
260            }
261            Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
262            Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
263            Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
264                "replicated_global_copy_unchecked_due_to_volume_loss"
265            }
266            Self::RecoveryMarginChecked => "recovery_margin_checked",
267            Self::RecoveryMarginFailed => "recovery_margin_failed",
268            Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
269        }
270    }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum PublicNoKeyDiagnostic {
275    PublicDataBlockCommitmentVerified,
276    PublicPhysicalCompletenessUnverified,
277    PublicRecoveryMarginUnchecked,
278}
279
280impl PublicNoKeyDiagnostic {
281    pub const fn label(self) -> &'static str {
282        match self {
283            Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
284            Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
285            Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
286        }
287    }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct RootAuthVerification {
292    pub archive_root: [u8; 32],
293    pub authenticator_id: u16,
294    pub signer_identity_type: u16,
295    pub signer_identity_bytes: Vec<u8>,
296    pub total_data_block_count: u64,
297    pub diagnostics: Vec<RootAuthDiagnostic>,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct PublicNoKeyVerification {
302    pub archive_root: [u8; 32],
303    pub authenticator_id: u16,
304    pub signer_identity_type: u16,
305    pub signer_identity_bytes: Vec<u8>,
306    pub total_data_block_count: u64,
307    pub diagnostics: Vec<PublicNoKeyDiagnostic>,
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311struct RootAuthMaterial {
312    critical_metadata_digest: [u8; 32],
313    index_digest: [u8; 32],
314    fec_layout_digest: [u8; 32],
315    data_block_merkle_root: [u8; 32],
316    signer_identity_digest: [u8; 32],
317    archive_root: [u8; 32],
318    total_data_block_count: u64,
319}
320
321#[derive(Debug, Clone, Copy)]
322struct ObjectExtent {
323    first_block_index: u64,
324    data_block_count: u32,
325    parity_block_count: u32,
326    encrypted_size: u32,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330enum ParityReadPolicy {
331    Always,
332    RepairOnly,
333}
334
335pub(crate) struct StreamedArchiveOpenParts {
336    pub(crate) options: ReaderOptions,
337    pub(crate) observed_archive_bytes: u64,
338    pub(crate) subkeys: Subkeys,
339    pub(crate) blocks: BTreeMap<u64, BlockRecord>,
340    pub(crate) crypto_header_bytes: Vec<u8>,
341    pub(crate) volume_header: VolumeHeader,
342    pub(crate) crypto_header: CryptoHeaderFixed,
343    pub(crate) manifest_footer: ManifestFooter,
344    pub(crate) volume_trailer: VolumeTrailer,
345    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
346}
347
348#[derive(Clone, Copy)]
349struct WinningIndexEntry {
350    start: u64,
351    file_data_size: u64,
352    shard_index: usize,
353    file_index: usize,
354}
355
356struct LocatedIndexFile {
357    shard: IndexShard,
358    file_index: usize,
359    start: u64,
360}
361
362struct DecodedTarMemberGroupReader<'a> {
363    archive: &'a OpenedArchive,
364    shard: &'a IndexShard,
365    file: &'a FileEntry,
366    decompressor: zstd::bulk::Decompressor<'static>,
367    next_frame_offset: u64,
368    cached_envelope_index: Option<u64>,
369    cached_envelope_plaintext: Vec<u8>,
370    current_frame: Vec<u8>,
371    current_frame_offset: usize,
372    remaining_group_bytes: u64,
373}
374
375struct SeekableVolumeSource {
376    reader: Arc<dyn ArchiveReadAt>,
377    volume_index: u32,
378    crypto_end: u64,
379    block_count: u64,
380    record_len: u64,
381    block_size: usize,
382}
383
384impl std::fmt::Debug for SeekableVolumeSource {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        f.debug_struct("SeekableVolumeSource")
387            .field("volume_index", &self.volume_index)
388            .field("crypto_end", &self.crypto_end)
389            .field("block_count", &self.block_count)
390            .field("record_len", &self.record_len)
391            .field("block_size", &self.block_size)
392            .finish_non_exhaustive()
393    }
394}
395
396#[derive(Debug)]
397struct SeekableBlockSource {
398    stripe_width: u32,
399    volumes: Vec<Option<SeekableVolumeSource>>,
400}
401
402trait BlockProvider {
403    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
404}
405
406struct OpenedBlockProvider<'a> {
407    memory_blocks: &'a BTreeMap<u64, BlockRecord>,
408    lazy_blocks: Option<&'a SeekableBlockSource>,
409}
410
411impl SeekableBlockSource {
412    fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
413        if self.stripe_width == 0 {
414            return Err(FormatError::ZeroStripeWidth);
415        }
416        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
417            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
418        let Some(volume) = self
419            .volumes
420            .get(volume_index as usize)
421            .and_then(Option::as_ref)
422        else {
423            return Err(FormatError::InvalidArchive(
424                "repair output requires all archive volumes",
425            ));
426        };
427        let slot = block_index / self.stripe_width as u64;
428        if slot >= volume.block_count {
429            return Err(FormatError::InvalidArchive(
430                "BlockRecord global coverage has a gap",
431            ));
432        }
433        Ok((volume_index, volume.record_offset(slot)?))
434    }
435
436    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
437        if self.stripe_width == 0 {
438            return Err(FormatError::ZeroStripeWidth);
439        }
440        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
441            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
442        let Some(volume) = self
443            .volumes
444            .get(volume_index as usize)
445            .and_then(Option::as_ref)
446        else {
447            return Ok(None);
448        };
449        let slot = block_index / self.stripe_width as u64;
450        if slot >= volume.block_count {
451            return Ok(None);
452        }
453        match volume.read_slot(slot, block_index) {
454            Ok(record) => Ok(Some(record)),
455            Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
456            Err(err) => Err(err),
457        }
458    }
459
460    fn is_complete_volume_set(&self) -> bool {
461        self.volumes.iter().all(Option::is_some)
462    }
463
464    fn total_block_count(&self) -> Result<u64, FormatError> {
465        self.volumes
466            .iter()
467            .map(|volume| {
468                volume
469                    .as_ref()
470                    .map(|volume| volume.block_count)
471                    .ok_or(FormatError::InvalidArchive(
472                        "missing volume in complete set",
473                    ))
474            })
475            .try_fold(0u64, |sum, count| {
476                checked_u64_add(sum, count?, "BlockRecord count overflow")
477            })
478    }
479}
480
481impl SeekableVolumeSource {
482    fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
483        self.crypto_end
484            .checked_add(checked_u64_mul(
485                slot,
486                self.record_len,
487                "BlockRecord offset overflow",
488            )?)
489            .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
490    }
491
492    fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
493        let record_offset = self.record_offset(slot)?;
494        let raw = read_at_vec(
495            self.reader.as_ref(),
496            record_offset,
497            usize::try_from(self.record_len)
498                .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
499            "BlockRecord",
500        )?;
501        let record = BlockRecord::parse(&raw, self.block_size)?;
502        if record.block_index != expected_block_index {
503            return Err(FormatError::InvalidArchive(
504                "BlockRecord index does not match volume position",
505            ));
506        }
507        Ok(record)
508    }
509}
510
511impl BlockProvider for BTreeMap<u64, BlockRecord> {
512    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
513        Ok(self.get(&block_index).cloned())
514    }
515}
516
517impl BlockProvider for OpenedBlockProvider<'_> {
518    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
519        if let Some(record) = self.memory_blocks.get(&block_index) {
520            return Ok(Some(record.clone()));
521        }
522        match self.lazy_blocks {
523            Some(source) => source.block(block_index),
524            None => Ok(None),
525        }
526    }
527}
528
529fn subkeys_for_open(
530    master_key: Option<&MasterKey>,
531    aead_algo: AeadAlgo,
532    archive_uuid: &[u8; 16],
533    session_id: &[u8; 16],
534) -> Result<Subkeys, FormatError> {
535    if aead_algo.is_encrypted() {
536        Subkeys::derive(
537            master_key.ok_or(FormatError::KeyMaterialMismatch)?,
538            archive_uuid,
539            session_id,
540        )
541    } else {
542        Ok(Subkeys::unencrypted_placeholder())
543    }
544}
545
546type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
547pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
548const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
549    "fast full extract requires unique archive paths";
550
551#[derive(Debug)]
552struct PayloadIndexTables {
553    shards: Vec<IndexShard>,
554    file_count: u64,
555    frames: BTreeMap<u64, FrameEntry>,
556    envelopes: BTreeMap<u64, EnvelopeEntry>,
557}
558
559pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
560    OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
561}
562
563pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
564    require_unencrypted_volume_profile(bytes)?;
565    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
566    OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
567}
568
569pub fn open_archive_volumes(
570    volumes: &[&[u8]],
571    master_key: &MasterKey,
572) -> Result<OpenedArchive, FormatError> {
573    OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
574}
575
576pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
577    for volume in volumes {
578        require_unencrypted_volume_profile(volume)?;
579    }
580    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
581    OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
582}
583
584pub fn open_archive_with_bootstrap_sidecar(
585    bytes: &[u8],
586    bootstrap_sidecar: &[u8],
587    master_key: &MasterKey,
588) -> Result<OpenedArchive, FormatError> {
589    OpenedArchive::open_with_bootstrap_sidecar_options(
590        bytes,
591        bootstrap_sidecar,
592        master_key,
593        ReaderOptions::default(),
594    )
595}
596
597fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
598    if bytes.len() < VOLUME_HEADER_LEN {
599        return Err(FormatError::InvalidLength {
600            structure: "archive",
601            expected: VOLUME_HEADER_LEN,
602            actual: bytes.len(),
603        });
604    }
605    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
606    let crypto_start = volume_header.crypto_header_offset as usize;
607    let crypto_len = volume_header.crypto_header_length as usize;
608    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
609    let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
610    if crypto_header.fixed.aead_algo == AeadAlgo::None
611        && crypto_header.fixed.kdf_algo == KdfAlgo::None
612    {
613        Ok(())
614    } else {
615        Err(FormatError::KeyMaterialMismatch)
616    }
617}
618
619pub fn open_seekable_archive<R: ArchiveReadAt>(
620    reader: R,
621    master_key: &MasterKey,
622) -> Result<OpenedArchive, FormatError> {
623    OpenedArchive::open_seekable_volumes_with_options(
624        vec![reader],
625        master_key,
626        ReaderOptions::default(),
627    )
628}
629
630pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
631    readers: Vec<R>,
632    master_key: &MasterKey,
633) -> Result<OpenedArchive, FormatError> {
634    OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
635}
636
637pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
638    reader: R,
639    bootstrap_sidecar: &[u8],
640    master_key: &MasterKey,
641) -> Result<OpenedArchive, FormatError> {
642    open_seekable_archive_with_bootstrap_sidecar_options(
643        reader,
644        bootstrap_sidecar,
645        master_key,
646        ReaderOptions::default(),
647    )
648}
649
650pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
651    reader: R,
652    bootstrap_sidecar: &[u8],
653    master_key: &MasterKey,
654    options: ReaderOptions,
655) -> Result<OpenedArchive, FormatError> {
656    OpenedArchive::open_seekable_volumes_with_options_for_mode(
657        vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
658        master_key,
659        options,
660        Some(bootstrap_sidecar),
661    )
662}
663
664pub fn open_non_seekable_archive(
665    bytes: &[u8],
666    master_key: &MasterKey,
667    bootstrap_sidecar: Option<&[u8]>,
668) -> Result<OpenedArchive, FormatError> {
669    match bootstrap_sidecar {
670        Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
671            bytes,
672            sidecar,
673            master_key,
674            ReaderOptions::default(),
675            BootstrapSidecarUse::NonSeekableRandomAccess,
676        ),
677        None => Err(FormatError::ReaderUnsupported(
678            "non-seekable random access requires a bootstrap sidecar",
679        )),
680    }
681}
682
683pub fn public_no_key_verify_archive_with<F>(
684    bytes: &[u8],
685    verifier: F,
686) -> Result<PublicNoKeyVerification, FormatError>
687where
688    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
689{
690    public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
691}
692
693pub fn public_no_key_verify_volumes_with<F>(
694    volumes: &[&[u8]],
695    verifier: F,
696) -> Result<PublicNoKeyVerification, FormatError>
697where
698    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
699{
700    public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
701}
702
703/// Decode a single-volume, dictionary-free non-seekable archive image into tar
704/// bytes after authenticating its terminal ManifestFooter and VolumeTrailer.
705///
706/// This is a whole-buffer helper, not a live provisional-output API.
707/// Callers receive no decoded bytes if terminal authentication fails.
708pub fn sequential_extract_tar_stream(
709    bytes: &[u8],
710    master_key: &MasterKey,
711) -> Result<Vec<u8>, FormatError> {
712    sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
713}
714
715impl OpenedArchive {
716    fn block_provider(&self) -> OpenedBlockProvider<'_> {
717        OpenedBlockProvider {
718            memory_blocks: &self.blocks,
719            lazy_blocks: self.lazy_blocks.as_deref(),
720        }
721    }
722
723    fn missing_volume_count(&self) -> u32 {
724        self.crypto_header
725            .stripe_width
726            .saturating_sub(self.observed_volume_count)
727    }
728
729    fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
730        let mut diagnostics = vec![
731            RootAuthDiagnostic::RootAuthContentVerified,
732            RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
733            RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
734        ];
735        if self.missing_volume_count() > 0 {
736            diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
737        }
738        diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
739        diagnostics
740    }
741
742    pub fn open_with_options(
743        bytes: &[u8],
744        master_key: &MasterKey,
745        options: ReaderOptions,
746    ) -> Result<Self, FormatError> {
747        Self::open_volumes_with_options(&[bytes], master_key, options)
748    }
749
750    pub fn open_volumes_with_options(
751        volumes: &[&[u8]],
752        master_key: &MasterKey,
753        options: ReaderOptions,
754    ) -> Result<Self, FormatError> {
755        validate_reader_options(options)?;
756        if volumes.is_empty() {
757            return Err(FormatError::InvalidArchive("no volumes supplied"));
758        }
759
760        let observed_archive_bytes =
761            observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
762        let mut first: Option<ParsedSeekableVolume> = None;
763        let mut manifest_authority: Option<ManifestFooter> = None;
764        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
765        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
766        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
767        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
768        let mut saw_root_auth_absent = false;
769        let mut first_manifest_footer_error: Option<FormatError> = None;
770        let mut seen_volume_indexes = BTreeSet::new();
771        let mut blocks = BTreeMap::new();
772        let mut erased_block_indices = BTreeSet::new();
773
774        for volume_bytes in volumes {
775            let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
776            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
777                return Err(FormatError::InvalidArchive(
778                    "duplicate authenticated volume index",
779                ));
780            }
781
782            if let Some(first) = &first {
783                validate_volume_set_member(first, &parsed)?;
784            }
785
786            if let Some(footer) = &parsed.manifest_footer {
787                if let Some(authority) = &manifest_authority {
788                    if !manifest_bootstrap_fields_match(authority, footer) {
789                        return Err(FormatError::InvalidArchive(
790                            "ManifestFooter bootstrap fields differ",
791                        ));
792                    }
793                } else {
794                    manifest_authority = Some(footer.clone());
795                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
796                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
797                }
798            } else if first_manifest_footer_error.is_none() {
799                first_manifest_footer_error = parsed.manifest_footer_error.take();
800            }
801
802            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
803                (Some(footer), Some(bytes)) => {
804                    if saw_root_auth_absent {
805                        return Err(FormatError::InvalidArchive(
806                            "root-auth footer presence differs across volumes",
807                        ));
808                    }
809                    if let Some(authority_bytes) = &root_auth_authority_bytes {
810                        if authority_bytes != bytes {
811                            return Err(FormatError::InvalidArchive(
812                                "RootAuthFooter copies differ",
813                            ));
814                        }
815                    } else {
816                        root_auth_authority = Some(footer.clone());
817                        root_auth_authority_bytes = Some(bytes.clone());
818                    }
819                }
820                (None, None) => {
821                    if root_auth_authority_bytes.is_some() {
822                        return Err(FormatError::InvalidArchive(
823                            "root-auth footer presence differs across volumes",
824                        ));
825                    }
826                    saw_root_auth_absent = true;
827                }
828                _ => {
829                    return Err(FormatError::InvalidArchive(
830                        "root-auth footer terminal state is inconsistent",
831                    ));
832                }
833            }
834
835            for (block_index, record) in &parsed.blocks {
836                if blocks.insert(*block_index, record.clone()).is_some() {
837                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
838                }
839            }
840            for block_index in &parsed.erased_block_indices {
841                erased_block_indices.insert(*block_index);
842            }
843
844            if first.is_none() {
845                first = Some(parsed);
846            }
847        }
848
849        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
850        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
851            Some(err) => err,
852            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
853        })?;
854        let authority_volume_header = manifest_authority_volume_header.ok_or(
855            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
856        )?;
857        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
858            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
859        )?;
860        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
861            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
862        let missing_volume_count = first
863            .crypto_header
864            .stripe_width
865            .checked_sub(observed_volume_count)
866            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
867        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
868            return Err(FormatError::InvalidArchive(
869                "missing volume count exceeds volume_loss_tolerance",
870            ));
871        }
872        if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
873            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
874        }
875
876        let limits = metadata_limits(&first.crypto_header);
877        let index_root_plaintext = load_metadata_object_from_parts(
878            &blocks,
879            ObjectLoadContext::index_root(
880                &first.volume_header,
881                &first.crypto_header,
882                &first.subkeys,
883                ObjectExtent {
884                    first_block_index: manifest_footer.index_root_first_block,
885                    data_block_count: manifest_footer.index_root_data_block_count,
886                    parity_block_count: manifest_footer.index_root_parity_block_count,
887                    encrypted_size: manifest_footer.index_root_encrypted_size,
888                },
889            ),
890            manifest_footer.index_root_decompressed_size,
891        )?;
892        let index_root = IndexRoot::parse(
893            &index_root_plaintext,
894            first.crypto_header.has_dictionary != 0,
895            limits,
896        )?;
897        let payload_dictionary = load_archive_dictionary(
898            &blocks,
899            &first.subkeys,
900            &first.volume_header,
901            &first.crypto_header,
902            &index_root,
903        )?;
904
905        Ok(Self {
906            options,
907            observed_archive_bytes,
908            observed_volume_count,
909            subkeys: first.subkeys,
910            blocks,
911            lazy_blocks: None,
912            crypto_header_bytes: first.crypto_header_bytes,
913            volume_header: authority_volume_header,
914            crypto_header: first.crypto_header,
915            manifest_footer,
916            volume_trailer: Some(authority_volume_trailer),
917            root_auth_footer: root_auth_authority,
918            index_root,
919            payload_dictionary,
920        })
921    }
922
923    pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
924        readers: Vec<R>,
925        master_key: &MasterKey,
926        options: ReaderOptions,
927    ) -> Result<Self, FormatError> {
928        let readers = readers
929            .into_iter()
930            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
931            .collect::<Vec<_>>();
932        Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
933    }
934
935    fn open_seekable_volumes_with_options_for_mode(
936        readers: Vec<Arc<dyn ArchiveReadAt>>,
937        master_key: &MasterKey,
938        options: ReaderOptions,
939        bootstrap_sidecar: Option<&[u8]>,
940    ) -> Result<Self, FormatError> {
941        validate_reader_options(options)?;
942        if readers.is_empty() {
943            return Err(FormatError::InvalidArchive("no volumes supplied"));
944        }
945        if bootstrap_sidecar.is_some() && readers.len() > 1 {
946            return Err(FormatError::ReaderUnsupported(
947                "multi-volume inputs with bootstrap sidecar are not supported",
948            ));
949        }
950
951        let observed_archive_bytes = observed_archive_size(
952            readers
953                .iter()
954                .map(|reader| reader.len())
955                .collect::<Result<Vec<_>, _>>()?
956                .into_iter()
957                .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
958        )?;
959        let mut first: Option<ParsedSeekableReadAtVolume> = None;
960        let mut manifest_authority: Option<ManifestFooter> = None;
961        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
962        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
963        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
964        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
965        let mut saw_root_auth_absent = false;
966        let mut first_manifest_footer_error: Option<FormatError> = None;
967        let mut seen_volume_indexes = BTreeSet::new();
968        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
969
970        for reader in readers {
971            let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
972            if bootstrap_sidecar.is_some() {
973                validate_bootstrap_single_volume_input(
974                    &parsed.volume_header,
975                    &parsed.crypto_header,
976                )?;
977            }
978            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
979                return Err(FormatError::InvalidArchive(
980                    "duplicate authenticated volume index",
981                ));
982            }
983
984            if let Some(first) = &first {
985                validate_volume_set_member_metadata(
986                    &first.volume_header,
987                    &first.crypto_header,
988                    &first.crypto_header_bytes,
989                    &parsed.volume_header,
990                    &parsed.crypto_header,
991                    &parsed.crypto_header_bytes,
992                )?;
993            } else {
994                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
995            }
996
997            if let Some(footer) = &parsed.manifest_footer {
998                if let Some(authority) = &manifest_authority {
999                    if !manifest_bootstrap_fields_match(authority, footer) {
1000                        return Err(FormatError::InvalidArchive(
1001                            "ManifestFooter bootstrap fields differ",
1002                        ));
1003                    }
1004                } else {
1005                    manifest_authority = Some(footer.clone());
1006                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1007                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1008                }
1009            } else if first_manifest_footer_error.is_none() {
1010                first_manifest_footer_error = parsed.manifest_footer_error.take();
1011            }
1012
1013            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1014                (Some(footer), Some(bytes)) => {
1015                    if saw_root_auth_absent {
1016                        return Err(FormatError::InvalidArchive(
1017                            "root-auth footer presence differs across volumes",
1018                        ));
1019                    }
1020                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1021                        if authority_bytes != bytes {
1022                            return Err(FormatError::InvalidArchive(
1023                                "RootAuthFooter copies differ",
1024                            ));
1025                        }
1026                    } else {
1027                        root_auth_authority = Some(footer.clone());
1028                        root_auth_authority_bytes = Some(bytes.clone());
1029                    }
1030                }
1031                (None, None) => {
1032                    if root_auth_authority_bytes.is_some() {
1033                        return Err(FormatError::InvalidArchive(
1034                            "root-auth footer presence differs across volumes",
1035                        ));
1036                    }
1037                    saw_root_auth_absent = true;
1038                }
1039                _ => {
1040                    return Err(FormatError::InvalidArchive(
1041                        "root-auth footer terminal state is inconsistent",
1042                    ));
1043                }
1044            }
1045
1046            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1047            let source = SeekableVolumeSource {
1048                reader: parsed.reader.clone(),
1049                volume_index: parsed.volume_header.volume_index,
1050                crypto_end: parsed.crypto_end,
1051                block_count: parsed.volume_trailer.block_count,
1052                record_len,
1053                block_size: parsed.crypto_header.block_size as usize,
1054            };
1055            let slot = parsed.volume_header.volume_index as usize;
1056            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1057            {
1058                return Err(FormatError::InvalidArchive(
1059                    "duplicate authenticated volume index",
1060                ));
1061            }
1062
1063            if first.is_none() {
1064                first = Some(parsed);
1065            }
1066        }
1067
1068        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1069        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1070            Some(err) => err,
1071            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1072        })?;
1073        let authority_volume_header = manifest_authority_volume_header.ok_or(
1074            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1075        )?;
1076        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1077            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1078        )?;
1079        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1080            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1081        let missing_volume_count = first
1082            .crypto_header
1083            .stripe_width
1084            .checked_sub(observed_volume_count)
1085            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1086        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1087            return Err(FormatError::InvalidArchive(
1088                "missing volume count exceeds volume_loss_tolerance",
1089            ));
1090        }
1091
1092        let mut blocks = BTreeMap::new();
1093        let sidecar = if let Some(bytes) = bootstrap_sidecar {
1094            let sidecar = parse_bootstrap_sidecar(
1095                bytes,
1096                &first.volume_header,
1097                &first.crypto_header,
1098                &first.subkeys,
1099            )?;
1100            sidecar
1101                .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1102            if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1103                if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1104                    return Err(FormatError::InvalidArchive(
1105                        "bootstrap sidecar conflicts with terminal ManifestFooter",
1106                    ));
1107                }
1108            }
1109            Some((bytes, sidecar))
1110        } else {
1111            None
1112        };
1113
1114        if let Some((sidecar_bytes, sidecar)) = &sidecar {
1115            if let Some((offset, length)) = sidecar.index_root_records_section {
1116                let index_root_records = parse_sidecar_block_records(
1117                    sidecar_bytes,
1118                    first.crypto_header.block_size as usize,
1119                    SidecarBlockRecordsSection {
1120                        offset,
1121                        length,
1122                        extent: index_root_extent_from_manifest(&manifest_footer),
1123                        data_kind: BlockKind::IndexRootData,
1124                        parity_kind: BlockKind::IndexRootParity,
1125                        structure: "IndexRoot",
1126                    },
1127                )?;
1128                insert_sidecar_records(&mut blocks, index_root_records)?;
1129            }
1130        }
1131
1132        let lazy_source = Arc::new(SeekableBlockSource {
1133            stripe_width: first.crypto_header.stripe_width,
1134            volumes: lazy_volume_slots,
1135        });
1136        let block_provider = OpenedBlockProvider {
1137            memory_blocks: &blocks,
1138            lazy_blocks: Some(lazy_source.as_ref()),
1139        };
1140        let limits = metadata_limits(&first.crypto_header);
1141        let index_root_plaintext = load_metadata_object_from_parts(
1142            &block_provider,
1143            ObjectLoadContext::index_root(
1144                &first.volume_header,
1145                &first.crypto_header,
1146                &first.subkeys,
1147                index_root_extent_from_manifest(&manifest_footer),
1148            ),
1149            manifest_footer.index_root_decompressed_size,
1150        )?;
1151        let index_root = IndexRoot::parse(
1152            &index_root_plaintext,
1153            first.crypto_header.has_dictionary != 0,
1154            limits,
1155        )?;
1156        if first.crypto_header.has_dictionary != 0 {
1157            if let Some((sidecar_bytes, sidecar)) = &sidecar {
1158                if let Some((offset, length)) = sidecar.dictionary_records_section {
1159                    let dictionary_records = parse_sidecar_block_records(
1160                        sidecar_bytes,
1161                        first.crypto_header.block_size as usize,
1162                        SidecarBlockRecordsSection {
1163                            offset,
1164                            length,
1165                            extent: dictionary_extent_from_index_root(&index_root)?,
1166                            data_kind: BlockKind::DictionaryData,
1167                            parity_kind: BlockKind::DictionaryParity,
1168                            structure: "Dictionary",
1169                        },
1170                    )?;
1171                    insert_sidecar_records(&mut blocks, dictionary_records)?;
1172                }
1173            }
1174        }
1175        let block_provider = OpenedBlockProvider {
1176            memory_blocks: &blocks,
1177            lazy_blocks: Some(lazy_source.as_ref()),
1178        };
1179        let payload_dictionary = load_archive_dictionary(
1180            &block_provider,
1181            &first.subkeys,
1182            &first.volume_header,
1183            &first.crypto_header,
1184            &index_root,
1185        )?;
1186
1187        Ok(Self {
1188            options,
1189            observed_archive_bytes,
1190            observed_volume_count,
1191            subkeys: first.subkeys,
1192            blocks,
1193            lazy_blocks: Some(lazy_source),
1194            crypto_header_bytes: first.crypto_header_bytes,
1195            volume_header: authority_volume_header,
1196            crypto_header: first.crypto_header,
1197            manifest_footer,
1198            volume_trailer: Some(authority_volume_trailer),
1199            root_auth_footer: root_auth_authority,
1200            index_root,
1201            payload_dictionary,
1202        })
1203    }
1204
1205    pub fn open_with_bootstrap_sidecar_options(
1206        bytes: &[u8],
1207        bootstrap_sidecar: &[u8],
1208        master_key: &MasterKey,
1209        options: ReaderOptions,
1210    ) -> Result<Self, FormatError> {
1211        Self::open_with_bootstrap_sidecar_options_for_mode(
1212            bytes,
1213            bootstrap_sidecar,
1214            master_key,
1215            options,
1216            BootstrapSidecarUse::SeekableAssist,
1217        )
1218    }
1219
1220    fn open_with_bootstrap_sidecar_options_for_mode(
1221        bytes: &[u8],
1222        bootstrap_sidecar: &[u8],
1223        master_key: &MasterKey,
1224        options: ReaderOptions,
1225        sidecar_use: BootstrapSidecarUse,
1226    ) -> Result<Self, FormatError> {
1227        let observed_archive_bytes =
1228            observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1229        if bytes.len() < VOLUME_HEADER_LEN {
1230            return Err(FormatError::InvalidLength {
1231                structure: "archive",
1232                expected: VOLUME_HEADER_LEN,
1233                actual: bytes.len(),
1234            });
1235        }
1236
1237        let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1238        let crypto_start = volume_header.crypto_header_offset as usize;
1239        let crypto_len = volume_header.crypto_header_length as usize;
1240        let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1241        let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1242        let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1243        let subkeys = subkeys_for_open(
1244            Some(master_key),
1245            parsed_crypto.fixed.aead_algo,
1246            &volume_header.archive_uuid,
1247            &volume_header.session_id,
1248        )?;
1249        verify_integrity_tag(
1250            HmacDomain::CryptoHeader,
1251            parsed_crypto.fixed.aead_algo,
1252            Some(&subkeys.mac_key),
1253            &volume_header.archive_uuid,
1254            &volume_header.session_id,
1255            parsed_crypto.hmac_covered_bytes,
1256            &parsed_crypto.header_hmac,
1257        )?;
1258        parsed_crypto.validate_extension_semantics()?;
1259        reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1260        validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1261        validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1262
1263        let sidecar = parse_bootstrap_sidecar(
1264            bootstrap_sidecar,
1265            &volume_header,
1266            &parsed_crypto.fixed,
1267            &subkeys,
1268        )?;
1269        sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1270
1271        let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1272            bytes,
1273            crypto_end,
1274            parsed_crypto.fixed.block_size as usize,
1275            &volume_header,
1276        )?;
1277        let terminal_material = match sidecar_use {
1278            BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1279                bytes,
1280                terminal_offset,
1281                observed_block_count,
1282                KeyHoldingTerminalContext {
1283                    subkeys: &subkeys,
1284                    volume_header: &volume_header,
1285                    crypto_header: &parsed_crypto.fixed,
1286                    crypto_header_bytes: crypto_bytes,
1287                },
1288                options,
1289            )?),
1290            BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1291                bytes,
1292                terminal_offset,
1293                observed_block_count,
1294                KeyHoldingTerminalContext {
1295                    subkeys: &subkeys,
1296                    volume_header: &volume_header,
1297                    crypto_header: &parsed_crypto.fixed,
1298                    crypto_header_bytes: crypto_bytes,
1299                },
1300                options,
1301            )
1302            .ok(),
1303        };
1304        let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1305        let manifest_authority = match sidecar_use {
1306            BootstrapSidecarUse::SeekableAssist => {
1307                let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1308                    "terminal ManifestFooter/VolumeTrailer is required",
1309                ))?;
1310                if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1311                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1312                        return Err(FormatError::InvalidArchive(
1313                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1314                        ));
1315                    }
1316                }
1317                terminal_manifest.clone()
1318            }
1319            BootstrapSidecarUse::NonSeekableRandomAccess => {
1320                let sidecar_manifest = sidecar
1321                    .manifest_footer
1322                    .as_ref()
1323                    .ok_or(FormatError::ReaderUnsupported(
1324                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1325                ))?;
1326                if let Some(terminal_manifest) = terminal_manifest {
1327                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1328                        return Err(FormatError::InvalidArchive(
1329                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1330                        ));
1331                    }
1332                }
1333                sidecar_manifest.clone()
1334            }
1335        };
1336        manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1337
1338        if let Some((offset, length)) = sidecar.index_root_records_section {
1339            let index_root_records = parse_sidecar_block_records(
1340                bootstrap_sidecar,
1341                parsed_crypto.fixed.block_size as usize,
1342                SidecarBlockRecordsSection {
1343                    offset,
1344                    length,
1345                    extent: index_root_extent_from_manifest(&manifest_authority),
1346                    data_kind: BlockKind::IndexRootData,
1347                    parity_kind: BlockKind::IndexRootParity,
1348                    structure: "IndexRoot",
1349                },
1350            )?;
1351            insert_sidecar_records(&mut blocks, index_root_records)?;
1352        }
1353
1354        let limits = metadata_limits(&parsed_crypto.fixed);
1355        let index_root_plaintext = load_metadata_object_from_parts(
1356            &blocks,
1357            ObjectLoadContext::index_root(
1358                &volume_header,
1359                &parsed_crypto.fixed,
1360                &subkeys,
1361                index_root_extent_from_manifest(&manifest_authority),
1362            ),
1363            manifest_authority.index_root_decompressed_size,
1364        )?;
1365        let index_root = IndexRoot::parse(
1366            &index_root_plaintext,
1367            parsed_crypto.fixed.has_dictionary != 0,
1368            limits,
1369        )?;
1370        if parsed_crypto.fixed.has_dictionary != 0 {
1371            if let Some((offset, length)) = sidecar.dictionary_records_section {
1372                let dictionary_records = parse_sidecar_block_records(
1373                    bootstrap_sidecar,
1374                    parsed_crypto.fixed.block_size as usize,
1375                    SidecarBlockRecordsSection {
1376                        offset,
1377                        length,
1378                        extent: dictionary_extent_from_index_root(&index_root)?,
1379                        data_kind: BlockKind::DictionaryData,
1380                        parity_kind: BlockKind::DictionaryParity,
1381                        structure: "dictionary",
1382                    },
1383                )?;
1384                insert_sidecar_records(&mut blocks, dictionary_records)?;
1385            }
1386        }
1387        let payload_dictionary = load_archive_dictionary(
1388            &blocks,
1389            &subkeys,
1390            &volume_header,
1391            &parsed_crypto.fixed,
1392            &index_root,
1393        )?;
1394
1395        Ok(Self {
1396            options,
1397            observed_archive_bytes,
1398            observed_volume_count: 1,
1399            subkeys,
1400            blocks,
1401            lazy_blocks: None,
1402            crypto_header_bytes: crypto_bytes.to_vec(),
1403            volume_header,
1404            crypto_header: parsed_crypto.fixed,
1405            manifest_footer: manifest_authority,
1406            volume_trailer: terminal_material
1407                .as_ref()
1408                .map(|(_, trailer, _)| trailer.clone()),
1409            root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
1410            index_root,
1411            payload_dictionary,
1412        })
1413    }
1414
1415    /// Return path and payload-size entries from encrypted index metadata only.
1416    ///
1417    /// Unlike [`Self::list_files`], this does not decode tar member groups, so
1418    /// it does not read or decrypt payload envelopes after the index shards are
1419    /// available.
1420    pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
1421        let shards = self.load_all_index_shards()?;
1422        final_index_entry_winners(&shards)?
1423            .into_iter()
1424            .map(|(path, winner)| {
1425                Ok(ArchiveIndexEntry {
1426                    path,
1427                    file_data_size: winner.file_data_size,
1428                })
1429            })
1430            .collect()
1431    }
1432
1433    /// Look up one archive path using encrypted index metadata only.
1434    pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
1435        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1436        self.locate_index_file(&normalized)?
1437            .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
1438            .transpose()
1439    }
1440
1441    pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
1442        let shards = self.load_all_index_shards()?;
1443        final_index_entry_winners(&shards)?
1444            .into_iter()
1445            .map(|(path, winner)| {
1446                let shard = &shards[winner.shard_index];
1447                let member =
1448                    self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
1449                Ok(ArchiveEntry {
1450                    path,
1451                    file_data_size: winner.file_data_size,
1452                    kind: member.kind,
1453                    mode: member.mode,
1454                    mtime: member.mtime,
1455                    diagnostics: member.diagnostics,
1456                })
1457            })
1458            .collect()
1459    }
1460
1461    /// Return only the regular-file payload bytes for `path`.
1462    ///
1463    /// This is a payload-only convenience for callers that do not need tar
1464    /// metadata fidelity diagnostics. Use [`Self::extract_file_with_diagnostics`]
1465    /// or [`Self::extract_member`] when unsupported local PAX/GNU metadata must
1466    /// be reported to users.
1467    pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
1468        self.extract_member(path)?
1469            .map(|member| {
1470                if member.kind != TarEntryKind::Regular {
1471                    return Err(FormatError::ReaderUnsupported(
1472                        "extract_file returns only regular file payloads",
1473                    ));
1474                }
1475                Ok(member.data)
1476            })
1477            .transpose()
1478    }
1479
1480    /// Return regular-file payload bytes together with parsed tar metadata
1481    /// diagnostics for `path`.
1482    pub fn extract_file_with_diagnostics(
1483        &self,
1484        path: &str,
1485    ) -> Result<Option<ExtractedRegularFile>, FormatError> {
1486        self.extract_member(path)?
1487            .map(|member| {
1488                if member.kind != TarEntryKind::Regular {
1489                    return Err(FormatError::ReaderUnsupported(
1490                        "extract_file_with_diagnostics returns only regular file payloads",
1491                    ));
1492                }
1493                Ok((member.data, member.diagnostics))
1494            })
1495            .transpose()
1496    }
1497
1498    /// Stream regular-file payload bytes for `path` into `writer`.
1499    ///
1500    /// This keeps extraction memory bounded by the selected payload envelope,
1501    /// one decompressed frame, and small tar metadata buffers. It returns the
1502    /// same metadata diagnostics as [`Self::extract_file_with_diagnostics`].
1503    pub fn extract_file_to_writer<W: Write>(
1504        &self,
1505        path: &str,
1506        writer: &mut W,
1507    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1508        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1509        self.locate_index_file(&normalized)?
1510            .map(|located| {
1511                self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
1512            })
1513            .transpose()
1514    }
1515
1516    pub fn extract_member(
1517        &self,
1518        path: &str,
1519    ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
1520        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1521        self.locate_index_file(&normalized)?
1522            .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
1523            .transpose()
1524    }
1525
1526    pub fn extract_file_to(
1527        &self,
1528        path: &str,
1529        root: &std::path::Path,
1530        options: SafeExtractionOptions,
1531    ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
1532        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1533        self.locate_index_file(&normalized)?
1534            .map(|located| {
1535                self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
1536            })
1537            .transpose()
1538    }
1539
1540    pub fn extract_indexed_files_to(
1541        &self,
1542        root: &std::path::Path,
1543        options: SafeExtractionOptions,
1544        jobs: usize,
1545    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1546        if jobs == 0 {
1547            return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
1548        }
1549
1550        let shards = self.load_all_index_shards()?;
1551        let entries = final_index_entry_winners(&shards)?.into_iter().collect();
1552        self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
1553    }
1554
1555    pub fn verify(&self) -> Result<(), FormatError> {
1556        self.verify_content().map(|_| ())
1557    }
1558
1559    pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
1560        let tables = self.load_payload_index_tables()?;
1561        let streamed = self.scan_seekable_payload(
1562            &tables,
1563            u64::MAX,
1564            NoopTarStreamObserver,
1565            true,
1566            ParityReadPolicy::Always,
1567        )?;
1568        self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
1569        Ok(ArchiveContentVerification { archive: self })
1570    }
1571
1572    pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
1573        let lazy_source = self
1574            .lazy_blocks
1575            .as_ref()
1576            .ok_or(FormatError::ReaderUnsupported(
1577                "repair output requires seekable archive input",
1578            ))?;
1579        if !lazy_source.is_complete_volume_set() {
1580            return Err(FormatError::ReaderUnsupported(
1581                "repair output requires all archive volumes",
1582            ));
1583        }
1584
1585        let shards = self.load_all_index_shards()?;
1586        let rows = self.root_auth_fec_layout_rows(&shards)?;
1587        let block_provider = self.block_provider();
1588        let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
1589        for row in rows.into_iter().filter(|row| row.present) {
1590            self.collect_repair_patches_for_object(
1591                &block_provider,
1592                lazy_source,
1593                row,
1594                &mut patches,
1595            )?;
1596        }
1597        Ok(patches.into_values().collect())
1598    }
1599
1600    pub fn extract_all_to(
1601        &self,
1602        root: &std::path::Path,
1603        options: SafeExtractionOptions,
1604    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1605        let tables = self.load_payload_index_tables()?;
1606        if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
1607            return Err(FormatError::ReaderUnsupported(
1608                FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
1609            ));
1610        }
1611
1612        let observer = TarStreamFilesystemRestoreObserver::new(root, options);
1613        let streamed = self.scan_seekable_payload(
1614            &tables,
1615            total_extraction_size_cap(self.options, self.observed_archive_bytes),
1616            observer,
1617            false,
1618            ParityReadPolicy::RepairOnly,
1619        )?;
1620        self.validate_streamed_payload_summary(&tables, &streamed, true, false)?;
1621        streamed
1622            .tar
1623            .members
1624            .into_iter()
1625            .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
1626            .collect()
1627    }
1628
1629    fn collect_repair_patches_for_object(
1630        &self,
1631        blocks: &impl BlockProvider,
1632        source: &SeekableBlockSource,
1633        row: FecLayoutObjectRow,
1634        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1635    ) -> Result<(), FormatError> {
1636        let (data_kind, parity_kind, data_max, parity_max) =
1637            self.fec_object_class_shape(row.object_class)?;
1638        let extent = ObjectExtent {
1639            first_block_index: row.first_block_index,
1640            data_block_count: row.data_block_count,
1641            parity_block_count: row.parity_block_count,
1642            encrypted_size: row.encrypted_size,
1643        };
1644        validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
1645
1646        let block_size = self.crypto_header.block_size as usize;
1647        let data_count = extent.data_block_count as usize;
1648        let parity_count = extent.parity_block_count as usize;
1649        let mut data_shards = Vec::with_capacity(data_count);
1650        let mut parity_shards = Vec::with_capacity(parity_count);
1651
1652        for offset in 0..data_count {
1653            let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1654            match blocks.block(block_index)? {
1655                Some(record) => {
1656                    if record.kind != data_kind {
1657                        return Err(FormatError::InvalidArchive(
1658                            "object data block has unexpected kind",
1659                        ));
1660                    }
1661                    let should_be_last = offset + 1 == data_count;
1662                    if record.is_last_data() != should_be_last {
1663                        return Err(FormatError::InvalidArchive(
1664                            "object last-data flag is not on the final data block",
1665                        ));
1666                    }
1667                    data_shards.push(Some(record.payload.clone()));
1668                }
1669                None => data_shards.push(None),
1670            }
1671        }
1672
1673        for offset in 0..parity_count {
1674            let block_index = checked_u64_add(
1675                extent.first_block_index,
1676                data_count as u64 + offset as u64,
1677                "object",
1678            )?;
1679            match blocks.block(block_index)? {
1680                Some(record) => {
1681                    if record.kind != parity_kind {
1682                        return Err(FormatError::InvalidArchive(
1683                            "object parity block has unexpected kind",
1684                        ));
1685                    }
1686                    if record.is_last_data() {
1687                        return Err(FormatError::InvalidArchive(
1688                            "object parity block has last-data flag",
1689                        ));
1690                    }
1691                    parity_shards.push(Some(record.payload.clone()));
1692                }
1693                None => parity_shards.push(None),
1694            }
1695        }
1696
1697        let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
1698        for (offset, payload) in repaired_data.iter().enumerate() {
1699            if data_shards[offset].is_none() {
1700                let block_index =
1701                    checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1702                let flags = if offset + 1 == data_count { 0x01 } else { 0 };
1703                self.insert_repair_patch(
1704                    patches,
1705                    source,
1706                    block_index,
1707                    data_kind,
1708                    flags,
1709                    payload.clone(),
1710                )?;
1711            }
1712        }
1713
1714        if parity_count > 0 {
1715            let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
1716            for (offset, payload) in repaired_parity.into_iter().enumerate() {
1717                if parity_shards[offset].as_ref() != Some(&payload) {
1718                    let block_index = checked_u64_add(
1719                        extent.first_block_index,
1720                        data_count as u64 + offset as u64,
1721                        "object",
1722                    )?;
1723                    self.insert_repair_patch(
1724                        patches,
1725                        source,
1726                        block_index,
1727                        parity_kind,
1728                        0,
1729                        payload,
1730                    )?;
1731                }
1732            }
1733        }
1734
1735        Ok(())
1736    }
1737
1738    fn insert_repair_patch(
1739        &self,
1740        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1741        source: &SeekableBlockSource,
1742        block_index: u64,
1743        kind: BlockKind,
1744        flags: u8,
1745        payload: Vec<u8>,
1746    ) -> Result<(), FormatError> {
1747        let (volume_index, record_offset) = source.record_location(block_index)?;
1748        let record = BlockRecord {
1749            block_index,
1750            kind,
1751            flags,
1752            payload,
1753            record_crc32c: 0,
1754        };
1755        let patch = ArchiveRepairPatch {
1756            volume_index,
1757            block_index,
1758            record_offset,
1759            record_bytes: record.to_bytes(),
1760        };
1761        if let Some(existing) = patches.insert(block_index, patch.clone()) {
1762            if existing != patch {
1763                return Err(FormatError::InvalidArchive(
1764                    "conflicting repair patch for BlockRecord",
1765                ));
1766            }
1767        }
1768        Ok(())
1769    }
1770
1771    fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
1772        if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
1773            && self.index_root.directory_hint_shards.is_empty()
1774        {
1775            return Err(FormatError::InvalidArchive(
1776                "IndexRoot file_count requires directory hints",
1777            ));
1778        }
1779
1780        let shards = self.load_all_index_shards()?;
1781        let mut file_count = 0u64;
1782        let mut frames = BTreeMap::<u64, FrameEntry>::new();
1783        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
1784
1785        for shard in &shards {
1786            file_count = file_count
1787                .checked_add(shard.files.len() as u64)
1788                .ok_or(FormatError::InvalidArchive("file count overflow"))?;
1789            for frame in &shard.frames {
1790                if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
1791                    if existing != *frame {
1792                        return Err(FormatError::InvalidArchive(
1793                            "duplicate FrameEntry rows do not match",
1794                        ));
1795                    }
1796                }
1797            }
1798            for envelope in &shard.envelopes {
1799                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
1800                {
1801                    if existing != *envelope {
1802                        return Err(FormatError::InvalidArchive(
1803                            "duplicate EnvelopeEntry rows do not match",
1804                        ));
1805                    }
1806                }
1807            }
1808        }
1809        validate_global_file_table_order(&shards)?;
1810
1811        if file_count != self.index_root.header.file_count {
1812            return Err(FormatError::InvalidArchive(
1813                "IndexRoot file_count does not match decoded shards",
1814            ));
1815        }
1816        verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
1817        verify_dense_keys(
1818            &envelopes,
1819            self.index_root.header.envelope_count,
1820            "EnvelopeEntry",
1821        )?;
1822        validate_envelope_frame_coverage(&frames, &envelopes)?;
1823        self.validate_encrypted_object_block_ranges(&envelopes)?;
1824
1825        let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
1826            sum.checked_add(envelope.data_block_count as u64)
1827                .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1828        })?;
1829        if payload_block_count != self.index_root.header.payload_block_count {
1830            return Err(FormatError::InvalidArchive(
1831                "IndexRoot payload_block_count does not match envelopes",
1832            ));
1833        }
1834
1835        Ok(PayloadIndexTables {
1836            shards,
1837            file_count,
1838            frames,
1839            envelopes,
1840        })
1841    }
1842
1843    fn scan_seekable_payload<O: TarStreamObserver>(
1844        &self,
1845        tables: &PayloadIndexTables,
1846        extraction_cap: u64,
1847        observer: O,
1848        hash_content: bool,
1849        parity_policy: ParityReadPolicy,
1850    ) -> Result<StreamedPayloadSummary, FormatError> {
1851        let mut tar = TarStreamSummaryValidator::with_observer(
1852            self.crypto_header.max_path_length,
1853            extraction_cap,
1854            usize::MAX,
1855            self.index_root.header.file_count,
1856            observer,
1857        );
1858        let mut content_hasher = hash_content.then(Sha256::new);
1859        let mut streamed_frames = Vec::with_capacity(tables.frames.len());
1860        let streamed_envelopes = tables
1861            .envelopes
1862            .values()
1863            .map(|envelope| StreamedEnvelopeSummary {
1864                envelope_index: envelope.envelope_index,
1865                first_block_index: envelope.first_block_index,
1866                data_block_count: envelope.data_block_count,
1867                parity_block_count: envelope.parity_block_count,
1868                encrypted_size: envelope.encrypted_size,
1869                plaintext_size: envelope.plaintext_size,
1870                first_frame_index: envelope.first_frame_index,
1871                frame_count: envelope.frame_count,
1872            })
1873            .collect::<Vec<_>>();
1874        let mut cached_envelope_index = None;
1875        let mut cached_envelope_plaintext = Vec::new();
1876        let mut decompressor = self.new_payload_decompressor()?;
1877
1878        for frame in tables.frames.values() {
1879            let envelope =
1880                tables
1881                    .envelopes
1882                    .get(&frame.envelope_index)
1883                    .ok_or(FormatError::InvalidArchive(
1884                        "FrameEntry references missing EnvelopeEntry",
1885                    ))?;
1886            if cached_envelope_index != Some(envelope.envelope_index) {
1887                cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
1888                cached_envelope_index = Some(envelope.envelope_index);
1889            }
1890            let compressed = slice(
1891                &cached_envelope_plaintext,
1892                frame.offset_in_envelope as usize,
1893                frame.compressed_size as usize,
1894                "FrameEntry",
1895            )?;
1896            let tar_stream_offset = tar.tar_total_size();
1897            let decoded = self.decompress_payload_frame_with(
1898                &mut decompressor,
1899                compressed,
1900                frame.decompressed_size,
1901            )?;
1902            if decoded.is_empty() {
1903                return Err(FormatError::InvalidArchive(
1904                    "zstd payload frame decompressed to zero bytes",
1905                ));
1906            }
1907            if let Some(hasher) = &mut content_hasher {
1908                hasher.update(&decoded);
1909            }
1910            tar.observe(&decoded)?;
1911            streamed_frames.push(StreamedFrameSummary {
1912                frame_index: frame.frame_index,
1913                envelope_index: frame.envelope_index,
1914                offset_in_envelope: frame.offset_in_envelope,
1915                compressed_size: u32::try_from(compressed.len()).map_err(|_| {
1916                    FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
1917                })?,
1918                decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
1919                    FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
1920                })?,
1921                tar_stream_offset,
1922            });
1923        }
1924
1925        let mut content_sha256 = [0u8; 32];
1926        if let Some(hasher) = content_hasher {
1927            let digest = hasher.finalize();
1928            content_sha256.copy_from_slice(&digest);
1929        }
1930        Ok(StreamedPayloadSummary {
1931            tar: tar.finish()?,
1932            content_sha256,
1933            envelopes: streamed_envelopes,
1934            frames: streamed_frames,
1935        })
1936    }
1937
1938    fn validate_streamed_payload_summary(
1939        &self,
1940        tables: &PayloadIndexTables,
1941        streamed: &StreamedPayloadSummary,
1942        enforce_total_extraction_cap: bool,
1943        enforce_content_sha256: bool,
1944    ) -> Result<(), FormatError> {
1945        if enforce_total_extraction_cap
1946            && streamed.tar.total_extraction_size
1947                > total_extraction_size_cap(self.options, self.observed_archive_bytes)
1948        {
1949            return Err(FormatError::ReaderUnsupported(
1950                "total extraction size exceeds configured cap",
1951            ));
1952        }
1953
1954        let streamed_payload_block_count =
1955            streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
1956                sum.checked_add(envelope.data_block_count as u64)
1957                    .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1958            })?;
1959        if streamed_payload_block_count != self.index_root.header.payload_block_count {
1960            return Err(FormatError::InvalidArchive(
1961                "streamed payload block count does not match IndexRoot",
1962            ));
1963        }
1964
1965        if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
1966            return Err(FormatError::InvalidArchive(
1967                "IndexRoot tar_total_size does not match streamed tar stream",
1968            ));
1969        }
1970        if enforce_content_sha256
1971            && streamed.content_sha256 != self.index_root.header.content_sha256
1972        {
1973            return Err(FormatError::InvalidArchive(
1974                "IndexRoot content_sha256 does not match decoded tar stream",
1975            ));
1976        }
1977
1978        let streamed_envelopes = streamed.envelope_map()?;
1979        for envelope in tables.envelopes.values() {
1980            let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
1981                FormatError::InvalidArchive(
1982                    "metadata references missing streamed payload envelope",
1983                ),
1984            )?;
1985            if actual.first_block_index != envelope.first_block_index
1986                || actual.data_block_count != envelope.data_block_count
1987                || actual.parity_block_count != envelope.parity_block_count
1988                || actual.encrypted_size != envelope.encrypted_size
1989                || actual.plaintext_size != envelope.plaintext_size
1990                || actual.first_frame_index != envelope.first_frame_index
1991                || actual.frame_count != envelope.frame_count
1992            {
1993                return Err(FormatError::InvalidArchive(
1994                    "EnvelopeEntry does not match streamed payload envelope",
1995                ));
1996            }
1997        }
1998
1999        let streamed_frames = streamed.frame_map()?;
2000        for frame in tables.frames.values() {
2001            let actual =
2002                streamed_frames
2003                    .get(&frame.frame_index)
2004                    .ok_or(FormatError::InvalidArchive(
2005                        "metadata references missing streamed payload frame",
2006                    ))?;
2007            if actual.envelope_index != frame.envelope_index
2008                || actual.offset_in_envelope != frame.offset_in_envelope
2009                || actual.compressed_size != frame.compressed_size
2010                || actual.decompressed_size != frame.decompressed_size
2011                || actual.tar_stream_offset != frame.tar_stream_offset
2012                || streamed.frame_flags(actual)? != frame.flags
2013            {
2014                return Err(FormatError::InvalidArchive(
2015                    "FrameEntry does not match streamed payload frame",
2016                ));
2017            }
2018        }
2019
2020        let streamed_members = streamed.member_start_map()?;
2021        if streamed.tar.members.len() as u64 != tables.file_count {
2022            return Err(FormatError::InvalidArchive(
2023                "streamed tar member count does not match decoded shards",
2024            ));
2025        }
2026        let mut file_extents = Vec::new();
2027        let mut directory_hint_map = DirectoryHintMap::new();
2028        for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2029            let shard_row_index = u32::try_from(shard_row_index)
2030                .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2031            for idx in 0..shard.files.len() {
2032                let file = &shard.files[idx];
2033                let start =
2034                    shard
2035                        .tar_member_group_start(idx)
2036                        .ok_or(FormatError::InvalidArchive(
2037                            "FileEntry tar member start is missing",
2038                        ))?;
2039                file_extents.push((start, file.tar_member_group_size));
2040                let path = shard
2041                    .file_path(idx)
2042                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2043                let member = streamed_members
2044                    .get(&start)
2045                    .ok_or(FormatError::InvalidArchive(
2046                        "FileEntry tar member start is missing from streamed tar",
2047                    ))?;
2048                if member.path != path {
2049                    return Err(FormatError::InvalidArchive(
2050                        "tar member path does not match FileEntry path",
2051                    ));
2052                }
2053                if member.logical_size != file.file_data_size {
2054                    return Err(FormatError::InvalidArchive(
2055                        "tar member size does not match FileEntry file_data_size",
2056                    ));
2057                }
2058                if member.group_size != file.tar_member_group_size {
2059                    return Err(FormatError::InvalidArchive(
2060                        "FileEntry does not match streamed tar member",
2061                    ));
2062                }
2063                add_expected_directory_hint_rows(
2064                    &mut directory_hint_map,
2065                    shard_row_index,
2066                    path,
2067                    member.kind,
2068                );
2069            }
2070        }
2071        validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2072        if !self.index_root.directory_hint_shards.is_empty() {
2073            let hint_tables = self.load_all_directory_hint_tables()?;
2074            validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2075        }
2076
2077        Ok(())
2078    }
2079
2080    pub(crate) fn from_streamed_parts(
2081        parts: StreamedArchiveOpenParts,
2082    ) -> Result<Self, FormatError> {
2083        let limits = metadata_limits(&parts.crypto_header);
2084        let index_root_plaintext = load_metadata_object_from_parts(
2085            &parts.blocks,
2086            ObjectLoadContext::index_root(
2087                &parts.volume_header,
2088                &parts.crypto_header,
2089                &parts.subkeys,
2090                ObjectExtent {
2091                    first_block_index: parts.manifest_footer.index_root_first_block,
2092                    data_block_count: parts.manifest_footer.index_root_data_block_count,
2093                    parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2094                    encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2095                },
2096            ),
2097            parts.manifest_footer.index_root_decompressed_size,
2098        )?;
2099        let index_root = IndexRoot::parse(
2100            &index_root_plaintext,
2101            parts.crypto_header.has_dictionary != 0,
2102            limits,
2103        )?;
2104        let payload_dictionary = load_archive_dictionary(
2105            &parts.blocks,
2106            &parts.subkeys,
2107            &parts.volume_header,
2108            &parts.crypto_header,
2109            &index_root,
2110        )?;
2111
2112        Ok(Self {
2113            options: parts.options,
2114            observed_archive_bytes: parts.observed_archive_bytes,
2115            observed_volume_count: 1,
2116            subkeys: parts.subkeys,
2117            blocks: parts.blocks,
2118            lazy_blocks: None,
2119            crypto_header_bytes: parts.crypto_header_bytes,
2120            volume_header: parts.volume_header,
2121            crypto_header: parts.crypto_header,
2122            manifest_footer: parts.manifest_footer,
2123            volume_trailer: Some(parts.volume_trailer),
2124            root_auth_footer: parts.root_auth_footer,
2125            index_root,
2126            payload_dictionary,
2127        })
2128    }
2129
2130    pub(crate) fn verify_streamed_payload_summary(
2131        &self,
2132        streamed: &StreamedPayloadSummary,
2133    ) -> Result<(), FormatError> {
2134        let tables = self.load_payload_index_tables()?;
2135        self.validate_streamed_payload_summary(&tables, streamed, true, true)
2136    }
2137
2138    pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2139    where
2140        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2141    {
2142        let content_verification = self.verify_content()?;
2143        self.verify_root_auth_with_verified_content(&content_verification, verifier)
2144    }
2145
2146    pub fn verify_root_auth_with_verified_content<F>(
2147        &self,
2148        content_verification: &ArchiveContentVerification<'_>,
2149        mut verifier: F,
2150    ) -> Result<RootAuthVerification, FormatError>
2151    where
2152        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2153    {
2154        if !std::ptr::eq(content_verification.archive, self) {
2155            return Err(FormatError::InvalidArchive(
2156                "content verification does not match archive",
2157            ));
2158        }
2159        let footer = self
2160            .root_auth_footer
2161            .as_ref()
2162            .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2163        let material = self.recompute_root_auth_material(footer)?;
2164        if material.critical_metadata_digest != footer.critical_metadata_digest
2165            || material.index_digest != footer.index_digest
2166            || material.fec_layout_digest != footer.fec_layout_digest
2167            || material.data_block_merkle_root != footer.data_block_merkle_root
2168            || material.signer_identity_digest != footer.signer_identity_digest
2169            || material.archive_root != footer.archive_root
2170            || material.total_data_block_count != footer.total_data_block_count
2171        {
2172            return Err(FormatError::InvalidArchive(
2173                "RootAuthFooter commitments do not match recomputed archive root",
2174            ));
2175        }
2176        if !verifier(footer, &material.archive_root)? {
2177            return Err(FormatError::InvalidArchive(
2178                "root-auth authenticator verification failed",
2179            ));
2180        }
2181        Ok(RootAuthVerification {
2182            archive_root: material.archive_root,
2183            authenticator_id: footer.authenticator_id,
2184            signer_identity_type: footer.signer_identity_type,
2185            signer_identity_bytes: footer.signer_identity_bytes.clone(),
2186            total_data_block_count: footer.total_data_block_count,
2187            diagnostics: self.root_auth_success_diagnostics(),
2188        })
2189    }
2190
2191    fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2192        parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2193            self.load_index_shard(entry)
2194        })
2195    }
2196
2197    fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2198        let block_provider = self.block_provider();
2199        let plaintext = load_metadata_object_from_parts(
2200            &block_provider,
2201            ObjectLoadContext::index_shard(
2202                &self.volume_header,
2203                &self.crypto_header,
2204                &self.subkeys,
2205                entry,
2206            ),
2207            entry.decompressed_size,
2208        )?;
2209        IndexShard::parse(&plaintext, entry, self.metadata_limits())
2210    }
2211
2212    fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2213        parallel_map_ref(
2214            &self.index_root.directory_hint_shards,
2215            self.options.jobs,
2216            |entry| self.load_directory_hint_table(entry),
2217        )
2218    }
2219
2220    fn load_directory_hint_table(
2221        &self,
2222        entry: &DirectoryHintShardEntry,
2223    ) -> Result<DirectoryHintTable, FormatError> {
2224        let block_provider = self.block_provider();
2225        let plaintext = load_metadata_object_from_parts(
2226            &block_provider,
2227            ObjectLoadContext::directory_hint(
2228                &self.volume_header,
2229                &self.crypto_header,
2230                &self.subkeys,
2231                entry,
2232            ),
2233            entry.decompressed_size,
2234        )?;
2235        DirectoryHintTable::parse(
2236            &plaintext,
2237            entry,
2238            self.index_root.header.shard_count,
2239            self.metadata_limits(),
2240        )
2241    }
2242
2243    fn load_payload_envelope(
2244        &self,
2245        envelope: &EnvelopeEntry,
2246        parity_policy: ParityReadPolicy,
2247    ) -> Result<Vec<u8>, FormatError> {
2248        let block_provider = self.block_provider();
2249        let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2250            &block_provider,
2251            ObjectLoadContext::payload(
2252                &self.volume_header,
2253                &self.crypto_header,
2254                &self.subkeys,
2255                envelope,
2256            ),
2257            parity_policy,
2258        )?;
2259        if plaintext.len() != envelope.plaintext_size as usize {
2260            return Err(FormatError::InvalidArchive(
2261                "payload envelope plaintext_size mismatch",
2262            ));
2263        }
2264        Ok(plaintext)
2265    }
2266
2267    fn locate_index_file(
2268        &self,
2269        normalized: &[u8],
2270    ) -> Result<Option<LocatedIndexFile>, FormatError> {
2271        let candidate_indexes = self
2272            .index_root
2273            .candidate_shards_for_path(normalized, self.metadata_limits())?;
2274        let mut winner: Option<LocatedIndexFile> = None;
2275
2276        for row_index in candidate_indexes {
2277            let locating =
2278                self.index_root
2279                    .shards
2280                    .get(row_index)
2281                    .ok_or(FormatError::InvalidArchive(
2282                        "candidate shard row is out of bounds",
2283                    ))?;
2284            let shard = self.load_index_shard(locating)?;
2285            if let Some(file_index) = shard.lookup_file_index(normalized) {
2286                let start =
2287                    shard
2288                        .tar_member_group_start(file_index)
2289                        .ok_or(FormatError::InvalidArchive(
2290                            "FileEntry tar member start is missing",
2291                        ))?;
2292                if winner
2293                    .as_ref()
2294                    .map(|existing| start > existing.start)
2295                    .unwrap_or(true)
2296                {
2297                    winner = Some(LocatedIndexFile {
2298                        shard,
2299                        file_index,
2300                        start,
2301                    });
2302                }
2303            }
2304        }
2305
2306        Ok(winner)
2307    }
2308
2309    fn extract_loaded_member(
2310        &self,
2311        shard: &IndexShard,
2312        file_index: usize,
2313    ) -> Result<ExtractedArchiveMember, FormatError> {
2314        let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
2315        Ok(ExtractedArchiveMember {
2316            path: utf8_path(&member.path)?,
2317            kind: member.kind,
2318            data: member.data,
2319            link_target: member
2320                .link_target
2321                .map(|target| utf8_path(&target))
2322                .transpose()?,
2323            diagnostics: member.diagnostics,
2324        })
2325    }
2326
2327    fn extract_loaded_owned_tar_member(
2328        &self,
2329        shard: &IndexShard,
2330        file_index: usize,
2331    ) -> Result<OwnedTarMember, FormatError> {
2332        self.decode_loaded_owned_tar_member(shard, file_index, true)
2333    }
2334
2335    fn stream_loaded_file_to_writer<W: Write>(
2336        &self,
2337        shard: &IndexShard,
2338        file_index: usize,
2339        writer: &mut W,
2340    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
2341        let file = shard
2342            .files
2343            .get(file_index)
2344            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2345        self.validate_total_extraction_size(file.file_data_size)?;
2346        let expected_path = shard
2347            .file_path(file_index)
2348            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2349        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2350        stream_regular_tar_member_group_to_writer(
2351            &mut reader,
2352            expected_path,
2353            file.file_data_size,
2354            file.tar_member_group_size,
2355            self.crypto_header.max_path_length,
2356            writer,
2357        )
2358    }
2359
2360    fn stream_loaded_file_to_path(
2361        &self,
2362        shard: &IndexShard,
2363        file_index: usize,
2364        root: &std::path::Path,
2365        options: SafeExtractionOptions,
2366    ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
2367        let file = shard
2368            .files
2369            .get(file_index)
2370            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2371        self.validate_total_extraction_size(file.file_data_size)?;
2372        let expected_path = shard
2373            .file_path(file_index)
2374            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2375        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2376        restore_streaming_tar_member_group(
2377            root,
2378            expected_path,
2379            file.file_data_size,
2380            file.tar_member_group_size,
2381            self.crypto_header.max_path_length,
2382            options,
2383            &mut reader,
2384        )
2385        .map_err(format_error_from_extract_error)
2386    }
2387
2388    fn extract_winning_index_entries_to(
2389        &self,
2390        shards: &[IndexShard],
2391        entries: Vec<(String, WinningIndexEntry)>,
2392        root: &std::path::Path,
2393        options: SafeExtractionOptions,
2394        jobs: usize,
2395    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2396        if entries.is_empty() {
2397            return Ok(Vec::new());
2398        }
2399        if jobs <= 1 || entries.len() <= 1 {
2400            return entries
2401                .into_iter()
2402                .map(|(path, entry)| {
2403                    let shard =
2404                        shards
2405                            .get(entry.shard_index)
2406                            .ok_or(FormatError::InvalidArchive(
2407                                "winning FileEntry shard is out of bounds",
2408                            ))?;
2409                    let diagnostics =
2410                        self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
2411                    Ok((path, diagnostics))
2412                })
2413                .collect();
2414        }
2415
2416        let worker_count = jobs.min(entries.len());
2417        let chunk_size = entries.len().div_ceil(worker_count);
2418        std::thread::scope(|scope| {
2419            let handles = entries
2420                .chunks(chunk_size)
2421                .map(|chunk| {
2422                    scope.spawn(move || {
2423                        let mut out = Vec::with_capacity(chunk.len());
2424                        for (path, entry) in chunk {
2425                            let shard = shards.get(entry.shard_index).ok_or(
2426                                FormatError::InvalidArchive(
2427                                    "winning FileEntry shard is out of bounds",
2428                                ),
2429                            )?;
2430                            let diagnostics = self.stream_loaded_file_to_path(
2431                                shard,
2432                                entry.file_index,
2433                                root,
2434                                options,
2435                            )?;
2436                            out.push((path.clone(), diagnostics));
2437                        }
2438                        Ok(out)
2439                    })
2440                })
2441                .collect::<Vec<_>>();
2442            let mut out = Vec::new();
2443            for handle in handles {
2444                let mut chunk = handle
2445                    .join()
2446                    .map_err(|_| FormatError::ReaderUnsupported("extract worker panicked"))??;
2447                out.append(&mut chunk);
2448            }
2449            Ok(out)
2450        })
2451    }
2452
2453    fn decode_loaded_owned_tar_member(
2454        &self,
2455        shard: &IndexShard,
2456        file_index: usize,
2457        enforce_extraction_cap: bool,
2458    ) -> Result<OwnedTarMember, FormatError> {
2459        let file = shard
2460            .files
2461            .get(file_index)
2462            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2463        if enforce_extraction_cap {
2464            self.validate_total_extraction_size(file.file_data_size)?;
2465        }
2466        let expected_path = shard
2467            .file_path(file_index)
2468            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2469        let frames = frame_range_for_file(shard, file)?;
2470        let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
2471        let mut decoded = Vec::new();
2472
2473        for frame in frames {
2474            let envelope = shard
2475                .envelopes
2476                .iter()
2477                .find(|entry| entry.envelope_index == frame.envelope_index)
2478                .ok_or(FormatError::InvalidArchive(
2479                    "FrameEntry references missing EnvelopeEntry",
2480                ))?;
2481            if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
2482                entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
2483            }
2484            let envelope_plaintext = envelope_cache
2485                .get(&envelope.envelope_index)
2486                .expect("inserted above");
2487            let compressed = slice(
2488                envelope_plaintext,
2489                frame.offset_in_envelope as usize,
2490                frame.compressed_size as usize,
2491                "FrameEntry",
2492            )?;
2493            decoded.extend_from_slice(
2494                &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
2495            );
2496        }
2497
2498        let offset = file.offset_in_first_frame_plaintext as usize;
2499        let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
2500        let group = slice(&decoded, offset, group_len, "FileEntry")?;
2501        let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
2502        if member.path != expected_path {
2503            return Err(FormatError::InvalidArchive(
2504                "tar member path does not match FileEntry path",
2505            ));
2506        }
2507        if member.logical_size != file.file_data_size {
2508            return Err(FormatError::InvalidArchive(
2509                "tar member size does not match FileEntry file_data_size",
2510            ));
2511        }
2512        Ok(member.to_owned_member())
2513    }
2514
2515    fn metadata_limits(&self) -> MetadataLimits {
2516        metadata_limits(&self.crypto_header)
2517    }
2518
2519    fn recompute_root_auth_material(
2520        &self,
2521        footer: &RootAuthFooterV1,
2522    ) -> Result<RootAuthMaterial, FormatError> {
2523        let footer_length = footer.footer_length()?;
2524        let root_auth_descriptor_digest = root_auth_descriptor_digest(
2525            footer.authenticator_id,
2526            footer.signer_identity_type,
2527            &footer.signer_identity_bytes,
2528            u32::try_from(footer.authenticator_value.len()).map_err(|_| {
2529                FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
2530            })?,
2531            footer_length,
2532        )?;
2533        let signer_identity_digest =
2534            signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
2535        let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
2536        let crypto_pre_hmac_len = self
2537            .crypto_header_bytes
2538            .len()
2539            .checked_sub(CRYPTO_HEADER_HMAC_LEN)
2540            .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
2541        let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
2542            archive_uuid: self.volume_header.archive_uuid,
2543            session_id: self.volume_header.session_id,
2544            stripe_width: self.crypto_header.stripe_width,
2545            total_volumes: self.manifest_footer.total_volumes,
2546            compression_algo: self.crypto_header.compression_algo,
2547            aead_algo: self.crypto_header.aead_algo,
2548            fec_algo: self.crypto_header.fec_algo,
2549            kdf_algo: self.crypto_header.kdf_algo,
2550            crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
2551            chunk_size: self.crypto_header.chunk_size,
2552            envelope_target_size: self.crypto_header.envelope_target_size,
2553            block_size: self.crypto_header.block_size,
2554            fec_data_shards: self.crypto_header.fec_data_shards,
2555            fec_parity_shards: self.crypto_header.fec_parity_shards,
2556            index_fec_data_shards: self.crypto_header.index_fec_data_shards,
2557            index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
2558            index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
2559            index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
2560            volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
2561            bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
2562            has_dictionary: self.crypto_header.has_dictionary,
2563            manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
2564            index_root_first_block: self.manifest_footer.index_root_first_block,
2565            index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
2566            index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
2567            index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
2568            index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
2569            root_auth_descriptor_digest,
2570        })?;
2571        let index_root_plaintext = self.index_root.to_bytes();
2572        let index_digest = index_digest(&index_root_plaintext);
2573        let shards = self.load_all_index_shards()?;
2574        let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
2575        let fec_layout_digest = fec_layout_digest(&fec_layout_rows)?;
2576        let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
2577        let total_data_block_count = u64::try_from(data_leaves.len())
2578            .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
2579        let data_block_merkle_root = data_block_merkle_root(&data_leaves);
2580        let archive_root = archive_root(ArchiveRootInputs {
2581            archive_uuid: self.volume_header.archive_uuid,
2582            session_id: self.volume_header.session_id,
2583            format_version: FORMAT_VERSION,
2584            volume_format_rev: VOLUME_FORMAT_REV,
2585            compression_algo: self.crypto_header.compression_algo,
2586            aead_algo: self.crypto_header.aead_algo,
2587            fec_algo: self.crypto_header.fec_algo,
2588            kdf_algo: self.crypto_header.kdf_algo,
2589            critical_metadata_digest,
2590            index_digest,
2591            fec_layout_digest,
2592            total_data_block_count,
2593            data_block_merkle_root,
2594            root_auth_descriptor_digest,
2595            signer_identity_digest,
2596        });
2597        Ok(RootAuthMaterial {
2598            critical_metadata_digest,
2599            index_digest,
2600            fec_layout_digest,
2601            data_block_merkle_root,
2602            signer_identity_digest,
2603            archive_root,
2604            total_data_block_count,
2605        })
2606    }
2607
2608    fn root_auth_fec_layout_rows(
2609        &self,
2610        shards: &[IndexShard],
2611    ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
2612        let mut rows = Vec::new();
2613        rows.push(FecLayoutObjectRow {
2614            object_class: 1,
2615            present: true,
2616            object_id: 0,
2617            first_block_index: self.manifest_footer.index_root_first_block,
2618            data_block_count: self.manifest_footer.index_root_data_block_count,
2619            parity_block_count: self.manifest_footer.index_root_parity_block_count,
2620            encrypted_size: self.manifest_footer.index_root_encrypted_size,
2621            plain_size: self.manifest_footer.index_root_decompressed_size,
2622        });
2623        if self.crypto_header.has_dictionary != 0 {
2624            rows.push(FecLayoutObjectRow {
2625                object_class: 2,
2626                present: true,
2627                object_id: 0,
2628                first_block_index: self.index_root.header.dictionary_first_block,
2629                data_block_count: self.index_root.header.dictionary_data_block_count,
2630                parity_block_count: self.index_root.header.dictionary_parity_block_count,
2631                encrypted_size: self.index_root.header.dictionary_encrypted_size,
2632                plain_size: self.index_root.header.dictionary_decompressed_size,
2633            });
2634        } else {
2635            rows.push(FecLayoutObjectRow {
2636                object_class: 2,
2637                present: false,
2638                object_id: 0,
2639                first_block_index: 0,
2640                data_block_count: 0,
2641                parity_block_count: 0,
2642                encrypted_size: 0,
2643                plain_size: 0,
2644            });
2645        }
2646        for entry in &self.index_root.shards {
2647            rows.push(FecLayoutObjectRow {
2648                object_class: 3,
2649                present: true,
2650                object_id: entry.shard_index,
2651                first_block_index: entry.first_block_index,
2652                data_block_count: entry.data_block_count,
2653                parity_block_count: entry.parity_block_count,
2654                encrypted_size: entry.encrypted_size,
2655                plain_size: entry.decompressed_size,
2656            });
2657        }
2658        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2659        for shard in shards {
2660            for envelope in &shard.envelopes {
2661                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2662                {
2663                    if existing != *envelope {
2664                        return Err(FormatError::InvalidArchive(
2665                            "duplicate EnvelopeEntry rows do not match",
2666                        ));
2667                    }
2668                }
2669            }
2670        }
2671        for envelope in envelopes.values() {
2672            rows.push(FecLayoutObjectRow {
2673                object_class: 4,
2674                present: true,
2675                object_id: envelope.envelope_index,
2676                first_block_index: envelope.first_block_index,
2677                data_block_count: envelope.data_block_count,
2678                parity_block_count: envelope.parity_block_count,
2679                encrypted_size: envelope.encrypted_size,
2680                plain_size: envelope.plaintext_size,
2681            });
2682        }
2683        for entry in &self.index_root.directory_hint_shards {
2684            rows.push(FecLayoutObjectRow {
2685                object_class: 5,
2686                present: true,
2687                object_id: entry.hint_shard_index,
2688                first_block_index: entry.first_block_index,
2689                data_block_count: entry.data_block_count,
2690                parity_block_count: entry.parity_block_count,
2691                encrypted_size: entry.encrypted_size,
2692                plain_size: entry.decompressed_size,
2693            });
2694        }
2695        Ok(rows)
2696    }
2697
2698    fn fec_object_class_shape(
2699        &self,
2700        object_class: u8,
2701    ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
2702        match object_class {
2703            1 => Ok((
2704                BlockKind::IndexRootData,
2705                BlockKind::IndexRootParity,
2706                self.crypto_header.index_root_fec_data_shards,
2707                self.crypto_header.index_root_fec_parity_shards,
2708            )),
2709            2 => Ok((
2710                BlockKind::DictionaryData,
2711                BlockKind::DictionaryParity,
2712                self.crypto_header.index_root_fec_data_shards,
2713                self.crypto_header.index_root_fec_parity_shards,
2714            )),
2715            3 => Ok((
2716                BlockKind::IndexShardData,
2717                BlockKind::IndexShardParity,
2718                self.crypto_header.index_fec_data_shards,
2719                self.crypto_header.index_fec_parity_shards,
2720            )),
2721            4 => Ok((
2722                BlockKind::PayloadData,
2723                BlockKind::PayloadParity,
2724                self.crypto_header.fec_data_shards,
2725                self.crypto_header.fec_parity_shards,
2726            )),
2727            5 => Ok((
2728                BlockKind::DirectoryHintData,
2729                BlockKind::DirectoryHintParity,
2730                self.crypto_header.index_fec_data_shards,
2731                self.crypto_header.index_fec_parity_shards,
2732            )),
2733            _ => Err(FormatError::InvalidArchive(
2734                "unknown root-auth FEC row class",
2735            )),
2736        }
2737    }
2738
2739    fn root_auth_data_block_leaves(
2740        &self,
2741        rows: &[FecLayoutObjectRow],
2742    ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
2743        let block_provider = self.block_provider();
2744        let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
2745        let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
2746            let row = **row;
2747            let (data_kind, parity_kind, data_max, parity_max) =
2748                self.fec_object_class_shape(row.object_class)?;
2749            let extent = ObjectExtent {
2750                first_block_index: row.first_block_index,
2751                data_block_count: row.data_block_count,
2752                parity_block_count: row.parity_block_count,
2753                encrypted_size: row.encrypted_size,
2754            };
2755            let repaired = load_repaired_object_data_shards_from_parts(
2756                &block_provider,
2757                &self.crypto_header,
2758                extent,
2759                data_kind,
2760                parity_kind,
2761                data_max,
2762                parity_max,
2763            )?;
2764            let mut leaves = Vec::new();
2765            for (offset, payload) in repaired.into_iter().enumerate() {
2766                leaves.push(DataBlockMerkleLeaf {
2767                    block_index: checked_u64_add(
2768                        row.first_block_index,
2769                        offset as u64,
2770                        "root-auth data block",
2771                    )?,
2772                    kind: data_kind,
2773                    flags: if offset + 1 == row.data_block_count as usize {
2774                        0x01
2775                    } else {
2776                        0
2777                    },
2778                    payload,
2779                });
2780            }
2781            Ok(leaves)
2782        })?;
2783        let mut leaves = Vec::new();
2784        for mut chunk in chunks {
2785            leaves.append(&mut chunk);
2786        }
2787        leaves.sort_by_key(|leaf| leaf.block_index);
2788        Ok(leaves)
2789    }
2790
2791    fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
2792        let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
2793        if logical_size > cap {
2794            return Err(FormatError::ReaderUnsupported(
2795                "total extraction size exceeds configured cap",
2796            ));
2797        }
2798        Ok(())
2799    }
2800
2801    fn decompress_payload_frame(
2802        &self,
2803        compressed: &[u8],
2804        decompressed_size: u32,
2805    ) -> Result<Vec<u8>, FormatError> {
2806        let mut decompressor = self.new_payload_decompressor()?;
2807        self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
2808    }
2809
2810    fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
2811        match &self.payload_dictionary {
2812            Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
2813            None => zstd::bulk::Decompressor::new(),
2814        }
2815        .map_err(|_| FormatError::ZstdDecompressionFailure)
2816    }
2817
2818    fn decompress_payload_frame_with(
2819        &self,
2820        decompressor: &mut zstd::bulk::Decompressor<'static>,
2821        compressed: &[u8],
2822        decompressed_size: u32,
2823    ) -> Result<Vec<u8>, FormatError> {
2824        validate_exact_zstd_frame(compressed)?;
2825        let expected = decompressed_size as usize;
2826        let decoded = decompressor
2827            .decompress(compressed, expected)
2828            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
2829        if decoded.len() != expected {
2830            return Err(FormatError::ZstdDecompressedSizeMismatch {
2831                expected,
2832                actual: decoded.len(),
2833            });
2834        }
2835        Ok(decoded)
2836    }
2837
2838    fn validate_encrypted_object_block_ranges(
2839        &self,
2840        envelopes: &BTreeMap<u64, EnvelopeEntry>,
2841    ) -> Result<(), FormatError> {
2842        let mut ranges = Vec::new();
2843        ranges.push(object_block_range(
2844            self.manifest_footer.index_root_first_block,
2845            self.manifest_footer.index_root_data_block_count,
2846            self.manifest_footer.index_root_parity_block_count,
2847            "IndexRoot",
2848        )?);
2849        for shard in &self.index_root.shards {
2850            ranges.push(object_block_range(
2851                shard.first_block_index,
2852                shard.data_block_count,
2853                shard.parity_block_count,
2854                "IndexShard",
2855            )?);
2856        }
2857        for hint in &self.index_root.directory_hint_shards {
2858            ranges.push(object_block_range(
2859                hint.first_block_index,
2860                hint.data_block_count,
2861                hint.parity_block_count,
2862                "DirectoryHintShardEntry",
2863            )?);
2864        }
2865        if self.crypto_header.has_dictionary != 0 {
2866            ranges.push(object_block_range(
2867                self.index_root.header.dictionary_first_block,
2868                self.index_root.header.dictionary_data_block_count,
2869                self.index_root.header.dictionary_parity_block_count,
2870                "dictionary",
2871            )?);
2872        }
2873        for envelope in envelopes.values() {
2874            ranges.push(object_block_range(
2875                envelope.first_block_index,
2876                envelope.data_block_count,
2877                envelope.parity_block_count,
2878                "EnvelopeEntry",
2879            )?);
2880        }
2881        validate_non_overlapping_object_ranges(&mut ranges)?;
2882        if let Some(source) = &self.lazy_blocks {
2883            if source.is_complete_volume_set() {
2884                validate_exact_coverage_ranges_u64(
2885                    &mut ranges,
2886                    source.total_block_count()?,
2887                    "encrypted object block ranges do not cover complete archive exactly",
2888                )?;
2889            }
2890        }
2891        Ok(())
2892    }
2893}
2894
2895impl<'a> DecodedTarMemberGroupReader<'a> {
2896    fn new(
2897        archive: &'a OpenedArchive,
2898        shard: &'a IndexShard,
2899        file: &'a FileEntry,
2900    ) -> Result<Self, FormatError> {
2901        Ok(Self {
2902            archive,
2903            shard,
2904            file,
2905            decompressor: archive.new_payload_decompressor()?,
2906            next_frame_offset: 0,
2907            cached_envelope_index: None,
2908            cached_envelope_plaintext: Vec::new(),
2909            current_frame: Vec::new(),
2910            current_frame_offset: 0,
2911            remaining_group_bytes: file.tar_member_group_size,
2912        })
2913    }
2914
2915    fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
2916        while self.current_frame_offset >= self.current_frame.len() {
2917            if self.next_frame_offset >= self.file.frame_count as u64 {
2918                return Err(
2919                    FormatError::InvalidArchive("tar member group exceeds frame range").into(),
2920                );
2921            }
2922            let frame_index = self
2923                .file
2924                .first_frame_index
2925                .checked_add(self.next_frame_offset)
2926                .ok_or(FormatError::InvalidArchive(
2927                    "FileEntry frame range overflow",
2928                ))?;
2929            let frame = frame_by_index(self.shard, frame_index)?;
2930            let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
2931            if self.cached_envelope_index != Some(envelope.envelope_index) {
2932                self.cached_envelope_plaintext = self
2933                    .archive
2934                    .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
2935                self.cached_envelope_index = Some(envelope.envelope_index);
2936            }
2937            let compressed = slice(
2938                &self.cached_envelope_plaintext,
2939                frame.offset_in_envelope as usize,
2940                frame.compressed_size as usize,
2941                "FrameEntry",
2942            )?;
2943            let decoded = self.archive.decompress_payload_frame_with(
2944                &mut self.decompressor,
2945                compressed,
2946                frame.decompressed_size,
2947            )?;
2948            let offset = if self.next_frame_offset == 0 {
2949                self.file.offset_in_first_frame_plaintext as usize
2950            } else {
2951                0
2952            };
2953            if offset > decoded.len() {
2954                return Err(FormatError::InvalidArchive(
2955                    "offset in first frame is outside the first referenced frame",
2956                )
2957                .into());
2958            }
2959            self.next_frame_offset += 1;
2960            self.current_frame = decoded;
2961            self.current_frame_offset = offset;
2962        }
2963        Ok(())
2964    }
2965}
2966
2967impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
2968    fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
2969        if buf.is_empty() {
2970            return Ok(0);
2971        }
2972        if self.remaining_group_bytes == 0 {
2973            return Ok(0);
2974        }
2975        self.ensure_frame_available()?;
2976        let available = self.current_frame.len() - self.current_frame_offset;
2977        let len = available
2978            .min(buf.len())
2979            .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
2980        if len == 0 {
2981            return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
2982        }
2983        buf[..len].copy_from_slice(
2984            &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
2985        );
2986        self.current_frame_offset += len;
2987        self.remaining_group_bytes -= len as u64;
2988        Ok(len)
2989    }
2990}
2991
2992fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
2993    shard
2994        .frames
2995        .binary_search_by_key(&frame_index, |entry| entry.frame_index)
2996        .map(|idx| &shard.frames[idx])
2997        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
2998}
2999
3000fn envelope_by_index(
3001    shard: &IndexShard,
3002    envelope_index: u64,
3003) -> Result<&EnvelopeEntry, FormatError> {
3004    shard
3005        .envelopes
3006        .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3007        .map(|idx| &shard.envelopes[idx])
3008        .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3009}
3010
3011fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3012    match err {
3013        ExtractError::Format(err) => err,
3014        ExtractError::Output(_) => {
3015            FormatError::FilesystemExtractionFailed("failed to write regular file")
3016        }
3017    }
3018}
3019
3020fn final_index_entry_winners(
3021    shards: &[IndexShard],
3022) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3023    let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3024    for (shard_index, shard) in shards.iter().enumerate() {
3025        for (idx, file) in shard.files.iter().enumerate() {
3026            let path = utf8_path(
3027                shard
3028                    .file_path(idx)
3029                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3030            )?;
3031            let start = shard
3032                .tar_member_group_start(idx)
3033                .ok_or(FormatError::InvalidArchive(
3034                    "FileEntry tar member start is missing",
3035                ))?;
3036            if let Some(winner) = final_entries.get_mut(&path) {
3037                if start >= winner.start {
3038                    winner.start = start;
3039                    winner.file_data_size = file.file_data_size;
3040                    winner.shard_index = shard_index;
3041                    winner.file_index = idx;
3042                }
3043            } else {
3044                final_entries.insert(
3045                    path,
3046                    WinningIndexEntry {
3047                        start,
3048                        file_data_size: file.file_data_size,
3049                        shard_index,
3050                        file_index: idx,
3051                    },
3052                );
3053            }
3054        }
3055    }
3056    Ok(final_entries)
3057}
3058
3059fn archive_index_entry_from_loaded_file(
3060    shard: &IndexShard,
3061    file_index: usize,
3062) -> Result<ArchiveIndexEntry, FormatError> {
3063    let file = shard
3064        .files
3065        .get(file_index)
3066        .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3067    let path = utf8_path(
3068        shard
3069            .file_path(file_index)
3070            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3071    )?;
3072    Ok(ArchiveIndexEntry {
3073        path,
3074        file_data_size: file.file_data_size,
3075    })
3076}
3077
3078#[derive(Debug)]
3079struct ParsedSeekableVolume {
3080    volume_header: VolumeHeader,
3081    crypto_header: CryptoHeaderFixed,
3082    crypto_header_bytes: Vec<u8>,
3083    subkeys: Subkeys,
3084    manifest_footer: Option<ManifestFooter>,
3085    manifest_footer_error: Option<FormatError>,
3086    root_auth_footer: Option<RootAuthFooterV1>,
3087    root_auth_footer_bytes: Option<Vec<u8>>,
3088    volume_trailer: VolumeTrailer,
3089    blocks: BTreeMap<u64, BlockRecord>,
3090    erased_block_indices: BTreeSet<u64>,
3091}
3092
3093struct ParsedSeekableReadAtVolume {
3094    reader: Arc<dyn ArchiveReadAt>,
3095    volume_header: VolumeHeader,
3096    crypto_header: CryptoHeaderFixed,
3097    crypto_header_bytes: Vec<u8>,
3098    subkeys: Subkeys,
3099    manifest_footer: Option<ManifestFooter>,
3100    manifest_footer_error: Option<FormatError>,
3101    root_auth_footer: Option<RootAuthFooterV1>,
3102    root_auth_footer_bytes: Option<Vec<u8>>,
3103    volume_trailer: VolumeTrailer,
3104    crypto_end: u64,
3105}
3106
3107struct ParsedOpenPrefix {
3108    volume_header: VolumeHeader,
3109    crypto_header: CryptoHeaderFixed,
3110    crypto_header_bytes: Vec<u8>,
3111    crypto_end: usize,
3112    subkeys: Subkeys,
3113}
3114
3115struct ParsedReadAtOpenPrefix {
3116    volume_header: VolumeHeader,
3117    crypto_header: CryptoHeaderFixed,
3118    crypto_header_bytes: Vec<u8>,
3119    crypto_end: u64,
3120    subkeys: Subkeys,
3121}
3122
3123fn parse_seekable_volume(
3124    bytes: &[u8],
3125    master_key: &MasterKey,
3126    options: ReaderOptions,
3127) -> Result<ParsedSeekableVolume, FormatError> {
3128    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3129        return Err(FormatError::InvalidLength {
3130            structure: "archive",
3131            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3132            actual: bytes.len(),
3133        });
3134    }
3135
3136    let prefix = match parse_open_prefix(bytes, master_key) {
3137        Ok(prefix) => prefix,
3138        Err(prefix_err) => {
3139            return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
3140                .or(Err(prefix_err));
3141        }
3142    };
3143    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3144    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
3145        Ok(parsed) => Ok(parsed),
3146        Err(prefix_err) => {
3147            match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
3148                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3149                    Ok(recovered)
3150                }
3151                Ok(_) | Err(_) => Err(prefix_err),
3152            }
3153        }
3154    }
3155}
3156
3157fn parse_seekable_volume_with_prefix(
3158    bytes: &[u8],
3159    prefix: ParsedOpenPrefix,
3160    options: ReaderOptions,
3161) -> Result<ParsedSeekableVolume, FormatError> {
3162    let ParsedOpenPrefix {
3163        volume_header,
3164        crypto_header,
3165        crypto_header_bytes,
3166        crypto_end,
3167        subkeys,
3168    } = prefix;
3169    let crypto_bytes = crypto_header_bytes.as_slice();
3170
3171    let terminal = locate_v41_terminal(
3172        bytes,
3173        KeyHoldingTerminalContext {
3174            subkeys: &subkeys,
3175            volume_header: &volume_header,
3176            crypto_header: &crypto_header,
3177            crypto_header_bytes: crypto_bytes,
3178        },
3179        options,
3180    )?;
3181    finish_parse_seekable_volume(
3182        bytes,
3183        volume_header,
3184        crypto_header,
3185        crypto_header_bytes,
3186        crypto_end,
3187        subkeys,
3188        terminal,
3189    )
3190}
3191
3192fn parse_seekable_volume_from_recovered_terminal(
3193    bytes: &[u8],
3194    master_key: &MasterKey,
3195    options: ReaderOptions,
3196) -> Result<ParsedSeekableVolume, FormatError> {
3197    let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
3198    let crypto_end = checked_add(
3199        authority.volume_header.crypto_header_offset as usize,
3200        authority.volume_header.crypto_header_length as usize,
3201        "CryptoHeader",
3202    )?;
3203    finish_parse_seekable_volume(
3204        bytes,
3205        authority.volume_header,
3206        authority.crypto_header,
3207        authority.crypto_header_bytes,
3208        crypto_end,
3209        authority.subkeys,
3210        authority.terminal,
3211    )
3212}
3213
3214fn parse_open_prefix(
3215    bytes: &[u8],
3216    master_key: &MasterKey,
3217) -> Result<ParsedOpenPrefix, FormatError> {
3218    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3219    let crypto_start = volume_header.crypto_header_offset as usize;
3220    let crypto_len = volume_header.crypto_header_length as usize;
3221    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3222    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3223    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3224    let subkeys = subkeys_for_open(
3225        Some(master_key),
3226        parsed_crypto.fixed.aead_algo,
3227        &volume_header.archive_uuid,
3228        &volume_header.session_id,
3229    )?;
3230    verify_integrity_tag(
3231        HmacDomain::CryptoHeader,
3232        parsed_crypto.fixed.aead_algo,
3233        Some(&subkeys.mac_key),
3234        &volume_header.archive_uuid,
3235        &volume_header.session_id,
3236        parsed_crypto.hmac_covered_bytes,
3237        &parsed_crypto.header_hmac,
3238    )?;
3239    parsed_crypto.validate_extension_semantics()?;
3240    validate_seekable_supported_volume(
3241        &volume_header,
3242        &parsed_crypto.fixed,
3243        &parsed_crypto.extensions,
3244    )?;
3245    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3246    let crypto_header = parsed_crypto.fixed.clone();
3247    Ok(ParsedOpenPrefix {
3248        volume_header,
3249        crypto_header,
3250        crypto_header_bytes: crypto_bytes.to_vec(),
3251        crypto_end,
3252        subkeys,
3253    })
3254}
3255
3256fn finish_parse_seekable_volume(
3257    bytes: &[u8],
3258    volume_header: VolumeHeader,
3259    crypto_header: CryptoHeaderFixed,
3260    crypto_header_bytes: Vec<u8>,
3261    crypto_end: usize,
3262    subkeys: Subkeys,
3263    terminal: V41Terminal,
3264) -> Result<ParsedSeekableVolume, FormatError> {
3265    let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
3266    let volume_trailer = terminal.volume_trailer.clone();
3267    validate_trailer_identity(&volume_header, &volume_trailer)?;
3268
3269    let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
3270    let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
3271    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3272        if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
3273            || volume_trailer
3274                .root_auth_footer_offset
3275                .checked_add(volume_trailer.root_auth_footer_length as u64)
3276                .ok_or(FormatError::InvalidArchive(
3277                    "RootAuthFooter terminal boundary overflow",
3278                ))?
3279                != trailer_offset as u64
3280        {
3281            return Err(FormatError::InvalidArchive(
3282                "RootAuthFooter does not sit before selected trailer",
3283            ));
3284        }
3285    } else if manifest_end != trailer_offset {
3286        return Err(FormatError::InvalidArchive(
3287            "ManifestFooter does not end at selected trailer",
3288        ));
3289    }
3290    let manifest_bytes = &terminal.manifest_footer_bytes;
3291    let (manifest_footer, manifest_footer_error) =
3292        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3293        {
3294            Ok(footer) => (Some(footer), None),
3295            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3296            Err(err) => return Err(err),
3297        };
3298
3299    let block_region = parse_block_region(
3300        bytes,
3301        crypto_end,
3302        manifest_offset,
3303        crypto_header.block_size as usize,
3304        &volume_header,
3305        &volume_trailer,
3306    )?;
3307
3308    Ok(ParsedSeekableVolume {
3309        volume_header,
3310        crypto_header,
3311        crypto_header_bytes,
3312        subkeys,
3313        manifest_footer,
3314        manifest_footer_error,
3315        root_auth_footer: terminal.root_auth_footer,
3316        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3317        volume_trailer,
3318        blocks: block_region.blocks,
3319        erased_block_indices: block_region.erased_block_indices,
3320    })
3321}
3322
3323fn parse_seekable_read_at_volume(
3324    reader: Arc<dyn ArchiveReadAt>,
3325    master_key: &MasterKey,
3326    options: ReaderOptions,
3327) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3328    let observed_len = reader.len()?;
3329    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
3330        return Err(FormatError::InvalidLength {
3331            structure: "archive",
3332            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3333            actual: to_usize(observed_len, "archive")?,
3334        });
3335    }
3336
3337    let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
3338        Ok(prefix) => prefix,
3339        Err(prefix_err) => {
3340            return parse_seekable_read_at_volume_from_recovered_terminal(
3341                reader,
3342                observed_len,
3343                master_key,
3344                options,
3345            )
3346            .or(Err(prefix_err));
3347        }
3348    };
3349    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3350    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
3351        Ok(parsed) => Ok(parsed),
3352        Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
3353            reader,
3354            observed_len,
3355            master_key,
3356            options,
3357        ) {
3358            Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3359                Ok(recovered)
3360            }
3361            Ok(_) | Err(_) => Err(prefix_err),
3362        },
3363    }
3364}
3365
3366fn parse_seekable_read_at_volume_with_prefix(
3367    reader: Arc<dyn ArchiveReadAt>,
3368    observed_len: u64,
3369    prefix: ParsedReadAtOpenPrefix,
3370    options: ReaderOptions,
3371) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3372    let ParsedReadAtOpenPrefix {
3373        volume_header,
3374        crypto_header,
3375        crypto_header_bytes,
3376        crypto_end,
3377        subkeys,
3378    } = prefix;
3379
3380    let terminal = locate_v41_terminal_read_at(
3381        reader.as_ref(),
3382        observed_len,
3383        KeyHoldingTerminalContext {
3384            subkeys: &subkeys,
3385            volume_header: &volume_header,
3386            crypto_header: &crypto_header,
3387            crypto_header_bytes: &crypto_header_bytes,
3388        },
3389        options,
3390    )?;
3391    finish_parse_seekable_read_at_volume(
3392        reader,
3393        volume_header,
3394        crypto_header,
3395        crypto_header_bytes,
3396        crypto_end,
3397        subkeys,
3398        terminal,
3399    )
3400}
3401
3402fn parse_seekable_read_at_volume_from_recovered_terminal(
3403    reader: Arc<dyn ArchiveReadAt>,
3404    observed_len: u64,
3405    master_key: &MasterKey,
3406    options: ReaderOptions,
3407) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3408    let authority =
3409        locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
3410    let crypto_end = checked_u64_add(
3411        authority.volume_header.crypto_header_offset as u64,
3412        authority.volume_header.crypto_header_length as u64,
3413        "CryptoHeader",
3414    )?;
3415    finish_parse_seekable_read_at_volume(
3416        reader,
3417        authority.volume_header,
3418        authority.crypto_header,
3419        authority.crypto_header_bytes,
3420        crypto_end,
3421        authority.subkeys,
3422        authority.terminal,
3423    )
3424}
3425
3426fn parse_read_at_open_prefix(
3427    reader: &dyn ArchiveReadAt,
3428    master_key: &MasterKey,
3429) -> Result<ParsedReadAtOpenPrefix, FormatError> {
3430    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
3431    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
3432    let crypto_start = volume_header.crypto_header_offset as u64;
3433    let crypto_len = volume_header.crypto_header_length as u64;
3434    let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
3435    let crypto_bytes = read_at_vec(
3436        reader,
3437        crypto_start,
3438        to_usize(crypto_len, "CryptoHeader")?,
3439        "CryptoHeader",
3440    )?;
3441    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
3442    let subkeys = subkeys_for_open(
3443        Some(master_key),
3444        parsed_crypto.fixed.aead_algo,
3445        &volume_header.archive_uuid,
3446        &volume_header.session_id,
3447    )?;
3448    verify_integrity_tag(
3449        HmacDomain::CryptoHeader,
3450        parsed_crypto.fixed.aead_algo,
3451        Some(&subkeys.mac_key),
3452        &volume_header.archive_uuid,
3453        &volume_header.session_id,
3454        parsed_crypto.hmac_covered_bytes,
3455        &parsed_crypto.header_hmac,
3456    )?;
3457    parsed_crypto.validate_extension_semantics()?;
3458    validate_seekable_supported_volume(
3459        &volume_header,
3460        &parsed_crypto.fixed,
3461        &parsed_crypto.extensions,
3462    )?;
3463    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3464    let crypto_header = parsed_crypto.fixed.clone();
3465    drop(parsed_crypto);
3466    Ok(ParsedReadAtOpenPrefix {
3467        volume_header,
3468        crypto_header,
3469        crypto_header_bytes: crypto_bytes,
3470        crypto_end,
3471        subkeys,
3472    })
3473}
3474
3475fn finish_parse_seekable_read_at_volume(
3476    reader: Arc<dyn ArchiveReadAt>,
3477    volume_header: VolumeHeader,
3478    crypto_header: CryptoHeaderFixed,
3479    crypto_header_bytes: Vec<u8>,
3480    crypto_end: u64,
3481    subkeys: Subkeys,
3482    terminal: V41Terminal,
3483) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3484    let volume_trailer = terminal.volume_trailer.clone();
3485    validate_trailer_identity(&volume_header, &volume_trailer)?;
3486
3487    let manifest_offset = volume_trailer.manifest_footer_offset;
3488    let manifest_end = checked_u64_add(
3489        manifest_offset,
3490        MANIFEST_FOOTER_LEN as u64,
3491        "ManifestFooter",
3492    )?;
3493    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3494        if volume_trailer.root_auth_footer_offset != manifest_end
3495            || volume_trailer
3496                .root_auth_footer_offset
3497                .checked_add(volume_trailer.root_auth_footer_length as u64)
3498                .ok_or(FormatError::InvalidArchive(
3499                    "RootAuthFooter terminal boundary overflow",
3500                ))?
3501                != terminal.image.volume_trailer_offset
3502        {
3503            return Err(FormatError::InvalidArchive(
3504                "RootAuthFooter does not sit before selected trailer",
3505            ));
3506        }
3507    } else if manifest_end != terminal.image.volume_trailer_offset {
3508        return Err(FormatError::InvalidArchive(
3509            "ManifestFooter does not end at selected trailer",
3510        ));
3511    }
3512    validate_seekable_block_region_layout(
3513        crypto_end,
3514        manifest_offset,
3515        crypto_header.block_size as usize,
3516        &volume_trailer,
3517    )?;
3518
3519    let manifest_bytes = &terminal.manifest_footer_bytes;
3520    let (manifest_footer, manifest_footer_error) =
3521        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3522        {
3523            Ok(footer) => (Some(footer), None),
3524            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3525            Err(err) => return Err(err),
3526        };
3527
3528    Ok(ParsedSeekableReadAtVolume {
3529        reader,
3530        volume_header,
3531        crypto_header,
3532        crypto_header_bytes,
3533        subkeys,
3534        manifest_footer,
3535        manifest_footer_error,
3536        root_auth_footer: terminal.root_auth_footer,
3537        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3538        volume_trailer,
3539        crypto_end,
3540    })
3541}
3542
3543#[derive(Debug)]
3544struct ParsedPublicNoKeyVolume {
3545    volume_header: VolumeHeader,
3546    crypto_header: CryptoHeaderFixed,
3547    root_auth_footer: RootAuthFooterV1,
3548    root_auth_footer_bytes: Vec<u8>,
3549    blocks: BTreeMap<u64, BlockRecord>,
3550}
3551
3552pub fn public_no_key_verify_volumes_with_options<F>(
3553    volumes: &[&[u8]],
3554    mut verifier: F,
3555    options: ReaderOptions,
3556) -> Result<PublicNoKeyVerification, FormatError>
3557where
3558    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3559{
3560    validate_reader_options(options)?;
3561    if volumes.is_empty() {
3562        return Err(FormatError::InvalidArchive("no volumes supplied"));
3563    }
3564    let mut parsed = Vec::with_capacity(volumes.len());
3565    for volume in volumes {
3566        parsed.push(parse_public_no_key_volume(volume, options)?);
3567    }
3568    let first = parsed
3569        .first()
3570        .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
3571    if parsed.len() != first.crypto_header.stripe_width as usize {
3572        return Err(FormatError::ReaderUnsupported(
3573            "public no-key verification requires a complete volume set",
3574        ));
3575    }
3576
3577    let mut seen_volume_indexes = BTreeSet::new();
3578    let mut blocks = BTreeMap::new();
3579    for volume in &parsed {
3580        if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
3581            || volume.volume_header.session_id != first.volume_header.session_id
3582            || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
3583        {
3584            return Err(FormatError::InvalidArchive(
3585                "public no-key volume global metadata differs",
3586            ));
3587        }
3588        if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
3589            return Err(FormatError::InvalidArchive(
3590                "public no-key RootAuthFooter copies differ",
3591            ));
3592        }
3593        if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
3594            return Err(FormatError::InvalidArchive(
3595                "duplicate public no-key volume index",
3596            ));
3597        }
3598        for (block_index, record) in &volume.blocks {
3599            if blocks.insert(*block_index, record.clone()).is_some() {
3600                return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
3601            }
3602        }
3603    }
3604    validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
3605
3606    let footer = &first.root_auth_footer;
3607    let mut data_leaves = blocks
3608        .values()
3609        .filter(|record| record.kind.is_data())
3610        .map(|record| DataBlockMerkleLeaf {
3611            block_index: record.block_index,
3612            kind: record.kind,
3613            flags: record.flags,
3614            payload: record.payload.clone(),
3615        })
3616        .collect::<Vec<_>>();
3617    data_leaves.sort_by_key(|leaf| leaf.block_index);
3618    let total_data_block_count = u64::try_from(data_leaves.len())
3619        .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
3620    let observed_data_root = data_block_merkle_root(&data_leaves);
3621    if total_data_block_count != footer.total_data_block_count
3622        || observed_data_root != footer.data_block_merkle_root
3623    {
3624        return Err(FormatError::InvalidArchive(
3625            "public no-key data-block commitment mismatch",
3626        ));
3627    }
3628    let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
3629    if archive_root != footer.archive_root {
3630        return Err(FormatError::InvalidArchive(
3631            "public no-key archive_root mismatch",
3632        ));
3633    }
3634    if !verifier(footer, &archive_root)? {
3635        return Err(FormatError::InvalidArchive(
3636            "public no-key authenticator verification failed",
3637        ));
3638    }
3639    Ok(PublicNoKeyVerification {
3640        archive_root,
3641        authenticator_id: footer.authenticator_id,
3642        signer_identity_type: footer.signer_identity_type,
3643        signer_identity_bytes: footer.signer_identity_bytes.clone(),
3644        total_data_block_count,
3645        diagnostics: vec![
3646            PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
3647            PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
3648            PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
3649        ],
3650    })
3651}
3652
3653fn parse_public_no_key_volume(
3654    bytes: &[u8],
3655    options: ReaderOptions,
3656) -> Result<ParsedPublicNoKeyVolume, FormatError> {
3657    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3658        return Err(FormatError::InvalidLength {
3659            structure: "archive",
3660            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3661            actual: bytes.len(),
3662        });
3663    }
3664    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3665    let crypto_start = volume_header.crypto_header_offset as usize;
3666    let crypto_len = volume_header.crypto_header_length as usize;
3667    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3668    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3669    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3670    parsed_crypto.validate_extension_semantics()?;
3671    validate_seekable_supported_volume(
3672        &volume_header,
3673        &parsed_crypto.fixed,
3674        &parsed_crypto.extensions,
3675    )?;
3676    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3677
3678    let terminal =
3679        locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
3680    let block_region = parse_public_block_observation(
3681        bytes,
3682        crypto_end,
3683        &terminal.image,
3684        parsed_crypto.fixed.block_size as usize,
3685        &volume_header,
3686    )?;
3687    Ok(ParsedPublicNoKeyVolume {
3688        volume_header,
3689        crypto_header: parsed_crypto.fixed,
3690        root_auth_footer: terminal.root_auth_footer,
3691        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3692        blocks: block_region,
3693    })
3694}
3695
3696fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3697    left.length == right.length
3698        && left.stripe_width == right.stripe_width
3699        && left.block_size == right.block_size
3700        && left.compression_algo == right.compression_algo
3701        && left.aead_algo == right.aead_algo
3702        && left.fec_algo == right.fec_algo
3703        && left.kdf_algo == right.kdf_algo
3704}
3705
3706fn recompute_public_archive_root(
3707    footer: &RootAuthFooterV1,
3708    crypto_header: &CryptoHeaderFixed,
3709) -> Result<[u8; 32], FormatError> {
3710    let descriptor_digest = root_auth_descriptor_digest(
3711        footer.authenticator_id,
3712        footer.signer_identity_type,
3713        &footer.signer_identity_bytes,
3714        u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3715            FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3716        })?,
3717        footer.footer_length()?,
3718    )?;
3719    let signer_digest =
3720        signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3721    if signer_digest != footer.signer_identity_digest {
3722        return Err(FormatError::InvalidArchive(
3723            "public no-key signer identity digest mismatch",
3724        ));
3725    }
3726    Ok(archive_root(ArchiveRootInputs {
3727        archive_uuid: footer.archive_uuid,
3728        session_id: footer.session_id,
3729        format_version: FORMAT_VERSION,
3730        volume_format_rev: VOLUME_FORMAT_REV,
3731        compression_algo: crypto_header.compression_algo,
3732        aead_algo: crypto_header.aead_algo,
3733        fec_algo: crypto_header.fec_algo,
3734        kdf_algo: crypto_header.kdf_algo,
3735        critical_metadata_digest: footer.critical_metadata_digest,
3736        index_digest: footer.index_digest,
3737        fec_layout_digest: footer.fec_layout_digest,
3738        total_data_block_count: footer.total_data_block_count,
3739        data_block_merkle_root: footer.data_block_merkle_root,
3740        root_auth_descriptor_digest: descriptor_digest,
3741        signer_identity_digest: signer_digest,
3742    }))
3743}
3744
3745fn parse_valid_manifest_footer(
3746    volume_header: &VolumeHeader,
3747    crypto_header: &CryptoHeaderFixed,
3748    subkeys: &Subkeys,
3749    manifest_bytes: &[u8],
3750) -> Result<ManifestFooter, FormatError> {
3751    let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3752    validate_manifest_footer(
3753        volume_header,
3754        crypto_header,
3755        &manifest_footer,
3756        subkeys,
3757        manifest_bytes,
3758    )?;
3759    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
3760    Ok(manifest_footer)
3761}
3762
3763fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3764    matches!(
3765        error,
3766        FormatError::BadMagic {
3767            structure: "ManifestFooter",
3768        } | FormatError::NonZeroReserved {
3769            structure: "ManifestFooter",
3770        } | FormatError::InvalidAuthoritativeFlag(_)
3771            | FormatError::HmacMismatch {
3772                structure: "ManifestFooter",
3773            }
3774            | FormatError::IntegrityDigestMismatch {
3775                structure: "ManifestFooter",
3776            }
3777    )
3778}
3779
3780fn validate_seekable_supported_volume(
3781    volume_header: &VolumeHeader,
3782    crypto_header: &CryptoHeaderFixed,
3783    extensions: &[ExtensionTlv<'_>],
3784) -> Result<(), FormatError> {
3785    reject_unsupported_raw_stream_profile(extensions)?;
3786    if crypto_header.stripe_width != volume_header.stripe_width {
3787        return Err(FormatError::InvalidArchive(
3788            "VolumeHeader and CryptoHeader stripe_width differ",
3789        ));
3790    }
3791    Ok(())
3792}
3793
3794pub(crate) fn validate_crypto_class_parity_exactness(
3795    crypto_header: &CryptoHeaderFixed,
3796) -> Result<(), FormatError> {
3797    let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3798    if crypto_header.fec_parity_shards as u32 != fec {
3799        return Err(FormatError::InvalidArchive(
3800            "fec_parity_shards does not match v41 compute_parity",
3801        ));
3802    }
3803    let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3804    if crypto_header.index_fec_parity_shards as u32 != index {
3805        return Err(FormatError::InvalidArchive(
3806            "index_fec_parity_shards does not match v41 compute_parity",
3807        ));
3808    }
3809    let index_root = required_object_parity(
3810        crypto_header.index_root_fec_data_shards as u64,
3811        crypto_header,
3812    )?;
3813    if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3814        return Err(FormatError::InvalidArchive(
3815            "index_root_fec_parity_shards does not match v41 compute_parity",
3816        ));
3817    }
3818    Ok(())
3819}
3820
3821fn validate_volume_set_member(
3822    first: &ParsedSeekableVolume,
3823    candidate: &ParsedSeekableVolume,
3824) -> Result<(), FormatError> {
3825    validate_volume_set_member_metadata(
3826        &first.volume_header,
3827        &first.crypto_header,
3828        &first.crypto_header_bytes,
3829        &candidate.volume_header,
3830        &candidate.crypto_header,
3831        &candidate.crypto_header_bytes,
3832    )
3833}
3834
3835fn validate_volume_set_member_metadata(
3836    first_volume_header: &VolumeHeader,
3837    first_crypto_header: &CryptoHeaderFixed,
3838    first_crypto_header_bytes: &[u8],
3839    candidate_volume_header: &VolumeHeader,
3840    candidate_crypto_header: &CryptoHeaderFixed,
3841    candidate_crypto_header_bytes: &[u8],
3842) -> Result<(), FormatError> {
3843    if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3844        || candidate_volume_header.session_id != first_volume_header.session_id
3845    {
3846        return Err(FormatError::InvalidArchive(
3847            "mixed archive or session IDs in volume set",
3848        ));
3849    }
3850    if candidate_crypto_header_bytes != first_crypto_header_bytes
3851        || candidate_crypto_header != first_crypto_header
3852    {
3853        return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3854    }
3855    Ok(())
3856}
3857
3858pub(crate) fn manifest_bootstrap_fields_match(
3859    left: &ManifestFooter,
3860    right: &ManifestFooter,
3861) -> bool {
3862    left.archive_uuid == right.archive_uuid
3863        && left.session_id == right.session_id
3864        && left.is_authoritative == right.is_authoritative
3865        && left.total_volumes == right.total_volumes
3866        && left.index_root_first_block == right.index_root_first_block
3867        && left.index_root_data_block_count == right.index_root_data_block_count
3868        && left.index_root_parity_block_count == right.index_root_parity_block_count
3869        && left.index_root_encrypted_size == right.index_root_encrypted_size
3870        && left.index_root_decompressed_size == right.index_root_decompressed_size
3871}
3872
3873fn validate_complete_global_block_coverage(
3874    blocks: &BTreeMap<u64, BlockRecord>,
3875    erased_block_indices: &BTreeSet<u64>,
3876) -> Result<(), FormatError> {
3877    let mut expected = 0u64;
3878    let mut block_iter = blocks.keys().copied().peekable();
3879    let mut erasure_iter = erased_block_indices.iter().copied().peekable();
3880
3881    loop {
3882        let next_block = block_iter.peek().copied();
3883        let next_erasure = erasure_iter.peek().copied();
3884        let next = match (next_block, next_erasure) {
3885            (Some(block), Some(erasure)) if block == erasure => {
3886                return Err(FormatError::InvalidArchive(
3887                    "BlockRecord index is both present and erased",
3888                ));
3889            }
3890            (Some(block), Some(erasure)) => block.min(erasure),
3891            (Some(block), None) => block,
3892            (None, Some(erasure)) => erasure,
3893            (None, None) => return Ok(()),
3894        };
3895
3896        if next != expected {
3897            return Err(FormatError::InvalidArchive(
3898                "complete volume set has missing global blocks",
3899            ));
3900        }
3901        if next_block == Some(next) {
3902            block_iter.next();
3903        }
3904        if next_erasure == Some(next) {
3905            erasure_iter.next();
3906        }
3907        expected = expected
3908            .checked_add(1)
3909            .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
3910    }
3911}
3912
3913#[derive(Debug)]
3914struct V41Terminal {
3915    image: CriticalMetadataImage,
3916    manifest_footer_bytes: Vec<u8>,
3917    root_auth_footer_bytes: Option<Vec<u8>>,
3918    root_auth_footer: Option<RootAuthFooterV1>,
3919    volume_trailer: VolumeTrailer,
3920}
3921
3922pub(crate) struct SequentialTerminalMaterial {
3923    pub(crate) manifest_footer: ManifestFooter,
3924    pub(crate) volume_trailer: VolumeTrailer,
3925    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
3926}
3927
3928#[derive(Debug)]
3929struct V41PublicTerminal {
3930    image: CriticalMetadataImage,
3931    root_auth_footer_bytes: Vec<u8>,
3932    root_auth_footer: RootAuthFooterV1,
3933}
3934
3935#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3936struct CmraDecoderTuple {
3937    shard_size: u32,
3938    data_shard_count: u16,
3939    parity_shard_count: u16,
3940    image_length: u32,
3941    image_sha256: [u8; 32],
3942}
3943
3944#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3945struct CmraIdentityHints {
3946    archive_uuid: [u8; 16],
3947    session_id: [u8; 16],
3948    volume_index: u32,
3949}
3950
3951impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
3952    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3953        Self {
3954            shard_size: header.shard_size,
3955            data_shard_count: header.data_shard_count,
3956            parity_shard_count: header.parity_shard_count,
3957            image_length: header.image_length,
3958            image_sha256: header.image_sha256,
3959        }
3960    }
3961}
3962
3963impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
3964    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3965        Self {
3966            archive_uuid: header.archive_uuid_hint,
3967            session_id: header.session_id_hint,
3968            volume_index: header.volume_index_hint,
3969        }
3970    }
3971}
3972
3973impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
3974    fn from(locator: CriticalRecoveryLocator) -> Self {
3975        Self {
3976            shard_size: locator.cmra_shard_size,
3977            data_shard_count: locator.cmra_data_shard_count,
3978            parity_shard_count: locator.cmra_parity_shard_count,
3979            image_length: locator.cmra_image_length,
3980            image_sha256: locator.cmra_image_sha256,
3981        }
3982    }
3983}
3984
3985impl From<CriticalRecoveryLocator> for CmraIdentityHints {
3986    fn from(locator: CriticalRecoveryLocator) -> Self {
3987        Self {
3988            archive_uuid: locator.archive_uuid_hint,
3989            session_id: locator.session_id_hint,
3990            volume_index: locator.volume_index_hint,
3991        }
3992    }
3993}
3994
3995#[derive(Debug)]
3996struct RecoveredCmra {
3997    image: CriticalMetadataImage,
3998    tuple: CmraDecoderTuple,
3999    header_hints: Option<CmraIdentityHints>,
4000    cmra_length: u64,
4001}
4002
4003#[derive(Debug)]
4004struct TerminalCandidate {
4005    terminal: V41Terminal,
4006    anchor: usize,
4007    locator_sequence: Option<u32>,
4008    cmra_offset: u64,
4009    cmra_length: u64,
4010}
4011
4012#[derive(Debug)]
4013struct PublicTerminalCandidate {
4014    terminal: V41PublicTerminal,
4015    anchor: usize,
4016    cmra_offset: u64,
4017    cmra_length: u64,
4018}
4019
4020#[derive(Debug)]
4021struct RecoveredTerminalAuthority {
4022    terminal: V41Terminal,
4023    volume_header: VolumeHeader,
4024    crypto_header: CryptoHeaderFixed,
4025    crypto_header_bytes: Vec<u8>,
4026    subkeys: Subkeys,
4027}
4028
4029#[derive(Debug)]
4030struct TerminalAuthorityCandidate {
4031    authority: RecoveredTerminalAuthority,
4032    anchor: usize,
4033    cmra_offset: u64,
4034    cmra_length: u64,
4035}
4036
4037#[derive(Debug, Clone, Copy)]
4038enum CmraRecoveryMode {
4039    KeyHolding,
4040    PublicNoKey,
4041}
4042
4043#[derive(Clone, Copy)]
4044pub(crate) struct KeyHoldingTerminalContext<'a> {
4045    pub(crate) subkeys: &'a Subkeys,
4046    pub(crate) volume_header: &'a VolumeHeader,
4047    pub(crate) crypto_header: &'a CryptoHeaderFixed,
4048    pub(crate) crypto_header_bytes: &'a [u8],
4049}
4050
4051fn locate_v41_terminal(
4052    bytes: &[u8],
4053    context: KeyHoldingTerminalContext<'_>,
4054    options: ReaderOptions,
4055) -> Result<V41Terminal, FormatError> {
4056    locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
4057}
4058
4059fn locate_v41_terminal_read_at(
4060    reader: &dyn ArchiveReadAt,
4061    len: u64,
4062    context: KeyHoldingTerminalContext<'_>,
4063    options: ReaderOptions,
4064) -> Result<V41Terminal, FormatError> {
4065    let mut candidates = Vec::new();
4066    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4067        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4068        collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
4069    }
4070    if len >= LOCATOR_PAIR_LEN as u64 {
4071        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4072        collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
4073    }
4074
4075    if candidates.is_empty() {
4076        let scan = max_critical_recovery_scan(options)? as u64;
4077        let scan_start = len.saturating_sub(scan);
4078        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4079        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4080        let mut offset = tail.len().saturating_sub(4);
4081        while offset < tail.len() {
4082            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4083            if tail.get(offset..offset + 4) == Some(b"TZCL") {
4084                collect_v41_locator_candidate_read_at(
4085                    reader,
4086                    absolute_offset,
4087                    2,
4088                    context,
4089                    &mut candidates,
4090                );
4091            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4092                if let Ok(candidate) =
4093                    parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
4094                {
4095                    candidates.push(candidate);
4096                }
4097            }
4098            if offset == 0 {
4099                break;
4100            }
4101            offset -= 1;
4102        }
4103    }
4104
4105    choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4106}
4107
4108fn locate_v41_terminal_authority(
4109    bytes: &[u8],
4110    master_key: &MasterKey,
4111    options: ReaderOptions,
4112) -> Result<RecoveredTerminalAuthority, FormatError> {
4113    let mut candidates = Vec::new();
4114    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4115        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4116        collect_v41_locator_authority_candidate(
4117            bytes,
4118            final_offset,
4119            0,
4120            master_key,
4121            &mut candidates,
4122        );
4123    }
4124    if bytes.len() >= LOCATOR_PAIR_LEN {
4125        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4126        collect_v41_locator_authority_candidate(
4127            bytes,
4128            mirror_offset,
4129            1,
4130            master_key,
4131            &mut candidates,
4132        );
4133    }
4134
4135    if candidates.is_empty() {
4136        let scan = max_critical_recovery_scan(options)?;
4137        let scan_start = bytes.len().saturating_sub(scan);
4138        let mut offset = bytes.len().saturating_sub(4);
4139        while offset >= scan_start {
4140            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4141                collect_v41_locator_authority_candidate(
4142                    bytes,
4143                    offset,
4144                    2,
4145                    master_key,
4146                    &mut candidates,
4147                );
4148            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4149                if let Ok(candidate) =
4150                    parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
4151                {
4152                    candidates.push(candidate);
4153                }
4154            }
4155            if offset == 0 {
4156                break;
4157            }
4158            offset -= 1;
4159        }
4160    }
4161
4162    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4163}
4164
4165fn locate_v41_terminal_authority_read_at(
4166    reader: &dyn ArchiveReadAt,
4167    len: u64,
4168    master_key: &MasterKey,
4169    options: ReaderOptions,
4170) -> Result<RecoveredTerminalAuthority, FormatError> {
4171    let mut candidates = Vec::new();
4172    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4173        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4174        collect_v41_locator_authority_candidate_read_at(
4175            reader,
4176            final_offset,
4177            0,
4178            master_key,
4179            &mut candidates,
4180        );
4181    }
4182    if len >= LOCATOR_PAIR_LEN as u64 {
4183        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4184        collect_v41_locator_authority_candidate_read_at(
4185            reader,
4186            mirror_offset,
4187            1,
4188            master_key,
4189            &mut candidates,
4190        );
4191    }
4192
4193    if candidates.is_empty() {
4194        let scan = max_critical_recovery_scan(options)? as u64;
4195        let scan_start = len.saturating_sub(scan);
4196        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4197        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4198        let mut offset = tail.len().saturating_sub(4);
4199        while offset < tail.len() {
4200            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4201            if tail.get(offset..offset + 4) == Some(b"TZCL") {
4202                collect_v41_locator_authority_candidate_read_at(
4203                    reader,
4204                    absolute_offset,
4205                    2,
4206                    master_key,
4207                    &mut candidates,
4208                );
4209            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4210                if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
4211                    reader,
4212                    absolute_offset,
4213                    master_key,
4214                ) {
4215                    candidates.push(candidate);
4216                }
4217            }
4218            if offset == 0 {
4219                break;
4220            }
4221            offset -= 1;
4222        }
4223    }
4224
4225    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4226}
4227
4228fn locate_v41_terminal_candidate(
4229    bytes: &[u8],
4230    context: KeyHoldingTerminalContext<'_>,
4231    options: ReaderOptions,
4232) -> Result<TerminalCandidate, FormatError> {
4233    let mut candidates = Vec::new();
4234    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4235        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4236        collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
4237    }
4238    if bytes.len() >= LOCATOR_PAIR_LEN {
4239        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4240        collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
4241    }
4242
4243    if candidates.is_empty() {
4244        let scan = max_critical_recovery_scan(options)?;
4245        let scan_start = bytes.len().saturating_sub(scan);
4246        let mut offset = bytes.len().saturating_sub(4);
4247        while offset >= scan_start {
4248            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4249                collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
4250            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4251                if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
4252                    candidates.push(candidate);
4253                }
4254            }
4255            if offset == 0 {
4256                break;
4257            }
4258            offset -= 1;
4259        }
4260    }
4261
4262    choose_v41_terminal_candidate(candidates)
4263}
4264
4265fn locate_v41_public_terminal(
4266    bytes: &[u8],
4267    volume_header: &VolumeHeader,
4268    crypto_header: &CryptoHeaderFixed,
4269    options: ReaderOptions,
4270) -> Result<V41PublicTerminal, FormatError> {
4271    let mut candidates = Vec::new();
4272    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4273        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4274        collect_v41_public_locator_candidate(
4275            bytes,
4276            final_offset,
4277            0,
4278            volume_header,
4279            crypto_header,
4280            &mut candidates,
4281        );
4282    }
4283    if bytes.len() >= LOCATOR_PAIR_LEN {
4284        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4285        collect_v41_public_locator_candidate(
4286            bytes,
4287            mirror_offset,
4288            1,
4289            volume_header,
4290            crypto_header,
4291            &mut candidates,
4292        );
4293    }
4294
4295    if candidates.is_empty() {
4296        let scan = max_critical_recovery_scan(options)?;
4297        let scan_start = bytes.len().saturating_sub(scan);
4298        let mut offset = bytes.len().saturating_sub(4);
4299        while offset >= scan_start {
4300            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4301                collect_v41_public_locator_candidate(
4302                    bytes,
4303                    offset,
4304                    2,
4305                    volume_header,
4306                    crypto_header,
4307                    &mut candidates,
4308                );
4309            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4310                if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
4311                    bytes,
4312                    offset,
4313                    volume_header,
4314                    crypto_header,
4315                ) {
4316                    candidates.push(candidate);
4317                }
4318            }
4319            if offset == 0 {
4320                break;
4321            }
4322            offset -= 1;
4323        }
4324    }
4325
4326    choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4327}
4328
4329fn collect_v41_locator_candidate(
4330    bytes: &[u8],
4331    offset: usize,
4332    expected_sequence: u32,
4333    context: KeyHoldingTerminalContext<'_>,
4334    candidates: &mut Vec<TerminalCandidate>,
4335) {
4336    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4337        return;
4338    };
4339    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4340        return;
4341    };
4342    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4343        return;
4344    }
4345    if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
4346        candidates.push(candidate);
4347    }
4348}
4349
4350fn collect_v41_locator_candidate_read_at(
4351    reader: &dyn ArchiveReadAt,
4352    offset: u64,
4353    expected_sequence: u32,
4354    context: KeyHoldingTerminalContext<'_>,
4355    candidates: &mut Vec<TerminalCandidate>,
4356) {
4357    let Ok(raw) = read_at_vec(
4358        reader,
4359        offset,
4360        CRITICAL_RECOVERY_LOCATOR_LEN,
4361        "CriticalRecoveryLocator",
4362    ) else {
4363        return;
4364    };
4365    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4366        return;
4367    };
4368    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4369        return;
4370    }
4371    if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
4372        candidates.push(candidate);
4373    }
4374}
4375
4376fn collect_v41_locator_authority_candidate(
4377    bytes: &[u8],
4378    offset: usize,
4379    expected_sequence: u32,
4380    master_key: &MasterKey,
4381    candidates: &mut Vec<TerminalAuthorityCandidate>,
4382) {
4383    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4384        return;
4385    };
4386    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4387        return;
4388    };
4389    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4390        return;
4391    }
4392    if let Ok(candidate) =
4393        parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
4394    {
4395        candidates.push(candidate);
4396    }
4397}
4398
4399fn collect_v41_locator_authority_candidate_read_at(
4400    reader: &dyn ArchiveReadAt,
4401    offset: u64,
4402    expected_sequence: u32,
4403    master_key: &MasterKey,
4404    candidates: &mut Vec<TerminalAuthorityCandidate>,
4405) {
4406    let Ok(raw) = read_at_vec(
4407        reader,
4408        offset,
4409        CRITICAL_RECOVERY_LOCATOR_LEN,
4410        "CriticalRecoveryLocator",
4411    ) else {
4412        return;
4413    };
4414    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4415        return;
4416    };
4417    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4418        return;
4419    }
4420    if let Ok(candidate) =
4421        parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
4422    {
4423        candidates.push(candidate);
4424    }
4425}
4426
4427fn collect_v41_public_locator_candidate(
4428    bytes: &[u8],
4429    offset: usize,
4430    expected_sequence: u32,
4431    volume_header: &VolumeHeader,
4432    crypto_header: &CryptoHeaderFixed,
4433    candidates: &mut Vec<PublicTerminalCandidate>,
4434) {
4435    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4436        return;
4437    };
4438    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4439        return;
4440    };
4441    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4442        return;
4443    }
4444    if let Ok(candidate) =
4445        parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
4446    {
4447        candidates.push(candidate);
4448    }
4449}
4450
4451fn choose_v41_terminal_candidate(
4452    mut candidates: Vec<TerminalCandidate>,
4453) -> Result<TerminalCandidate, FormatError> {
4454    candidates.sort_by_key(|candidate| candidate.anchor);
4455    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4456        "no valid v41 CMRA candidate found",
4457    ))?;
4458    if let Some(previous) = candidates.last() {
4459        if previous.anchor == winner.anchor
4460            && (previous.cmra_offset != winner.cmra_offset
4461                || previous.cmra_length != winner.cmra_length)
4462        {
4463            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4464        }
4465    }
4466    Ok(winner)
4467}
4468
4469fn choose_v41_terminal_authority_candidate(
4470    mut candidates: Vec<TerminalAuthorityCandidate>,
4471) -> Result<TerminalAuthorityCandidate, FormatError> {
4472    candidates.sort_by_key(|candidate| candidate.anchor);
4473    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4474        "no valid v41 CMRA candidate found",
4475    ))?;
4476    if let Some(previous) = candidates.last() {
4477        if previous.anchor == winner.anchor
4478            && (previous.cmra_offset != winner.cmra_offset
4479                || previous.cmra_length != winner.cmra_length)
4480        {
4481            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4482        }
4483    }
4484    Ok(winner)
4485}
4486
4487fn choose_v41_public_terminal_candidate(
4488    mut candidates: Vec<PublicTerminalCandidate>,
4489) -> Result<PublicTerminalCandidate, FormatError> {
4490    candidates.sort_by_key(|candidate| candidate.anchor);
4491    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4492        "no valid v41 public CMRA candidate found",
4493    ))?;
4494    if let Some(previous) = candidates.last() {
4495        if previous.anchor == winner.anchor
4496            && (previous.cmra_offset != winner.cmra_offset
4497                || previous.cmra_length != winner.cmra_length)
4498        {
4499            return Err(FormatError::InvalidArchive(
4500                "ambiguous v41 public CMRA candidates",
4501            ));
4502        }
4503    }
4504    Ok(winner)
4505}
4506
4507fn parse_locator_cmra_candidate(
4508    bytes: &[u8],
4509    locator_offset: usize,
4510    locator: CriticalRecoveryLocator,
4511    context: KeyHoldingTerminalContext<'_>,
4512) -> Result<TerminalCandidate, FormatError> {
4513    let tuple = CmraDecoderTuple::from(locator);
4514    validate_cmra_decoder_tuple(tuple)?;
4515    let expected_cmra_length = cmra_serialized_length(tuple)?;
4516    if locator.cmra_length as u64 != expected_cmra_length {
4517        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4518    }
4519    validate_locator_position(locator_offset, locator)?;
4520    let recovered = recover_cmra(
4521        bytes,
4522        locator.cmra_offset,
4523        Some(tuple),
4524        CmraRecoveryMode::KeyHolding,
4525    )?;
4526    if recovered.tuple != tuple {
4527        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4528    }
4529    if expected_cmra_length != recovered.cmra_length {
4530        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4531    }
4532    validate_locator_image_boundary(locator, &recovered.image)?;
4533    validate_cmra_identity_hints(
4534        recovered.header_hints,
4535        Some(CmraIdentityHints::from(locator)),
4536        &recovered.image,
4537    )?;
4538    let terminal =
4539        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
4540    Ok(TerminalCandidate {
4541        terminal,
4542        anchor: locator_offset
4543            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4544            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4545        locator_sequence: Some(locator.locator_sequence),
4546        cmra_offset: locator.cmra_offset,
4547        cmra_length: recovered.cmra_length,
4548    })
4549}
4550
4551fn parse_locator_cmra_candidate_read_at(
4552    reader: &dyn ArchiveReadAt,
4553    locator_offset: u64,
4554    locator: CriticalRecoveryLocator,
4555    context: KeyHoldingTerminalContext<'_>,
4556) -> Result<TerminalCandidate, FormatError> {
4557    let tuple = CmraDecoderTuple::from(locator);
4558    validate_cmra_decoder_tuple(tuple)?;
4559    let expected_cmra_length = cmra_serialized_length(tuple)?;
4560    if locator.cmra_length as u64 != expected_cmra_length {
4561        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4562    }
4563    validate_locator_position(
4564        to_usize(locator_offset, "CriticalRecoveryLocator")?,
4565        locator,
4566    )?;
4567    let recovered = recover_cmra_read_at(
4568        reader,
4569        locator.cmra_offset,
4570        Some(tuple),
4571        CmraRecoveryMode::KeyHolding,
4572    )?;
4573    if recovered.tuple != tuple {
4574        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4575    }
4576    if expected_cmra_length != recovered.cmra_length {
4577        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4578    }
4579    validate_locator_image_boundary(locator, &recovered.image)?;
4580    validate_cmra_identity_hints(
4581        recovered.header_hints,
4582        Some(CmraIdentityHints::from(locator)),
4583        &recovered.image,
4584    )?;
4585    let terminal = validate_recovered_terminal_read_at(
4586        recovered.image,
4587        recovered.tuple,
4588        reader,
4589        context,
4590        false,
4591    )?;
4592    Ok(TerminalCandidate {
4593        terminal,
4594        anchor: to_usize(
4595            checked_u64_add(
4596                locator_offset,
4597                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4598                "locator anchor overflow",
4599            )?,
4600            "locator anchor overflow",
4601        )?,
4602        locator_sequence: Some(locator.locator_sequence),
4603        cmra_offset: locator.cmra_offset,
4604        cmra_length: recovered.cmra_length,
4605    })
4606}
4607
4608fn parse_locator_cmra_authority_candidate(
4609    bytes: &[u8],
4610    locator_offset: usize,
4611    locator: CriticalRecoveryLocator,
4612    master_key: &MasterKey,
4613) -> Result<TerminalAuthorityCandidate, FormatError> {
4614    let tuple = CmraDecoderTuple::from(locator);
4615    validate_cmra_decoder_tuple(tuple)?;
4616    let expected_cmra_length = cmra_serialized_length(tuple)?;
4617    if locator.cmra_length as u64 != expected_cmra_length {
4618        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4619    }
4620    validate_locator_position(locator_offset, locator)?;
4621    let recovered = recover_cmra(
4622        bytes,
4623        locator.cmra_offset,
4624        Some(tuple),
4625        CmraRecoveryMode::KeyHolding,
4626    )?;
4627    if recovered.tuple != tuple {
4628        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4629    }
4630    if expected_cmra_length != recovered.cmra_length {
4631        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4632    }
4633    validate_locator_image_boundary(locator, &recovered.image)?;
4634    validate_cmra_identity_hints(
4635        recovered.header_hints,
4636        Some(CmraIdentityHints::from(locator)),
4637        &recovered.image,
4638    )?;
4639    let cmra_length = recovered.cmra_length;
4640    let authority =
4641        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4642    Ok(TerminalAuthorityCandidate {
4643        authority,
4644        anchor: locator_offset
4645            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4646            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4647        cmra_offset: locator.cmra_offset,
4648        cmra_length,
4649    })
4650}
4651
4652fn parse_locator_cmra_authority_candidate_read_at(
4653    reader: &dyn ArchiveReadAt,
4654    locator_offset: u64,
4655    locator: CriticalRecoveryLocator,
4656    master_key: &MasterKey,
4657) -> Result<TerminalAuthorityCandidate, FormatError> {
4658    let tuple = CmraDecoderTuple::from(locator);
4659    validate_cmra_decoder_tuple(tuple)?;
4660    let expected_cmra_length = cmra_serialized_length(tuple)?;
4661    if locator.cmra_length as u64 != expected_cmra_length {
4662        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4663    }
4664    validate_locator_position(
4665        to_usize(locator_offset, "CriticalRecoveryLocator")?,
4666        locator,
4667    )?;
4668    let recovered = recover_cmra_read_at(
4669        reader,
4670        locator.cmra_offset,
4671        Some(tuple),
4672        CmraRecoveryMode::KeyHolding,
4673    )?;
4674    if recovered.tuple != tuple {
4675        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4676    }
4677    if expected_cmra_length != recovered.cmra_length {
4678        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4679    }
4680    validate_locator_image_boundary(locator, &recovered.image)?;
4681    validate_cmra_identity_hints(
4682        recovered.header_hints,
4683        Some(CmraIdentityHints::from(locator)),
4684        &recovered.image,
4685    )?;
4686    let cmra_length = recovered.cmra_length;
4687    let authority =
4688        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4689    Ok(TerminalAuthorityCandidate {
4690        authority,
4691        anchor: to_usize(
4692            checked_u64_add(
4693                locator_offset,
4694                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4695                "locator anchor overflow",
4696            )?,
4697            "locator anchor overflow",
4698        )?,
4699        cmra_offset: locator.cmra_offset,
4700        cmra_length,
4701    })
4702}
4703
4704fn parse_public_locator_cmra_candidate(
4705    bytes: &[u8],
4706    locator_offset: usize,
4707    locator: CriticalRecoveryLocator,
4708    volume_header: &VolumeHeader,
4709    crypto_header: &CryptoHeaderFixed,
4710) -> Result<PublicTerminalCandidate, FormatError> {
4711    let tuple = CmraDecoderTuple::from(locator);
4712    validate_cmra_decoder_tuple(tuple)?;
4713    let expected_cmra_length = cmra_serialized_length(tuple)?;
4714    if locator.cmra_length as u64 != expected_cmra_length {
4715        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4716    }
4717    validate_locator_position(locator_offset, locator)?;
4718    let recovered = recover_cmra(
4719        bytes,
4720        locator.cmra_offset,
4721        Some(tuple),
4722        CmraRecoveryMode::PublicNoKey,
4723    )?;
4724    if recovered.tuple != tuple {
4725        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4726    }
4727    if expected_cmra_length != recovered.cmra_length {
4728        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4729    }
4730    validate_locator_image_boundary(locator, &recovered.image)?;
4731    validate_cmra_identity_hints(
4732        recovered.header_hints,
4733        Some(CmraIdentityHints::from(locator)),
4734        &recovered.image,
4735    )?;
4736    let terminal = validate_recovered_public_terminal(
4737        recovered.image,
4738        bytes,
4739        volume_header,
4740        crypto_header,
4741        false,
4742    )?;
4743    Ok(PublicTerminalCandidate {
4744        terminal,
4745        anchor: locator_offset
4746            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4747            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4748        cmra_offset: locator.cmra_offset,
4749        cmra_length: recovered.cmra_length,
4750    })
4751}
4752
4753fn parse_locatorless_cmra_candidate(
4754    bytes: &[u8],
4755    cmra_offset: usize,
4756    context: KeyHoldingTerminalContext<'_>,
4757) -> Result<TerminalCandidate, FormatError> {
4758    let recovered = recover_cmra(
4759        bytes,
4760        cmra_offset as u64,
4761        None,
4762        CmraRecoveryMode::KeyHolding,
4763    )?;
4764    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4765        return Err(FormatError::InvalidArchive(
4766            "locatorless CMRA boundary mismatch",
4767        ));
4768    }
4769    if recovered
4770        .image
4771        .volume_trailer_offset
4772        .checked_add(VOLUME_TRAILER_LEN as u64)
4773        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4774        != cmra_offset as u64
4775    {
4776        return Err(FormatError::InvalidArchive(
4777            "locatorless trailer boundary mismatch",
4778        ));
4779    }
4780    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4781    let terminal =
4782        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
4783    Ok(TerminalCandidate {
4784        terminal,
4785        anchor: cmra_offset
4786            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4787            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4788        locator_sequence: None,
4789        cmra_offset: cmra_offset as u64,
4790        cmra_length: recovered.cmra_length,
4791    })
4792}
4793
4794fn parse_locatorless_cmra_candidate_read_at(
4795    reader: &dyn ArchiveReadAt,
4796    cmra_offset: u64,
4797    context: KeyHoldingTerminalContext<'_>,
4798) -> Result<TerminalCandidate, FormatError> {
4799    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4800    if recovered.image.body_bytes_before_cmra != cmra_offset {
4801        return Err(FormatError::InvalidArchive(
4802            "locatorless CMRA boundary mismatch",
4803        ));
4804    }
4805    if recovered
4806        .image
4807        .volume_trailer_offset
4808        .checked_add(VOLUME_TRAILER_LEN as u64)
4809        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4810        != cmra_offset
4811    {
4812        return Err(FormatError::InvalidArchive(
4813            "locatorless trailer boundary mismatch",
4814        ));
4815    }
4816    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4817    let terminal = validate_recovered_terminal_read_at(
4818        recovered.image,
4819        recovered.tuple,
4820        reader,
4821        context,
4822        true,
4823    )?;
4824    Ok(TerminalCandidate {
4825        terminal,
4826        anchor: to_usize(
4827            checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
4828            "CMRA anchor overflow",
4829        )?,
4830        locator_sequence: None,
4831        cmra_offset,
4832        cmra_length: recovered.cmra_length,
4833    })
4834}
4835
4836fn parse_locatorless_cmra_authority_candidate(
4837    bytes: &[u8],
4838    cmra_offset: usize,
4839    master_key: &MasterKey,
4840) -> Result<TerminalAuthorityCandidate, FormatError> {
4841    let recovered = recover_cmra(
4842        bytes,
4843        cmra_offset as u64,
4844        None,
4845        CmraRecoveryMode::KeyHolding,
4846    )?;
4847    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4848        return Err(FormatError::InvalidArchive(
4849            "locatorless CMRA boundary mismatch",
4850        ));
4851    }
4852    if recovered
4853        .image
4854        .volume_trailer_offset
4855        .checked_add(VOLUME_TRAILER_LEN as u64)
4856        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4857        != cmra_offset as u64
4858    {
4859        return Err(FormatError::InvalidArchive(
4860            "locatorless trailer boundary mismatch",
4861        ));
4862    }
4863    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4864    let cmra_length = recovered.cmra_length;
4865    let authority =
4866        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
4867    Ok(TerminalAuthorityCandidate {
4868        authority,
4869        anchor: cmra_offset
4870            .checked_add(to_usize(cmra_length, "CMRA")?)
4871            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4872        cmra_offset: cmra_offset as u64,
4873        cmra_length,
4874    })
4875}
4876
4877fn parse_locatorless_cmra_authority_candidate_read_at(
4878    reader: &dyn ArchiveReadAt,
4879    cmra_offset: u64,
4880    master_key: &MasterKey,
4881) -> Result<TerminalAuthorityCandidate, FormatError> {
4882    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4883    if recovered.image.body_bytes_before_cmra != cmra_offset {
4884        return Err(FormatError::InvalidArchive(
4885            "locatorless CMRA boundary mismatch",
4886        ));
4887    }
4888    if recovered
4889        .image
4890        .volume_trailer_offset
4891        .checked_add(VOLUME_TRAILER_LEN as u64)
4892        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4893        != cmra_offset
4894    {
4895        return Err(FormatError::InvalidArchive(
4896            "locatorless trailer boundary mismatch",
4897        ));
4898    }
4899    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4900    let cmra_length = recovered.cmra_length;
4901    let authority =
4902        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
4903    Ok(TerminalAuthorityCandidate {
4904        authority,
4905        anchor: to_usize(
4906            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
4907            "CMRA anchor overflow",
4908        )?,
4909        cmra_offset,
4910        cmra_length,
4911    })
4912}
4913
4914fn parse_public_locatorless_cmra_candidate(
4915    bytes: &[u8],
4916    cmra_offset: usize,
4917    volume_header: &VolumeHeader,
4918    crypto_header: &CryptoHeaderFixed,
4919) -> Result<PublicTerminalCandidate, FormatError> {
4920    let recovered = recover_cmra(
4921        bytes,
4922        cmra_offset as u64,
4923        None,
4924        CmraRecoveryMode::PublicNoKey,
4925    )?;
4926    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4927        return Err(FormatError::InvalidArchive(
4928            "locatorless CMRA boundary mismatch",
4929        ));
4930    }
4931    if recovered
4932        .image
4933        .volume_trailer_offset
4934        .checked_add(VOLUME_TRAILER_LEN as u64)
4935        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4936        != cmra_offset as u64
4937    {
4938        return Err(FormatError::InvalidArchive(
4939            "locatorless trailer boundary mismatch",
4940        ));
4941    }
4942    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4943    let terminal = validate_recovered_public_terminal(
4944        recovered.image,
4945        bytes,
4946        volume_header,
4947        crypto_header,
4948        true,
4949    )?;
4950    Ok(PublicTerminalCandidate {
4951        terminal,
4952        anchor: cmra_offset
4953            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4954            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4955        cmra_offset: cmra_offset as u64,
4956        cmra_length: recovered.cmra_length,
4957    })
4958}
4959
4960fn validate_locator_position(
4961    locator_offset: usize,
4962    locator: CriticalRecoveryLocator,
4963) -> Result<(), FormatError> {
4964    if locator.cmra_offset != locator.body_bytes_before_cmra {
4965        return Err(FormatError::InvalidArchive(
4966            "locator CMRA boundary mismatch",
4967        ));
4968    }
4969    if locator
4970        .volume_trailer_offset
4971        .checked_add(VOLUME_TRAILER_LEN as u64)
4972        .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
4973        != locator.cmra_offset
4974    {
4975        return Err(FormatError::InvalidArchive(
4976            "locator trailer boundary mismatch",
4977        ));
4978    }
4979    let expected_offset = match locator.locator_sequence {
4980        1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
4981        0 => locator
4982            .cmra_offset
4983            .checked_add(locator.cmra_length as u64)
4984            .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
4985        _ => None,
4986    }
4987    .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
4988    if expected_offset != locator_offset as u64 {
4989        return Err(FormatError::InvalidArchive(
4990            "locator position does not match sequence",
4991        ));
4992    }
4993    Ok(())
4994}
4995
4996fn validate_locator_image_boundary(
4997    locator: CriticalRecoveryLocator,
4998    image: &CriticalMetadataImage,
4999) -> Result<(), FormatError> {
5000    if locator.volume_trailer_offset != image.volume_trailer_offset
5001        || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
5002        || image
5003            .volume_trailer_offset
5004            .checked_add(VOLUME_TRAILER_LEN as u64)
5005            .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5006            != locator.cmra_offset
5007    {
5008        return Err(FormatError::InvalidArchive(
5009            "locator and CMRA image boundaries differ",
5010        ));
5011    }
5012    Ok(())
5013}
5014
5015fn validate_cmra_identity_hints(
5016    header_hints: Option<CmraIdentityHints>,
5017    locator_hints: Option<CmraIdentityHints>,
5018    image: &CriticalMetadataImage,
5019) -> Result<(), FormatError> {
5020    if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
5021        if header != locator {
5022            return Err(FormatError::InvalidArchive(
5023                "CMRA header and locator identity hints differ",
5024            ));
5025        }
5026    }
5027    for hints in [header_hints, locator_hints].into_iter().flatten() {
5028        if hints.archive_uuid != image.archive_uuid
5029            || hints.session_id != image.session_id
5030            || hints.volume_index != image.volume_index
5031        {
5032            return Err(FormatError::InvalidArchive(
5033                "CMRA identity hints do not match recovered image",
5034            ));
5035        }
5036    }
5037    Ok(())
5038}
5039
5040fn recover_cmra(
5041    bytes: &[u8],
5042    cmra_offset: u64,
5043    locator_tuple: Option<CmraDecoderTuple>,
5044    mode: CmraRecoveryMode,
5045) -> Result<RecoveredCmra, FormatError> {
5046    let offset = to_usize(cmra_offset, "CMRA")?;
5047    let header_bytes = slice(
5048        bytes,
5049        offset,
5050        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5051        "CriticalMetadataRecoveryHeader",
5052    )?;
5053    let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
5054    validate_cmra_decoder_tuple(tuple)?;
5055    let cmra_length = cmra_serialized_length(tuple)?;
5056    let cmra_len = to_usize(cmra_length, "CMRA")?;
5057    let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
5058    recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
5059}
5060
5061fn recover_cmra_read_at(
5062    reader: &dyn ArchiveReadAt,
5063    cmra_offset: u64,
5064    locator_tuple: Option<CmraDecoderTuple>,
5065    mode: CmraRecoveryMode,
5066) -> Result<RecoveredCmra, FormatError> {
5067    let header_bytes = read_at_vec(
5068        reader,
5069        cmra_offset,
5070        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5071        "CriticalMetadataRecoveryHeader",
5072    )?;
5073    let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
5074    validate_cmra_decoder_tuple(tuple)?;
5075    let cmra_length = cmra_serialized_length(tuple)?;
5076    let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
5077    recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
5078}
5079
5080fn recover_cmra_header_tuple(
5081    header_bytes: &[u8],
5082    locator_tuple: Option<CmraDecoderTuple>,
5083) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
5084    let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
5085    Ok(match (parsed_header, locator_tuple) {
5086        (Ok(header), Some(locator_tuple)) => {
5087            let header_tuple = CmraDecoderTuple::from(header);
5088            if header_tuple != locator_tuple {
5089                return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
5090            }
5091            (locator_tuple, Some(CmraIdentityHints::from(header)))
5092        }
5093        (Ok(header), None) => (
5094            CmraDecoderTuple::from(header),
5095            Some(CmraIdentityHints::from(header)),
5096        ),
5097        (Err(_), Some(tuple)) => (tuple, None),
5098        (Err(err), _) => return Err(err),
5099    })
5100}
5101
5102fn recover_cmra_from_bytes(
5103    cmra_bytes: &[u8],
5104    tuple: CmraDecoderTuple,
5105    header_hints: Option<CmraIdentityHints>,
5106    cmra_length: u64,
5107    mode: CmraRecoveryMode,
5108) -> Result<RecoveredCmra, FormatError> {
5109    let shard_size = tuple.shard_size as usize;
5110    let mut data_shards = vec![None; tuple.data_shard_count as usize];
5111    let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
5112    let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
5113    for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
5114        let raw = slice(
5115            cmra_bytes,
5116            cursor,
5117            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5118            "CriticalMetadataRecoveryShard",
5119        )?;
5120        let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
5121        if let Some(shard) = shard {
5122            validate_cmra_shard(&shard, idx, tuple)?;
5123            if shard.shard_role == 0 {
5124                let data_slot = data_shards
5125                    .get_mut(idx)
5126                    .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
5127                *data_slot = Some(shard.payload);
5128            } else {
5129                let parity_idx = idx - tuple.data_shard_count as usize;
5130                let parity_slot =
5131                    parity_shards
5132                        .get_mut(parity_idx)
5133                        .ok_or(FormatError::InvalidArchive(
5134                            "CMRA parity shard out of range",
5135                        ))?;
5136                *parity_slot = Some(shard.payload);
5137            }
5138        }
5139        cursor = checked_add(
5140            cursor,
5141            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5142            "CriticalMetadataRecoveryShard",
5143        )?;
5144    }
5145    let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
5146    let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
5147    for shard in repaired {
5148        image_bytes.extend_from_slice(&shard);
5149    }
5150    image_bytes.truncate(tuple.image_length as usize);
5151    if sha256_bytes(&image_bytes) != tuple.image_sha256 {
5152        return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
5153    }
5154    let image = CriticalMetadataImage::parse(&image_bytes)?;
5155    validate_critical_metadata_image(&image, mode)?;
5156    Ok(RecoveredCmra {
5157        image,
5158        tuple,
5159        header_hints,
5160        cmra_length,
5161    })
5162}
5163
5164fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
5165    let shard_size = tuple.shard_size as u64;
5166    if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
5167        return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
5168    }
5169    let image_length = tuple.image_length as u64;
5170    let min = critical_image_min();
5171    let cap = critical_image_cap()?;
5172    if image_length < min || image_length > cap {
5173        return Err(FormatError::InvalidArchive(
5174            "CMRA image_length is outside bounds",
5175        ));
5176    }
5177    let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
5178    if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
5179        return Err(FormatError::InvalidArchive(
5180            "CMRA data_shard_count does not match image length",
5181        ));
5182    }
5183    let max_parity = 2u64.max(ceil_div_u64(
5184        checked_u64_mul(
5185            expected_data_shards,
5186            READER_MAX_CMRA_PARITY_PCT as u64,
5187            "CMRA parity overflow",
5188        )?,
5189        100,
5190    )?);
5191    if tuple.parity_shard_count as u64 > max_parity {
5192        return Err(FormatError::ReaderResourceLimitExceeded {
5193            field: "CMRA parity shard count",
5194            cap: max_parity,
5195            actual: tuple.parity_shard_count as u64,
5196        });
5197    }
5198    let total = expected_data_shards
5199        .checked_add(tuple.parity_shard_count as u64)
5200        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5201    if total > 65_535 {
5202        return Err(FormatError::FecTooManyShards(total as usize));
5203    }
5204    Ok(())
5205}
5206
5207fn validate_cmra_writer_parity_lower_bound(
5208    tuple: CmraDecoderTuple,
5209    bit_rot_buffer_pct: u8,
5210) -> Result<(), FormatError> {
5211    let min_parity = 2u64.max(ceil_div_u64(
5212        checked_u64_mul(
5213            tuple.data_shard_count as u64,
5214            bit_rot_buffer_pct as u64,
5215            "CMRA parity lower-bound overflow",
5216        )?,
5217        100,
5218    )?);
5219    if (tuple.parity_shard_count as u64) < min_parity {
5220        return Err(FormatError::InvalidArchive(
5221            "CMRA parity shard count is below authenticated bit-rot lower bound",
5222        ));
5223    }
5224    Ok(())
5225}
5226
5227fn validate_cmra_shard(
5228    shard: &CriticalMetadataRecoveryShard,
5229    serialized_idx: usize,
5230    tuple: CmraDecoderTuple,
5231) -> Result<(), FormatError> {
5232    if shard.shard_index as usize != serialized_idx {
5233        return Err(FormatError::InvalidArchive(
5234            "CMRA shards are not in canonical order",
5235        ));
5236    }
5237    let data_count = tuple.data_shard_count as usize;
5238    let shard_size = tuple.shard_size as usize;
5239    if serialized_idx < data_count {
5240        if shard.shard_role != 0 {
5241            return Err(FormatError::InvalidArchive(
5242                "CMRA data shard has wrong role",
5243            ));
5244        }
5245        let expected_len = if serialized_idx + 1 == data_count {
5246            let used = tuple.image_length as usize - serialized_idx * shard_size;
5247            if used == 0 {
5248                shard_size
5249            } else {
5250                used
5251            }
5252        } else {
5253            shard_size
5254        };
5255        if shard.shard_payload_length as usize != expected_len {
5256            return Err(FormatError::InvalidArchive(
5257                "CMRA data shard payload length is non-canonical",
5258            ));
5259        }
5260        if serialized_idx + 1 == data_count
5261            && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
5262        {
5263            return Err(FormatError::InvalidArchive(
5264                "CMRA final data shard padding is non-zero",
5265            ));
5266        }
5267    } else {
5268        if shard.shard_role != 1 {
5269            return Err(FormatError::InvalidArchive(
5270                "CMRA parity shard has wrong role",
5271            ));
5272        }
5273        if shard.shard_payload_length as usize != shard_size {
5274            return Err(FormatError::InvalidArchive(
5275                "CMRA parity shard payload length is non-canonical",
5276            ));
5277        }
5278    }
5279    Ok(())
5280}
5281
5282fn validate_critical_metadata_image(
5283    image: &CriticalMetadataImage,
5284    mode: CmraRecoveryMode,
5285) -> Result<(), FormatError> {
5286    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5287    if image.volume_header_offset != 0
5288        || image.volume_header_length != VOLUME_HEADER_LEN as u32
5289        || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
5290        || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5291        || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
5292        || image.body_bytes_before_cmra
5293            != image
5294                .volume_trailer_offset
5295                .checked_add(VOLUME_TRAILER_LEN as u64)
5296                .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5297    {
5298        return Err(FormatError::InvalidArchive(
5299            "CriticalMetadataImage fixed layout is invalid",
5300        ));
5301    }
5302    if root_auth_present {
5303        if image.root_auth_footer_offset == 0
5304            || image.root_auth_footer_length == 0
5305            || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5306        {
5307            return Err(FormatError::InvalidArchive(
5308                "CriticalMetadataImage root-auth range is invalid",
5309            ));
5310        }
5311    } else if image.root_auth_footer_offset != 0
5312        || image.root_auth_footer_length != 0
5313        || image.root_auth_footer_sha256 != [0u8; 32]
5314    {
5315        return Err(FormatError::InvalidArchive(
5316            "CriticalMetadataImage root-auth fields must be zero when absent",
5317        ));
5318    }
5319    let block_record_len = image_block_record_len_from_region(image)?;
5320    let block_record_len_u64 = u64::try_from(block_record_len)
5321        .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
5322    match mode {
5323        CmraRecoveryMode::KeyHolding => {
5324            let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
5325                FormatError::InvalidArchive("BlockRecord region length overflow"),
5326            )?;
5327            if image.block_records_length != expected_len {
5328                return Err(FormatError::InvalidArchive(
5329                    "CriticalMetadataImage terminal equations are invalid",
5330                ));
5331            }
5332        }
5333        CmraRecoveryMode::PublicNoKey => {
5334            if image.block_records_length % block_record_len_u64 != 0 {
5335                return Err(FormatError::InvalidArchive(
5336                    "CriticalMetadataImage BlockRecord region is not aligned",
5337                ));
5338            }
5339        }
5340    }
5341    if image.block_records_offset
5342        != image
5343            .crypto_header_offset
5344            .checked_add(image.crypto_header_length as u64)
5345            .ok_or(FormatError::InvalidArchive(
5346                "CryptoHeader boundary overflow",
5347            ))?
5348        || image.manifest_footer_offset
5349            != image
5350                .block_records_offset
5351                .checked_add(image.block_records_length)
5352                .ok_or(FormatError::InvalidArchive(
5353                    "ManifestFooter boundary overflow",
5354                ))?
5355    {
5356        return Err(FormatError::InvalidArchive(
5357            "CriticalMetadataImage terminal equations are invalid",
5358        ));
5359    }
5360    let manifest_end = image
5361        .manifest_footer_offset
5362        .checked_add(MANIFEST_FOOTER_LEN as u64)
5363        .ok_or(FormatError::InvalidArchive(
5364            "RootAuthFooter boundary overflow",
5365        ))?;
5366    if root_auth_present {
5367        if image.root_auth_footer_offset != manifest_end
5368            || image
5369                .root_auth_footer_offset
5370                .checked_add(image.root_auth_footer_length as u64)
5371                .ok_or(FormatError::InvalidArchive(
5372                    "VolumeTrailer boundary overflow",
5373                ))?
5374                != image.volume_trailer_offset
5375        {
5376            return Err(FormatError::InvalidArchive(
5377                "CriticalMetadataImage root-auth terminal equations are invalid",
5378            ));
5379        }
5380    } else if image.volume_trailer_offset != manifest_end {
5381        return Err(FormatError::InvalidArchive(
5382            "CriticalMetadataImage unsigned terminal equations are invalid",
5383        ));
5384    }
5385    let expected_types: &[u16] = if root_auth_present {
5386        &[1, 2, 3, 4, 5]
5387    } else {
5388        &[1, 2, 3, 5]
5389    };
5390    if image.regions.len() != expected_types.len()
5391        || image
5392            .regions
5393            .iter()
5394            .map(|region| region.region_type)
5395            .ne(expected_types.iter().copied())
5396    {
5397        return Err(FormatError::InvalidArchive(
5398            "CriticalMetadataImage regions are not canonical",
5399        ));
5400    }
5401    validate_image_region(
5402        image,
5403        1,
5404        image.volume_header_offset,
5405        image.volume_header_length,
5406    )?;
5407    validate_image_region(
5408        image,
5409        2,
5410        image.crypto_header_offset,
5411        image.crypto_header_length,
5412    )?;
5413    validate_image_region(
5414        image,
5415        3,
5416        image.manifest_footer_offset,
5417        image.manifest_footer_length,
5418    )?;
5419    if root_auth_present {
5420        validate_image_region(
5421            image,
5422            4,
5423            image.root_auth_footer_offset,
5424            image.root_auth_footer_length,
5425        )?;
5426    }
5427    validate_image_region(
5428        image,
5429        5,
5430        image.volume_trailer_offset,
5431        image.volume_trailer_length,
5432    )?;
5433    if sha256_region(image, 1)? != image.volume_header_sha256
5434        || sha256_region(image, 2)? != image.crypto_header_sha256
5435        || sha256_region(image, 3)? != image.manifest_footer_sha256
5436        || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
5437        || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
5438        || sha256_region(image, 5)? != image.volume_trailer_sha256
5439    {
5440        return Err(FormatError::InvalidArchive(
5441            "CriticalMetadataImage region digest mismatch",
5442        ));
5443    }
5444    Ok(())
5445}
5446
5447fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
5448    let crypto_region = image
5449        .region(2)
5450        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5451    let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5452    crypto.fixed.validate_supported_profile()?;
5453    Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
5454}
5455
5456fn validate_image_region(
5457    image: &CriticalMetadataImage,
5458    region_type: u16,
5459    offset: u64,
5460    length: u32,
5461) -> Result<(), FormatError> {
5462    let region = image
5463        .region(region_type)
5464        .ok_or(FormatError::InvalidArchive(
5465            "missing CriticalMetadataImage region",
5466        ))?;
5467    if region.offset != offset || region.bytes.len() != length as usize {
5468        return Err(FormatError::InvalidArchive(
5469            "CriticalMetadataImage region range mismatch",
5470        ));
5471    }
5472    Ok(())
5473}
5474
5475fn validate_image_identity(
5476    image: &CriticalMetadataImage,
5477    volume_header: &VolumeHeader,
5478    crypto_header: &CryptoHeaderFixed,
5479) -> Result<(), FormatError> {
5480    if image.archive_uuid != volume_header.archive_uuid
5481        || image.session_id != volume_header.session_id
5482        || image.volume_index != volume_header.volume_index
5483        || image.stripe_width != volume_header.stripe_width
5484        || image.stripe_width != crypto_header.stripe_width
5485    {
5486        return Err(FormatError::InvalidArchive(
5487            "CriticalMetadataImage identity does not match selected volume",
5488        ));
5489    }
5490    Ok(())
5491}
5492
5493fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
5494    Ok(sha256_bytes(
5495        &image
5496            .region(region_type)
5497            .ok_or(FormatError::InvalidArchive(
5498                "missing CriticalMetadataImage region",
5499            ))?
5500            .bytes,
5501    ))
5502}
5503
5504fn validate_recovered_terminal_authority(
5505    image: CriticalMetadataImage,
5506    tuple: CmraDecoderTuple,
5507    master_key: &MasterKey,
5508    require_cmra_boundary_magic: bool,
5509) -> Result<RecoveredTerminalAuthority, FormatError> {
5510    let volume_header_region = image
5511        .region(1)
5512        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5513    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5514    let crypto_region = image
5515        .region(2)
5516        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5517    let crypto_header_bytes = crypto_region.bytes.clone();
5518    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
5519    let subkeys = subkeys_for_open(
5520        Some(master_key),
5521        parsed_crypto.fixed.aead_algo,
5522        &volume_header.archive_uuid,
5523        &volume_header.session_id,
5524    )?;
5525    verify_integrity_tag(
5526        HmacDomain::CryptoHeader,
5527        parsed_crypto.fixed.aead_algo,
5528        Some(&subkeys.mac_key),
5529        &volume_header.archive_uuid,
5530        &volume_header.session_id,
5531        parsed_crypto.hmac_covered_bytes,
5532        &parsed_crypto.header_hmac,
5533    )?;
5534    parsed_crypto.validate_extension_semantics()?;
5535    validate_seekable_supported_volume(
5536        &volume_header,
5537        &parsed_crypto.fixed,
5538        &parsed_crypto.extensions,
5539    )?;
5540    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5541    let crypto_header = parsed_crypto.fixed.clone();
5542    if crypto_header.bit_rot_buffer_pct == 0 {
5543        return Err(FormatError::InvalidArchive(
5544            "CMRA startup recovery requires a nonzero bit-rot budget",
5545        ));
5546    }
5547    drop(parsed_crypto);
5548
5549    let terminal = validate_recovered_terminal_inner(
5550        image,
5551        tuple,
5552        require_cmra_boundary_magic,
5553        true,
5554        KeyHoldingTerminalContext {
5555            subkeys: &subkeys,
5556            volume_header: &volume_header,
5557            crypto_header: &crypto_header,
5558            crypto_header_bytes: &crypto_header_bytes,
5559        },
5560    )?;
5561    Ok(RecoveredTerminalAuthority {
5562        terminal,
5563        volume_header,
5564        crypto_header,
5565        crypto_header_bytes,
5566        subkeys,
5567    })
5568}
5569
5570fn validate_recovered_terminal(
5571    image: CriticalMetadataImage,
5572    tuple: CmraDecoderTuple,
5573    bytes: &[u8],
5574    context: KeyHoldingTerminalContext<'_>,
5575    require_cmra_boundary_magic: bool,
5576) -> Result<V41Terminal, FormatError> {
5577    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5578    let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
5579    validate_recovered_terminal_inner(
5580        image,
5581        tuple,
5582        require_cmra_boundary_magic,
5583        cmra_boundary_magic_ok,
5584        context,
5585    )
5586}
5587
5588fn validate_recovered_terminal_read_at(
5589    image: CriticalMetadataImage,
5590    tuple: CmraDecoderTuple,
5591    reader: &dyn ArchiveReadAt,
5592    context: KeyHoldingTerminalContext<'_>,
5593    require_cmra_boundary_magic: bool,
5594) -> Result<V41Terminal, FormatError> {
5595    let mut magic = [0u8; 4];
5596    reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
5597    validate_recovered_terminal_inner(
5598        image,
5599        tuple,
5600        require_cmra_boundary_magic,
5601        magic == *b"TZCR",
5602        context,
5603    )
5604}
5605
5606fn validate_recovered_terminal_inner(
5607    image: CriticalMetadataImage,
5608    tuple: CmraDecoderTuple,
5609    require_cmra_boundary_magic: bool,
5610    cmra_boundary_magic_ok: bool,
5611    context: KeyHoldingTerminalContext<'_>,
5612) -> Result<V41Terminal, FormatError> {
5613    let subkeys = context.subkeys;
5614    let volume_header = context.volume_header;
5615    let crypto_header = context.crypto_header;
5616    let volume_header_region = image
5617        .region(1)
5618        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5619    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5620    if &recovered_volume_header != volume_header {
5621        return Err(FormatError::InvalidArchive(
5622            "CMRA VolumeHeader differs from parsed VolumeHeader",
5623        ));
5624    }
5625    validate_image_identity(&image, volume_header, crypto_header)?;
5626    let crypto_region = image
5627        .region(2)
5628        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5629    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5630    if recovered_crypto.fixed != *crypto_header {
5631        return Err(FormatError::InvalidArchive(
5632            "CMRA CryptoHeader differs from parsed CryptoHeader",
5633        ));
5634    }
5635    let recovered_pre_hmac_len = crypto_region
5636        .bytes
5637        .len()
5638        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5639        .ok_or(FormatError::InvalidArchive(
5640            "CMRA CryptoHeader is too short",
5641        ))?;
5642    let parsed_pre_hmac_len = context
5643        .crypto_header_bytes
5644        .len()
5645        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5646        .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
5647    if recovered_pre_hmac_len != parsed_pre_hmac_len
5648        || crypto_region.bytes[..recovered_pre_hmac_len]
5649            != context.crypto_header_bytes[..parsed_pre_hmac_len]
5650    {
5651        return Err(FormatError::InvalidArchive(
5652            "CMRA CryptoHeader differs from parsed CryptoHeader",
5653        ));
5654    }
5655    verify_integrity_tag(
5656        HmacDomain::CryptoHeader,
5657        recovered_crypto.fixed.aead_algo,
5658        Some(&subkeys.mac_key),
5659        &volume_header.archive_uuid,
5660        &volume_header.session_id,
5661        recovered_crypto.hmac_covered_bytes,
5662        &recovered_crypto.header_hmac,
5663    )?;
5664    validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
5665    recovered_crypto.validate_extension_semantics()?;
5666
5667    let manifest_region = image
5668        .region(3)
5669        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5670    let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
5671    validate_manifest_footer(
5672        volume_header,
5673        crypto_header,
5674        &manifest_footer,
5675        subkeys,
5676        &manifest_region.bytes,
5677    )?;
5678    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5679
5680    let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
5681        let root_auth_region = image
5682            .region(4)
5683            .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5684        let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5685        if footer.archive_uuid != volume_header.archive_uuid
5686            || footer.session_id != volume_header.session_id
5687            || footer.footer_length()? != image.root_auth_footer_length
5688        {
5689            return Err(FormatError::InvalidArchive(
5690                "RootAuthFooter identity or length does not match terminal image",
5691            ));
5692        }
5693        Some(footer)
5694    } else {
5695        None
5696    };
5697
5698    let trailer_region = image
5699        .region(5)
5700        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5701    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5702    verify_integrity_tag(
5703        HmacDomain::VolumeTrailer,
5704        crypto_header.aead_algo,
5705        Some(&subkeys.mac_key),
5706        &volume_header.archive_uuid,
5707        &volume_header.session_id,
5708        &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
5709        &trailer.trailer_hmac,
5710    )?;
5711    validate_trailer_identity(volume_header, &trailer)?;
5712    validate_v41_trailer_equations(&image, &trailer)?;
5713
5714    if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
5715        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5716    }
5717
5718    let manifest_footer_bytes = manifest_region.bytes.clone();
5719    let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
5720    Ok(V41Terminal {
5721        image,
5722        manifest_footer_bytes,
5723        root_auth_footer_bytes,
5724        root_auth_footer,
5725        volume_trailer: trailer,
5726    })
5727}
5728
5729fn validate_recovered_public_terminal(
5730    image: CriticalMetadataImage,
5731    bytes: &[u8],
5732    volume_header: &VolumeHeader,
5733    crypto_header: &CryptoHeaderFixed,
5734    require_cmra_boundary_magic: bool,
5735) -> Result<V41PublicTerminal, FormatError> {
5736    if image.layout_flags & 0x0000_0001 == 0 {
5737        return Err(FormatError::ReaderUnsupported(
5738            "public no-key verification requires root-auth",
5739        ));
5740    }
5741    let volume_header_region = image
5742        .region(1)
5743        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5744    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5745    if &recovered_volume_header != volume_header {
5746        return Err(FormatError::InvalidArchive(
5747            "CMRA VolumeHeader differs from parsed VolumeHeader",
5748        ));
5749    }
5750    validate_image_identity(&image, volume_header, crypto_header)?;
5751    let crypto_region = image
5752        .region(2)
5753        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5754    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5755    if recovered_crypto.fixed != *crypto_header {
5756        return Err(FormatError::InvalidArchive(
5757            "CMRA CryptoHeader differs from parsed CryptoHeader",
5758        ));
5759    }
5760    recovered_crypto.validate_extension_semantics()?;
5761
5762    image
5763        .region(3)
5764        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5765
5766    let root_auth_region = image
5767        .region(4)
5768        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5769    let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5770    if root_auth_footer.archive_uuid != volume_header.archive_uuid
5771        || root_auth_footer.session_id != volume_header.session_id
5772        || root_auth_footer.footer_length()? != image.root_auth_footer_length
5773    {
5774        return Err(FormatError::InvalidArchive(
5775            "public RootAuthFooter identity or length does not match terminal image",
5776        ));
5777    }
5778
5779    let trailer_region = image
5780        .region(5)
5781        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5782    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5783    validate_trailer_identity(volume_header, &trailer)?;
5784    validate_v41_public_trailer_profile(&image, &trailer)?;
5785
5786    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5787    if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
5788        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5789    }
5790
5791    let root_auth_footer_bytes = root_auth_region.bytes.clone();
5792    Ok(V41PublicTerminal {
5793        image,
5794        root_auth_footer_bytes,
5795        root_auth_footer,
5796    })
5797}
5798
5799fn validate_v41_trailer_equations(
5800    image: &CriticalMetadataImage,
5801    trailer: &VolumeTrailer,
5802) -> Result<(), FormatError> {
5803    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5804    if trailer.bytes_written != image.volume_trailer_offset
5805        || trailer.manifest_footer_offset != image.manifest_footer_offset
5806        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5807        || trailer.block_count != image.block_count
5808    {
5809        return Err(FormatError::InvalidArchive(
5810            "VolumeTrailer does not match v41 terminal layout",
5811        ));
5812    }
5813    if root_auth_present {
5814        if trailer.root_auth_flags != 0x0000_0001
5815            || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5816            || trailer.root_auth_footer_length != image.root_auth_footer_length
5817            || image.root_auth_footer_offset
5818                != image
5819                    .manifest_footer_offset
5820                    .checked_add(MANIFEST_FOOTER_LEN as u64)
5821                    .ok_or(FormatError::InvalidArchive(
5822                        "RootAuthFooter trailer boundary overflow",
5823                    ))?
5824            || image
5825                .root_auth_footer_offset
5826                .checked_add(image.root_auth_footer_length as u64)
5827                .ok_or(FormatError::InvalidArchive(
5828                    "RootAuthFooter trailer boundary overflow",
5829                ))?
5830                != image.volume_trailer_offset
5831        {
5832            return Err(FormatError::InvalidArchive(
5833                "VolumeTrailer root-auth fields do not match v41 terminal layout",
5834            ));
5835        }
5836    } else if trailer.root_auth_footer_offset != 0
5837        || trailer.root_auth_footer_length != 0
5838        || trailer.root_auth_flags != 0
5839    {
5840        return Err(FormatError::InvalidArchive(
5841            "VolumeTrailer root-auth fields must be zero when absent",
5842        ));
5843    }
5844    Ok(())
5845}
5846
5847fn validate_v41_public_trailer_profile(
5848    image: &CriticalMetadataImage,
5849    trailer: &VolumeTrailer,
5850) -> Result<(), FormatError> {
5851    if trailer.bytes_written != image.volume_trailer_offset
5852        || trailer.manifest_footer_offset != image.manifest_footer_offset
5853        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5854    {
5855        return Err(FormatError::InvalidArchive(
5856            "VolumeTrailer does not match v41 public terminal layout",
5857        ));
5858    }
5859    if trailer.root_auth_flags != 0x0000_0001
5860        || trailer.root_auth_footer_offset == 0
5861        || trailer.root_auth_footer_length == 0
5862        || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5863        || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5864        || trailer.root_auth_footer_length != image.root_auth_footer_length
5865        || image.root_auth_footer_offset
5866            != image
5867                .manifest_footer_offset
5868                .checked_add(MANIFEST_FOOTER_LEN as u64)
5869                .ok_or(FormatError::InvalidArchive(
5870                    "RootAuthFooter trailer boundary overflow",
5871                ))?
5872        || image
5873            .root_auth_footer_offset
5874            .checked_add(image.root_auth_footer_length as u64)
5875            .ok_or(FormatError::InvalidArchive(
5876                "RootAuthFooter trailer boundary overflow",
5877            ))?
5878            != image.volume_trailer_offset
5879    {
5880        return Err(FormatError::InvalidArchive(
5881            "VolumeTrailer root-auth fields do not match v41 public terminal layout",
5882        ));
5883    }
5884    Ok(())
5885}
5886
5887fn critical_image_min() -> u64 {
5888    const MIN_CRYPTO_HEADER_LEN: u64 = 116;
5889    CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
5890        + 4 * SERIALIZED_REGION_HEADER_LEN as u64
5891        + VOLUME_HEADER_LEN as u64
5892        + MIN_CRYPTO_HEADER_LEN
5893        + MANIFEST_FOOTER_LEN as u64
5894        + VOLUME_TRAILER_LEN as u64
5895        + IMAGE_CRC_LEN as u64
5896}
5897
5898fn critical_image_cap() -> Result<u64, FormatError> {
5899    [
5900        CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
5901        5 * SERIALIZED_REGION_HEADER_LEN as u64,
5902        VOLUME_HEADER_LEN as u64,
5903        READER_MAX_CRYPTO_HEADER_LEN as u64,
5904        MANIFEST_FOOTER_LEN as u64,
5905        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5906        VOLUME_TRAILER_LEN as u64,
5907        IMAGE_CRC_LEN as u64,
5908    ]
5909    .into_iter()
5910    .try_fold(0u64, |total, value| {
5911        total
5912            .checked_add(value)
5913            .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
5914    })
5915}
5916
5917fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
5918    let shard_total = (tuple.data_shard_count as u64)
5919        .checked_add(tuple.parity_shard_count as u64)
5920        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5921    let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
5922        .checked_add(tuple.shard_size as u64)
5923        .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
5924    checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
5925        .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
5926        .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
5927}
5928
5929fn cmra_worst_case_cap() -> Result<u64, FormatError> {
5930    let cap = critical_image_cap()?;
5931    let mut worst = 0u64;
5932    let mut shard_size = 512u64;
5933    while shard_size <= 4096 {
5934        let data = ceil_div_u64(cap, shard_size)?;
5935        let parity = 2u64.max(ceil_div_u64(
5936            checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
5937            100,
5938        )?);
5939        let tuple = CmraDecoderTuple {
5940            shard_size: shard_size as u32,
5941            data_shard_count: u16::try_from(data)
5942                .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
5943            parity_shard_count: u16::try_from(parity)
5944                .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
5945            image_length: u32::try_from(cap)
5946                .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
5947            image_sha256: [0u8; 32],
5948        };
5949        worst = worst.max(cmra_serialized_length(tuple)?);
5950        shard_size += 2;
5951    }
5952    Ok(worst)
5953}
5954
5955pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
5956    let total = [
5957        MANIFEST_FOOTER_LEN as u64,
5958        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5959        VOLUME_TRAILER_LEN as u64,
5960        cmra_worst_case_cap()?,
5961        LOCATOR_PAIR_LEN as u64,
5962    ]
5963    .into_iter()
5964    .try_fold(0u64, |sum, value| {
5965        sum.checked_add(value)
5966            .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
5967    })?;
5968    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
5969}
5970
5971fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
5972    let worst = cmra_worst_case_cap()?;
5973    let total = options
5974        .max_trailing_garbage_scan
5975        .try_into()
5976        .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5977        .and_then(|scan: u64| {
5978            scan.checked_add(worst)
5979                .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
5980                .ok_or(FormatError::InvalidArchive("scan cap overflow"))
5981        })?;
5982    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5983}
5984
5985fn validate_bootstrap_single_volume_input(
5986    volume_header: &VolumeHeader,
5987    crypto_header: &CryptoHeaderFixed,
5988) -> Result<(), FormatError> {
5989    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
5990        return Err(FormatError::ReaderUnsupported(
5991            "bootstrap sidecar reader supports only single-volume archive input",
5992        ));
5993    }
5994    if crypto_header.stripe_width != volume_header.stripe_width {
5995        return Err(FormatError::InvalidArchive(
5996            "VolumeHeader and CryptoHeader stripe_width differ",
5997        ));
5998    }
5999    Ok(())
6000}
6001
6002#[derive(Debug)]
6003struct ParsedBootstrapSidecar {
6004    manifest_footer: Option<ManifestFooter>,
6005    index_root_records_section: Option<(u64, u64)>,
6006    dictionary_records_section: Option<(u64, u64)>,
6007}
6008
6009pub(crate) struct NonSeekableBootstrapMaterial {
6010    pub(crate) manifest_footer: ManifestFooter,
6011    pub(crate) payload_dictionary: Option<Vec<u8>>,
6012}
6013
6014#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6015enum BootstrapSidecarUse {
6016    SeekableAssist,
6017    NonSeekableRandomAccess,
6018}
6019
6020impl ParsedBootstrapSidecar {
6021    fn require_sections_for(
6022        &self,
6023        sidecar_use: BootstrapSidecarUse,
6024        crypto_header: &CryptoHeaderFixed,
6025    ) -> Result<(), FormatError> {
6026        if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
6027            if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
6028                return Err(FormatError::ReaderUnsupported(
6029                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6030                ));
6031            }
6032            if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
6033                return Err(FormatError::ReaderUnsupported(
6034                    "dictionary bootstrap required",
6035                ));
6036            }
6037        }
6038        Ok(())
6039    }
6040}
6041
6042pub(crate) fn parse_non_seekable_bootstrap_material(
6043    bootstrap_sidecar: &[u8],
6044    volume_header: &VolumeHeader,
6045    crypto_header: &CryptoHeaderFixed,
6046    subkeys: &Subkeys,
6047) -> Result<NonSeekableBootstrapMaterial, FormatError> {
6048    validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
6049    let sidecar =
6050        parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
6051    sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
6052    let manifest_footer = sidecar
6053        .manifest_footer
6054        .clone()
6055        .ok_or(FormatError::ReaderUnsupported(
6056            "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6057        ))?;
6058
6059    let mut blocks = BTreeMap::new();
6060    let (offset, length) =
6061        sidecar
6062            .index_root_records_section
6063            .ok_or(FormatError::ReaderUnsupported(
6064                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6065            ))?;
6066    let index_root_records = parse_sidecar_block_records(
6067        bootstrap_sidecar,
6068        crypto_header.block_size as usize,
6069        SidecarBlockRecordsSection {
6070            offset,
6071            length,
6072            extent: index_root_extent_from_manifest(&manifest_footer),
6073            data_kind: BlockKind::IndexRootData,
6074            parity_kind: BlockKind::IndexRootParity,
6075            structure: "IndexRoot",
6076        },
6077    )?;
6078    insert_sidecar_records(&mut blocks, index_root_records)?;
6079
6080    let limits = metadata_limits(crypto_header);
6081    let index_root_plaintext = load_metadata_object_from_parts(
6082        &blocks,
6083        ObjectLoadContext::index_root(
6084            volume_header,
6085            crypto_header,
6086            subkeys,
6087            index_root_extent_from_manifest(&manifest_footer),
6088        ),
6089        manifest_footer.index_root_decompressed_size,
6090    )?;
6091    let index_root = IndexRoot::parse(
6092        &index_root_plaintext,
6093        crypto_header.has_dictionary != 0,
6094        limits,
6095    )?;
6096
6097    if crypto_header.has_dictionary != 0 {
6098        let (offset, length) =
6099            sidecar
6100                .dictionary_records_section
6101                .ok_or(FormatError::ReaderUnsupported(
6102                    "dictionary bootstrap required",
6103                ))?;
6104        let dictionary_records = parse_sidecar_block_records(
6105            bootstrap_sidecar,
6106            crypto_header.block_size as usize,
6107            SidecarBlockRecordsSection {
6108                offset,
6109                length,
6110                extent: dictionary_extent_from_index_root(&index_root)?,
6111                data_kind: BlockKind::DictionaryData,
6112                parity_kind: BlockKind::DictionaryParity,
6113                structure: "dictionary",
6114            },
6115        )?;
6116        insert_sidecar_records(&mut blocks, dictionary_records)?;
6117    }
6118    let payload_dictionary =
6119        load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
6120
6121    Ok(NonSeekableBootstrapMaterial {
6122        manifest_footer,
6123        payload_dictionary,
6124    })
6125}
6126
6127fn parse_bootstrap_sidecar(
6128    bytes: &[u8],
6129    volume_header: &VolumeHeader,
6130    crypto_header: &CryptoHeaderFixed,
6131    subkeys: &Subkeys,
6132) -> Result<ParsedBootstrapSidecar, FormatError> {
6133    let header_bytes = slice(
6134        bytes,
6135        0,
6136        BOOTSTRAP_SIDECAR_HEADER_LEN,
6137        "BootstrapSidecarHeader",
6138    )?;
6139    let header = BootstrapSidecarHeader::parse(header_bytes)?;
6140    if header.archive_uuid != volume_header.archive_uuid
6141        || header.session_id != volume_header.session_id
6142    {
6143        return Err(FormatError::InvalidArchive(
6144            "bootstrap sidecar identity does not match VolumeHeader",
6145        ));
6146    }
6147    verify_integrity_tag(
6148        HmacDomain::BootstrapSidecar,
6149        crypto_header.aead_algo,
6150        Some(&subkeys.mac_key),
6151        &volume_header.archive_uuid,
6152        &volume_header.session_id,
6153        &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
6154        &header.sidecar_hmac,
6155    )?;
6156    header.validate_packed_layout(bytes.len() as u64)?;
6157    validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
6158
6159    if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
6160        return Err(FormatError::InvalidArchive(
6161            "bootstrap sidecar has dictionary records while has_dictionary is false",
6162        ));
6163    }
6164
6165    let manifest_footer = if header.has_manifest_footer() {
6166        let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
6167        let manifest_bytes = slice(
6168            bytes,
6169            manifest_offset,
6170            MANIFEST_FOOTER_LEN,
6171            "ManifestFooter",
6172        )?;
6173        let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
6174        validate_sidecar_manifest_footer(
6175            volume_header,
6176            crypto_header,
6177            &manifest_footer,
6178            subkeys,
6179            manifest_bytes,
6180        )?;
6181        manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
6182        Some(manifest_footer)
6183    } else {
6184        None
6185    };
6186
6187    Ok(ParsedBootstrapSidecar {
6188        manifest_footer,
6189        index_root_records_section: header.has_index_root_records().then_some((
6190            header.index_root_records_offset,
6191            header.index_root_records_length,
6192        )),
6193        dictionary_records_section: header.has_dictionary_records().then_some((
6194            header.dictionary_records_offset,
6195            header.dictionary_records_length,
6196        )),
6197    })
6198}
6199
6200fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
6201    ObjectExtent {
6202        first_block_index: manifest_footer.index_root_first_block,
6203        data_block_count: manifest_footer.index_root_data_block_count,
6204        parity_block_count: manifest_footer.index_root_parity_block_count,
6205        encrypted_size: manifest_footer.index_root_encrypted_size,
6206    }
6207}
6208
6209fn insert_sidecar_records(
6210    blocks: &mut BTreeMap<u64, BlockRecord>,
6211    records: Vec<BlockRecord>,
6212) -> Result<(), FormatError> {
6213    for record in records {
6214        if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
6215            if existing != record {
6216                return Err(FormatError::InvalidArchive(
6217                    "bootstrap sidecar conflicts with volume BlockRecord",
6218                ));
6219            }
6220        }
6221    }
6222    Ok(())
6223}
6224
6225fn validate_sidecar_manifest_footer(
6226    volume_header: &VolumeHeader,
6227    crypto_header: &CryptoHeaderFixed,
6228    footer: &ManifestFooter,
6229    subkeys: &Subkeys,
6230    raw: &[u8],
6231) -> Result<(), FormatError> {
6232    if footer.archive_uuid != volume_header.archive_uuid
6233        || footer.session_id != volume_header.session_id
6234    {
6235        return Err(FormatError::InvalidArchive(
6236            "sidecar ManifestFooter identity does not match VolumeHeader",
6237        ));
6238    }
6239    if footer.volume_index != 0 {
6240        return Err(FormatError::InvalidArchive(
6241            "sidecar ManifestFooter volume_index must be zero",
6242        ));
6243    }
6244    if footer.total_volumes != crypto_header.stripe_width {
6245        return Err(FormatError::InvalidArchive(
6246            "sidecar ManifestFooter total_volumes does not match stripe_width",
6247        ));
6248    }
6249    if footer.is_authoritative != 1 {
6250        return Err(FormatError::InvalidArchive(
6251            "sidecar ManifestFooter is not authoritative",
6252        ));
6253    }
6254    verify_integrity_tag(
6255        HmacDomain::ManifestFooter,
6256        crypto_header.aead_algo,
6257        Some(&subkeys.mac_key),
6258        &volume_header.archive_uuid,
6259        &volume_header.session_id,
6260        &raw[..MANIFEST_HMAC_COVERED_LEN],
6261        &footer.manifest_hmac,
6262    )
6263}
6264
6265fn validate_sidecar_size_cap(
6266    header: &BootstrapSidecarHeader,
6267    crypto_header: &CryptoHeaderFixed,
6268    file_size: u64,
6269) -> Result<(), FormatError> {
6270    let record_len = checked_u64_add(
6271        crypto_header.block_size as u64,
6272        BLOCK_RECORD_FRAMING_LEN as u64,
6273        "bootstrap sidecar cap overflow",
6274    )?;
6275    let max_index_records = crypto_header.index_root_fec_data_shards as u64
6276        + crypto_header.index_root_fec_parity_shards as u64;
6277    let max_record_section_bytes = checked_u64_mul(
6278        max_index_records,
6279        record_len,
6280        "bootstrap sidecar cap overflow",
6281    )?;
6282    if header.index_root_records_length % record_len != 0 {
6283        return Err(FormatError::InvalidArchive(
6284            "bootstrap sidecar IndexRoot records length is not aligned",
6285        ));
6286    }
6287    if header.index_root_records_length / record_len > max_index_records {
6288        return Err(FormatError::InvalidArchive(
6289            "bootstrap sidecar IndexRoot records exceed resource cap",
6290        ));
6291    }
6292    if header.dictionary_records_length % record_len != 0 {
6293        return Err(FormatError::InvalidArchive(
6294            "bootstrap sidecar dictionary records length is not aligned",
6295        ));
6296    }
6297    if header.dictionary_records_length / record_len > max_index_records {
6298        return Err(FormatError::InvalidArchive(
6299            "bootstrap sidecar dictionary records exceed resource cap",
6300        ));
6301    }
6302
6303    let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
6304    if header.has_manifest_footer() {
6305        cap = cap
6306            .checked_add(MANIFEST_FOOTER_LEN as u64)
6307            .ok_or(FormatError::InvalidArchive(
6308                "bootstrap sidecar cap overflow",
6309            ))?;
6310    }
6311    if header.has_index_root_records() {
6312        cap = checked_u64_add(
6313            cap,
6314            max_record_section_bytes,
6315            "bootstrap sidecar cap overflow",
6316        )?;
6317    }
6318    if header.has_dictionary_records() {
6319        cap = checked_u64_add(
6320            cap,
6321            max_record_section_bytes,
6322            "bootstrap sidecar cap overflow",
6323        )?;
6324    }
6325    if file_size > cap {
6326        return Err(FormatError::InvalidArchive(
6327            "bootstrap sidecar exceeds resource cap",
6328        ));
6329    }
6330    Ok(())
6331}
6332
6333#[derive(Debug, Clone, Copy)]
6334struct SidecarBlockRecordsSection {
6335    offset: u64,
6336    length: u64,
6337    extent: ObjectExtent,
6338    data_kind: BlockKind,
6339    parity_kind: BlockKind,
6340    structure: &'static str,
6341}
6342
6343fn parse_sidecar_block_records(
6344    sidecar_bytes: &[u8],
6345    block_size: usize,
6346    section: SidecarBlockRecordsSection,
6347) -> Result<Vec<BlockRecord>, FormatError> {
6348    let record_len = block_size
6349        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6350        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6351    if section.length % record_len as u64 != 0 {
6352        return Err(FormatError::InvalidArchive(
6353            "sidecar BlockRecord section is not aligned",
6354        ));
6355    }
6356    let expected_count =
6357        section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
6358    let actual_count = usize::try_from(section.length / record_len as u64)
6359        .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
6360    if actual_count != expected_count {
6361        return Err(FormatError::InvalidArchive(
6362            "sidecar BlockRecord section does not match declared extent",
6363        ));
6364    }
6365    let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
6366    let raw = slice(
6367        sidecar_bytes,
6368        start,
6369        to_usize(section.length, "BootstrapSidecarHeader")?,
6370        "BootstrapSidecarHeader",
6371    )?;
6372    let mut records = Vec::with_capacity(expected_count);
6373
6374    for idx in 0..expected_count {
6375        let record = BlockRecord::parse(
6376            slice(raw, idx * record_len, record_len, "BlockRecord")?,
6377            block_size,
6378        )?;
6379        let expected_block_index = checked_u64_add(
6380            section.extent.first_block_index,
6381            idx as u64,
6382            section.structure,
6383        )?;
6384        if record.block_index != expected_block_index {
6385            return Err(FormatError::InvalidArchive(
6386                "sidecar BlockRecord section has missing or duplicate blocks",
6387            ));
6388        }
6389        let expected_kind = if idx < section.extent.data_block_count as usize {
6390            section.data_kind
6391        } else {
6392            section.parity_kind
6393        };
6394        if record.kind != expected_kind {
6395            return Err(FormatError::InvalidArchive(
6396                "sidecar BlockRecord section has wrong kind",
6397            ));
6398        }
6399        let should_be_last = idx + 1 == section.extent.data_block_count as usize;
6400        if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
6401        {
6402            return Err(FormatError::InvalidArchive(
6403                "sidecar BlockRecord section has wrong last-data flag",
6404            ));
6405        }
6406        records.push(record);
6407    }
6408
6409    Ok(records)
6410}
6411
6412fn validate_trailer_identity(
6413    volume_header: &VolumeHeader,
6414    trailer: &VolumeTrailer,
6415) -> Result<(), FormatError> {
6416    if trailer.archive_uuid != volume_header.archive_uuid
6417        || trailer.session_id != volume_header.session_id
6418        || trailer.volume_index != volume_header.volume_index
6419    {
6420        return Err(FormatError::InvalidArchive(
6421            "VolumeTrailer identity does not match VolumeHeader",
6422        ));
6423    }
6424    Ok(())
6425}
6426
6427fn validate_manifest_footer(
6428    volume_header: &VolumeHeader,
6429    crypto_header: &CryptoHeaderFixed,
6430    footer: &ManifestFooter,
6431    subkeys: &Subkeys,
6432    raw: &[u8],
6433) -> Result<(), FormatError> {
6434    if footer.archive_uuid != volume_header.archive_uuid
6435        || footer.session_id != volume_header.session_id
6436        || footer.volume_index != volume_header.volume_index
6437    {
6438        return Err(FormatError::InvalidArchive(
6439            "ManifestFooter identity does not match VolumeHeader",
6440        ));
6441    }
6442    if footer.total_volumes != volume_header.stripe_width {
6443        return Err(FormatError::InvalidArchive(
6444            "ManifestFooter total_volumes does not match stripe_width",
6445        ));
6446    }
6447    if footer.is_authoritative != 1 {
6448        return Err(FormatError::InvalidArchive(
6449            "ManifestFooter is not authoritative",
6450        ));
6451    }
6452    verify_integrity_tag(
6453        HmacDomain::ManifestFooter,
6454        crypto_header.aead_algo,
6455        Some(&subkeys.mac_key),
6456        &volume_header.archive_uuid,
6457        &volume_header.session_id,
6458        &raw[..MANIFEST_HMAC_COVERED_LEN],
6459        &footer.manifest_hmac,
6460    )
6461}
6462
6463#[derive(Debug)]
6464struct ParsedBlockRegion {
6465    blocks: BTreeMap<u64, BlockRecord>,
6466    erased_block_indices: BTreeSet<u64>,
6467}
6468
6469fn parse_block_region(
6470    bytes: &[u8],
6471    start: usize,
6472    end: usize,
6473    block_size: usize,
6474    volume_header: &VolumeHeader,
6475    trailer: &VolumeTrailer,
6476) -> Result<ParsedBlockRegion, FormatError> {
6477    if end < start {
6478        return Err(FormatError::InvalidArchive(
6479            "ManifestFooter starts before BlockRecord region",
6480        ));
6481    }
6482    let record_len = block_size
6483        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6484        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6485    let region_len = end - start;
6486    if region_len % record_len != 0 {
6487        return Err(FormatError::InvalidArchive(
6488            "BlockRecord region length is not aligned",
6489        ));
6490    }
6491    let observed_count = region_len / record_len;
6492    if observed_count as u64 != trailer.block_count {
6493        return Err(FormatError::InvalidArchive(
6494            "VolumeTrailer block_count does not match BlockRecord region",
6495        ));
6496    }
6497
6498    let mut blocks = BTreeMap::new();
6499    let mut erased_block_indices = BTreeSet::new();
6500    for idx in 0..observed_count {
6501        let offset = start + idx * record_len;
6502        let expected_block_index = checked_u64_add(
6503            volume_header.volume_index as u64,
6504            checked_u64_mul(
6505                idx as u64,
6506                volume_header.stripe_width as u64,
6507                "BlockRecord index overflow",
6508            )?,
6509            "BlockRecord index overflow",
6510        )?;
6511        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6512        match BlockRecord::parse(raw, block_size) {
6513            Ok(record) => {
6514                if record.block_index != expected_block_index {
6515                    return Err(FormatError::InvalidArchive(
6516                        "BlockRecord index does not match volume position",
6517                    ));
6518                }
6519                if blocks.insert(record.block_index, record).is_some() {
6520                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6521                }
6522            }
6523            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
6524                if !erased_block_indices.insert(expected_block_index) {
6525                    return Err(FormatError::InvalidArchive(
6526                        "duplicate erased BlockRecord index",
6527                    ));
6528                }
6529            }
6530            Err(err) => return Err(err),
6531        }
6532    }
6533
6534    Ok(ParsedBlockRegion {
6535        blocks,
6536        erased_block_indices,
6537    })
6538}
6539
6540fn validate_seekable_block_region_layout(
6541    start: u64,
6542    end: u64,
6543    block_size: usize,
6544    trailer: &VolumeTrailer,
6545) -> Result<(), FormatError> {
6546    if end < start {
6547        return Err(FormatError::InvalidArchive(
6548            "ManifestFooter starts before BlockRecord region",
6549        ));
6550    }
6551    let record_len = block_record_len(block_size)?;
6552    let region_len = end - start;
6553    if region_len % record_len != 0 {
6554        return Err(FormatError::InvalidArchive(
6555            "BlockRecord region length is not aligned",
6556        ));
6557    }
6558    let observed_count = region_len / record_len;
6559    if observed_count != trailer.block_count {
6560        return Err(FormatError::InvalidArchive(
6561            "VolumeTrailer block_count does not match BlockRecord region",
6562        ));
6563    }
6564    Ok(())
6565}
6566
6567fn parse_public_block_observation(
6568    bytes: &[u8],
6569    start: usize,
6570    image: &CriticalMetadataImage,
6571    block_size: usize,
6572    volume_header: &VolumeHeader,
6573) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
6574    let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
6575    if start != image_start {
6576        return Err(FormatError::InvalidArchive(
6577            "public BlockRecord observation start mismatch",
6578        ));
6579    }
6580    let scan_limit_u64 = image
6581        .block_records_offset
6582        .checked_add(image.block_records_length)
6583        .ok_or(FormatError::InvalidArchive(
6584            "public BlockRecord observation limit overflow",
6585        ))?;
6586    if scan_limit_u64 != image.manifest_footer_offset {
6587        return Err(FormatError::InvalidArchive(
6588            "public BlockRecord observation limit mismatch",
6589        ));
6590    }
6591    let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
6592    if scan_limit < start {
6593        return Err(FormatError::InvalidArchive(
6594            "public BlockRecord observation limit before start",
6595        ));
6596    }
6597    let record_len = block_size
6598        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6599        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6600    let region_len = scan_limit - start;
6601    if region_len % record_len != 0 {
6602        return Err(FormatError::InvalidArchive(
6603            "public BlockRecord observation window is not aligned",
6604        ));
6605    }
6606
6607    let mut blocks = BTreeMap::new();
6608    let mut offset = start;
6609    let mut observed_slot = 0u64;
6610    while offset < scan_limit {
6611        let magic_end = checked_add(offset, 4, "BlockRecord")?;
6612        if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
6613            break;
6614        }
6615        let record_end = checked_add(offset, record_len, "BlockRecord")?;
6616        if record_end > scan_limit {
6617            return Err(FormatError::InvalidArchive(
6618                "public BlockRecord observation slot is incomplete",
6619            ));
6620        }
6621        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6622        let record = BlockRecord::parse(raw, block_size)?;
6623        let expected_block_index = checked_u64_add(
6624            volume_header.volume_index as u64,
6625            checked_u64_mul(
6626                observed_slot,
6627                volume_header.stripe_width as u64,
6628                "BlockRecord index overflow",
6629            )?,
6630            "BlockRecord index overflow",
6631        )?;
6632        if record.block_index != expected_block_index {
6633            return Err(FormatError::InvalidArchive(
6634                "public BlockRecord index does not match volume position",
6635            ));
6636        }
6637        if blocks.insert(record.block_index, record).is_some() {
6638            return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6639        }
6640        offset = record_end;
6641        observed_slot = observed_slot
6642            .checked_add(1)
6643            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6644    }
6645
6646    let mut scan = if offset < scan_limit {
6647        checked_add(offset, record_len, "BlockRecord")?
6648    } else {
6649        scan_limit
6650    };
6651    while scan < scan_limit {
6652        let magic_end = checked_add(scan, 4, "BlockRecord")?;
6653        let record_end = checked_add(scan, record_len, "BlockRecord")?;
6654        if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
6655            let raw = slice(bytes, scan, record_len, "BlockRecord")?;
6656            if BlockRecord::parse(raw, block_size).is_ok() {
6657                return Err(FormatError::InvalidArchive(
6658                    "public observation has ambiguous extra BlockRecord",
6659                ));
6660            }
6661        }
6662        scan = record_end;
6663    }
6664
6665    Ok(blocks)
6666}
6667
6668pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
6669    matches!(
6670        error,
6671        FormatError::BadCrc {
6672            structure: "BlockRecord",
6673        } | FormatError::BadMagic {
6674            structure: "BlockRecord",
6675        } | FormatError::NonZeroReserved {
6676            structure: "BlockRecord",
6677        }
6678    )
6679}
6680
6681fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
6682    let len = block_size
6683        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6684        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6685    u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
6686}
6687
6688fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
6689    lhs.checked_mul(rhs)
6690        .ok_or(FormatError::InvalidArchive(reason))
6691}
6692
6693fn parse_stream_block_prefix(
6694    bytes: &[u8],
6695    start: usize,
6696    block_size: usize,
6697    volume_header: &VolumeHeader,
6698) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
6699    let record_len = block_size
6700        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6701        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6702    let mut blocks = BTreeMap::new();
6703    let mut offset = start;
6704    let mut observed_block_count = 0u64;
6705
6706    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6707        let expected_block_index =
6708            expected_stream_block_index(volume_header, observed_block_count)?;
6709        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6710        match BlockRecord::parse(raw, block_size) {
6711            Ok(record) => {
6712                if record.block_index != expected_block_index {
6713                    return Err(FormatError::InvalidArchive(
6714                        "BlockRecord index does not match stream position",
6715                    ));
6716                }
6717                if blocks.insert(record.block_index, record).is_some() {
6718                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6719                }
6720            }
6721            Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
6722            Err(err) => return Err(err),
6723        }
6724        offset = checked_add(offset, record_len, "BlockRecord")?;
6725        observed_block_count = observed_block_count
6726            .checked_add(1)
6727            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6728    }
6729
6730    Ok((blocks, offset, observed_block_count))
6731}
6732
6733pub(crate) fn expected_stream_block_index(
6734    volume_header: &VolumeHeader,
6735    observed_block_count: u64,
6736) -> Result<u64, FormatError> {
6737    checked_u64_add(
6738        volume_header.volume_index as u64,
6739        checked_u64_mul(
6740            observed_block_count,
6741            volume_header.stripe_width as u64,
6742            "BlockRecord index overflow",
6743        )?,
6744        "BlockRecord index overflow",
6745    )
6746}
6747
6748fn parse_sequential_block_or_erasure(
6749    bytes: &[u8],
6750    offset: usize,
6751    record_len: usize,
6752    block_size: usize,
6753    volume_header: &VolumeHeader,
6754    observed_block_count: u64,
6755) -> Result<Option<BlockRecord>, FormatError> {
6756    let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
6757    let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6758    match BlockRecord::parse(raw, block_size) {
6759        Ok(record) => {
6760            if record.block_index != expected_block_index {
6761                return Err(FormatError::InvalidArchive(
6762                    "BlockRecord index does not match stream position",
6763                ));
6764            }
6765            Ok(Some(record))
6766        }
6767        Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
6768        Err(err) => Err(err),
6769    }
6770}
6771
6772fn parse_terminal_material(
6773    bytes: &[u8],
6774    manifest_offset: usize,
6775    observed_block_count: u64,
6776    context: KeyHoldingTerminalContext<'_>,
6777    options: ReaderOptions,
6778) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
6779    let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
6780    if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
6781        return Err(FormatError::InvalidArchive(
6782            "sequential terminal does not end at EOF",
6783        ));
6784    }
6785    let terminal = candidate.terminal;
6786    if terminal.image.manifest_footer_offset != manifest_offset as u64 {
6787        return Err(FormatError::InvalidArchive(
6788            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6789        ));
6790    }
6791    if terminal.volume_trailer.block_count != observed_block_count {
6792        return Err(FormatError::InvalidArchive(
6793            "VolumeTrailer block_count does not match observed stream",
6794        ));
6795    }
6796    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6797    Ok((
6798        manifest_footer,
6799        terminal.volume_trailer,
6800        terminal.root_auth_footer,
6801    ))
6802}
6803
6804pub(crate) fn parse_terminal_material_read_at(
6805    reader: &dyn ArchiveReadAt,
6806    input_len: u64,
6807    manifest_offset: u64,
6808    observed_block_count: u64,
6809    context: KeyHoldingTerminalContext<'_>,
6810) -> Result<SequentialTerminalMaterial, FormatError> {
6811    let mut candidates = Vec::new();
6812    if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
6813        collect_v41_locator_candidate_read_at(
6814            reader,
6815            input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6816            0,
6817            context,
6818            &mut candidates,
6819        );
6820    }
6821    if input_len >= LOCATOR_PAIR_LEN as u64 {
6822        collect_v41_locator_candidate_read_at(
6823            reader,
6824            input_len - LOCATOR_PAIR_LEN as u64,
6825            1,
6826            context,
6827            &mut candidates,
6828        );
6829    }
6830
6831    let candidate = choose_v41_terminal_candidate(candidates)?;
6832    if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
6833        return Err(FormatError::InvalidArchive(
6834            "sequential terminal does not end at EOF",
6835        ));
6836    }
6837    let terminal = candidate.terminal;
6838    if terminal.image.manifest_footer_offset != manifest_offset {
6839        return Err(FormatError::InvalidArchive(
6840            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6841        ));
6842    }
6843    if terminal.volume_trailer.block_count != observed_block_count {
6844        return Err(FormatError::InvalidArchive(
6845            "VolumeTrailer block_count does not match observed stream",
6846        ));
6847    }
6848    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6849    Ok(SequentialTerminalMaterial {
6850        manifest_footer,
6851        volume_trailer: terminal.volume_trailer,
6852        root_auth_footer: terminal.root_auth_footer,
6853    })
6854}
6855
6856fn terminal_candidate_reaches_eof(
6857    candidate: &TerminalCandidate,
6858    input_len: usize,
6859) -> Result<bool, FormatError> {
6860    let expected_end =
6861        match candidate.locator_sequence {
6862            Some(0) => candidate.anchor,
6863            Some(1) => candidate
6864                .anchor
6865                .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6866                .ok_or(FormatError::InvalidArchive(
6867                    "terminal EOF boundary overflow",
6868                ))?,
6869            None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
6870                FormatError::InvalidArchive("terminal EOF boundary overflow"),
6871            )?,
6872            Some(_) => {
6873                return Err(FormatError::InvalidArchive(
6874                    "invalid terminal locator sequence",
6875                ))
6876            }
6877        };
6878    Ok(expected_end == input_len)
6879}
6880
6881#[derive(Debug, Default)]
6882struct PendingSequentialEnvelope {
6883    data_shards: Vec<Option<Vec<u8>>>,
6884    parity_shards: Vec<Option<Vec<u8>>>,
6885    saw_last_data: bool,
6886    awaiting_tentative_parity: bool,
6887}
6888
6889impl PendingSequentialEnvelope {
6890    fn is_empty(&self) -> bool {
6891        self.data_shards.is_empty() && self.parity_shards.is_empty()
6892    }
6893}
6894
6895fn handle_sequential_payload_erasure(
6896    pending: &mut PendingSequentialEnvelope,
6897    crypto_header: &CryptoHeaderFixed,
6898    metadata_seen: bool,
6899) -> Result<(), FormatError> {
6900    if metadata_seen || pending.saw_last_data {
6901        return Err(FormatError::BadCrc {
6902            structure: "BlockRecord",
6903        });
6904    }
6905    if !sequential_payload_parity_is_guaranteed(crypto_header) {
6906        return Err(FormatError::BadCrc {
6907            structure: "BlockRecord",
6908        });
6909    }
6910    pending.data_shards.push(None);
6911    pending.awaiting_tentative_parity = true;
6912    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
6913        return Err(FormatError::InvalidArchive(
6914            "sequential payload envelope exceeds data-shard cap",
6915        ));
6916    }
6917    Ok(())
6918}
6919
6920fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
6921    crypto_header.fec_parity_shards > 0
6922        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
6923}
6924
6925fn sequential_extract_tar_stream_with_options(
6926    bytes: &[u8],
6927    master_key: &MasterKey,
6928    options: ReaderOptions,
6929) -> Result<Vec<u8>, FormatError> {
6930    validate_reader_options(options)?;
6931    if bytes.len() < VOLUME_HEADER_LEN {
6932        return Err(FormatError::InvalidLength {
6933            structure: "archive",
6934            expected: VOLUME_HEADER_LEN,
6935            actual: bytes.len(),
6936        });
6937    }
6938
6939    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
6940    let crypto_start = volume_header.crypto_header_offset as usize;
6941    let crypto_len = volume_header.crypto_header_length as usize;
6942    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
6943    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
6944    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
6945    let subkeys = subkeys_for_open(
6946        Some(master_key),
6947        parsed_crypto.fixed.aead_algo,
6948        &volume_header.archive_uuid,
6949        &volume_header.session_id,
6950    )?;
6951    verify_integrity_tag(
6952        HmacDomain::CryptoHeader,
6953        parsed_crypto.fixed.aead_algo,
6954        Some(&subkeys.mac_key),
6955        &volume_header.archive_uuid,
6956        &volume_header.session_id,
6957        parsed_crypto.hmac_covered_bytes,
6958        &parsed_crypto.header_hmac,
6959    )?;
6960    parsed_crypto.validate_extension_semantics()?;
6961    validate_sequential_supported_volume(
6962        &volume_header,
6963        &parsed_crypto.fixed,
6964        &parsed_crypto.extensions,
6965    )?;
6966    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
6967
6968    let block_size = parsed_crypto.fixed.block_size as usize;
6969    let record_len = block_size
6970        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6971        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6972    let mut offset = crypto_end;
6973    let mut observed_block_count = 0u64;
6974    let mut metadata_seen = false;
6975    let mut pending = PendingSequentialEnvelope::default();
6976    let mut next_envelope_index = 0u64;
6977    let mut tar_stream = Vec::new();
6978    let max_tar_stream_size = options.max_verify_tar_size;
6979    let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
6980    let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
6981    let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
6982        parsed_crypto.fixed.max_path_length,
6983        total_extraction_cap,
6984    );
6985
6986    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6987        let record = parse_sequential_block_or_erasure(
6988            bytes,
6989            offset,
6990            record_len,
6991            block_size,
6992            &volume_header,
6993            observed_block_count,
6994        )?;
6995        observed_block_count = observed_block_count
6996            .checked_add(1)
6997            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6998        let Some(record) = record else {
6999            handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
7000            offset = checked_add(offset, record_len, "BlockRecord")?;
7001            continue;
7002        };
7003
7004        match record.kind {
7005            BlockKind::PayloadData => {
7006                if metadata_seen {
7007                    return Err(FormatError::InvalidArchive(
7008                        "payload BlockRecord appears after metadata",
7009                    ));
7010                }
7011                if pending.awaiting_tentative_parity {
7012                    return Err(FormatError::InvalidArchive(
7013                        "sequential payload envelope boundary is ambiguous after CRC erasure",
7014                    ));
7015                }
7016                if pending.saw_last_data {
7017                    finalize_sequential_envelope(
7018                        &mut pending,
7019                        SequentialEnvelopeDecodeContext {
7020                            crypto_header: &parsed_crypto.fixed,
7021                            subkeys: &subkeys,
7022                            volume_header: &volume_header,
7023                            next_envelope_index: &mut next_envelope_index,
7024                            tar_stream: &mut tar_stream,
7025                            max_tar_stream_size,
7026                            tar_stream_total_validator: &mut tar_stream_total_validator,
7027                        },
7028                    )?;
7029                }
7030                let is_last_data = record.is_last_data();
7031                pending.data_shards.push(Some(record.payload));
7032                if is_last_data {
7033                    pending.saw_last_data = true;
7034                }
7035                if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
7036                    return Err(FormatError::InvalidArchive(
7037                        "sequential payload envelope exceeds data-shard cap",
7038                    ));
7039                }
7040            }
7041            BlockKind::PayloadParity => {
7042                if metadata_seen {
7043                    return Err(FormatError::InvalidArchive(
7044                        "payload parity BlockRecord appears after metadata",
7045                    ));
7046                }
7047                if pending.awaiting_tentative_parity {
7048                    pending.awaiting_tentative_parity = false;
7049                    pending.saw_last_data = true;
7050                } else if pending.data_shards.is_empty() || !pending.saw_last_data {
7051                    return Err(FormatError::InvalidArchive(
7052                        "payload parity appears before envelope data is complete",
7053                    ));
7054                }
7055                pending.parity_shards.push(Some(record.payload));
7056                if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
7057                    return Err(FormatError::InvalidArchive(
7058                        "sequential payload envelope exceeds parity-shard cap",
7059                    ));
7060                }
7061            }
7062            _ => {
7063                if !pending.is_empty() {
7064                    finalize_sequential_envelope(
7065                        &mut pending,
7066                        SequentialEnvelopeDecodeContext {
7067                            crypto_header: &parsed_crypto.fixed,
7068                            subkeys: &subkeys,
7069                            volume_header: &volume_header,
7070                            next_envelope_index: &mut next_envelope_index,
7071                            tar_stream: &mut tar_stream,
7072                            max_tar_stream_size,
7073                            tar_stream_total_validator: &mut tar_stream_total_validator,
7074                        },
7075                    )?;
7076                }
7077                metadata_seen = true;
7078            }
7079        }
7080
7081        offset = checked_add(offset, record_len, "BlockRecord")?;
7082    }
7083
7084    if !pending.is_empty() {
7085        finalize_sequential_envelope(
7086            &mut pending,
7087            SequentialEnvelopeDecodeContext {
7088                crypto_header: &parsed_crypto.fixed,
7089                subkeys: &subkeys,
7090                volume_header: &volume_header,
7091                next_envelope_index: &mut next_envelope_index,
7092                tar_stream: &mut tar_stream,
7093                max_tar_stream_size,
7094                tar_stream_total_validator: &mut tar_stream_total_validator,
7095            },
7096        )?;
7097    }
7098
7099    parse_terminal_material(
7100        bytes,
7101        offset,
7102        observed_block_count,
7103        KeyHoldingTerminalContext {
7104            subkeys: &subkeys,
7105            volume_header: &volume_header,
7106            crypto_header: &parsed_crypto.fixed,
7107            crypto_header_bytes: crypto_bytes,
7108        },
7109        options,
7110    )?;
7111    // This public helper is intentionally whole-buffer: decoded payload bytes
7112    // stay internal until terminal ManifestFooter and VolumeTrailer HMACs pass.
7113    validate_tar_stream_total_extraction_size(
7114        &tar_stream,
7115        parsed_crypto.fixed.max_path_length,
7116        total_extraction_cap,
7117    )?;
7118    Ok(tar_stream)
7119}
7120
7121fn validate_sequential_supported_volume(
7122    volume_header: &VolumeHeader,
7123    crypto_header: &CryptoHeaderFixed,
7124    extensions: &[ExtensionTlv<'_>],
7125) -> Result<(), FormatError> {
7126    reject_unsupported_raw_stream_profile(extensions)?;
7127    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
7128        return Err(FormatError::ReaderUnsupported(
7129            "sequential reader supports only single-volume archive input",
7130        ));
7131    }
7132    if crypto_header.stripe_width != volume_header.stripe_width {
7133        return Err(FormatError::InvalidArchive(
7134            "VolumeHeader and CryptoHeader stripe_width differ",
7135        ));
7136    }
7137    if crypto_header.has_dictionary != 0 {
7138        return Err(FormatError::ReaderUnsupported(
7139            "dictionary bootstrap required for non-seekable sequential extraction",
7140        ));
7141    }
7142    Ok(())
7143}
7144
7145struct SequentialEnvelopeDecodeContext<'a> {
7146    crypto_header: &'a CryptoHeaderFixed,
7147    subkeys: &'a Subkeys,
7148    volume_header: &'a VolumeHeader,
7149    next_envelope_index: &'a mut u64,
7150    tar_stream: &'a mut Vec<u8>,
7151    max_tar_stream_size: usize,
7152    tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
7153}
7154
7155fn finalize_sequential_envelope(
7156    pending: &mut PendingSequentialEnvelope,
7157    context: SequentialEnvelopeDecodeContext<'_>,
7158) -> Result<(), FormatError> {
7159    if !pending.saw_last_data {
7160        return Err(FormatError::InvalidArchive(
7161            "sequential payload envelope is missing last-data flag",
7162        ));
7163    }
7164    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
7165        return Err(FormatError::InvalidArchive(
7166            "sequential payload envelope exceeds data-shard cap",
7167        ));
7168    }
7169    if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
7170        return Err(FormatError::InvalidArchive(
7171            "sequential payload envelope exceeds parity-shard cap",
7172        ));
7173    }
7174    let required_parity =
7175        required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
7176    if pending.parity_shards.len() < required_parity as usize {
7177        return Err(FormatError::InvalidArchive(
7178            "sequential payload envelope has insufficient parity for recovery settings",
7179        ));
7180    }
7181
7182    let repaired = repair_data_gf16(
7183        &pending.data_shards,
7184        &pending.parity_shards,
7185        context.crypto_header.block_size as usize,
7186    )?;
7187    let mut encrypted =
7188        Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
7189    for shard in repaired {
7190        encrypted.extend_from_slice(&shard);
7191    }
7192    let plaintext = decrypt_padded_aead_object(
7193        AeadObjectContext {
7194            algo: context.crypto_header.aead_algo,
7195            key: &context.subkeys.enc_key,
7196            nonce_seed: &context.subkeys.nonce_seed,
7197            domain: b"envelope",
7198            archive_uuid: &context.volume_header.archive_uuid,
7199            session_id: &context.volume_header.session_id,
7200            counter: *context.next_envelope_index,
7201        },
7202        &encrypted,
7203    )?;
7204    decode_concatenated_zstd_frames_with_cap(
7205        &plaintext,
7206        None,
7207        context.tar_stream,
7208        context.max_tar_stream_size,
7209        Some(context.tar_stream_total_validator),
7210    )?;
7211    *context.next_envelope_index = (*context.next_envelope_index)
7212        .checked_add(1)
7213        .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
7214    *pending = PendingSequentialEnvelope::default();
7215    Ok(())
7216}
7217
7218fn decode_concatenated_zstd_frames_with_cap(
7219    plaintext: &[u8],
7220    dictionary: Option<&[u8]>,
7221    output: &mut Vec<u8>,
7222    max_output_len: usize,
7223    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7224) -> Result<(), FormatError> {
7225    let mut cursor = 0usize;
7226    while cursor < plaintext.len() {
7227        let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
7228            .map_err(|_| FormatError::InvalidZstdFrame)?;
7229        if frame_len == 0 {
7230            return Err(FormatError::InvalidZstdFrame);
7231        }
7232        let end = checked_add(cursor, frame_len, "zstd frame")?;
7233        validate_exact_zstd_frame(&plaintext[cursor..end])?;
7234        if let Some(dictionary) = dictionary {
7235            let mut decoder =
7236                zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
7237                    .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7238            read_zstd_frame_to_capped_output(
7239                &mut decoder,
7240                output,
7241                max_output_len,
7242                tar_stream_total_validator.as_deref_mut(),
7243            )?;
7244        } else {
7245            let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
7246                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7247            read_zstd_frame_to_capped_output(
7248                &mut decoder,
7249                output,
7250                max_output_len,
7251                tar_stream_total_validator.as_deref_mut(),
7252            )?;
7253        }
7254        cursor = end;
7255    }
7256    Ok(())
7257}
7258
7259fn read_zstd_frame_to_capped_output<R: Read>(
7260    decoder: &mut R,
7261    output: &mut Vec<u8>,
7262    max_output_len: usize,
7263    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7264) -> Result<(), FormatError> {
7265    let mut buf = [0u8; 64 * 1024];
7266    loop {
7267        let read = decoder
7268            .read(&mut buf)
7269            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7270        if read == 0 {
7271            return Ok(());
7272        }
7273        let next_len = output
7274            .len()
7275            .checked_add(read)
7276            .ok_or(FormatError::ReaderUnsupported(
7277                "sequential tar stream exceeds configured verification cap",
7278            ))?;
7279        if next_len > max_output_len {
7280            return Err(FormatError::ReaderUnsupported(
7281                "sequential tar stream exceeds configured verification cap",
7282            ));
7283        }
7284        output.extend_from_slice(&buf[..read]);
7285        if let Some(validator) = tar_stream_total_validator.as_mut() {
7286            validator.observe(output)?;
7287        }
7288    }
7289}
7290
7291fn load_archive_dictionary(
7292    blocks: &impl BlockProvider,
7293    subkeys: &Subkeys,
7294    volume_header: &VolumeHeader,
7295    crypto_header: &CryptoHeaderFixed,
7296    index_root: &IndexRoot,
7297) -> Result<Option<Vec<u8>>, FormatError> {
7298    if crypto_header.has_dictionary == 0 {
7299        return Ok(None);
7300    }
7301    let plaintext = load_metadata_object_from_parts(
7302        blocks,
7303        ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
7304        index_root.header.dictionary_decompressed_size,
7305    )?;
7306    Ok(Some(plaintext))
7307}
7308
7309#[derive(Clone, Copy)]
7310struct ObjectLoadContext<'a> {
7311    volume_header: &'a VolumeHeader,
7312    crypto_header: &'a CryptoHeaderFixed,
7313    extent: ObjectExtent,
7314    data_kind: BlockKind,
7315    parity_kind: BlockKind,
7316    key: &'a [u8; 32],
7317    nonce_seed: &'a [u8; 32],
7318    domain: &'a [u8],
7319    counter: u64,
7320    class_data_shard_max: u16,
7321    class_parity_shard_max: u16,
7322}
7323
7324impl<'a> ObjectLoadContext<'a> {
7325    fn index_root(
7326        volume_header: &'a VolumeHeader,
7327        crypto_header: &'a CryptoHeaderFixed,
7328        subkeys: &'a Subkeys,
7329        extent: ObjectExtent,
7330    ) -> Self {
7331        Self {
7332            volume_header,
7333            crypto_header,
7334            extent,
7335            data_kind: BlockKind::IndexRootData,
7336            parity_kind: BlockKind::IndexRootParity,
7337            key: &subkeys.index_root_key,
7338            nonce_seed: &subkeys.index_nonce_seed,
7339            domain: b"idxroot",
7340            counter: 0,
7341            class_data_shard_max: crypto_header.index_root_fec_data_shards,
7342            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7343        }
7344    }
7345
7346    fn index_shard(
7347        volume_header: &'a VolumeHeader,
7348        crypto_header: &'a CryptoHeaderFixed,
7349        subkeys: &'a Subkeys,
7350        entry: &ShardEntry,
7351    ) -> Self {
7352        Self {
7353            volume_header,
7354            crypto_header,
7355            extent: ObjectExtent {
7356                first_block_index: entry.first_block_index,
7357                data_block_count: entry.data_block_count,
7358                parity_block_count: entry.parity_block_count,
7359                encrypted_size: entry.encrypted_size,
7360            },
7361            data_kind: BlockKind::IndexShardData,
7362            parity_kind: BlockKind::IndexShardParity,
7363            key: &subkeys.index_shard_key,
7364            nonce_seed: &subkeys.index_nonce_seed,
7365            domain: b"idxshard",
7366            counter: entry.shard_index,
7367            class_data_shard_max: crypto_header.index_fec_data_shards,
7368            class_parity_shard_max: crypto_header.index_fec_parity_shards,
7369        }
7370    }
7371
7372    fn directory_hint(
7373        volume_header: &'a VolumeHeader,
7374        crypto_header: &'a CryptoHeaderFixed,
7375        subkeys: &'a Subkeys,
7376        entry: &DirectoryHintShardEntry,
7377    ) -> Self {
7378        Self {
7379            volume_header,
7380            crypto_header,
7381            extent: ObjectExtent {
7382                first_block_index: entry.first_block_index,
7383                data_block_count: entry.data_block_count,
7384                parity_block_count: entry.parity_block_count,
7385                encrypted_size: entry.encrypted_size,
7386            },
7387            data_kind: BlockKind::DirectoryHintData,
7388            parity_kind: BlockKind::DirectoryHintParity,
7389            key: &subkeys.dir_hint_key,
7390            nonce_seed: &subkeys.index_nonce_seed,
7391            domain: b"dirhint",
7392            counter: entry.hint_shard_index,
7393            class_data_shard_max: crypto_header.index_fec_data_shards,
7394            class_parity_shard_max: crypto_header.index_fec_parity_shards,
7395        }
7396    }
7397
7398    fn dictionary(
7399        volume_header: &'a VolumeHeader,
7400        crypto_header: &'a CryptoHeaderFixed,
7401        subkeys: &'a Subkeys,
7402        index_root: &IndexRoot,
7403    ) -> Result<Self, FormatError> {
7404        Ok(Self {
7405            volume_header,
7406            crypto_header,
7407            extent: dictionary_extent_from_index_root(index_root)?,
7408            data_kind: BlockKind::DictionaryData,
7409            parity_kind: BlockKind::DictionaryParity,
7410            key: &subkeys.dictionary_key,
7411            nonce_seed: &subkeys.index_nonce_seed,
7412            domain: b"dict",
7413            counter: 0,
7414            class_data_shard_max: crypto_header.index_root_fec_data_shards,
7415            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7416        })
7417    }
7418
7419    fn payload(
7420        volume_header: &'a VolumeHeader,
7421        crypto_header: &'a CryptoHeaderFixed,
7422        subkeys: &'a Subkeys,
7423        envelope: &EnvelopeEntry,
7424    ) -> Self {
7425        Self {
7426            volume_header,
7427            crypto_header,
7428            extent: ObjectExtent {
7429                first_block_index: envelope.first_block_index,
7430                data_block_count: envelope.data_block_count,
7431                parity_block_count: envelope.parity_block_count,
7432                encrypted_size: envelope.encrypted_size,
7433            },
7434            data_kind: BlockKind::PayloadData,
7435            parity_kind: BlockKind::PayloadParity,
7436            key: &subkeys.enc_key,
7437            nonce_seed: &subkeys.nonce_seed,
7438            domain: b"envelope",
7439            counter: envelope.envelope_index,
7440            class_data_shard_max: crypto_header.fec_data_shards,
7441            class_parity_shard_max: crypto_header.fec_parity_shards,
7442        }
7443    }
7444}
7445
7446fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
7447    if index_root.header.dictionary_data_block_count == 0
7448        || index_root.header.dictionary_encrypted_size == 0
7449        || index_root.header.dictionary_decompressed_size == 0
7450    {
7451        return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
7452    }
7453    Ok(ObjectExtent {
7454        first_block_index: index_root.header.dictionary_first_block,
7455        data_block_count: index_root.header.dictionary_data_block_count,
7456        parity_block_count: index_root.header.dictionary_parity_block_count,
7457        encrypted_size: index_root.header.dictionary_encrypted_size,
7458    })
7459}
7460
7461fn load_metadata_object_from_parts(
7462    blocks: &impl BlockProvider,
7463    context: ObjectLoadContext<'_>,
7464    decompressed_size: u32,
7465) -> Result<Vec<u8>, FormatError> {
7466    let compressed = load_decrypted_object_from_parts(blocks, context)?;
7467    decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
7468}
7469
7470fn load_decrypted_object_from_parts(
7471    blocks: &impl BlockProvider,
7472    context: ObjectLoadContext<'_>,
7473) -> Result<Vec<u8>, FormatError> {
7474    load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
7475}
7476
7477fn load_decrypted_object_from_parts_with_parity_policy(
7478    blocks: &impl BlockProvider,
7479    context: ObjectLoadContext<'_>,
7480    parity_policy: ParityReadPolicy,
7481) -> Result<Vec<u8>, FormatError> {
7482    let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
7483        blocks,
7484        context.crypto_header,
7485        context.extent,
7486        context.data_kind,
7487        context.parity_kind,
7488        context.class_data_shard_max,
7489        context.class_parity_shard_max,
7490        parity_policy,
7491    )?;
7492    let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
7493    for shard in repaired {
7494        encrypted.extend_from_slice(&shard);
7495    }
7496    if encrypted.len() != context.extent.encrypted_size as usize {
7497        return Err(FormatError::InvalidArchive(
7498            "object encrypted size does not match repaired shards",
7499        ));
7500    }
7501
7502    decrypt_padded_aead_object(
7503        AeadObjectContext {
7504            algo: context.crypto_header.aead_algo,
7505            key: context.key,
7506            nonce_seed: context.nonce_seed,
7507            domain: context.domain,
7508            archive_uuid: &context.volume_header.archive_uuid,
7509            session_id: &context.volume_header.session_id,
7510            counter: context.counter,
7511        },
7512        &encrypted,
7513    )
7514}
7515
7516fn load_repaired_object_data_shards_from_parts(
7517    blocks: &impl BlockProvider,
7518    crypto_header: &CryptoHeaderFixed,
7519    extent: ObjectExtent,
7520    data_kind: BlockKind,
7521    parity_kind: BlockKind,
7522    class_data_shard_max: u16,
7523    class_parity_shard_max: u16,
7524) -> Result<Vec<Vec<u8>>, FormatError> {
7525    load_repaired_object_data_shards_from_parts_with_parity_policy(
7526        blocks,
7527        crypto_header,
7528        extent,
7529        data_kind,
7530        parity_kind,
7531        class_data_shard_max,
7532        class_parity_shard_max,
7533        ParityReadPolicy::Always,
7534    )
7535}
7536
7537#[allow(clippy::too_many_arguments)]
7538fn load_repaired_object_data_shards_from_parts_with_parity_policy(
7539    blocks: &impl BlockProvider,
7540    crypto_header: &CryptoHeaderFixed,
7541    extent: ObjectExtent,
7542    data_kind: BlockKind,
7543    parity_kind: BlockKind,
7544    class_data_shard_max: u16,
7545    class_parity_shard_max: u16,
7546    parity_policy: ParityReadPolicy,
7547) -> Result<Vec<Vec<u8>>, FormatError> {
7548    validate_object_extent(
7549        extent,
7550        crypto_header,
7551        class_data_shard_max,
7552        class_parity_shard_max,
7553    )?;
7554    let block_size = crypto_header.block_size as usize;
7555    let data_count = extent.data_block_count as usize;
7556    let parity_count = extent.parity_block_count as usize;
7557    let mut data_shards = Vec::with_capacity(data_count);
7558    let mut parity_shards = Vec::with_capacity(parity_count);
7559
7560    for offset in 0..data_count {
7561        let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
7562        if let Some(record) = blocks.block(block_index)? {
7563            if record.kind != data_kind {
7564                return Err(FormatError::InvalidArchive(
7565                    "object data block has unexpected kind",
7566                ));
7567            }
7568            let should_be_last = offset + 1 == data_count;
7569            if record.is_last_data() != should_be_last {
7570                return Err(FormatError::InvalidArchive(
7571                    "object last-data flag is not on the final data block",
7572                ));
7573            }
7574            data_shards.push(Some(record.payload.clone()));
7575        } else {
7576            data_shards.push(None);
7577        }
7578    }
7579
7580    if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
7581        return repair_data_gf16(&data_shards, &[], block_size);
7582    }
7583
7584    for offset in 0..parity_count {
7585        let block_index = checked_u64_add(
7586            extent.first_block_index,
7587            data_count as u64 + offset as u64,
7588            "object",
7589        )?;
7590        if let Some(record) = blocks.block(block_index)? {
7591            if record.kind != parity_kind {
7592                return Err(FormatError::InvalidArchive(
7593                    "object parity block has unexpected kind",
7594                ));
7595            }
7596            if record.is_last_data() {
7597                return Err(FormatError::InvalidArchive(
7598                    "object parity block has last-data flag",
7599                ));
7600            }
7601            parity_shards.push(Some(record.payload.clone()));
7602        } else {
7603            parity_shards.push(None);
7604        }
7605    }
7606
7607    repair_data_gf16(&data_shards, &parity_shards, block_size)
7608}
7609
7610fn validate_object_extent(
7611    extent: ObjectExtent,
7612    crypto_header: &CryptoHeaderFixed,
7613    class_data_shard_max: u16,
7614    class_parity_shard_max: u16,
7615) -> Result<(), FormatError> {
7616    if extent.data_block_count == 0 || extent.encrypted_size == 0 {
7617        return Err(FormatError::InvalidArchive(
7618            "encrypted object has zero data blocks or size",
7619        ));
7620    }
7621    if extent.data_block_count > class_data_shard_max as u32 {
7622        return Err(FormatError::InvalidArchive(
7623            "encrypted object exceeds its class data-shard maximum",
7624        ));
7625    }
7626    if extent.parity_block_count > class_parity_shard_max as u32 {
7627        return Err(FormatError::InvalidArchive(
7628            "encrypted object exceeds its class parity-shard maximum",
7629        ));
7630    }
7631    let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
7632    if extent.parity_block_count != required_parity {
7633        return Err(FormatError::InvalidArchive(
7634            "encrypted object parity does not match v41 compute_parity",
7635        ));
7636    }
7637    let total = checked_u64_add(
7638        extent.data_block_count as u64,
7639        extent.parity_block_count as u64,
7640        "encrypted object shard count overflow",
7641    )?;
7642    if total > 65_535 {
7643        return Err(FormatError::FecTooManyShards(total as usize));
7644    }
7645    let expected = checked_u64_mul(
7646        extent.data_block_count as u64,
7647        crypto_header.block_size as u64,
7648        "encrypted object size overflow",
7649    )?;
7650    if expected != extent.encrypted_size as u64 {
7651        return Err(FormatError::InvalidArchive(
7652            "encrypted object size is not data_block_count * block_size",
7653        ));
7654    }
7655    if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
7656        return Err(FormatError::InvalidArchive(
7657            "encrypted object is too small for AEAD tag",
7658        ));
7659    }
7660    Ok(())
7661}
7662
7663pub(crate) fn required_object_parity(
7664    data_block_count: u64,
7665    crypto_header: &CryptoHeaderFixed,
7666) -> Result<u32, FormatError> {
7667    let min_parity =
7668        if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
7669            1
7670        } else {
7671            0
7672        };
7673    let mut parity = 0u64;
7674    for _ in 0..100 {
7675        let total = data_block_count
7676            .checked_add(parity)
7677            .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
7678        let by_volume = checked_u64_mul(
7679            crypto_header.volume_loss_tolerance as u64,
7680            ceil_div_u64(total, crypto_header.stripe_width as u64)?,
7681            "volume-loss parity overflow",
7682        )?;
7683        let by_bitrot = ceil_div_u64(
7684            checked_u64_mul(
7685                total,
7686                crypto_header.bit_rot_buffer_pct as u64,
7687                "bit-rot parity overflow",
7688            )?,
7689            100,
7690        )?;
7691        let next = by_volume
7692            .checked_add(by_bitrot)
7693            .ok_or(FormatError::InvalidArchive("parity overflow"))?
7694            .max(min_parity);
7695        if next == parity {
7696            return u32::try_from(next)
7697                .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
7698        }
7699        parity = next;
7700    }
7701    Err(FormatError::InvalidArchive(
7702        "parity calculation did not converge",
7703    ))
7704}
7705
7706fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
7707    if denominator == 0 {
7708        return Err(FormatError::InvalidArchive("division by zero"));
7709    }
7710    numerator
7711        .checked_add(denominator - 1)
7712        .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
7713        .map(|value| value / denominator)
7714}
7715
7716fn frame_range_for_file<'b>(
7717    shard: &'b IndexShard,
7718    file: &FileEntry,
7719) -> Result<Vec<&'b FrameEntry>, FormatError> {
7720    let mut frames = Vec::with_capacity(file.frame_count as usize);
7721    for offset in 0..file.frame_count as u64 {
7722        let frame_index =
7723            file.first_frame_index
7724                .checked_add(offset)
7725                .ok_or(FormatError::InvalidArchive(
7726                    "FileEntry frame range overflow",
7727                ))?;
7728        let frame = shard
7729            .frames
7730            .iter()
7731            .find(|entry| entry.frame_index == frame_index)
7732            .ok_or(FormatError::InvalidArchive(
7733                "FileEntry references missing FrameEntry",
7734            ))?;
7735        frames.push(frame);
7736    }
7737    Ok(frames)
7738}
7739
7740fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
7741    MetadataLimits {
7742        block_size: crypto_header.block_size,
7743        max_path_length: crypto_header.max_path_length,
7744        max_payload_data_shards: crypto_header.fec_data_shards,
7745        max_payload_parity_shards: crypto_header.fec_parity_shards,
7746        max_index_data_shards: crypto_header.index_fec_data_shards,
7747        max_index_parity_shards: crypto_header.index_fec_parity_shards,
7748        max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
7749        max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
7750        ..MetadataLimits::default()
7751    }
7752}
7753
7754fn verify_dense_keys<T>(
7755    entries: &BTreeMap<u64, T>,
7756    expected_count: u64,
7757    structure: &'static str,
7758) -> Result<(), FormatError> {
7759    if entries.len() as u64 != expected_count {
7760        return Err(FormatError::InvalidArchive(
7761            "decoded table count does not match IndexRoot",
7762        ));
7763    }
7764    for expected in 0..expected_count {
7765        if !entries.contains_key(&expected) {
7766            return Err(FormatError::InvalidMetadata {
7767                structure,
7768                reason: "global index coverage has a gap",
7769            });
7770        }
7771    }
7772    Ok(())
7773}
7774
7775fn validate_envelope_frame_coverage(
7776    frames: &BTreeMap<u64, FrameEntry>,
7777    envelopes: &BTreeMap<u64, EnvelopeEntry>,
7778) -> Result<(), FormatError> {
7779    let mut accounted_frames = BTreeSet::new();
7780    for envelope in envelopes.values() {
7781        let first = envelope.first_frame_index;
7782        let end =
7783            first
7784                .checked_add(envelope.frame_count as u64)
7785                .ok_or(FormatError::InvalidArchive(
7786                    "EnvelopeEntry frame range overflow",
7787                ))?;
7788        let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
7789        for frame_index in first..end {
7790            let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
7791                "EnvelopeEntry references missing FrameEntry",
7792            ))?;
7793            if frame.envelope_index != envelope.envelope_index {
7794                return Err(FormatError::InvalidArchive(
7795                    "FrameEntry envelope_index does not match containing EnvelopeEntry",
7796                ));
7797            }
7798            if !accounted_frames.insert(frame_index) {
7799                return Err(FormatError::InvalidArchive(
7800                    "FrameEntry is covered by multiple EnvelopeEntries",
7801                ));
7802            }
7803            let start = frame.offset_in_envelope as usize;
7804            let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
7805            if end > envelope.plaintext_size as usize {
7806                return Err(FormatError::InvalidArchive(
7807                    "FrameEntry exceeds EnvelopeEntry plaintext_size",
7808                ));
7809            }
7810            ranges.push((start, end));
7811        }
7812        validate_exact_coverage_ranges(
7813            &mut ranges,
7814            envelope.plaintext_size as usize,
7815            "EnvelopeEntry frame coverage has a gap or overlap",
7816        )?;
7817    }
7818
7819    for frame_index in frames.keys() {
7820        if !accounted_frames.contains(frame_index) {
7821            return Err(FormatError::InvalidArchive(
7822                "FrameEntry is not covered by any EnvelopeEntry",
7823            ));
7824        }
7825    }
7826    Ok(())
7827}
7828
7829fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
7830    let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
7831    for shard in shards {
7832        for (idx, file) in shard.files.iter().enumerate() {
7833            let path = shard
7834                .file_path(idx)
7835                .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
7836                .to_vec();
7837            let start = shard
7838                .tar_member_group_start(idx)
7839                .ok_or(FormatError::InvalidArchive(
7840                    "FileEntry tar member start is missing",
7841                ))?;
7842            let key = (file.path_hash, path, start);
7843            validate_global_file_table_key_step(previous.as_ref(), &key)?;
7844            previous = Some(key);
7845        }
7846    }
7847    Ok(())
7848}
7849
7850fn validate_global_file_table_key_step(
7851    previous: Option<&([u8; 8], Vec<u8>, u64)>,
7852    current: &([u8; 8], Vec<u8>, u64),
7853) -> Result<(), FormatError> {
7854    if let Some(previous) = previous {
7855        if previous >= current {
7856            return Err(FormatError::InvalidArchive(
7857                "global FileEntry rows are not sorted and unique",
7858            ));
7859        }
7860    }
7861    Ok(())
7862}
7863
7864fn validate_file_extent_coverage_ranges(
7865    extents: &[(u64, u64)],
7866    tar_len: u64,
7867) -> Result<(), FormatError> {
7868    let mut ranges = Vec::with_capacity(extents.len());
7869    for (start, len) in extents {
7870        let end = checked_u64_add(*start, *len, "FileEntry")?;
7871        if end > tar_len {
7872            return Err(FormatError::InvalidArchive(
7873                "FileEntry extent exceeds IndexRoot tar_total_size",
7874            ));
7875        }
7876        ranges.push((*start, end));
7877    }
7878    validate_exact_coverage_ranges_u64(
7879        &mut ranges,
7880        tar_len,
7881        "FileEntry extents do not cover tar stream exactly",
7882    )
7883}
7884
7885fn add_expected_directory_hint_rows(
7886    map: &mut DirectoryHintMap,
7887    shard_row_index: u32,
7888    path: &[u8],
7889    kind: TarEntryKind,
7890) {
7891    map.entry(Vec::new()).or_default().insert(shard_row_index);
7892    for (idx, byte) in path.iter().enumerate() {
7893        if *byte == b'/' {
7894            map.entry(path[..idx].to_vec())
7895                .or_default()
7896                .insert(shard_row_index);
7897        }
7898    }
7899    if kind == TarEntryKind::Directory {
7900        map.entry(path.to_vec())
7901            .or_default()
7902            .insert(shard_row_index);
7903    }
7904}
7905
7906fn validate_directory_hint_tables_against_expected(
7907    tables: &[DirectoryHintTable],
7908    expected: &DirectoryHintMap,
7909) -> Result<(), FormatError> {
7910    let mut actual = Vec::new();
7911    let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
7912
7913    for table in tables {
7914        for entry_index in 0..table.entries.len() {
7915            let path = table
7916                .entry_path(entry_index)
7917                .ok_or(FormatError::InvalidArchive(
7918                    "DirectoryHintEntry path is missing",
7919                ))?;
7920            let key = (hash_prefix(path), path.to_vec());
7921            if let Some(previous) = &previous_key {
7922                if previous >= &key {
7923                    return Err(FormatError::InvalidArchive(
7924                        "DirectoryHintEntry rows are not globally sorted",
7925                    ));
7926                }
7927            }
7928            previous_key = Some(key);
7929
7930            let rows =
7931                table
7932                    .shard_rows_for_entry(entry_index)
7933                    .ok_or(FormatError::InvalidArchive(
7934                        "DirectoryHintEntry shard rows are missing",
7935                    ))?;
7936            actual.push((path.to_vec(), rows.to_vec()));
7937        }
7938    }
7939
7940    if actual != sorted_directory_hint_rows(expected) {
7941        return Err(FormatError::InvalidArchive(
7942            "directory hint map does not match decoded files",
7943        ));
7944    }
7945    Ok(())
7946}
7947
7948fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
7949    let mut rows = map
7950        .iter()
7951        .map(|(path, shard_rows)| {
7952            (
7953                path.clone(),
7954                shard_rows.iter().copied().collect::<Vec<u32>>(),
7955            )
7956        })
7957        .collect::<Vec<_>>();
7958    rows.sort_by(|(left_path, _), (right_path, _)| {
7959        hash_prefix(left_path)
7960            .cmp(&hash_prefix(right_path))
7961            .then_with(|| left_path.cmp(right_path))
7962    });
7963    rows
7964}
7965
7966fn validate_exact_coverage_ranges(
7967    ranges: &mut [(usize, usize)],
7968    expected_end: usize,
7969    reason: &'static str,
7970) -> Result<(), FormatError> {
7971    ranges.sort_unstable();
7972    let mut cursor = 0usize;
7973    for (start, end) in ranges.iter().copied() {
7974        if start != cursor || end < start {
7975            return Err(FormatError::InvalidArchive(reason));
7976        }
7977        cursor = end;
7978    }
7979    if cursor != expected_end {
7980        return Err(FormatError::InvalidArchive(reason));
7981    }
7982    Ok(())
7983}
7984
7985fn validate_exact_coverage_ranges_u64(
7986    ranges: &mut [(u64, u64)],
7987    expected_end: u64,
7988    reason: &'static str,
7989) -> Result<(), FormatError> {
7990    ranges.sort_unstable();
7991    let mut cursor = 0u64;
7992    for (start, end) in ranges.iter().copied() {
7993        if start != cursor || end < start {
7994            return Err(FormatError::InvalidArchive(reason));
7995        }
7996        cursor = end;
7997    }
7998    if cursor != expected_end {
7999        return Err(FormatError::InvalidArchive(reason));
8000    }
8001    Ok(())
8002}
8003
8004fn object_block_range(
8005    first_block_index: u64,
8006    data_block_count: u32,
8007    parity_block_count: u32,
8008    structure: &'static str,
8009) -> Result<(u64, u64), FormatError> {
8010    let total = data_block_count as u64 + parity_block_count as u64;
8011    if total == 0 {
8012        return Err(FormatError::InvalidArchive(structure));
8013    }
8014    let end = checked_u64_add(first_block_index, total, structure)?;
8015    Ok((first_block_index, end))
8016}
8017
8018fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
8019    ranges.sort_unstable();
8020    for pair in ranges.windows(2) {
8021        if pair[0].1 > pair[1].0 {
8022            return Err(FormatError::InvalidArchive(
8023                "encrypted object block ranges overlap",
8024            ));
8025        }
8026    }
8027    Ok(())
8028}
8029
8030pub(crate) fn observed_archive_size(
8031    sizes: impl IntoIterator<Item = u64>,
8032) -> Result<u64, FormatError> {
8033    sizes.into_iter().try_fold(0u64, |sum, size| {
8034        sum.checked_add(size).ok_or(FormatError::InvalidArchive(
8035            "observed archive size overflow",
8036        ))
8037    })
8038}
8039
8040pub(crate) fn total_extraction_size_cap(
8041    options: ReaderOptions,
8042    observed_archive_bytes: u64,
8043) -> u64 {
8044    options
8045        .max_total_extraction_size
8046        .min(observed_archive_bytes.saturating_mul(10))
8047}
8048
8049fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
8050    std::str::from_utf8(bytes)
8051        .map(|path| path.to_owned())
8052        .map_err(|_| FormatError::UnsafeArchivePath)
8053}
8054
8055fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
8056    let mut bytes = [0u8; 104];
8057    bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
8058    bytes[36..40].fill(0);
8059    bytes
8060}
8061
8062fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
8063    let digest = Sha256::digest(bytes);
8064    let mut out = [0u8; 32];
8065    out.copy_from_slice(&digest);
8066    out
8067}
8068
8069fn slice<'b>(
8070    bytes: &'b [u8],
8071    offset: usize,
8072    len: usize,
8073    structure: &'static str,
8074) -> Result<&'b [u8], FormatError> {
8075    let end = checked_add(offset, len, structure)?;
8076    bytes.get(offset..end).ok_or(FormatError::InvalidLength {
8077        structure,
8078        expected: end,
8079        actual: bytes.len(),
8080    })
8081}
8082
8083fn read_at_vec(
8084    reader: &dyn ArchiveReadAt,
8085    offset: u64,
8086    len: usize,
8087    structure: &'static str,
8088) -> Result<Vec<u8>, FormatError> {
8089    let expected_end = offset
8090        .checked_add(len as u64)
8091        .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
8092    let observed_len = reader.len()?;
8093    if expected_end > observed_len {
8094        return Err(FormatError::InvalidLength {
8095            structure,
8096            expected: to_usize(expected_end, structure)?,
8097            actual: to_usize(observed_len, structure)?,
8098        });
8099    }
8100    let mut out = vec![0u8; len];
8101    reader.read_exact_at(offset, &mut out)?;
8102    Ok(out)
8103}
8104
8105fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
8106where
8107    T: Sync,
8108    U: Send,
8109    F: Fn(&T) -> Result<U, FormatError> + Sync,
8110{
8111    if jobs <= 1 || items.len() <= 1 {
8112        return items.iter().map(f).collect();
8113    }
8114    let worker_count = jobs.min(items.len());
8115    let chunk_size = items.len().div_ceil(worker_count);
8116    let mut out = Vec::with_capacity(items.len());
8117    thread::scope(|scope| {
8118        let handles = items
8119            .chunks(chunk_size)
8120            .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
8121            .collect::<Vec<_>>();
8122        for handle in handles {
8123            let mut chunk = handle
8124                .join()
8125                .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
8126            out.append(&mut chunk);
8127        }
8128        Ok(out)
8129    })
8130}
8131
8132fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
8133    lhs.checked_add(rhs)
8134        .ok_or(FormatError::InvalidArchive(structure))
8135}
8136
8137fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
8138    lhs.checked_add(rhs)
8139        .ok_or(FormatError::InvalidArchive(structure))
8140}
8141
8142fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
8143    usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
8144}
8145
8146#[cfg(test)]
8147mod tests {
8148    use std::fs;
8149
8150    use super::*;
8151    use crate::compression::compress_zstd_frame;
8152    use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
8153    use crate::fec::encode_parity_gf16;
8154    use crate::format::{
8155        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
8156        VOLUME_FORMAT_REV,
8157    };
8158    use crate::metadata::{
8159        DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
8160        ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
8161    };
8162    use crate::non_seekable_reader::{
8163        extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
8164        verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
8165        NonSeekableReaderOptions, SequentialRootAuthStatus,
8166    };
8167    use crate::writer::{
8168        write_archive, write_archive_with_dictionary, write_archive_with_kdf,
8169        write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
8170        WriterOptions,
8171    };
8172
8173    fn master_key() -> MasterKey {
8174        MasterKey::from_raw_key(&[0x42; 32]).unwrap()
8175    }
8176
8177    #[test]
8178    fn reader_defaults_use_available_parallelism_jobs() {
8179        let options = ReaderOptions::default();
8180
8181        assert_eq!(options.jobs, default_jobs());
8182        assert!(options.jobs >= 1);
8183    }
8184
8185    #[test]
8186    fn reader_options_reject_zero_jobs() {
8187        let err = OpenedArchive::open_with_options(
8188            &[],
8189            &master_key(),
8190            ReaderOptions {
8191                jobs: 0,
8192                ..ReaderOptions::default()
8193            },
8194        )
8195        .unwrap_err();
8196
8197        assert_eq!(
8198            err,
8199            FormatError::ReaderUnsupported("jobs must be at least 1")
8200        );
8201    }
8202
8203    const TEST_ROOT_AUTH_ID: u16 = 0xe001;
8204    const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
8205
8206    fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
8207        RootAuthWriterConfig {
8208            authenticator_id: TEST_ROOT_AUTH_ID,
8209            signer_identity_type: 0,
8210            signer_identity: &[],
8211            authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
8212        }
8213    }
8214
8215    fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
8216        request.archive_root.to_vec()
8217    }
8218
8219    fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
8220        footer.authenticator_id == TEST_ROOT_AUTH_ID
8221            && footer.signer_identity_type == 0
8222            && footer.signer_identity_bytes.is_empty()
8223            && footer.authenticator_value.as_slice() == archive_root
8224    }
8225
8226    fn dictionary() -> &'static [u8] {
8227        b"dir/dict.txt common words common words common words dictionary payload"
8228    }
8229
8230    #[derive(Clone)]
8231    struct CountingReadAt {
8232        bytes: std::sync::Arc<Vec<u8>>,
8233        reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
8234        denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
8235    }
8236
8237    impl CountingReadAt {
8238        fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
8239            Self {
8240                bytes: std::sync::Arc::new(bytes),
8241                reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
8242                denied_ranges: std::sync::Arc::new(denied_ranges),
8243            }
8244        }
8245
8246        fn reads(&self) -> Vec<(u64, u64)> {
8247            self.reads.lock().unwrap().clone()
8248        }
8249    }
8250
8251    impl ArchiveReadAt for CountingReadAt {
8252        fn len(&self) -> Result<u64, FormatError> {
8253            u64::try_from(self.bytes.as_ref().len())
8254                .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
8255        }
8256
8257        fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
8258            let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
8259            self.reads.lock().unwrap().push((offset, end));
8260            if self
8261                .denied_ranges
8262                .iter()
8263                .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
8264            {
8265                return Err(FormatError::InvalidArchive("denied test read"));
8266            }
8267            let start = to_usize(offset, "archive")?;
8268            let end_usize = checked_add(start, buf.len(), "archive")?;
8269            let source = self
8270                .bytes
8271                .get(start..end_usize)
8272                .ok_or(FormatError::InvalidLength {
8273                    structure: "archive",
8274                    expected: end_usize,
8275                    actual: self.bytes.as_ref().len(),
8276                })?;
8277            buf.copy_from_slice(source);
8278            Ok(())
8279        }
8280    }
8281
8282    fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
8283        left_start < right_end && right_start < left_end
8284    }
8285
8286    fn single_stream_options() -> WriterOptions {
8287        WriterOptions {
8288            stripe_width: 1,
8289            volume_loss_tolerance: 0,
8290            ..WriterOptions::default()
8291        }
8292    }
8293
8294    struct ChunkedReader {
8295        bytes: Vec<u8>,
8296        cursor: usize,
8297        max_chunk: usize,
8298    }
8299
8300    impl ChunkedReader {
8301        fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
8302            Self {
8303                bytes,
8304                cursor: 0,
8305                max_chunk,
8306            }
8307        }
8308    }
8309
8310    impl std::io::Read for ChunkedReader {
8311        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
8312            if self.cursor >= self.bytes.len() {
8313                return Ok(0);
8314            }
8315            let available = self.bytes.len() - self.cursor;
8316            let len = available.min(buf.len()).min(self.max_chunk);
8317            buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
8318            self.cursor += len;
8319            Ok(len)
8320        }
8321    }
8322
8323    #[test]
8324    fn global_file_table_key_step_rejects_distinct_path_regression() {
8325        let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
8326        let current = ([1u8; 8], b"a.txt".to_vec(), 0);
8327
8328        assert_eq!(
8329            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
8330            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8331        );
8332    }
8333
8334    #[test]
8335    fn global_file_table_key_step_rejects_duplicate_full_key() {
8336        let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
8337        let current = ([1u8; 8], b"a.txt".to_vec(), 7);
8338
8339        assert_eq!(
8340            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
8341            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8342        );
8343    }
8344
8345    fn small_block_recovery_options() -> WriterOptions {
8346        WriterOptions {
8347            block_size: 4096,
8348            chunk_size: 32 * 1024,
8349            envelope_target_size: 32 * 1024,
8350            stripe_width: 1,
8351            volume_loss_tolerance: 0,
8352            bit_rot_buffer_pct: 1,
8353            fec_data_shards: 16,
8354            fec_parity_shards: 1,
8355            index_fec_data_shards: 4,
8356            index_fec_parity_shards: 1,
8357            index_root_fec_data_shards: 16,
8358            index_root_fec_parity_shards: 1,
8359            ..WriterOptions::default()
8360        }
8361    }
8362
8363    fn parity_rich_recovery_options() -> WriterOptions {
8364        WriterOptions {
8365            block_size: 4096,
8366            chunk_size: 32 * 1024,
8367            envelope_target_size: 32 * 1024,
8368            stripe_width: 1,
8369            volume_loss_tolerance: 0,
8370            bit_rot_buffer_pct: 40,
8371            fec_data_shards: 16,
8372            fec_parity_shards: 16,
8373            index_fec_data_shards: 4,
8374            index_fec_parity_shards: 4,
8375            index_root_fec_data_shards: 16,
8376            index_root_fec_parity_shards: 16,
8377            ..WriterOptions::default()
8378        }
8379    }
8380
8381    fn pseudo_random_bytes(len: usize) -> Vec<u8> {
8382        let mut state = 0x1234_5678u32;
8383        (0..len)
8384            .map(|_| {
8385                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
8386                (state >> 24) as u8
8387            })
8388            .collect()
8389    }
8390
8391    #[test]
8392    fn opens_lists_verifies_and_extracts_one_file_archive() {
8393        let archive = write_archive(
8394            &[RegularFile::new("dir/hello.txt", b"hello m7")],
8395            &master_key(),
8396            single_stream_options(),
8397        )
8398        .unwrap();
8399        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8400
8401        assert_eq!(
8402            opened.list_files().unwrap(),
8403            vec![ArchiveEntry {
8404                path: "dir/hello.txt".to_string(),
8405                file_data_size: 8,
8406                kind: TarEntryKind::Regular,
8407                mode: 0o644,
8408                mtime: 0,
8409                diagnostics: Vec::new(),
8410            }]
8411        );
8412        opened.verify().unwrap();
8413        assert_eq!(
8414            opened.extract_file("dir/hello.txt").unwrap(),
8415            Some(b"hello m7".to_vec())
8416        );
8417        assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
8418    }
8419
8420    #[test]
8421    fn root_auth_archive_round_trips_and_verifies_with_callback() {
8422        let archive = write_archive_with_root_auth(
8423            &[RegularFile::new("signed.txt", b"root-auth payload")],
8424            &master_key(),
8425            single_stream_options(),
8426            RootAuthWriterConfig {
8427                authenticator_id: 0x7777,
8428                signer_identity_type: 1,
8429                signer_identity: b"test signer",
8430                authenticator_value_length: 32,
8431            },
8432            |request| Ok(request.archive_root.to_vec()),
8433        )
8434        .unwrap();
8435
8436        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8437        opened.verify().unwrap();
8438        let verified = opened
8439            .verify_root_auth_with(|footer, archive_root| {
8440                Ok(footer.authenticator_value == archive_root.as_slice())
8441            })
8442            .unwrap();
8443
8444        assert_eq!(verified.authenticator_id, 0x7777);
8445        assert_eq!(verified.signer_identity_type, 1);
8446        assert_eq!(verified.signer_identity_bytes, b"test signer");
8447        assert_eq!(
8448            verified.archive_root,
8449            opened.root_auth_footer.as_ref().unwrap().archive_root
8450        );
8451        assert_eq!(
8452            verified.diagnostics,
8453            vec![
8454                RootAuthDiagnostic::RootAuthContentVerified,
8455                RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
8456                RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
8457                RootAuthDiagnostic::RecoveryMarginUnchecked,
8458            ]
8459        );
8460    }
8461
8462    #[test]
8463    fn root_auth_verification_requires_authenticator_success() {
8464        let archive = write_archive_with_root_auth(
8465            &[RegularFile::new("signed.txt", b"root-auth payload")],
8466            &master_key(),
8467            single_stream_options(),
8468            RootAuthWriterConfig {
8469                authenticator_id: 9,
8470                signer_identity_type: 1,
8471                signer_identity: b"test signer",
8472                authenticator_value_length: 32,
8473            },
8474            |request| Ok(request.archive_root.to_vec()),
8475        )
8476        .unwrap();
8477        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8478
8479        assert_eq!(
8480            opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
8481            FormatError::InvalidArchive("root-auth authenticator verification failed")
8482        );
8483    }
8484
8485    #[test]
8486    fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
8487        let archive = write_archive_with_root_auth(
8488            &[RegularFile::new("public.txt", b"public commitment")],
8489            &master_key(),
8490            single_stream_options(),
8491            RootAuthWriterConfig {
8492                authenticator_id: 0x2222,
8493                signer_identity_type: 1,
8494                signer_identity: b"public verifier",
8495                authenticator_value_length: 32,
8496            },
8497            |request| Ok(request.archive_root.to_vec()),
8498        )
8499        .unwrap();
8500
8501        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8502            Ok(footer.authenticator_value == archive_root.as_slice())
8503        })
8504        .unwrap();
8505
8506        assert_eq!(verified.authenticator_id, 0x2222);
8507        assert_eq!(verified.signer_identity_bytes, b"public verifier");
8508        assert!(verified.total_data_block_count > 0);
8509    }
8510
8511    #[test]
8512    fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
8513        let archive = write_archive_with_root_auth(
8514            &[RegularFile::new(
8515                "public-fields.txt",
8516                b"public source authority",
8517            )],
8518            &master_key(),
8519            single_stream_options(),
8520            RootAuthWriterConfig {
8521                authenticator_id: 0x2222,
8522                signer_identity_type: 1,
8523                signer_identity: b"public verifier",
8524                authenticator_value_length: 32,
8525            },
8526            |request| Ok(request.archive_root.to_vec()),
8527        )
8528        .unwrap();
8529        let mut bytes = archive.bytes.clone();
8530
8531        rewrite_public_cmra_image(&mut bytes, |image| {
8532            let manifest_region = image
8533                .regions
8534                .iter_mut()
8535                .find(|region| region.region_type == 3)
8536                .unwrap();
8537            manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
8538
8539            let trailer_region = image
8540                .regions
8541                .iter_mut()
8542                .find(|region| region.region_type == 5)
8543                .unwrap();
8544            let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
8545            trailer.block_count += 7;
8546            trailer_region.bytes = trailer.to_bytes().to_vec();
8547        });
8548
8549        public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
8550            Ok(footer.authenticator_value == archive_root.as_slice())
8551        })
8552        .unwrap();
8553    }
8554
8555    #[test]
8556    fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
8557        let archive = write_archive_with_root_auth(
8558            &[RegularFile::new(
8559                "public-crypto.txt",
8560                b"cross-volume public profile",
8561            )],
8562            &master_key(),
8563            WriterOptions {
8564                stripe_width: 2,
8565                volume_loss_tolerance: 0,
8566                ..WriterOptions::default()
8567            },
8568            RootAuthWriterConfig {
8569                authenticator_id: 0x3333,
8570                signer_identity_type: 1,
8571                signer_identity: b"public verifier",
8572                authenticator_value_length: 32,
8573            },
8574            |request| Ok(request.archive_root.to_vec()),
8575        )
8576        .unwrap();
8577        let mut volumes = archive.volumes.clone();
8578        let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
8579        let crypto_offset = volume_header.crypto_header_offset as usize;
8580        let expected_volume_size = 123_456_789u64;
8581        volumes[1][crypto_offset + 52..crypto_offset + 60]
8582            .copy_from_slice(&expected_volume_size.to_le_bytes());
8583        rewrite_public_cmra_image(&mut volumes[1], |image| {
8584            let crypto_region = image
8585                .regions
8586                .iter_mut()
8587                .find(|region| region.region_type == 2)
8588                .unwrap();
8589            crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
8590        });
8591
8592        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
8593        public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
8594            Ok(footer.authenticator_value == archive_root.as_slice())
8595        })
8596        .unwrap();
8597    }
8598
8599    #[test]
8600    fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
8601        let archive = write_archive_with_root_auth(
8602            &[RegularFile::new("cmra-header.txt", b"header fallback")],
8603            &master_key(),
8604            single_stream_options(),
8605            RootAuthWriterConfig {
8606                authenticator_id: 0x4444,
8607                signer_identity_type: 1,
8608                signer_identity: b"public verifier",
8609                authenticator_value_length: 32,
8610            },
8611            |request| Ok(request.archive_root.to_vec()),
8612        )
8613        .unwrap();
8614        let final_locator = final_recovery_locator(&archive.bytes);
8615
8616        let mut bad_crc = archive.bytes.clone();
8617        let crc_offset =
8618            final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
8619        bad_crc[crc_offset] ^= 0x55;
8620        public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
8621            Ok(footer.authenticator_value == archive_root.as_slice())
8622        })
8623        .unwrap();
8624
8625        let mut bad_magic = archive.bytes.clone();
8626        bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
8627        public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
8628            Ok(footer.authenticator_value == archive_root.as_slice())
8629        })
8630        .unwrap();
8631
8632        let mut bad_hint = archive.bytes.clone();
8633        bad_hint[crc_offset] ^= 0xAA;
8634        for offset in [
8635            bad_hint.len() - LOCATOR_PAIR_LEN,
8636            bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
8637        ] {
8638            let mut locator = CriticalRecoveryLocator::parse(
8639                &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8640            )
8641            .unwrap();
8642            locator.volume_index_hint += 1;
8643            bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
8644                .copy_from_slice(&locator.to_bytes());
8645        }
8646        assert_eq!(
8647            public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
8648            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
8649        );
8650    }
8651
8652    #[test]
8653    fn recovers_physical_volume_header_magic_from_cmra_authority() {
8654        let payload = b"front header authority".to_vec();
8655        let archive = write_archive(
8656            &[RegularFile::new("volume-header.txt", &payload)],
8657            &master_key(),
8658            small_block_recovery_options(),
8659        )
8660        .unwrap();
8661
8662        let mut corrupted = archive.bytes;
8663        corrupted[0] ^= 0x55;
8664
8665        let opened = open_archive(&corrupted, &master_key()).unwrap();
8666        assert_eq!(
8667            opened.extract_file("volume-header.txt").unwrap(),
8668            Some(payload)
8669        );
8670        opened.verify().unwrap();
8671    }
8672
8673    #[test]
8674    fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8675        let payload = b"crc-valid wrong volume index".to_vec();
8676        let mut options = small_block_recovery_options();
8677        options.stripe_width = 2;
8678        options.volume_loss_tolerance = 0;
8679        let archive = write_archive(
8680            &[RegularFile::new("volume-index.txt", &payload)],
8681            &master_key(),
8682            options,
8683        )
8684        .unwrap();
8685
8686        let mut corrupted = archive.volumes[0].clone();
8687        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8688        assert_eq!(header.volume_index, 0);
8689        header.volume_index = 1;
8690        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8691        assert_eq!(
8692            VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
8693                .unwrap()
8694                .volume_index,
8695            1
8696        );
8697
8698        let opened = open_archive_volumes(
8699            &[corrupted.as_slice(), archive.volumes[1].as_slice()],
8700            &master_key(),
8701        )
8702        .unwrap();
8703        assert_eq!(opened.volume_header.volume_index, 0);
8704        assert_eq!(
8705            opened.extract_file("volume-index.txt").unwrap(),
8706            Some(payload)
8707        );
8708        opened.verify().unwrap();
8709    }
8710
8711    #[test]
8712    fn recovers_physical_crypto_header_magic_from_cmra_authority() {
8713        let payload = b"crypto header authority".to_vec();
8714        let archive = write_archive(
8715            &[RegularFile::new("crypto-header.txt", &payload)],
8716            &master_key(),
8717            small_block_recovery_options(),
8718        )
8719        .unwrap();
8720        let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
8721            .unwrap()
8722            .crypto_header_offset;
8723
8724        let mut corrupted = archive.bytes;
8725        corrupted[crypto_offset as usize] ^= 0x55;
8726
8727        let opened = open_archive(&corrupted, &master_key()).unwrap();
8728        assert_eq!(
8729            opened.extract_file("crypto-header.txt").unwrap(),
8730            Some(payload)
8731        );
8732        opened.verify().unwrap();
8733    }
8734
8735    #[test]
8736    fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
8737        let payload = b"read-at header authority".to_vec();
8738        let archive = write_archive(
8739            &[RegularFile::new("read-at-header.txt", &payload)],
8740            &master_key(),
8741            small_block_recovery_options(),
8742        )
8743        .unwrap();
8744
8745        let mut corrupted = archive.bytes;
8746        corrupted[0] ^= 0x55;
8747
8748        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8749        assert_eq!(
8750            opened.extract_file("read-at-header.txt").unwrap(),
8751            Some(payload)
8752        );
8753        opened.verify().unwrap();
8754    }
8755
8756    #[test]
8757    fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8758        let payload = b"read-at crc-valid wrong volume index".to_vec();
8759        let mut options = small_block_recovery_options();
8760        options.stripe_width = 2;
8761        options.volume_loss_tolerance = 0;
8762        let archive = write_archive(
8763            &[RegularFile::new("read-at-volume-index.txt", &payload)],
8764            &master_key(),
8765            options,
8766        )
8767        .unwrap();
8768
8769        let mut corrupted = archive.volumes[0].clone();
8770        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8771        assert_eq!(header.volume_index, 0);
8772        header.volume_index = 1;
8773        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8774
8775        let opened = open_seekable_archive_volumes(
8776            vec![corrupted, archive.volumes[1].clone()],
8777            &master_key(),
8778        )
8779        .unwrap();
8780        assert_eq!(opened.volume_header.volume_index, 0);
8781        assert_eq!(
8782            opened.extract_file("read-at-volume-index.txt").unwrap(),
8783            Some(payload)
8784        );
8785        opened.verify().unwrap();
8786    }
8787
8788    #[test]
8789    fn recovers_cmra_header_magic_from_locator_tuple() {
8790        let payload = b"cmra header authority".to_vec();
8791        let archive = write_archive(
8792            &[RegularFile::new("cmra-header-magic.txt", &payload)],
8793            &master_key(),
8794            small_block_recovery_options(),
8795        )
8796        .unwrap();
8797        let locator = final_recovery_locator(&archive.bytes);
8798
8799        let mut corrupted = archive.bytes;
8800        corrupted[locator.cmra_offset as usize] ^= 0x55;
8801
8802        let opened = open_archive(&corrupted, &master_key()).unwrap();
8803        assert_eq!(
8804            opened.extract_file("cmra-header-magic.txt").unwrap(),
8805            Some(payload)
8806        );
8807        opened.verify().unwrap();
8808    }
8809
8810    #[test]
8811    fn recovers_cmra_shard_magic_as_erasure() {
8812        let payload = b"cmra shard authority".to_vec();
8813        let archive = write_archive(
8814            &[RegularFile::new("cmra-shard-magic.txt", &payload)],
8815            &master_key(),
8816            small_block_recovery_options(),
8817        )
8818        .unwrap();
8819        let locator = final_recovery_locator(&archive.bytes);
8820        let first_shard_offset =
8821            locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
8822
8823        let mut corrupted = archive.bytes;
8824        corrupted[first_shard_offset] ^= 0x55;
8825
8826        let opened = open_archive(&corrupted, &master_key()).unwrap();
8827        assert_eq!(
8828            opened.extract_file("cmra-shard-magic.txt").unwrap(),
8829            Some(payload)
8830        );
8831        opened.verify().unwrap();
8832    }
8833
8834    #[test]
8835    fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
8836        let archive = write_archive(
8837            &[RegularFile::new(
8838                "cmra-floor.txt",
8839                b"authenticated CMRA floor",
8840            )],
8841            &master_key(),
8842            single_stream_options(),
8843        )
8844        .unwrap();
8845        let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
8846        let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8847        let locator = CriticalRecoveryLocator::parse(
8848            &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8849        )
8850        .unwrap();
8851        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
8852        let crypto_start = volume_header.crypto_header_offset as usize;
8853        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8854        let crypto_header = CryptoHeader::parse(
8855            &malformed[crypto_start..crypto_end],
8856            volume_header.crypto_header_length,
8857        )
8858        .unwrap();
8859        let subkeys = Subkeys::derive(
8860            &master_key(),
8861            &volume_header.archive_uuid,
8862            &volume_header.session_id,
8863        )
8864        .unwrap();
8865
8866        assert_eq!(
8867            parse_locator_cmra_candidate(
8868                &malformed,
8869                final_offset,
8870                locator,
8871                KeyHoldingTerminalContext {
8872                    subkeys: &subkeys,
8873                    volume_header: &volume_header,
8874                    crypto_header: &crypto_header.fixed,
8875                    crypto_header_bytes: &malformed[crypto_start..crypto_end],
8876                },
8877            )
8878            .unwrap_err(),
8879            FormatError::InvalidArchive(
8880                "CMRA parity shard count is below authenticated bit-rot lower bound"
8881            )
8882        );
8883        assert!(open_archive(&malformed, &master_key()).is_err());
8884    }
8885
8886    #[test]
8887    fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
8888        let archive = write_archive(
8889            &[RegularFile::new(
8890                "locator-order.txt",
8891                b"locator tuple first",
8892            )],
8893            &master_key(),
8894            single_stream_options(),
8895        )
8896        .unwrap();
8897        let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8898        let mut locator = final_recovery_locator(&archive.bytes);
8899        locator.cmra_shard_size = 513;
8900        locator.body_bytes_before_cmra = locator.cmra_offset + 1;
8901        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8902        let crypto_start = volume_header.crypto_header_offset as usize;
8903        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8904        let crypto_header = CryptoHeader::parse(
8905            &archive.bytes[crypto_start..crypto_end],
8906            volume_header.crypto_header_length,
8907        )
8908        .unwrap();
8909        let subkeys = Subkeys::derive(
8910            &master_key(),
8911            &volume_header.archive_uuid,
8912            &volume_header.session_id,
8913        )
8914        .unwrap();
8915
8916        assert_eq!(
8917            parse_locator_cmra_candidate(
8918                &archive.bytes,
8919                final_offset,
8920                locator,
8921                KeyHoldingTerminalContext {
8922                    subkeys: &subkeys,
8923                    volume_header: &volume_header,
8924                    crypto_header: &crypto_header.fixed,
8925                    crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
8926                },
8927            )
8928            .unwrap_err(),
8929            FormatError::InvalidArchive("CMRA shard_size is invalid")
8930        );
8931    }
8932
8933    #[test]
8934    fn sequential_extract_rejects_bytes_after_terminal_locator() {
8935        let archive = write_archive(
8936            &[RegularFile::new("seq.txt", b"sequential EOF")],
8937            &master_key(),
8938            single_stream_options(),
8939        )
8940        .unwrap();
8941        let mut appended = archive.bytes.clone();
8942        appended.extend_from_slice(&[0xAA; 32]);
8943
8944        assert_eq!(
8945            sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
8946            FormatError::InvalidArchive("sequential terminal does not end at EOF")
8947        );
8948    }
8949
8950    #[test]
8951    fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
8952        let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
8953        let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
8954
8955        assert_eq!(
8956            validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
8957            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8958        );
8959    }
8960
8961    #[test]
8962    fn root_auth_verifies_key_holding_and_public_no_key_modes() {
8963        let archive = write_archive_with_root_auth(
8964            &[RegularFile::new("signed.txt", b"ed25519 payload")],
8965            &master_key(),
8966            single_stream_options(),
8967            test_root_auth_config(),
8968            |request| Ok(test_root_auth_value(request)),
8969        )
8970        .unwrap();
8971
8972        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8973        let root_auth = opened
8974            .verify_root_auth_with(|footer, archive_root| {
8975                Ok(test_root_auth_verifies(footer, archive_root))
8976            })
8977            .unwrap();
8978        assert_eq!(
8979            root_auth.archive_root,
8980            opened.root_auth_footer.as_ref().unwrap().archive_root
8981        );
8982
8983        let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8984            Ok(test_root_auth_verifies(footer, archive_root))
8985        })
8986        .unwrap();
8987        assert_eq!(public.archive_root, root_auth.archive_root);
8988        assert_eq!(
8989            public.diagnostics,
8990            vec![
8991                PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
8992                PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
8993                PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
8994            ]
8995        );
8996    }
8997
8998    #[test]
8999    fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
9000        let options = WriterOptions {
9001            block_size: 4096,
9002            chunk_size: 16 * 1024,
9003            envelope_target_size: 16 * 1024,
9004            stripe_width: 2,
9005            volume_loss_tolerance: 1,
9006            bit_rot_buffer_pct: 0,
9007            fec_data_shards: 16,
9008            fec_parity_shards: 1,
9009            index_fec_data_shards: 4,
9010            index_fec_parity_shards: 1,
9011            index_root_fec_data_shards: 16,
9012            index_root_fec_parity_shards: 1,
9013            ..WriterOptions::default()
9014        };
9015        let archive = write_archive_with_root_auth(
9016            &[RegularFile::new("missing-volume.txt", b"recover me")],
9017            &master_key(),
9018            options,
9019            test_root_auth_config(),
9020            |request| Ok(test_root_auth_value(request)),
9021        )
9022        .unwrap();
9023
9024        let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
9025        let root_auth = opened
9026            .verify_root_auth_with(|footer, archive_root| {
9027                Ok(test_root_auth_verifies(footer, archive_root))
9028            })
9029            .unwrap();
9030        assert!(root_auth
9031            .diagnostics
9032            .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
9033    }
9034
9035    #[test]
9036    fn public_no_key_rejects_unsigned_archives() {
9037        let archive = write_archive(
9038            &[RegularFile::new("plain.txt", b"unsigned")],
9039            &master_key(),
9040            single_stream_options(),
9041        )
9042        .unwrap();
9043
9044        assert_eq!(
9045            public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
9046            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
9047        );
9048    }
9049
9050    #[test]
9051    fn unsigned_archive_reports_root_auth_absent() {
9052        let archive = write_archive(
9053            &[RegularFile::new("plain.txt", b"unsigned")],
9054            &master_key(),
9055            single_stream_options(),
9056        )
9057        .unwrap();
9058        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9059
9060        assert_eq!(
9061            opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
9062            FormatError::ReaderUnsupported("root-auth footer is absent")
9063        );
9064    }
9065
9066    #[test]
9067    fn safe_extract_writes_regular_file_under_root() {
9068        let archive = write_archive(
9069            &[RegularFile::new("dir/hello.txt", b"safe m8")],
9070            &master_key(),
9071            single_stream_options(),
9072        )
9073        .unwrap();
9074        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9075        let tmp = tempfile::tempdir().unwrap();
9076
9077        opened
9078            .extract_file_to(
9079                "dir/hello.txt",
9080                tmp.path(),
9081                SafeExtractionOptions::default(),
9082            )
9083            .unwrap()
9084            .unwrap();
9085
9086        assert_eq!(
9087            std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
9088            b"safe m8"
9089        );
9090    }
9091
9092    #[test]
9093    fn seekable_extract_all_to_streams_unique_archive() {
9094        let archive = write_archive(
9095            &[
9096                RegularFile::new("alpha.txt", b"alpha"),
9097                RegularFile::new("dir/beta.txt", b"beta"),
9098            ],
9099            &master_key(),
9100            single_stream_options(),
9101        )
9102        .unwrap();
9103        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9104        let tmp = tempfile::tempdir().unwrap();
9105
9106        let diagnostics = opened
9107            .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9108            .unwrap();
9109
9110        assert_eq!(diagnostics.len(), 2);
9111        assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
9112        assert_eq!(
9113            fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
9114            b"beta"
9115        );
9116    }
9117
9118    #[test]
9119    fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
9120        let archive = write_archive(
9121            &[
9122                RegularFile::new("same.txt", b"old"),
9123                RegularFile::new("same.txt", b"new"),
9124            ],
9125            &master_key(),
9126            single_stream_options(),
9127        )
9128        .unwrap();
9129        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9130        let tmp = tempfile::tempdir().unwrap();
9131
9132        assert_eq!(
9133            opened
9134                .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9135                .unwrap_err(),
9136            FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
9137        );
9138    }
9139
9140    #[test]
9141    fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
9142        let archive = write_archive(
9143            &[
9144                RegularFile::new("same.txt", b"old"),
9145                RegularFile::new("same.txt", b"new"),
9146            ],
9147            &master_key(),
9148            single_stream_options(),
9149        )
9150        .unwrap();
9151        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9152        let tmp = tempfile::tempdir().unwrap();
9153
9154        let diagnostics = opened
9155            .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
9156            .unwrap();
9157
9158        assert_eq!(diagnostics.len(), 1);
9159        assert_eq!(diagnostics[0].0, "same.txt");
9160        assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
9161    }
9162
9163    #[test]
9164    fn safe_extract_rejects_overwriting_existing_file_by_default() {
9165        let archive = write_archive(
9166            &[RegularFile::new("hello.txt", b"new")],
9167            &master_key(),
9168            single_stream_options(),
9169        )
9170        .unwrap();
9171        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9172        let tmp = tempfile::tempdir().unwrap();
9173        std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
9174
9175        assert_eq!(
9176            opened
9177                .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
9178                .unwrap_err(),
9179            FormatError::UnsafeOverwrite
9180        );
9181        assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
9182    }
9183
9184    #[test]
9185    fn opens_and_verifies_empty_archive() {
9186        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9187        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9188
9189        assert!(opened.list_files().unwrap().is_empty());
9190        opened.verify().unwrap();
9191    }
9192
9193    #[test]
9194    fn default_reader_options_allow_v36_trailing_garbage_scan() {
9195        let archive = write_archive(
9196            &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
9197            &master_key(),
9198            single_stream_options(),
9199        )
9200        .unwrap();
9201        let mut with_trailing_garbage = archive.bytes.clone();
9202        with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
9203
9204        let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
9205        assert_eq!(
9206            opened.extract_file("garbage-tolerant.txt").unwrap(),
9207            Some(b"still intact".to_vec())
9208        );
9209    }
9210
9211    #[test]
9212    fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
9213        assert_eq!(
9214            open_archive(
9215                &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
9216                &master_key()
9217            )
9218            .unwrap_err(),
9219            FormatError::InvalidLength {
9220                structure: "archive",
9221                expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9222                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
9223            }
9224        );
9225
9226        let mut header = test_volume_header();
9227        header.crypto_header_length = 512;
9228        let mut unavailable_crypto = header.to_bytes().to_vec();
9229        unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
9230
9231        assert_eq!(
9232            open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
9233            FormatError::InvalidLength {
9234                structure: "CryptoHeader",
9235                expected: VOLUME_HEADER_LEN + 512,
9236                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9237            }
9238        );
9239    }
9240
9241    #[test]
9242    fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
9243        let archive = write_archive(
9244            &[RegularFile::new("offset.txt", b"offset")],
9245            &master_key(),
9246            small_block_recovery_options(),
9247        )
9248        .unwrap();
9249        let mut mutated = archive.bytes;
9250        let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
9251        header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
9252        mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
9253
9254        let opened = open_archive(&mutated, &master_key()).unwrap();
9255        assert_eq!(
9256            opened.volume_header.crypto_header_offset,
9257            VOLUME_HEADER_LEN as u32
9258        );
9259        assert_eq!(
9260            opened.extract_file("offset.txt").unwrap(),
9261            Some(b"offset".to_vec())
9262        );
9263        opened.verify().unwrap();
9264    }
9265
9266    #[test]
9267    fn rejects_wrong_key_before_metadata_release() {
9268        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9269        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
9270
9271        assert_eq!(
9272            open_archive(&archive.bytes, &wrong).unwrap_err(),
9273            FormatError::HmacMismatch {
9274                structure: "CryptoHeader"
9275            }
9276        );
9277    }
9278
9279    #[test]
9280    fn rejects_payload_tamper_even_with_recomputed_block_crc() {
9281        let mut archive = write_archive(
9282            &[RegularFile::new("file.txt", b"authenticated")],
9283            &master_key(),
9284            single_stream_options(),
9285        )
9286        .unwrap()
9287        .bytes;
9288        let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
9289        let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
9290        let crypto = CryptoHeader::parse(
9291            &archive[VOLUME_HEADER_LEN..crypto_end],
9292            volume.crypto_header_length,
9293        )
9294        .unwrap();
9295        let block_size = crypto.fixed.block_size as usize;
9296        archive[crypto_end + 16] ^= 1;
9297        let crc_offset = crypto_end + 16 + block_size;
9298        let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
9299        archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9300
9301        let opened = open_archive(&archive, &master_key()).unwrap();
9302        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9303    }
9304
9305    #[test]
9306    fn list_and_extract_use_final_view_for_duplicate_paths() {
9307        let archive = write_archive(
9308            &[
9309                RegularFile::new("same.txt", b"old"),
9310                RegularFile::new("same.txt", b"newer"),
9311            ],
9312            &master_key(),
9313            single_stream_options(),
9314        )
9315        .unwrap();
9316        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9317
9318        assert_eq!(
9319            opened.list_index_entries().unwrap(),
9320            vec![ArchiveIndexEntry {
9321                path: "same.txt".to_string(),
9322                file_data_size: 5,
9323            }]
9324        );
9325        assert_eq!(
9326            opened.lookup_index_entry("same.txt").unwrap(),
9327            Some(ArchiveIndexEntry {
9328                path: "same.txt".to_string(),
9329                file_data_size: 5,
9330            })
9331        );
9332        assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
9333        assert_eq!(
9334            opened.list_files().unwrap(),
9335            vec![ArchiveEntry {
9336                path: "same.txt".to_string(),
9337                file_data_size: 5,
9338                kind: TarEntryKind::Regular,
9339                mode: 0o644,
9340                mtime: 0,
9341                diagnostics: Vec::new(),
9342            }]
9343        );
9344        assert_eq!(
9345            opened.extract_file("same.txt").unwrap(),
9346            Some(b"newer".to_vec())
9347        );
9348        opened.verify().unwrap();
9349    }
9350
9351    #[test]
9352    fn index_entries_do_not_decrypt_payload_envelopes() {
9353        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9354        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9355
9356        assert_eq!(
9357            opened.list_index_entries().unwrap(),
9358            vec![
9359                ArchiveIndexEntry {
9360                    path: "broken.txt".to_string(),
9361                    file_data_size: b"broken payload\n".len() as u64,
9362                },
9363                ArchiveIndexEntry {
9364                    path: "healthy.txt".to_string(),
9365                    file_data_size: b"healthy payload\n".len() as u64,
9366                },
9367            ]
9368        );
9369        assert_eq!(
9370            opened.lookup_index_entry("broken.txt").unwrap(),
9371            Some(ArchiveIndexEntry {
9372                path: "broken.txt".to_string(),
9373                file_data_size: b"broken payload\n".len() as u64,
9374            })
9375        );
9376        assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
9377    }
9378
9379    #[test]
9380    fn extract_file_does_not_decrypt_unselected_payload_envelope() {
9381        // This fixture corrupts only the unselected envelope, proving selected
9382        // extraction does not decrypt unrelated payload envelopes.
9383        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9384        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9385
9386        assert_eq!(
9387            opened.extract_file("healthy.txt").unwrap(),
9388            Some(b"healthy payload\n".to_vec())
9389        );
9390        assert_eq!(
9391            opened.extract_file("broken.txt").unwrap_err(),
9392            FormatError::AeadFailure
9393        );
9394        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9395    }
9396
9397    #[test]
9398    fn seekable_extract_does_not_read_unselected_payload_envelope() {
9399        let healthy = pseudo_random_bytes(64 * 1024);
9400        let broken = pseudo_random_bytes(64 * 1024);
9401        let options = WriterOptions {
9402            block_size: 4096,
9403            chunk_size: 4096,
9404            envelope_target_size: 8192,
9405            stripe_width: 1,
9406            volume_loss_tolerance: 0,
9407            bit_rot_buffer_pct: 0,
9408            fec_data_shards: 4,
9409            fec_parity_shards: 0,
9410            index_fec_data_shards: 4,
9411            index_fec_parity_shards: 0,
9412            index_root_fec_data_shards: 4,
9413            index_root_fec_parity_shards: 0,
9414            ..WriterOptions::default()
9415        };
9416        let archive = write_archive(
9417            &[
9418                RegularFile::new("healthy.bin", &healthy),
9419                RegularFile::new("broken.bin", &broken),
9420            ],
9421            &master_key(),
9422            options,
9423        )
9424        .unwrap();
9425        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9426        let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
9427        let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
9428        let denied_block_indices = broken_envelopes
9429            .iter()
9430            .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
9431            .flat_map(|envelope| {
9432                let block_count =
9433                    envelope.data_block_count as u64 + envelope.parity_block_count as u64;
9434                envelope.first_block_index..envelope.first_block_index + block_count
9435            })
9436            .collect::<BTreeSet<_>>();
9437        assert!(
9438            !denied_block_indices.is_empty(),
9439            "fixture must place broken.bin in at least one unshared envelope"
9440        );
9441        let denied_ranges = block_record_slots(&archive.bytes)
9442            .into_iter()
9443            .filter_map(|(offset, len, record)| {
9444                denied_block_indices
9445                    .contains(&record.block_index)
9446                    .then_some((offset as u64, (offset + len) as u64))
9447            })
9448            .collect::<Vec<_>>();
9449        assert!(!denied_ranges.is_empty());
9450
9451        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9452        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9453
9454        assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
9455        for (read_start, read_end) in reader.reads() {
9456            assert!(
9457                denied_ranges
9458                    .iter()
9459                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9460                "targeted extract read an unrelated payload BlockRecord range"
9461            );
9462        }
9463        assert_eq!(
9464            opened.extract_file("broken.bin").unwrap_err(),
9465            FormatError::InvalidArchive("denied test read")
9466        );
9467    }
9468
9469    #[test]
9470    fn extract_file_to_writer_streams_before_reading_later_envelopes() {
9471        struct FailOnFirstWrite;
9472
9473        impl std::io::Write for FailOnFirstWrite {
9474            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
9475                Err(std::io::Error::other("sink stopped"))
9476            }
9477
9478            fn flush(&mut self) -> std::io::Result<()> {
9479                Ok(())
9480            }
9481        }
9482
9483        let payload = pseudo_random_bytes(128 * 1024);
9484        let options = WriterOptions {
9485            block_size: 4096,
9486            chunk_size: 4096,
9487            envelope_target_size: 8192,
9488            stripe_width: 1,
9489            volume_loss_tolerance: 0,
9490            bit_rot_buffer_pct: 0,
9491            fec_data_shards: 4,
9492            fec_parity_shards: 0,
9493            index_fec_data_shards: 4,
9494            index_fec_parity_shards: 0,
9495            index_root_fec_data_shards: 4,
9496            index_root_fec_parity_shards: 0,
9497            ..WriterOptions::default()
9498        };
9499        let archive = write_archive(
9500            &[RegularFile::new("large.bin", &payload)],
9501            &master_key(),
9502            options,
9503        )
9504        .unwrap();
9505        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9506        let envelopes = envelope_entries_for_path(&eager, "large.bin");
9507        let first_envelope = envelopes
9508            .first()
9509            .expect("large fixture should have at least one envelope")
9510            .envelope_index;
9511        let later_envelope_blocks = envelopes
9512            .iter()
9513            .filter(|entry| entry.envelope_index != first_envelope)
9514            .flat_map(|entry| {
9515                let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
9516                entry.first_block_index..entry.first_block_index + block_count
9517            })
9518            .collect::<BTreeSet<_>>();
9519        assert!(
9520            !later_envelope_blocks.is_empty(),
9521            "fixture must span more than one payload envelope"
9522        );
9523        let denied_ranges = block_record_slots(&archive.bytes)
9524            .into_iter()
9525            .filter_map(|(offset, len, record)| {
9526                later_envelope_blocks
9527                    .contains(&record.block_index)
9528                    .then_some((offset as u64, (offset + len) as u64))
9529            })
9530            .collect::<Vec<_>>();
9531        assert!(!denied_ranges.is_empty());
9532
9533        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9534        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9535        let mut writer = FailOnFirstWrite;
9536
9537        let err = opened
9538            .extract_file_to_writer("large.bin", &mut writer)
9539            .unwrap_err();
9540        assert_eq!(err.to_string(), "extraction output write failed");
9541        for (read_start, read_end) in reader.reads() {
9542            assert!(
9543                denied_ranges
9544                    .iter()
9545                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9546                "streaming writer read a later payload envelope before surfacing writer failure"
9547            );
9548        }
9549    }
9550
9551    #[test]
9552    fn extract_file_to_writer_writes_bounded_chunks() {
9553        struct ChunkRecorder {
9554            total: usize,
9555            max_write: usize,
9556            writes: usize,
9557        }
9558
9559        impl std::io::Write for ChunkRecorder {
9560            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
9561                self.total += buf.len();
9562                self.max_write = self.max_write.max(buf.len());
9563                self.writes += 1;
9564                Ok(buf.len())
9565            }
9566
9567            fn flush(&mut self) -> std::io::Result<()> {
9568                Ok(())
9569            }
9570        }
9571
9572        let payload = pseudo_random_bytes(128 * 1024);
9573        let options = WriterOptions {
9574            block_size: 4096,
9575            chunk_size: 4096,
9576            envelope_target_size: 8192,
9577            stripe_width: 1,
9578            volume_loss_tolerance: 0,
9579            bit_rot_buffer_pct: 0,
9580            fec_data_shards: 4,
9581            fec_parity_shards: 0,
9582            index_fec_data_shards: 4,
9583            index_fec_parity_shards: 0,
9584            index_root_fec_data_shards: 4,
9585            index_root_fec_parity_shards: 0,
9586            ..WriterOptions::default()
9587        };
9588        let archive = write_archive(
9589            &[RegularFile::new("large.bin", &payload)],
9590            &master_key(),
9591            options,
9592        )
9593        .unwrap();
9594        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9595        let mut writer = ChunkRecorder {
9596            total: 0,
9597            max_write: 0,
9598            writes: 0,
9599        };
9600
9601        opened
9602            .extract_file_to_writer("large.bin", &mut writer)
9603            .unwrap()
9604            .unwrap();
9605
9606        assert_eq!(writer.total, payload.len());
9607        assert!(writer.writes > 1);
9608        assert!(
9609            writer.max_write <= options.chunk_size as usize,
9610            "writer saw a {} byte chunk, larger than the {} byte frame target",
9611            writer.max_write,
9612            options.chunk_size
9613        );
9614    }
9615
9616    #[test]
9617    fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
9618        let payload = pseudo_random_bytes(128 * 1024);
9619        let options = WriterOptions {
9620            block_size: 4096,
9621            chunk_size: 4096,
9622            envelope_target_size: 8192,
9623            stripe_width: 1,
9624            volume_loss_tolerance: 0,
9625            bit_rot_buffer_pct: 0,
9626            fec_data_shards: 4,
9627            fec_parity_shards: 0,
9628            index_fec_data_shards: 4,
9629            index_fec_parity_shards: 0,
9630            index_root_fec_data_shards: 4,
9631            index_root_fec_parity_shards: 0,
9632            ..WriterOptions::default()
9633        };
9634        let archive = write_archive(
9635            &[RegularFile::new("large.bin", &payload)],
9636            &master_key(),
9637            options,
9638        )
9639        .unwrap();
9640        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9641        let envelopes = envelope_entries_for_path(&eager, "large.bin");
9642        let last_envelope = envelopes
9643            .last()
9644            .expect("large fixture should have at least one envelope");
9645        assert_ne!(
9646            envelopes.first().unwrap().envelope_index,
9647            last_envelope.envelope_index,
9648            "fixture must span more than one payload envelope"
9649        );
9650        let corrupt_slot = block_record_slots(&archive.bytes)
9651            .into_iter()
9652            .enumerate()
9653            .find_map(|(slot, (_, _, record))| {
9654                (record.block_index == last_envelope.first_block_index).then_some(slot)
9655            })
9656            .unwrap();
9657        let mut corrupted = archive.bytes;
9658        corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
9659        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
9660        let tmp = tempfile::tempdir().unwrap();
9661
9662        assert!(matches!(
9663            opened
9664                .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
9665                .unwrap_err(),
9666            FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
9667        ));
9668        assert!(!tmp.path().join("large.bin").exists());
9669        assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
9670    }
9671
9672    #[test]
9673    fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
9674        let archive = write_archive(
9675            &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
9676            &master_key(),
9677            single_stream_options(),
9678        )
9679        .unwrap();
9680        let opened = open_archive_with_bootstrap_sidecar(
9681            &archive.bytes,
9682            &archive.bootstrap_sidecar,
9683            &master_key(),
9684        )
9685        .unwrap();
9686
9687        assert_eq!(
9688            opened.list_files().unwrap(),
9689            vec![ArchiveEntry {
9690                path: "dir/sidecar.txt".to_string(),
9691                file_data_size: 13,
9692                kind: TarEntryKind::Regular,
9693                mode: 0o644,
9694                mtime: 0,
9695                diagnostics: Vec::new(),
9696            }]
9697        );
9698        assert_eq!(
9699            opened.extract_file("dir/sidecar.txt").unwrap(),
9700            Some(b"hello sidecar".to_vec())
9701        );
9702        opened.verify().unwrap();
9703    }
9704
9705    #[test]
9706    fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
9707        let archive = write_archive_with_dictionary(
9708            &[RegularFile::new(
9709                "dir/dict.txt",
9710                b"common words common words dictionary payload",
9711            )],
9712            &master_key(),
9713            single_stream_options(),
9714            dictionary(),
9715        )
9716        .unwrap();
9717        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9718
9719        assert_eq!(opened.crypto_header.has_dictionary, 1);
9720        assert!(opened.index_root.header.dictionary_data_block_count > 0);
9721        assert_eq!(
9722            opened.list_files().unwrap(),
9723            vec![ArchiveEntry {
9724                path: "dir/dict.txt".to_string(),
9725                file_data_size: 44,
9726                kind: TarEntryKind::Regular,
9727                mode: 0o644,
9728                mtime: 0,
9729                diagnostics: Vec::new(),
9730            }]
9731        );
9732        assert_eq!(
9733            opened.extract_file("dir/dict.txt").unwrap(),
9734            Some(b"common words common words dictionary payload".to_vec())
9735        );
9736        opened.verify().unwrap();
9737    }
9738
9739    #[test]
9740    fn dictionary_object_tamper_fails_before_payload_decompression() {
9741        let archive = write_archive_with_dictionary(
9742            &[RegularFile::new(
9743                "dir/dict.txt",
9744                b"common words common words dictionary payload",
9745            )],
9746            &master_key(),
9747            single_stream_options(),
9748            dictionary(),
9749        )
9750        .unwrap();
9751        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9752        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9753        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
9754        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
9755        let dictionary_offset =
9756            crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
9757
9758        let mut tampered = archive.bytes.clone();
9759        tampered[dictionary_offset + 16] ^= 0x01;
9760        let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
9761        let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
9762        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9763
9764        assert_eq!(
9765            open_archive(&tampered, &master_key()).unwrap_err(),
9766            FormatError::AeadFailure
9767        );
9768    }
9769
9770    #[test]
9771    fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
9772        let archive = write_archive_with_dictionary(
9773            &[RegularFile::new(
9774                "dict-sidecar.txt",
9775                b"common words common words sidecar payload",
9776            )],
9777            &master_key(),
9778            single_stream_options(),
9779            dictionary(),
9780        )
9781        .unwrap();
9782        let opened = open_non_seekable_archive(
9783            &archive.bytes,
9784            &master_key(),
9785            Some(&archive.bootstrap_sidecar),
9786        )
9787        .unwrap();
9788
9789        assert_eq!(
9790            opened.extract_file("dict-sidecar.txt").unwrap(),
9791            Some(b"common words common words sidecar payload".to_vec())
9792        );
9793        opened.verify().unwrap();
9794    }
9795
9796    #[test]
9797    fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
9798        let archive = write_archive(
9799            &[RegularFile::new(
9800                "sidecar-terminal.txt",
9801                b"sidecar authority",
9802            )],
9803            &master_key(),
9804            single_stream_options(),
9805        )
9806        .unwrap();
9807        let mut corrupted = archive.bytes.clone();
9808        corrupt_v41_terminal_recovery(&mut corrupted);
9809        assert!(open_archive(&corrupted, &master_key()).is_err());
9810
9811        let opened =
9812            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
9813                .unwrap();
9814
9815        assert!(opened.volume_trailer.is_none());
9816        assert_eq!(
9817            opened.extract_file("sidecar-terminal.txt").unwrap(),
9818            Some(b"sidecar authority".to_vec())
9819        );
9820        opened.verify().unwrap();
9821    }
9822
9823    #[test]
9824    fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
9825        let archive = write_archive_with_dictionary(
9826            &[RegularFile::new(
9827                "dict-no-terminal.txt",
9828                b"common words common words without terminal",
9829            )],
9830            &master_key(),
9831            single_stream_options(),
9832            dictionary(),
9833        )
9834        .unwrap();
9835        let terminal_offset = terminal_material_offset(&archive.bytes);
9836        let truncated = archive.bytes[..terminal_offset].to_vec();
9837        assert!(open_archive(&truncated, &master_key()).is_err());
9838
9839        let opened =
9840            open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
9841                .unwrap();
9842
9843        assert!(opened.volume_trailer.is_none());
9844        assert_eq!(
9845            opened.extract_file("dict-no-terminal.txt").unwrap(),
9846            Some(b"common words common words without terminal".to_vec())
9847        );
9848        opened.verify().unwrap();
9849    }
9850
9851    #[test]
9852    fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
9853        let archive = write_archive(
9854            &[RegularFile::new(
9855                "sidecar-erasure.txt",
9856                b"repair through sidecar",
9857            )],
9858            &master_key(),
9859            single_stream_options(),
9860        )
9861        .unwrap();
9862        let mut corrupted = archive.bytes.clone();
9863        corrupt_first_block_record_payload(&mut corrupted);
9864
9865        let opened = open_archive_with_bootstrap_sidecar(
9866            &corrupted,
9867            &archive.bootstrap_sidecar,
9868            &master_key(),
9869        )
9870        .unwrap();
9871        assert_eq!(
9872            opened.extract_file("sidecar-erasure.txt").unwrap(),
9873            Some(b"repair through sidecar".to_vec())
9874        );
9875    }
9876
9877    #[test]
9878    fn extraction_rejects_logical_payload_above_total_size_cap() {
9879        let archive = write_archive(
9880            &[RegularFile::new("cap.txt", b"payload")],
9881            &master_key(),
9882            single_stream_options(),
9883        )
9884        .unwrap();
9885        let options = ReaderOptions {
9886            max_total_extraction_size: 3,
9887            ..ReaderOptions::default()
9888        };
9889        let opened =
9890            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9891
9892        assert_eq!(
9893            opened.extract_file("cap.txt").unwrap_err(),
9894            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9895        );
9896    }
9897
9898    #[test]
9899    fn verify_does_not_apply_extraction_payload_cap() {
9900        let archive = write_archive(
9901            &[RegularFile::new("verify-cap.txt", b"payload")],
9902            &master_key(),
9903            single_stream_options(),
9904        )
9905        .unwrap();
9906        let options = ReaderOptions {
9907            max_total_extraction_size: 3,
9908            ..ReaderOptions::default()
9909        };
9910        let opened =
9911            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9912
9913        opened.verify().unwrap();
9914        assert_eq!(
9915            opened.extract_file("verify-cap.txt").unwrap_err(),
9916            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9917        );
9918    }
9919
9920    #[test]
9921    fn verify_streams_past_legacy_in_memory_tar_cap() {
9922        let data = vec![0x5a; 4096];
9923        let archive = write_archive(
9924            &[RegularFile::new("verify-large.txt", &data)],
9925            &master_key(),
9926            single_stream_options(),
9927        )
9928        .unwrap();
9929        let options = ReaderOptions {
9930            max_verify_tar_size: 1,
9931            ..ReaderOptions::default()
9932        };
9933        let opened =
9934            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9935
9936        opened.verify().unwrap();
9937    }
9938
9939    #[test]
9940    fn dictionary_sidecar_requires_dictionary_record_section() {
9941        let archive = write_archive_with_dictionary(
9942            &[RegularFile::new("dict-missing.txt", b"common words")],
9943            &master_key(),
9944            single_stream_options(),
9945            dictionary(),
9946        )
9947        .unwrap();
9948        let header = BootstrapSidecarHeader::parse(
9949            &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
9950        )
9951        .unwrap();
9952        let mut missing_dictionary =
9953            archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
9954        rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
9955            header.flags &= !0x04;
9956            header.dictionary_records_offset = 0;
9957            header.dictionary_records_length = 0;
9958        });
9959
9960        assert_eq!(
9961            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
9962                .unwrap_err(),
9963            FormatError::ReaderUnsupported("dictionary bootstrap required")
9964        );
9965    }
9966
9967    #[test]
9968    fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
9969        let archive = write_archive_with_dictionary(
9970            &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
9971            &master_key(),
9972            single_stream_options(),
9973            dictionary(),
9974        )
9975        .unwrap();
9976
9977        let mut wrong_kind = archive.bootstrap_sidecar.clone();
9978        mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
9979            record.kind = BlockKind::IndexRootData;
9980        });
9981        assert_eq!(
9982            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
9983                .unwrap_err(),
9984            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
9985        );
9986
9987        let mut wrong_last = archive.bootstrap_sidecar.clone();
9988        mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
9989            record.flags = 0;
9990        });
9991        assert_eq!(
9992            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
9993                .unwrap_err(),
9994            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
9995        );
9996    }
9997
9998    #[test]
9999    fn non_seekable_random_access_requires_sidecar() {
10000        let archive = write_archive(
10001            &[RegularFile::new("file.txt", b"payload")],
10002            &master_key(),
10003            single_stream_options(),
10004        )
10005        .unwrap();
10006
10007        assert_eq!(
10008            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10009            FormatError::ReaderUnsupported(
10010                "non-seekable random access requires a bootstrap sidecar"
10011            )
10012        );
10013        assert!(open_non_seekable_archive(
10014            &archive.bytes,
10015            &master_key(),
10016            Some(&archive.bootstrap_sidecar)
10017        )
10018        .is_ok());
10019    }
10020
10021    #[test]
10022    fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
10023        let archive = write_archive(
10024            &[RegularFile::new("sparse.txt", b"sparse sidecar")],
10025            &master_key(),
10026            single_stream_options(),
10027        )
10028        .unwrap();
10029        let index_root_only = sparse_bootstrap_sidecar(
10030            &archive.bootstrap_sidecar,
10031            &master_key(),
10032            false,
10033            true,
10034            false,
10035        );
10036
10037        assert_eq!(
10038            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
10039                .unwrap_err(),
10040            FormatError::ReaderUnsupported(
10041                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10042            )
10043        );
10044    }
10045
10046    #[test]
10047    fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
10048        let archive = write_archive(
10049            &[RegularFile::new("sparse-index.txt", b"recover index root")],
10050            &master_key(),
10051            single_stream_options(),
10052        )
10053        .unwrap();
10054        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10055        let mut corrupted = archive.bytes.clone();
10056        corrupt_object_extent_records(
10057            &mut corrupted,
10058            index_root_extent_from_manifest(&opened.manifest_footer),
10059        );
10060        assert!(open_archive(&corrupted, &master_key()).is_err());
10061
10062        let index_root_only = sparse_bootstrap_sidecar(
10063            &archive.bootstrap_sidecar,
10064            &master_key(),
10065            false,
10066            true,
10067            false,
10068        );
10069        let recovered =
10070            open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
10071                .unwrap();
10072
10073        assert_eq!(
10074            recovered.extract_file("sparse-index.txt").unwrap(),
10075            Some(b"recover index root".to_vec())
10076        );
10077        recovered.verify().unwrap();
10078    }
10079
10080    #[test]
10081    fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
10082        let archive = write_archive_with_dictionary(
10083            &[RegularFile::new(
10084                "sparse-dict.txt",
10085                b"common words common words sparse dictionary",
10086            )],
10087            &master_key(),
10088            single_stream_options(),
10089            dictionary(),
10090        )
10091        .unwrap();
10092        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10093        let mut corrupted = archive.bytes.clone();
10094        corrupt_object_extent_records(
10095            &mut corrupted,
10096            dictionary_extent_from_index_root(&opened.index_root).unwrap(),
10097        );
10098        assert!(open_archive(&corrupted, &master_key()).is_err());
10099
10100        let dictionary_only = sparse_bootstrap_sidecar(
10101            &archive.bootstrap_sidecar,
10102            &master_key(),
10103            false,
10104            false,
10105            true,
10106        );
10107        assert_eq!(
10108            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
10109                .unwrap_err(),
10110            FormatError::ReaderUnsupported(
10111                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10112            )
10113        );
10114
10115        let recovered =
10116            open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
10117                .unwrap();
10118        assert_eq!(
10119            recovered.extract_file("sparse-dict.txt").unwrap(),
10120            Some(b"common words common words sparse dictionary".to_vec())
10121        );
10122        recovered.verify().unwrap();
10123    }
10124
10125    #[test]
10126    fn sequential_extracts_dictionary_free_tar_stream() {
10127        let archive = write_archive(
10128            &[RegularFile::new("seq.txt", b"streaming")],
10129            &master_key(),
10130            single_stream_options(),
10131        )
10132        .unwrap();
10133
10134        let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
10135        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10136        assert_eq!(member.path, b"seq.txt");
10137        assert_eq!(member.data, b"streaming");
10138    }
10139
10140    #[test]
10141    fn sequential_rejects_logical_payload_above_total_size_cap() {
10142        let archive = write_archive(
10143            &[RegularFile::new("seq-cap.txt", b"payload")],
10144            &master_key(),
10145            single_stream_options(),
10146        )
10147        .unwrap();
10148        let options = ReaderOptions {
10149            max_total_extraction_size: 3,
10150            ..ReaderOptions::default()
10151        };
10152
10153        assert_eq!(
10154            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10155                .unwrap_err(),
10156            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10157        );
10158    }
10159
10160    #[test]
10161    fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
10162        let archive = write_archive(
10163            &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
10164            &master_key(),
10165            single_stream_options(),
10166        )
10167        .unwrap();
10168        let options = ReaderOptions {
10169            max_verify_tar_size: 512,
10170            ..ReaderOptions::default()
10171        };
10172
10173        assert_eq!(
10174            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10175                .unwrap_err(),
10176            FormatError::ReaderUnsupported(
10177                "sequential tar stream exceeds configured verification cap"
10178            )
10179        );
10180    }
10181
10182    #[test]
10183    fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
10184        let archive = write_archive(
10185            &[RegularFile::new("seq-erasure.txt", b"stream repair")],
10186            &master_key(),
10187            single_stream_options(),
10188        )
10189        .unwrap();
10190        let mut corrupted = archive.bytes;
10191        corrupt_first_block_record_payload(&mut corrupted);
10192
10193        let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
10194        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10195        assert_eq!(member.path, b"seq-erasure.txt");
10196        assert_eq!(member.data, b"stream repair");
10197    }
10198
10199    #[test]
10200    fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
10201        let archive = write_archive(
10202            &[RegularFile::new("seq-no-parity.txt", b"no repair")],
10203            &master_key(),
10204            WriterOptions {
10205                bit_rot_buffer_pct: 0,
10206                fec_parity_shards: 0,
10207                index_fec_parity_shards: 0,
10208                index_root_fec_parity_shards: 0,
10209                ..single_stream_options()
10210            },
10211        )
10212        .unwrap();
10213        let mut corrupted = archive.bytes;
10214        corrupt_first_block_record_payload(&mut corrupted);
10215
10216        assert_eq!(
10217            sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
10218            FormatError::BadCrc {
10219                structure: "BlockRecord"
10220            }
10221        );
10222    }
10223
10224    #[test]
10225    fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
10226        let archive = write_archive(
10227            &[RegularFile::new(
10228                "seq.txt",
10229                b"payload must not be returned after terminal auth failure",
10230            )],
10231            &master_key(),
10232            single_stream_options(),
10233        )
10234        .unwrap();
10235        let mut corrupted = archive.bytes;
10236        corrupt_v41_terminal_recovery(&mut corrupted);
10237
10238        match sequential_extract_tar_stream(&corrupted, &master_key()) {
10239            Ok(bytes) => panic!(
10240                "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
10241                bytes.len()
10242            ),
10243            Err(err) => assert_eq!(
10244                err,
10245                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10246            ),
10247        }
10248    }
10249
10250    #[test]
10251    fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
10252        let archive = write_archive_with_dictionary(
10253            &[RegularFile::new(
10254                "seq-dict.txt",
10255                b"common words common words dictionary payload",
10256            )],
10257            &master_key(),
10258            single_stream_options(),
10259            b"common words dictionary",
10260        )
10261        .unwrap();
10262
10263        match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
10264            Ok(bytes) => panic!(
10265                "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
10266                bytes.len()
10267            ),
10268            Err(err) => assert_eq!(
10269                err,
10270                FormatError::ReaderUnsupported(
10271                    "dictionary bootstrap required for non-seekable sequential extraction"
10272                )
10273            ),
10274        }
10275    }
10276
10277    #[test]
10278    fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
10279        let archive = write_archive_with_dictionary(
10280            &[RegularFile::new(
10281                "seq-dict-open.txt",
10282                b"common words common words bootstrap required",
10283            )],
10284            &master_key(),
10285            single_stream_options(),
10286            b"common words bootstrap",
10287        )
10288        .unwrap();
10289
10290        assert_eq!(
10291            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10292            FormatError::ReaderUnsupported(
10293                "non-seekable random access requires a bootstrap sidecar"
10294            )
10295        );
10296    }
10297
10298    #[test]
10299    fn sequential_zstd_stream_rejects_skippable_frame_segments() {
10300        let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
10301        let mut output = Vec::new();
10302
10303        assert_eq!(
10304            decode_concatenated_zstd_frames_with_cap(
10305                &skippable,
10306                None,
10307                &mut output,
10308                usize::MAX,
10309                None,
10310            )
10311            .unwrap_err(),
10312            FormatError::NotStandardZstdFrame
10313        );
10314        assert!(output.is_empty());
10315    }
10316
10317    #[test]
10318    fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
10319        let archive = write_archive(
10320            &[RegularFile::new("live.txt", b"stream verify")],
10321            &master_key(),
10322            single_stream_options(),
10323        )
10324        .unwrap();
10325
10326        let report =
10327            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10328
10329        assert_eq!(report.file_count, 1);
10330        assert_eq!(report.total_volumes, 1);
10331        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
10332        assert!(report.payload_block_count > 0);
10333    }
10334
10335    #[test]
10336    fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
10337        let archive = write_archive(
10338            &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
10339            &master_key(),
10340            single_stream_options(),
10341        )
10342        .unwrap();
10343
10344        let report =
10345            verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
10346                .unwrap();
10347
10348        assert_eq!(report.file_count, 1);
10349        assert_eq!(report.tar_total_size % 512, 0);
10350    }
10351
10352    #[test]
10353    fn live_non_seekable_verify_stream_accepts_empty_archive() {
10354        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10355
10356        let report =
10357            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10358
10359        assert_eq!(report.file_count, 0);
10360        assert_eq!(report.payload_block_count, 0);
10361        assert_eq!(report.tar_total_size, 0);
10362    }
10363
10364    #[test]
10365    fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
10366        let archive = write_archive_with_dictionary(
10367            &[RegularFile::new(
10368                "live-dict.txt",
10369                b"common words common words dictionary payload",
10370            )],
10371            &master_key(),
10372            single_stream_options(),
10373            b"common words dictionary",
10374        )
10375        .unwrap();
10376
10377        assert_eq!(
10378            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
10379                .unwrap_err(),
10380            FormatError::ReaderUnsupported(
10381                "dictionary bootstrap required for non-seekable sequential verification"
10382            )
10383        );
10384    }
10385
10386    #[test]
10387    fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
10388        let archive = write_archive_with_dictionary(
10389            &[RegularFile::new(
10390                "live-dict-sidecar.txt",
10391                b"common words common words dictionary payload",
10392            )],
10393            &master_key(),
10394            single_stream_options(),
10395            b"common words dictionary",
10396        )
10397        .unwrap();
10398
10399        let report = verify_non_seekable_stream_with_bootstrap_sidecar(
10400            std::io::Cursor::new(archive.bytes),
10401            &archive.bootstrap_sidecar,
10402            &master_key(),
10403            NonSeekableReaderOptions::default(),
10404        )
10405        .unwrap();
10406
10407        assert_eq!(report.file_count, 1);
10408        assert_eq!(report.total_volumes, 1);
10409    }
10410
10411    #[test]
10412    fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
10413        let archive = write_archive(
10414            &[RegularFile::new("tail-cap.txt", b"payload")],
10415            &master_key(),
10416            single_stream_options(),
10417        )
10418        .unwrap();
10419        let options = NonSeekableReaderOptions {
10420            max_terminal_tail_size: 8,
10421            ..NonSeekableReaderOptions::default()
10422        };
10423
10424        assert_eq!(
10425            verify_non_seekable_stream_with_options(
10426                std::io::Cursor::new(archive.bytes),
10427                &master_key(),
10428                options
10429            )
10430            .unwrap_err(),
10431            FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
10432        );
10433    }
10434
10435    #[test]
10436    fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
10437        let archive = write_archive(
10438            &[RegularFile::new("metadata-cap.txt", b"payload")],
10439            &master_key(),
10440            single_stream_options(),
10441        )
10442        .unwrap();
10443        let options = NonSeekableReaderOptions {
10444            max_retained_metadata_bytes: 1,
10445            ..NonSeekableReaderOptions::default()
10446        };
10447
10448        assert_eq!(
10449            verify_non_seekable_stream_with_options(
10450                std::io::Cursor::new(archive.bytes),
10451                &master_key(),
10452                options
10453            )
10454            .unwrap_err(),
10455            FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
10456        );
10457    }
10458
10459    #[test]
10460    fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
10461        let archive = write_archive(
10462            &[RegularFile::new("metadata-erasure.txt", b"payload")],
10463            &master_key(),
10464            single_stream_options(),
10465        )
10466        .unwrap();
10467        let mut corrupted = archive.bytes;
10468        let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
10469        corrupt_block_record_payload_at_slot(&mut corrupted, slot);
10470
10471        let report =
10472            verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
10473
10474        assert_eq!(report.file_count, 1);
10475    }
10476
10477    #[test]
10478    fn live_non_seekable_verify_rejects_member_count_above_cap() {
10479        let archive = write_archive(
10480            &[RegularFile::new("member-cap.txt", b"payload")],
10481            &master_key(),
10482            single_stream_options(),
10483        )
10484        .unwrap();
10485        let options = NonSeekableReaderOptions {
10486            max_streamed_member_count: 0,
10487            ..NonSeekableReaderOptions::default()
10488        };
10489
10490        assert_eq!(
10491            verify_non_seekable_stream_with_options(
10492                std::io::Cursor::new(archive.bytes),
10493                &master_key(),
10494                options
10495            )
10496            .unwrap_err(),
10497            FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
10498        );
10499    }
10500
10501    #[test]
10502    fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
10503        let archive = write_archive(
10504            &[RegularFile::new("live-total-cap.txt", b"payload")],
10505            &master_key(),
10506            single_stream_options(),
10507        )
10508        .unwrap();
10509        let mut options = NonSeekableReaderOptions::default();
10510        options.reader.max_total_extraction_size = 3;
10511
10512        assert_eq!(
10513            verify_non_seekable_stream_with_options(
10514                std::io::Cursor::new(archive.bytes),
10515                &master_key(),
10516                options
10517            )
10518            .unwrap_err(),
10519            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10520        );
10521    }
10522
10523    #[test]
10524    fn live_non_seekable_verify_reports_root_auth_wire_only() {
10525        let archive = write_archive_with_root_auth(
10526            &[RegularFile::new("signed-live.txt", b"root-auth stream")],
10527            &master_key(),
10528            single_stream_options(),
10529            test_root_auth_config(),
10530            |request| Ok(test_root_auth_value(request)),
10531        )
10532        .unwrap();
10533
10534        let report =
10535            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10536
10537        assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
10538    }
10539
10540    #[test]
10541    fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
10542        let archive = write_archive(
10543            &[
10544                RegularFile::new("alpha.txt", b"alpha"),
10545                RegularFile::new("nested/beta.txt", b"beta"),
10546            ],
10547            &master_key(),
10548            single_stream_options(),
10549        )
10550        .unwrap();
10551        let tmp = tempfile::tempdir().unwrap();
10552        let out = tmp.path().join("out");
10553
10554        let report = extract_non_seekable_stream_to_dir(
10555            std::io::Cursor::new(archive.bytes),
10556            &master_key(),
10557            &out,
10558            NonSeekableReaderOptions::default(),
10559            SafeExtractionOptions::default(),
10560        )
10561        .unwrap();
10562
10563        assert_eq!(report.verification.file_count, 2);
10564        assert_eq!(report.extracted_member_count, 2);
10565        assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
10566        assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
10567    }
10568
10569    #[test]
10570    fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
10571        let archive = write_archive(
10572            &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
10573            &master_key(),
10574            single_stream_options(),
10575        )
10576        .unwrap();
10577        let tmp = tempfile::tempdir().unwrap();
10578        let out = tmp.path().join("out");
10579
10580        extract_non_seekable_stream_to_dir(
10581            ChunkedReader::new(archive.bytes, 1),
10582            &master_key(),
10583            &out,
10584            NonSeekableReaderOptions::default(),
10585            SafeExtractionOptions::default(),
10586        )
10587        .unwrap();
10588
10589        assert_eq!(
10590            fs::read(out.join("tiny-extract.txt")).unwrap(),
10591            b"chunked extraction"
10592        );
10593    }
10594
10595    #[test]
10596    fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
10597        let archive = write_archive(
10598            &[RegularFile::new("late-fail.txt", b"must remain staged")],
10599            &master_key(),
10600            single_stream_options(),
10601        )
10602        .unwrap();
10603        let mut corrupted = archive.bytes;
10604        corrupt_v41_terminal_recovery(&mut corrupted);
10605        let tmp = tempfile::tempdir().unwrap();
10606        let out = tmp.path().join("out");
10607
10608        match extract_non_seekable_stream_to_dir(
10609            std::io::Cursor::new(corrupted),
10610            &master_key(),
10611            &out,
10612            NonSeekableReaderOptions::default(),
10613            SafeExtractionOptions::default(),
10614        )
10615        .unwrap_err()
10616        {
10617            ExtractError::Format(err) => assert_eq!(
10618                err,
10619                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10620            ),
10621            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10622        }
10623        assert!(!out.exists());
10624    }
10625
10626    #[test]
10627    fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
10628        let archive = write_archive(
10629            &[RegularFile::new("same.txt", b"new")],
10630            &master_key(),
10631            single_stream_options(),
10632        )
10633        .unwrap();
10634        let tmp = tempfile::tempdir().unwrap();
10635        let out = tmp.path().join("out");
10636        fs::create_dir(&out).unwrap();
10637        fs::write(out.join("same.txt"), b"old").unwrap();
10638
10639        match extract_non_seekable_stream_to_dir(
10640            std::io::Cursor::new(archive.bytes.clone()),
10641            &master_key(),
10642            &out,
10643            NonSeekableReaderOptions::default(),
10644            SafeExtractionOptions::default(),
10645        )
10646        .unwrap_err()
10647        {
10648            ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
10649            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10650        }
10651        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
10652
10653        extract_non_seekable_stream_to_dir(
10654            std::io::Cursor::new(archive.bytes),
10655            &master_key(),
10656            &out,
10657            NonSeekableReaderOptions::default(),
10658            SafeExtractionOptions {
10659                overwrite_existing: true,
10660            },
10661        )
10662        .unwrap();
10663        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
10664    }
10665
10666    #[test]
10667    fn live_non_seekable_list_stream_matches_seekable_final_view() {
10668        let archive = write_archive(
10669            &[
10670                RegularFile::new("a.txt", b"a"),
10671                RegularFile::new("b.txt", b"bb"),
10672            ],
10673            &master_key(),
10674            single_stream_options(),
10675        )
10676        .unwrap();
10677        let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
10678        let expected = seekable.list_files().unwrap();
10679
10680        let report = list_non_seekable_stream(
10681            std::io::Cursor::new(archive.bytes),
10682            &master_key(),
10683            NonSeekableReaderOptions::default(),
10684        )
10685        .unwrap();
10686
10687        assert_eq!(report.verification.file_count, 2);
10688        assert_eq!(report.entries, expected);
10689    }
10690
10691    #[test]
10692    fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
10693        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10694        let mut bad_flags = archive.bootstrap_sidecar.clone();
10695        rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
10696            header.flags |= 0x08;
10697        });
10698        assert_eq!(
10699            open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
10700                .unwrap_err(),
10701            FormatError::UnknownBootstrapSidecarFlags(0x0b)
10702        );
10703
10704        let mut trailing = archive.bootstrap_sidecar.clone();
10705        trailing.push(0);
10706        assert_eq!(
10707            open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
10708                .unwrap_err(),
10709            FormatError::NonCanonicalBootstrapSidecarLayout
10710        );
10711    }
10712
10713    #[test]
10714    fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
10715        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10716        let mut wrong_volume = archive.bootstrap_sidecar.clone();
10717        mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
10718            footer.volume_index = 1;
10719        });
10720        assert_eq!(
10721            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
10722                .unwrap_err(),
10723            FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
10724        );
10725
10726        let mut non_authoritative = archive.bootstrap_sidecar.clone();
10727        mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
10728            footer.is_authoritative = 0;
10729        });
10730        assert_eq!(
10731            open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
10732                .unwrap_err(),
10733            FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
10734        );
10735    }
10736
10737    #[test]
10738    fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
10739        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10740        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10741        let crypto_start = volume_header.crypto_header_offset as usize;
10742        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
10743        let crypto_header = CryptoHeader::parse(
10744            &archive.bytes[crypto_start..crypto_end],
10745            volume_header.crypto_header_length,
10746        )
10747        .unwrap();
10748        let subkeys = Subkeys::derive(
10749            &master_key(),
10750            &volume_header.archive_uuid,
10751            &volume_header.session_id,
10752        )
10753        .unwrap();
10754        let mut opened_header = volume_header;
10755        opened_header.volume_index = 1;
10756
10757        let parsed = parse_bootstrap_sidecar(
10758            &archive.bootstrap_sidecar,
10759            &opened_header,
10760            &crypto_header.fixed,
10761            &subkeys,
10762        )
10763        .unwrap();
10764
10765        assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
10766    }
10767
10768    #[test]
10769    fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
10770        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10771        let mut conflicting = archive.bootstrap_sidecar.clone();
10772        mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
10773            footer.index_root_first_block += 1;
10774        });
10775
10776        assert_eq!(
10777            open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
10778                .unwrap_err(),
10779            FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
10780        );
10781    }
10782
10783    #[test]
10784    fn sidecar_size_cap_counts_only_present_sparse_sections() {
10785        let mut crypto_header = test_crypto_header();
10786        crypto_header.has_dictionary = 1;
10787        crypto_header.index_root_fec_data_shards = 1;
10788        crypto_header.index_root_fec_parity_shards = 0;
10789        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10790        let header = BootstrapSidecarHeader {
10791            archive_uuid: [0x31; 16],
10792            session_id: [0x42; 16],
10793            flags: 0x04,
10794            manifest_footer_offset: 0,
10795            manifest_footer_length: 0,
10796            index_root_records_offset: 0,
10797            index_root_records_length: 0,
10798            dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10799            dictionary_records_length: record_len,
10800            sidecar_hmac: [0u8; 32],
10801            header_crc32c: 0,
10802        };
10803
10804        validate_sidecar_size_cap(
10805            &header,
10806            &crypto_header,
10807            BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
10808        )
10809        .unwrap();
10810        assert_eq!(
10811            validate_sidecar_size_cap(
10812                &header,
10813                &crypto_header,
10814                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
10815            )
10816            .unwrap_err(),
10817            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10818        );
10819    }
10820
10821    #[test]
10822    fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
10823        let mut crypto_header = test_crypto_header();
10824        crypto_header.index_root_fec_data_shards = 1;
10825        crypto_header.index_root_fec_parity_shards = 0;
10826        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10827        let header = BootstrapSidecarHeader {
10828            archive_uuid: [0x31; 16],
10829            session_id: [0x42; 16],
10830            flags: 0x02,
10831            manifest_footer_offset: 0,
10832            manifest_footer_length: 0,
10833            index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10834            index_root_records_length: record_len * 2,
10835            dictionary_records_offset: 0,
10836            dictionary_records_length: 0,
10837            sidecar_hmac: [0u8; 32],
10838            header_crc32c: 0,
10839        };
10840
10841        assert_eq!(
10842            validate_sidecar_size_cap(
10843                &header,
10844                &crypto_header,
10845                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
10846            )
10847            .unwrap_err(),
10848            FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
10849        );
10850    }
10851
10852    #[test]
10853    fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
10854        let mut crypto_header = test_crypto_header();
10855        crypto_header.block_size = u32::MAX;
10856        crypto_header.index_root_fec_data_shards = u16::MAX;
10857        crypto_header.index_root_fec_parity_shards = u16::MAX;
10858        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10859        let max_records = crypto_header.index_root_fec_data_shards as u64
10860            + crypto_header.index_root_fec_parity_shards as u64;
10861        let max_section_len = max_records * record_len;
10862        let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
10863            + MANIFEST_FOOTER_LEN as u64
10864            + max_section_len
10865            + max_section_len;
10866        let header = BootstrapSidecarHeader {
10867            archive_uuid: [0x31; 16],
10868            session_id: [0x42; 16],
10869            flags: 0x01 | 0x02 | 0x04,
10870            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10871            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
10872            index_root_records_offset: 0,
10873            index_root_records_length: max_section_len,
10874            dictionary_records_offset: 0,
10875            dictionary_records_length: max_section_len,
10876            sidecar_hmac: [0u8; 32],
10877            header_crc32c: 0,
10878        };
10879
10880        validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
10881        assert_eq!(
10882            validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
10883            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10884        );
10885    }
10886
10887    #[test]
10888    fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
10889        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10890        let mut with_dictionary = archive.bootstrap_sidecar.clone();
10891        let header =
10892            BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
10893                .unwrap();
10894        let record_len = sidecar_record_len(&with_dictionary);
10895        let first_record = header.index_root_records_offset as usize;
10896        let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
10897        let dictionary_offset = with_dictionary.len() as u64;
10898        with_dictionary.extend_from_slice(&copied_record);
10899        rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
10900            header.flags |= 0x04;
10901            header.dictionary_records_offset = dictionary_offset;
10902            header.dictionary_records_length = record_len as u64;
10903        });
10904
10905        assert_eq!(
10906            open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
10907                .unwrap_err(),
10908            FormatError::InvalidArchive(
10909                "bootstrap sidecar has dictionary records while has_dictionary is false"
10910            )
10911        );
10912    }
10913
10914    #[test]
10915    fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
10916        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10917        let mut missing = archive.bootstrap_sidecar.clone();
10918        let record_len = sidecar_record_len(&missing);
10919        let new_len = missing.len() - record_len;
10920        missing.truncate(new_len);
10921        rewrite_sidecar_header(&mut missing, &master_key(), |header| {
10922            header.index_root_records_length -= record_len as u64;
10923        });
10924        assert_eq!(
10925            open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
10926                .unwrap_err(),
10927            FormatError::InvalidArchive(
10928                "sidecar BlockRecord section does not match declared extent"
10929            )
10930        );
10931
10932        let mut duplicate = archive.bootstrap_sidecar.clone();
10933        mutate_sidecar_index_record(&mut duplicate, 1, |record| {
10934            record.block_index -= 1;
10935        });
10936        assert_eq!(
10937            open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
10938                .unwrap_err(),
10939            FormatError::InvalidArchive(
10940                "sidecar BlockRecord section has missing or duplicate blocks"
10941            )
10942        );
10943
10944        let mut misordered = archive.bootstrap_sidecar.clone();
10945        swap_sidecar_index_records(&mut misordered, 0, 1);
10946        assert_eq!(
10947            open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
10948                .unwrap_err(),
10949            FormatError::InvalidArchive(
10950                "sidecar BlockRecord section has missing or duplicate blocks"
10951            )
10952        );
10953
10954        let mut wrong_kind = archive.bootstrap_sidecar.clone();
10955        mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
10956            record.kind = BlockKind::PayloadData;
10957        });
10958        assert_eq!(
10959            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
10960                .unwrap_err(),
10961            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
10962        );
10963
10964        let mut wrong_last = archive.bootstrap_sidecar.clone();
10965        mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
10966            record.flags = 0;
10967        });
10968        assert_eq!(
10969            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
10970                .unwrap_err(),
10971            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
10972        );
10973    }
10974
10975    #[test]
10976    fn verify_helper_rejects_envelope_frame_coverage_gap() {
10977        let frames = BTreeMap::from([(
10978            0,
10979            FrameEntry {
10980                frame_index: 0,
10981                envelope_index: 0,
10982                offset_in_envelope: 0,
10983                compressed_size: 10,
10984                decompressed_size: 512,
10985                flags: 0,
10986                tar_stream_offset: 0,
10987            },
10988        )]);
10989        let envelopes = BTreeMap::from([(
10990            0,
10991            EnvelopeEntry {
10992                envelope_index: 0,
10993                first_block_index: 0,
10994                data_block_count: 1,
10995                parity_block_count: 1,
10996                encrypted_size: 4096,
10997                plaintext_size: 11,
10998                first_frame_index: 0,
10999                frame_count: 1,
11000            },
11001        )]);
11002
11003        assert_eq!(
11004            validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
11005            FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
11006        );
11007    }
11008
11009    #[test]
11010    fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
11011        assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
11012        assert_eq!(
11013            validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
11014            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11015        );
11016        assert_eq!(
11017            validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
11018            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11019        );
11020    }
11021
11022    #[test]
11023    fn verify_rejects_authenticated_content_hash_mismatch() {
11024        let options = WriterOptions {
11025            index_root_fec_parity_shards: 0,
11026            ..single_stream_options()
11027        };
11028        let archive = write_archive(
11029            &[RegularFile::new("content-hash.txt", b"hash covered")],
11030            &master_key(),
11031            options,
11032        )
11033        .unwrap();
11034        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11035
11036        let mut root = opened.index_root.clone();
11037        root.header.content_sha256 = [0xa5; 32];
11038        let root_plaintext = root.to_bytes();
11039        IndexRoot::parse(
11040            &root_plaintext,
11041            false,
11042            metadata_limits(&opened.crypto_header),
11043        )
11044        .unwrap();
11045        assert_eq!(
11046            root_plaintext.len() as u32,
11047            opened.manifest_footer.index_root_decompressed_size
11048        );
11049
11050        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11051        let mut next_block_index = opened.manifest_footer.index_root_first_block;
11052        let replacement = encrypt_test_object(
11053            &compressed_root,
11054            &opened.subkeys.index_root_key,
11055            &opened.subkeys.index_nonce_seed,
11056            b"idxroot",
11057            0,
11058            BlockKind::IndexRootData,
11059            &mut next_block_index,
11060            &opened.crypto_header,
11061            &opened.volume_header,
11062        );
11063        assert_eq!(
11064            replacement.extent.data_block_count,
11065            opened.manifest_footer.index_root_data_block_count
11066        );
11067        assert_eq!(
11068            replacement.extent.encrypted_size,
11069            opened.manifest_footer.index_root_encrypted_size
11070        );
11071
11072        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11073        let crypto_end = volume_header.crypto_header_offset as usize
11074            + volume_header.crypto_header_length as usize;
11075        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11076        let mut malformed = archive.bytes.clone();
11077        for record in replacement.records {
11078            let offset = crypto_end + record.block_index as usize * record_len;
11079            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11080        }
11081
11082        let reopened = open_archive(&malformed, &master_key()).unwrap();
11083        assert_eq!(
11084            reopened.verify().unwrap_err(),
11085            FormatError::InvalidArchive(
11086                "IndexRoot content_sha256 does not match decoded tar stream"
11087            )
11088        );
11089    }
11090
11091    #[test]
11092    fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
11093        let (mut path_mismatch, _) = multi_envelope_reader_fixture();
11094        rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
11095            path[0] = b'x';
11096        });
11097        assert_eq!(
11098            path_mismatch.verify().unwrap_err(),
11099            FormatError::InvalidArchive("tar member path does not match FileEntry path")
11100        );
11101
11102        let (mut size_mismatch, _) = multi_envelope_reader_fixture();
11103        rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
11104            file.file_data_size += 1;
11105        });
11106        assert_eq!(
11107            size_mismatch.verify().unwrap_err(),
11108            FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
11109        );
11110    }
11111
11112    #[test]
11113    fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
11114        let (mut opened, _) = multi_envelope_reader_fixture();
11115        let locating = opened.index_root.shards[0].clone();
11116        let mut duplicate = opened.load_index_shard(&locating).unwrap();
11117        duplicate.header.shard_index = 1;
11118        duplicate.frames[0].flags ^= 0x0000_0001;
11119        let duplicate_plaintext = duplicate.to_bytes();
11120        let mut next_block_index = opened
11121            .blocks
11122            .keys()
11123            .last()
11124            .copied()
11125            .map(|index| index + 1)
11126            .unwrap_or(0);
11127        let duplicate_object = encrypt_test_object(
11128            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11129            &opened.subkeys.index_shard_key,
11130            &opened.subkeys.index_nonce_seed,
11131            b"idxshard",
11132            1,
11133            BlockKind::IndexShardData,
11134            &mut next_block_index,
11135            &opened.crypto_header,
11136            &opened.volume_header,
11137        );
11138        insert_records(&mut opened.blocks, &duplicate_object.records);
11139        opened.index_root.shards.push(ShardEntry {
11140            shard_index: 1,
11141            first_block_index: duplicate_object.extent.first_block_index,
11142            data_block_count: duplicate_object.extent.data_block_count,
11143            parity_block_count: 0,
11144            encrypted_size: duplicate_object.extent.encrypted_size,
11145            decompressed_size: duplicate_plaintext.len() as u32,
11146            file_count: locating.file_count,
11147            first_path_hash: locating.first_path_hash,
11148            last_path_hash: locating.last_path_hash,
11149        });
11150        opened.index_root.header.file_count += locating.file_count as u64;
11151
11152        assert_eq!(
11153            opened.verify().unwrap_err(),
11154            FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
11155        );
11156    }
11157
11158    #[test]
11159    fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
11160        let (mut opened, _) = multi_envelope_reader_fixture();
11161        let locating = opened.index_root.shards[0].clone();
11162        let mut duplicate = opened.load_index_shard(&locating).unwrap();
11163        duplicate.header.shard_index = 1;
11164        duplicate.envelopes[0].first_block_index += 1;
11165        let duplicate_plaintext = duplicate.to_bytes();
11166        let mut next_block_index = opened
11167            .blocks
11168            .keys()
11169            .last()
11170            .copied()
11171            .map(|index| index + 1)
11172            .unwrap_or(0);
11173        let duplicate_object = encrypt_test_object(
11174            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11175            &opened.subkeys.index_shard_key,
11176            &opened.subkeys.index_nonce_seed,
11177            b"idxshard",
11178            1,
11179            BlockKind::IndexShardData,
11180            &mut next_block_index,
11181            &opened.crypto_header,
11182            &opened.volume_header,
11183        );
11184        insert_records(&mut opened.blocks, &duplicate_object.records);
11185        opened.index_root.shards.push(ShardEntry {
11186            shard_index: 1,
11187            first_block_index: duplicate_object.extent.first_block_index,
11188            data_block_count: duplicate_object.extent.data_block_count,
11189            parity_block_count: 0,
11190            encrypted_size: duplicate_object.extent.encrypted_size,
11191            decompressed_size: duplicate_plaintext.len() as u32,
11192            file_count: locating.file_count,
11193            first_path_hash: locating.first_path_hash,
11194            last_path_hash: locating.last_path_hash,
11195        });
11196        opened.index_root.header.file_count += locating.file_count as u64;
11197
11198        assert_eq!(
11199            opened.verify().unwrap_err(),
11200            FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
11201        );
11202    }
11203
11204    #[test]
11205    fn verify_rejects_non_contiguous_global_envelope_indexes() {
11206        let (mut opened, _) = multi_envelope_reader_fixture();
11207        replace_first_index_shard(&mut opened, |shard| {
11208            let frame = shard
11209                .frames
11210                .iter_mut()
11211                .find(|entry| entry.frame_index == 1)
11212                .unwrap();
11213            frame.envelope_index = 2;
11214
11215            let envelope = shard
11216                .envelopes
11217                .iter_mut()
11218                .find(|entry| entry.envelope_index == 1)
11219                .unwrap();
11220            envelope.envelope_index = 2;
11221        });
11222
11223        assert_eq!(
11224            opened.verify().unwrap_err(),
11225            FormatError::InvalidMetadata {
11226                structure: "EnvelopeEntry",
11227                reason: "global index coverage has a gap",
11228            }
11229        );
11230    }
11231
11232    #[test]
11233    fn verify_rejects_payload_object_extent_overlap() {
11234        let (mut opened, _) = multi_envelope_reader_fixture();
11235        replace_first_index_shard(&mut opened, |shard| {
11236            let first_block_index = shard.envelopes[0].first_block_index;
11237            shard.envelopes[1].first_block_index = first_block_index;
11238        });
11239
11240        assert_eq!(
11241            opened.verify().unwrap_err(),
11242            FormatError::InvalidArchive("encrypted object block ranges overlap")
11243        );
11244    }
11245
11246    #[test]
11247    fn verify_accepts_cross_shard_shared_envelope_frame_union() {
11248        let volume_header = test_volume_header();
11249        let crypto_header = test_crypto_header();
11250        let subkeys = Subkeys::derive(
11251            &master_key(),
11252            &volume_header.archive_uuid,
11253            &volume_header.session_id,
11254        )
11255        .unwrap();
11256        let mut next_block_index = 0u64;
11257        let mut blocks = BTreeMap::new();
11258
11259        let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
11260        let beta = test_member(b"beta.txt", b"beta cross shard\n");
11261        let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
11262        let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
11263        let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
11264        let envelope_plaintext =
11265            [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
11266        let payload = encrypt_test_object(
11267            &envelope_plaintext,
11268            &subkeys.enc_key,
11269            &subkeys.nonce_seed,
11270            b"envelope",
11271            0,
11272            BlockKind::PayloadData,
11273            &mut next_block_index,
11274            &crypto_header,
11275            &volume_header,
11276        );
11277        insert_records(&mut blocks, &payload.records);
11278
11279        let envelope = EnvelopeEntry {
11280            envelope_index: 0,
11281            first_block_index: payload.extent.first_block_index,
11282            data_block_count: payload.extent.data_block_count,
11283            parity_block_count: 0,
11284            encrypted_size: payload.extent.encrypted_size,
11285            plaintext_size: envelope_plaintext.len() as u32,
11286            first_frame_index: 0,
11287            frame_count: 2,
11288        };
11289        let frame0 = FrameEntry {
11290            frame_index: 0,
11291            envelope_index: 0,
11292            offset_in_envelope: 0,
11293            compressed_size: frame0_plaintext.len() as u32,
11294            decompressed_size: alpha.len() as u32,
11295            flags: 0x0000_0003,
11296            tar_stream_offset: 0,
11297        };
11298        let frame1 = FrameEntry {
11299            frame_index: 1,
11300            envelope_index: 0,
11301            offset_in_envelope: frame0_plaintext.len() as u32,
11302            compressed_size: frame1_plaintext.len() as u32,
11303            decompressed_size: beta.len() as u32,
11304            flags: 0x0000_0003,
11305            tar_stream_offset: alpha.len() as u64,
11306        };
11307
11308        let (shard0_plaintext, first0, last0) = build_test_index_shard(
11309            &[TestFileMeta {
11310                path: b"alpha.txt".to_vec(),
11311                frame_index: 0,
11312                tar_stream_offset: 0,
11313                member_group_size: alpha.len() as u64,
11314                file_data_size: b"alpha cross shard\n".len() as u64,
11315            }],
11316            &[frame0],
11317            std::slice::from_ref(&envelope),
11318        );
11319        let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
11320            &[TestFileMeta {
11321                path: b"beta.txt".to_vec(),
11322                frame_index: 1,
11323                tar_stream_offset: alpha.len() as u64,
11324                member_group_size: beta.len() as u64,
11325                file_data_size: b"beta cross shard\n".len() as u64,
11326            }],
11327            &[frame1],
11328            std::slice::from_ref(&envelope),
11329        );
11330        shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
11331
11332        let shard0 = encrypt_test_object(
11333            &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
11334            &subkeys.index_shard_key,
11335            &subkeys.index_nonce_seed,
11336            b"idxshard",
11337            0,
11338            BlockKind::IndexShardData,
11339            &mut next_block_index,
11340            &crypto_header,
11341            &volume_header,
11342        );
11343        let shard1 = encrypt_test_object(
11344            &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
11345            &subkeys.index_shard_key,
11346            &subkeys.index_nonce_seed,
11347            b"idxshard",
11348            1,
11349            BlockKind::IndexShardData,
11350            &mut next_block_index,
11351            &crypto_header,
11352            &volume_header,
11353        );
11354        insert_records(&mut blocks, &shard0.records);
11355        insert_records(&mut blocks, &shard1.records);
11356
11357        let index_root = IndexRoot {
11358            header: IndexRootHeader {
11359                frame_count: 2,
11360                envelope_count: 1,
11361                file_count: 2,
11362                payload_block_count: payload.extent.data_block_count as u64,
11363                tar_total_size: tar_stream.len() as u64,
11364                content_sha256: sha256_bytes(&tar_stream),
11365                ..IndexRootHeader::empty()
11366            },
11367            shards: vec![
11368                ShardEntry {
11369                    shard_index: 0,
11370                    first_block_index: shard0.extent.first_block_index,
11371                    data_block_count: shard0.extent.data_block_count,
11372                    parity_block_count: 0,
11373                    encrypted_size: shard0.extent.encrypted_size,
11374                    decompressed_size: shard0_plaintext.len() as u32,
11375                    file_count: 1,
11376                    first_path_hash: first0,
11377                    last_path_hash: last0,
11378                },
11379                ShardEntry {
11380                    shard_index: 1,
11381                    first_block_index: shard1.extent.first_block_index,
11382                    data_block_count: shard1.extent.data_block_count,
11383                    parity_block_count: 0,
11384                    encrypted_size: shard1.extent.encrypted_size,
11385                    decompressed_size: shard1_plaintext.len() as u32,
11386                    file_count: 1,
11387                    first_path_hash: first1,
11388                    last_path_hash: last1,
11389                },
11390            ],
11391            directory_hint_shards: Vec::new(),
11392        };
11393
11394        let index_root_plaintext = index_root.to_bytes();
11395        let index_root_object = encrypt_test_object(
11396            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
11397            &subkeys.index_root_key,
11398            &subkeys.index_nonce_seed,
11399            b"idxroot",
11400            0,
11401            BlockKind::IndexRootData,
11402            &mut next_block_index,
11403            &crypto_header,
11404            &volume_header,
11405        );
11406        insert_records(&mut blocks, &index_root_object.records);
11407
11408        let archive_uuid = volume_header.archive_uuid;
11409        let session_id = volume_header.session_id;
11410        let opened = OpenedArchive {
11411            options: ReaderOptions::default(),
11412            observed_archive_bytes: 1_000_000,
11413            observed_volume_count: 1,
11414            subkeys,
11415            blocks,
11416            lazy_blocks: None,
11417            crypto_header_bytes: Vec::new(),
11418            volume_header,
11419            crypto_header,
11420            manifest_footer: ManifestFooter {
11421                archive_uuid,
11422                session_id,
11423                volume_index: 0,
11424                is_authoritative: 1,
11425                total_volumes: 1,
11426                index_root_first_block: index_root_object.extent.first_block_index,
11427                index_root_data_block_count: index_root_object.extent.data_block_count,
11428                index_root_parity_block_count: 0,
11429                index_root_encrypted_size: index_root_object.extent.encrypted_size,
11430                index_root_decompressed_size: index_root_plaintext.len() as u32,
11431                manifest_hmac: [0u8; 32],
11432            },
11433            volume_trailer: Some(VolumeTrailer {
11434                archive_uuid,
11435                session_id,
11436                volume_index: 0,
11437                block_count: next_block_index,
11438                bytes_written: 0,
11439                manifest_footer_offset: 0,
11440                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
11441                closed_at_ns: 0,
11442                root_auth_footer_offset: 0,
11443                root_auth_footer_length: 0,
11444                root_auth_flags: 0,
11445                trailer_hmac: [0u8; 32],
11446            }),
11447            root_auth_footer: None,
11448            index_root,
11449            payload_dictionary: None,
11450        };
11451
11452        opened.verify().unwrap();
11453    }
11454
11455    #[test]
11456    fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
11457        let options = WriterOptions {
11458            index_root_fec_parity_shards: 0,
11459            ..single_stream_options()
11460        };
11461        let archive = write_archive(
11462            &[RegularFile::new("only.txt", b"only payload")],
11463            &master_key(),
11464            options,
11465        )
11466        .unwrap();
11467        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11468        assert!(opened.index_root.directory_hint_shards.is_empty());
11469
11470        let mut root = opened.index_root.clone();
11471        root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
11472        root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
11473        let root_plaintext = root.to_bytes();
11474        IndexRoot::parse(
11475            &root_plaintext,
11476            false,
11477            metadata_limits(&opened.crypto_header),
11478        )
11479        .unwrap();
11480        assert_eq!(
11481            root_plaintext.len() as u32,
11482            opened.manifest_footer.index_root_decompressed_size
11483        );
11484
11485        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11486        let mut next_block_index = opened.manifest_footer.index_root_first_block;
11487        let replacement = encrypt_test_object(
11488            &compressed_root,
11489            &opened.subkeys.index_root_key,
11490            &opened.subkeys.index_nonce_seed,
11491            b"idxroot",
11492            0,
11493            BlockKind::IndexRootData,
11494            &mut next_block_index,
11495            &opened.crypto_header,
11496            &opened.volume_header,
11497        );
11498        assert_eq!(
11499            replacement.extent.first_block_index,
11500            opened.manifest_footer.index_root_first_block
11501        );
11502        assert_eq!(
11503            replacement.extent.data_block_count,
11504            opened.manifest_footer.index_root_data_block_count
11505        );
11506        assert_eq!(
11507            replacement.extent.encrypted_size,
11508            opened.manifest_footer.index_root_encrypted_size
11509        );
11510
11511        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11512        let crypto_end = volume_header.crypto_header_offset as usize
11513            + volume_header.crypto_header_length as usize;
11514        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11515        let mut malformed = archive.bytes.clone();
11516        for record in replacement.records {
11517            let offset = crypto_end + record.block_index as usize * record_len;
11518            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11519        }
11520
11521        let reopened = open_archive(&malformed, &master_key()).unwrap();
11522        assert_eq!(
11523            reopened.index_root.header.file_count,
11524            DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
11525        );
11526        assert!(reopened.index_root.directory_hint_shards.is_empty());
11527
11528        assert_eq!(
11529            reopened.verify().unwrap_err(),
11530            FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
11531        );
11532    }
11533
11534    #[test]
11535    fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
11536        let mut map = DirectoryHintMap::new();
11537        add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
11538        add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
11539
11540        assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
11541        assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
11542        assert_eq!(
11543            map.get(b"foo/bar".as_slice()),
11544            Some(&BTreeSet::from([2, 4]))
11545        );
11546        assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
11547        assert!(!map.contains_key(b"foobar".as_slice()));
11548    }
11549
11550    #[test]
11551    fn directory_hint_validation_requires_exact_global_map() {
11552        let mut expected = DirectoryHintMap::new();
11553        add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
11554        add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
11555        let rows = sorted_directory_hint_rows(&expected);
11556        let table = directory_hint_table_from_rows(7, &rows, 2);
11557
11558        validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
11559            .unwrap();
11560
11561        let mut missing_root = expected.clone();
11562        missing_root.remove(&Vec::new());
11563        let missing_root_rows = sorted_directory_hint_rows(&missing_root);
11564        let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
11565        assert_eq!(
11566            validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
11567                .unwrap_err(),
11568            FormatError::InvalidArchive("directory hint map does not match decoded files")
11569        );
11570
11571        let mut expected_missing_directory_entry = expected.clone();
11572        expected_missing_directory_entry
11573            .get_mut(b"foo".as_slice())
11574            .unwrap()
11575            .remove(&1);
11576        assert_eq!(
11577            validate_directory_hint_tables_against_expected(
11578                std::slice::from_ref(&table),
11579                &expected_missing_directory_entry,
11580            )
11581            .unwrap_err(),
11582            FormatError::InvalidArchive("directory hint map does not match decoded files")
11583        );
11584
11585        let mut extra = expected.clone();
11586        extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
11587        let extra_rows = sorted_directory_hint_rows(&extra);
11588        let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
11589        assert_eq!(
11590            validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
11591            FormatError::InvalidArchive("directory hint map does not match decoded files")
11592        );
11593    }
11594
11595    #[test]
11596    fn directory_hint_validation_rejects_global_order_mismatch() {
11597        let mut expected = DirectoryHintMap::new();
11598        expected.insert(Vec::new(), BTreeSet::from([0]));
11599        expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
11600        let rows = sorted_directory_hint_rows(&expected);
11601        let first = directory_hint_table_from_rows(8, &rows[..1], 1);
11602        let second = directory_hint_table_from_rows(9, &rows[1..], 1);
11603
11604        assert_eq!(
11605            validate_directory_hint_tables_against_expected(&[second, first], &expected)
11606                .unwrap_err(),
11607            FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
11608        );
11609    }
11610
11611    #[test]
11612    fn object_extent_rejects_parity_above_class_cap() {
11613        let crypto_header = CryptoHeaderFixed {
11614            length: 0,
11615            compression_algo: CompressionAlgo::ZstdFramed,
11616            aead_algo: AeadAlgo::AesGcmSiv256,
11617            fec_algo: FecAlgo::ReedSolomonGF16,
11618            kdf_algo: KdfAlgo::Raw,
11619            chunk_size: 1024,
11620            envelope_target_size: 4096,
11621            block_size: 4096,
11622            fec_data_shards: 1,
11623            fec_parity_shards: 1,
11624            index_fec_data_shards: 1,
11625            index_fec_parity_shards: 1,
11626            index_root_fec_data_shards: 1,
11627            index_root_fec_parity_shards: 1,
11628            stripe_width: 1,
11629            volume_loss_tolerance: 0,
11630            bit_rot_buffer_pct: 0,
11631            has_dictionary: 0,
11632            max_path_length: 4096,
11633            expected_volume_size: 0,
11634        };
11635        let extent = ObjectExtent {
11636            first_block_index: 0,
11637            data_block_count: 1,
11638            parity_block_count: 2,
11639            encrypted_size: 4096,
11640        };
11641
11642        assert_eq!(
11643            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11644            FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
11645        );
11646    }
11647
11648    #[test]
11649    fn object_extent_rejects_parity_below_recoverability_requirement() {
11650        let crypto_header = CryptoHeaderFixed {
11651            length: 0,
11652            compression_algo: CompressionAlgo::ZstdFramed,
11653            aead_algo: AeadAlgo::AesGcmSiv256,
11654            fec_algo: FecAlgo::ReedSolomonGF16,
11655            kdf_algo: KdfAlgo::Raw,
11656            chunk_size: 1024,
11657            envelope_target_size: 4096,
11658            block_size: 4096,
11659            fec_data_shards: 1,
11660            fec_parity_shards: 1,
11661            index_fec_data_shards: 1,
11662            index_fec_parity_shards: 1,
11663            index_root_fec_data_shards: 1,
11664            index_root_fec_parity_shards: 1,
11665            stripe_width: 2,
11666            volume_loss_tolerance: 1,
11667            bit_rot_buffer_pct: 0,
11668            has_dictionary: 0,
11669            max_path_length: 4096,
11670            expected_volume_size: 0,
11671        };
11672        let extent = ObjectExtent {
11673            first_block_index: 0,
11674            data_block_count: 1,
11675            parity_block_count: 0,
11676            encrypted_size: 4096,
11677        };
11678
11679        assert_eq!(
11680            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11681            FormatError::InvalidArchive(
11682                "encrypted object parity does not match v41 compute_parity"
11683            )
11684        );
11685    }
11686
11687    #[test]
11688    fn encrypted_object_extent_matrix_rejects_overlaps() {
11689        let (opened, _) = multi_envelope_reader_fixture();
11690        let loaded_shard = opened
11691            .load_index_shard(&opened.index_root.shards[0])
11692            .unwrap();
11693        let base_envelopes = loaded_shard
11694            .envelopes
11695            .iter()
11696            .map(|entry| (entry.envelope_index, entry.clone()))
11697            .collect::<BTreeMap<_, _>>();
11698        let payload_start = loaded_shard.envelopes[0].first_block_index;
11699        let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
11700
11701        let mut payload_overlap = base_envelopes.clone();
11702        payload_overlap
11703            .get_mut(&loaded_shard.envelopes[1].envelope_index)
11704            .unwrap()
11705            .first_block_index = payload_start;
11706        assert_eq!(
11707            opened
11708                .validate_encrypted_object_block_ranges(&payload_overlap)
11709                .unwrap_err(),
11710            overlap
11711        );
11712
11713        let mut shard_overlap = opened.clone();
11714        let shard = shard_overlap.index_root.shards[0].clone();
11715        shard_overlap.index_root.shards.push(ShardEntry {
11716            shard_index: 1,
11717            ..shard
11718        });
11719        assert_eq!(
11720            shard_overlap
11721                .validate_encrypted_object_block_ranges(&base_envelopes)
11722                .unwrap_err(),
11723            overlap
11724        );
11725
11726        let mut dictionary_overlap = opened.clone();
11727        dictionary_overlap.crypto_header.has_dictionary = 1;
11728        dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
11729        dictionary_overlap
11730            .index_root
11731            .header
11732            .dictionary_data_block_count = 1;
11733        dictionary_overlap
11734            .index_root
11735            .header
11736            .dictionary_parity_block_count = 0;
11737        dictionary_overlap
11738            .index_root
11739            .header
11740            .dictionary_encrypted_size = 4096;
11741        dictionary_overlap
11742            .index_root
11743            .header
11744            .dictionary_decompressed_size = 128;
11745        assert_eq!(
11746            dictionary_overlap
11747                .validate_encrypted_object_block_ranges(&base_envelopes)
11748                .unwrap_err(),
11749            overlap
11750        );
11751
11752        let mut hint_overlap = opened.clone();
11753        hint_overlap
11754            .index_root
11755            .directory_hint_shards
11756            .push(DirectoryHintShardEntry {
11757                hint_shard_index: 0,
11758                first_dir_hash: [0; 8],
11759                last_dir_hash: [0; 8],
11760                first_block_index: payload_start,
11761                data_block_count: 1,
11762                parity_block_count: 0,
11763                encrypted_size: 4096,
11764                decompressed_size: 128,
11765                entry_count: 1,
11766            });
11767        assert_eq!(
11768            hint_overlap
11769                .validate_encrypted_object_block_ranges(&base_envelopes)
11770                .unwrap_err(),
11771            overlap
11772        );
11773    }
11774
11775    #[test]
11776    fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
11777        let volume_header = test_volume_header();
11778        let crypto_header = test_crypto_header();
11779        let subkeys = Subkeys::derive(
11780            &master_key(),
11781            &volume_header.archive_uuid,
11782            &volume_header.session_id,
11783        )
11784        .unwrap();
11785        let mut next_block_index = 0u64;
11786
11787        let index_root_payload = b"index root metadata object";
11788        let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
11789        assert_metadata_object_from_compressed(
11790            &{
11791                let mut bytes = index_root_compressed.clone();
11792                bytes.push(0);
11793                bytes
11794            },
11795            index_root_payload.len(),
11796            &subkeys,
11797            &volume_header,
11798            &crypto_header,
11799            &subkeys.index_root_key,
11800            &subkeys.index_nonce_seed,
11801            b"idxroot",
11802            0,
11803            BlockKind::IndexRootData,
11804            BlockKind::IndexRootParity,
11805            crypto_header.index_root_fec_data_shards,
11806            crypto_header.index_root_fec_parity_shards,
11807            &mut next_block_index,
11808            FormatError::TrailingBytesAfterZstdFrame,
11809        );
11810        assert_metadata_object_from_compressed(
11811            &index_root_compressed,
11812            index_root_payload.len() + 1,
11813            &subkeys,
11814            &volume_header,
11815            &crypto_header,
11816            &subkeys.index_root_key,
11817            &subkeys.index_nonce_seed,
11818            b"idxroot",
11819            0,
11820            BlockKind::IndexRootData,
11821            BlockKind::IndexRootParity,
11822            crypto_header.index_root_fec_data_shards,
11823            crypto_header.index_root_fec_parity_shards,
11824            &mut next_block_index,
11825            FormatError::ZstdDecompressedSizeMismatch {
11826                expected: index_root_payload.len() + 1,
11827                actual: index_root_payload.len(),
11828            },
11829        );
11830
11831        let index_shard_payload = b"index shard metadata object";
11832        let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
11833        assert_metadata_object_from_compressed(
11834            &{
11835                let mut bytes = index_shard_compressed.clone();
11836                bytes.push(0);
11837                bytes
11838            },
11839            index_shard_payload.len(),
11840            &subkeys,
11841            &volume_header,
11842            &crypto_header,
11843            &subkeys.index_shard_key,
11844            &subkeys.index_nonce_seed,
11845            b"idxshard",
11846            1,
11847            BlockKind::IndexShardData,
11848            BlockKind::IndexShardParity,
11849            crypto_header.index_fec_data_shards,
11850            crypto_header.index_fec_parity_shards,
11851            &mut next_block_index,
11852            FormatError::TrailingBytesAfterZstdFrame,
11853        );
11854        assert_metadata_object_from_compressed(
11855            &index_shard_compressed,
11856            index_shard_payload.len() + 1,
11857            &subkeys,
11858            &volume_header,
11859            &crypto_header,
11860            &subkeys.index_shard_key,
11861            &subkeys.index_nonce_seed,
11862            b"idxshard",
11863            1,
11864            BlockKind::IndexShardData,
11865            BlockKind::IndexShardParity,
11866            crypto_header.index_fec_data_shards,
11867            crypto_header.index_fec_parity_shards,
11868            &mut next_block_index,
11869            FormatError::ZstdDecompressedSizeMismatch {
11870                expected: index_shard_payload.len() + 1,
11871                actual: index_shard_payload.len(),
11872            },
11873        );
11874
11875        let directory_hint_payload = b"directory hint metadata object";
11876        let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
11877        assert_metadata_object_from_compressed(
11878            &{
11879                let mut bytes = directory_hint_compressed.clone();
11880                bytes.push(0);
11881                bytes
11882            },
11883            directory_hint_payload.len(),
11884            &subkeys,
11885            &volume_header,
11886            &crypto_header,
11887            &subkeys.dir_hint_key,
11888            &subkeys.index_nonce_seed,
11889            b"dirhint",
11890            0,
11891            BlockKind::DirectoryHintData,
11892            BlockKind::DirectoryHintParity,
11893            crypto_header.index_fec_data_shards,
11894            crypto_header.index_fec_parity_shards,
11895            &mut next_block_index,
11896            FormatError::TrailingBytesAfterZstdFrame,
11897        );
11898        assert_metadata_object_from_compressed(
11899            &directory_hint_compressed,
11900            directory_hint_payload.len() + 1,
11901            &subkeys,
11902            &volume_header,
11903            &crypto_header,
11904            &subkeys.dir_hint_key,
11905            &subkeys.index_nonce_seed,
11906            b"dirhint",
11907            0,
11908            BlockKind::DirectoryHintData,
11909            BlockKind::DirectoryHintParity,
11910            crypto_header.index_fec_data_shards,
11911            crypto_header.index_fec_parity_shards,
11912            &mut next_block_index,
11913            FormatError::ZstdDecompressedSizeMismatch {
11914                expected: directory_hint_payload.len() + 1,
11915                actual: directory_hint_payload.len(),
11916            },
11917        );
11918
11919        let dictionary_payload = b"dictionary metadata object";
11920        let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
11921        assert_metadata_object_from_compressed(
11922            &{
11923                let mut bytes = dictionary_compressed.clone();
11924                bytes.push(0);
11925                bytes
11926            },
11927            dictionary_payload.len(),
11928            &subkeys,
11929            &volume_header,
11930            &crypto_header,
11931            &subkeys.dictionary_key,
11932            &subkeys.index_nonce_seed,
11933            b"dict",
11934            0,
11935            BlockKind::DictionaryData,
11936            BlockKind::DictionaryParity,
11937            crypto_header.index_root_fec_data_shards,
11938            crypto_header.index_root_fec_parity_shards,
11939            &mut next_block_index,
11940            FormatError::TrailingBytesAfterZstdFrame,
11941        );
11942        assert_metadata_object_from_compressed(
11943            &dictionary_compressed,
11944            dictionary_payload.len() + 1,
11945            &subkeys,
11946            &volume_header,
11947            &crypto_header,
11948            &subkeys.dictionary_key,
11949            &subkeys.index_nonce_seed,
11950            b"dict",
11951            0,
11952            BlockKind::DictionaryData,
11953            BlockKind::DictionaryParity,
11954            crypto_header.index_root_fec_data_shards,
11955            crypto_header.index_root_fec_parity_shards,
11956            &mut next_block_index,
11957            FormatError::ZstdDecompressedSizeMismatch {
11958                expected: dictionary_payload.len() + 1,
11959                actual: dictionary_payload.len(),
11960            },
11961        );
11962    }
11963
11964    #[test]
11965    fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
11966        let volume_header = test_volume_header();
11967        let crypto_header = test_crypto_header();
11968        let subkeys = Subkeys::derive(
11969            &master_key(),
11970            &volume_header.archive_uuid,
11971            &volume_header.session_id,
11972        )
11973        .unwrap();
11974        let mut next_block_index = 0u64;
11975
11976        let index_root_payload = b"index root metadata object";
11977        let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
11978            index_root_payload,
11979            &subkeys,
11980            &volume_header,
11981            &crypto_header,
11982            &subkeys.index_root_key,
11983            &subkeys.index_nonce_seed,
11984            b"idxroot",
11985            0,
11986            BlockKind::IndexRootData,
11987            &mut next_block_index,
11988        );
11989        let mut index_root_extent = index_root_extent;
11990        index_root_extent.encrypted_size = index_root_extent
11991            .encrypted_size
11992            .saturating_add(crypto_header.block_size);
11993        assert_eq!(
11994            load_metadata_object_from_parts(
11995                &index_root_records,
11996                ObjectLoadContext::index_root(
11997                    &volume_header,
11998                    &crypto_header,
11999                    &subkeys,
12000                    index_root_extent,
12001                ),
12002                index_root_payload.len() as u32,
12003            )
12004            .unwrap_err(),
12005            FormatError::InvalidArchive(
12006                "encrypted object size is not data_block_count * block_size"
12007            )
12008        );
12009
12010        let index_shard_payload = b"index shard metadata object";
12011        let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
12012            index_shard_payload,
12013            &subkeys,
12014            &volume_header,
12015            &crypto_header,
12016            &subkeys.index_shard_key,
12017            &subkeys.index_nonce_seed,
12018            b"idxshard",
12019            1,
12020            BlockKind::IndexShardData,
12021            &mut next_block_index,
12022        );
12023        let mut index_shard_extent = index_shard_extent;
12024        index_shard_extent.encrypted_size = index_shard_extent
12025            .encrypted_size
12026            .saturating_add(crypto_header.block_size);
12027        assert_eq!(
12028            load_metadata_object_from_parts(
12029                &index_shard_records,
12030                ObjectLoadContext {
12031                    volume_header: &volume_header,
12032                    crypto_header: &crypto_header,
12033                    extent: index_shard_extent,
12034                    data_kind: BlockKind::IndexShardData,
12035                    parity_kind: BlockKind::IndexShardParity,
12036                    key: &subkeys.index_shard_key,
12037                    nonce_seed: &subkeys.index_nonce_seed,
12038                    domain: b"idxshard",
12039                    counter: 1,
12040                    class_data_shard_max: crypto_header.index_fec_data_shards,
12041                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
12042                },
12043                index_shard_payload.len() as u32,
12044            )
12045            .unwrap_err(),
12046            FormatError::InvalidArchive(
12047                "encrypted object size is not data_block_count * block_size"
12048            )
12049        );
12050
12051        let directory_hint_payload = b"directory hint metadata object";
12052        let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
12053            directory_hint_payload,
12054            &subkeys,
12055            &volume_header,
12056            &crypto_header,
12057            &subkeys.dir_hint_key,
12058            &subkeys.index_nonce_seed,
12059            b"dirhint",
12060            0,
12061            BlockKind::DirectoryHintData,
12062            &mut next_block_index,
12063        );
12064        let mut directory_hint_extent = directory_hint_extent;
12065        directory_hint_extent.encrypted_size = directory_hint_extent
12066            .encrypted_size
12067            .saturating_add(crypto_header.block_size);
12068        assert_eq!(
12069            load_metadata_object_from_parts(
12070                &directory_hint_records,
12071                ObjectLoadContext {
12072                    volume_header: &volume_header,
12073                    crypto_header: &crypto_header,
12074                    extent: directory_hint_extent,
12075                    data_kind: BlockKind::DirectoryHintData,
12076                    parity_kind: BlockKind::DirectoryHintParity,
12077                    key: &subkeys.dir_hint_key,
12078                    nonce_seed: &subkeys.index_nonce_seed,
12079                    domain: b"dirhint",
12080                    counter: 0,
12081                    class_data_shard_max: crypto_header.index_fec_data_shards,
12082                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
12083                },
12084                directory_hint_payload.len() as u32,
12085            )
12086            .unwrap_err(),
12087            FormatError::InvalidArchive(
12088                "encrypted object size is not data_block_count * block_size"
12089            )
12090        );
12091
12092        let dictionary_payload = b"dictionary metadata object";
12093        let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
12094            dictionary_payload,
12095            &subkeys,
12096            &volume_header,
12097            &crypto_header,
12098            &subkeys.dictionary_key,
12099            &subkeys.index_nonce_seed,
12100            b"dict",
12101            0,
12102            BlockKind::DictionaryData,
12103            &mut next_block_index,
12104        );
12105        let mut dictionary_extent = dictionary_extent;
12106        dictionary_extent.encrypted_size = dictionary_extent
12107            .encrypted_size
12108            .saturating_add(crypto_header.block_size);
12109        assert_eq!(
12110            load_metadata_object_from_parts(
12111                &dictionary_records,
12112                ObjectLoadContext {
12113                    volume_header: &volume_header,
12114                    crypto_header: &crypto_header,
12115                    extent: dictionary_extent,
12116                    data_kind: BlockKind::DictionaryData,
12117                    parity_kind: BlockKind::DictionaryParity,
12118                    key: &subkeys.dictionary_key,
12119                    nonce_seed: &subkeys.index_nonce_seed,
12120                    domain: b"dict",
12121                    counter: 0,
12122                    class_data_shard_max: crypto_header.index_root_fec_data_shards,
12123                    class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
12124                },
12125                dictionary_payload.len() as u32,
12126            )
12127            .unwrap_err(),
12128            FormatError::InvalidArchive(
12129                "encrypted object size is not data_block_count * block_size"
12130            )
12131        );
12132    }
12133
12134    #[test]
12135    fn opens_complete_multi_volume_archive() {
12136        let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
12137        let archive = write_archive(
12138            &files,
12139            &master_key(),
12140            WriterOptions {
12141                stripe_width: 2,
12142                volume_loss_tolerance: 1,
12143                ..single_stream_options()
12144            },
12145        )
12146        .unwrap();
12147        assert_eq!(archive.volumes.len(), 2);
12148
12149        let volume_refs = archive
12150            .volumes
12151            .iter()
12152            .map(Vec::as_slice)
12153            .collect::<Vec<_>>();
12154        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12155
12156        assert_eq!(opened.volume_header.stripe_width, 2);
12157        assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
12158        assert_eq!(
12159            opened.extract_file("alpha.txt").unwrap(),
12160            Some(b"hello from volume stripes".to_vec())
12161        );
12162        opened.verify().unwrap();
12163    }
12164
12165    #[test]
12166    fn recovers_from_one_missing_volume_when_parity_allows() {
12167        let files = [RegularFile::new("alpha.txt", b"recover me")];
12168        let archive = write_archive(
12169            &files,
12170            &master_key(),
12171            WriterOptions {
12172                stripe_width: 2,
12173                volume_loss_tolerance: 1,
12174                ..single_stream_options()
12175            },
12176        )
12177        .unwrap();
12178
12179        let recovered =
12180            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
12181        assert_eq!(
12182            recovered.extract_file("alpha.txt").unwrap(),
12183            Some(b"recover me".to_vec())
12184        );
12185        recovered.verify().unwrap();
12186    }
12187
12188    #[test]
12189    fn recovers_from_crc_corrupted_block_when_parity_allows() {
12190        let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
12191        let archive = write_archive(
12192            &files,
12193            &master_key(),
12194            WriterOptions {
12195                stripe_width: 2,
12196                volume_loss_tolerance: 1,
12197                ..single_stream_options()
12198            },
12199        )
12200        .unwrap();
12201        let mut volumes = archive.volumes.clone();
12202        corrupt_first_block_record_payload(&mut volumes[0]);
12203
12204        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12205        let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12206
12207        assert_eq!(
12208            recovered.extract_file("alpha.txt").unwrap(),
12209            Some(b"repair corrupt block".to_vec())
12210        );
12211        recovered.verify().unwrap();
12212    }
12213
12214    #[test]
12215    fn rejects_multi_volume_count_mismatch_without_tolerance() {
12216        let files = [RegularFile::new("alpha.txt", b"count check")];
12217        let archive = write_archive(
12218            &files,
12219            &master_key(),
12220            WriterOptions {
12221                stripe_width: 3,
12222                volume_loss_tolerance: 0,
12223                ..single_stream_options()
12224            },
12225        )
12226        .unwrap();
12227
12228        assert_eq!(
12229            open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
12230            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12231        );
12232    }
12233
12234    #[test]
12235    fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
12236        let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
12237        let archive = write_archive(
12238            &files,
12239            &master_key(),
12240            WriterOptions {
12241                stripe_width: 2,
12242                volume_loss_tolerance: 1,
12243                ..single_stream_options()
12244            },
12245        )
12246        .unwrap();
12247
12248        let mut bad_first = archive.volumes[0].clone();
12249        rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
12250            footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
12251        });
12252
12253        open_archive_volumes(
12254            &[bad_first.as_slice(), archive.volumes[1].as_slice()],
12255            &master_key(),
12256        )
12257        .unwrap();
12258    }
12259
12260    #[test]
12261    fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
12262        let files = [RegularFile::new("alpha.txt", b"repair meta root")];
12263        let archive = write_archive(
12264            &files,
12265            &master_key(),
12266            WriterOptions {
12267                stripe_width: 2,
12268                volume_loss_tolerance: 1,
12269                ..single_stream_options()
12270            },
12271        )
12272        .unwrap();
12273        let mut volumes = archive.volumes.clone();
12274
12275        let mut corrupted = false;
12276        for volume in &mut volumes {
12277            if let Some(slot) =
12278                block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
12279            {
12280                corrupt_block_record_payload_at_slot(volume, *slot);
12281                corrupted = true;
12282                break;
12283            }
12284        }
12285        assert!(corrupted, "expected an IndexRootData record");
12286
12287        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12288        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12289        assert_eq!(
12290            opened.extract_file("alpha.txt").unwrap(),
12291            Some(b"repair meta root".to_vec())
12292        );
12293        opened.verify().unwrap();
12294    }
12295
12296    #[test]
12297    fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
12298        let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
12299        let archive = write_archive(
12300            &files,
12301            &master_key(),
12302            WriterOptions {
12303                stripe_width: 2,
12304                volume_loss_tolerance: 1,
12305                ..single_stream_options()
12306            },
12307        )
12308        .unwrap();
12309        let mut volumes = archive.volumes.clone();
12310
12311        let mut corrupted = false;
12312        for volume in &mut volumes {
12313            if let Some(slot) =
12314                block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
12315            {
12316                corrupt_block_record_payload_at_slot(volume, *slot);
12317                corrupted = true;
12318                break;
12319            }
12320        }
12321        assert!(corrupted, "expected an IndexShardData record");
12322
12323        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12324        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12325        assert_eq!(
12326            opened.extract_file("alpha.txt").unwrap(),
12327            Some(b"repair meta shard".to_vec())
12328        );
12329        opened.verify().unwrap();
12330    }
12331
12332    #[test]
12333    fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
12334        let files = [RegularFile::new(
12335            "alpha.txt",
12336            b"bitrot parity is not volume loss",
12337        )];
12338        let archive = write_archive(
12339            &files,
12340            &master_key(),
12341            WriterOptions {
12342                stripe_width: 2,
12343                volume_loss_tolerance: 0,
12344                bit_rot_buffer_pct: 1,
12345                ..single_stream_options()
12346            },
12347        )
12348        .unwrap();
12349
12350        assert_eq!(
12351            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
12352            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12353        );
12354    }
12355
12356    #[test]
12357    fn repairs_crc_erasure_only_within_parity_budget() {
12358        let payload = pseudo_random_bytes(12_000);
12359        let archive = write_archive(
12360            &[RegularFile::new("rot.bin", &payload)],
12361            &master_key(),
12362            small_block_recovery_options(),
12363        )
12364        .unwrap();
12365        let payload_slots = first_payload_data_run_slots(&archive.bytes);
12366        assert!(
12367            payload_slots.len() >= 2,
12368            "fixture must contain a multi-block payload object"
12369        );
12370
12371        let mut one_erasure = archive.bytes.clone();
12372        corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
12373        let repaired = open_archive(&one_erasure, &master_key()).unwrap();
12374        assert_eq!(
12375            repaired.extract_file("rot.bin").unwrap(),
12376            Some(payload.clone())
12377        );
12378
12379        let mut two_erasures = archive.bytes.clone();
12380        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
12381        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
12382        let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
12383        assert_eq!(
12384            unrepaired.extract_file("rot.bin").unwrap_err(),
12385            FormatError::FecTooFewAvailableShards
12386        );
12387    }
12388
12389    #[test]
12390    fn verify_rejects_missing_required_object_block_extent() {
12391        let (mut opened, missing_block) = multi_envelope_reader_fixture();
12392        assert!(opened.blocks.remove(&missing_block).is_some());
12393
12394        assert_eq!(
12395            opened.verify().unwrap_err(),
12396            FormatError::FecTooFewAvailableShards
12397        );
12398    }
12399
12400    #[test]
12401    fn parity_crc_erasure_does_not_hide_authenticated_data() {
12402        let payload = pseudo_random_bytes(12_000);
12403        let archive = write_archive(
12404            &[RegularFile::new("parity-erasure.bin", &payload)],
12405            &master_key(),
12406            parity_rich_recovery_options(),
12407        )
12408        .unwrap();
12409        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12410        let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
12411        assert!(
12412            parity_slots.len() >= 2,
12413            "fixture must contain redundant parity shards"
12414        );
12415        let mut corrupted = archive.bytes;
12416        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12417        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
12418
12419        let opened = open_archive(&corrupted, &master_key()).unwrap();
12420        assert_eq!(
12421            opened.extract_file("parity-erasure.bin").unwrap(),
12422            Some(payload)
12423        );
12424        opened.verify().unwrap();
12425    }
12426
12427    #[test]
12428    fn repair_patches_restore_crc_erased_payload_block() {
12429        let payload = pseudo_random_bytes(12_000);
12430        let archive = write_archive(
12431            &[RegularFile::new("rot.bin", &payload)],
12432            &master_key(),
12433            small_block_recovery_options(),
12434        )
12435        .unwrap();
12436        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12437        let mut corrupted = archive.bytes.clone();
12438        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12439
12440        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12441        opened.verify().unwrap();
12442        let patches = opened.repair_patches().unwrap();
12443        assert_eq!(patches.len(), 1);
12444        apply_repair_patches(&mut corrupted, &patches);
12445
12446        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12447        repaired.verify().unwrap();
12448        assert!(repaired.repair_patches().unwrap().is_empty());
12449    }
12450
12451    #[test]
12452    fn repair_patches_restore_crc_erased_payload_parity_block() {
12453        let payload = pseudo_random_bytes(12_000);
12454        let archive = write_archive(
12455            &[RegularFile::new("parity-erasure.bin", &payload)],
12456            &master_key(),
12457            parity_rich_recovery_options(),
12458        )
12459        .unwrap();
12460        let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
12461        let mut corrupted = archive.bytes.clone();
12462        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
12463
12464        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12465        opened.verify().unwrap();
12466        let patches = opened.repair_patches().unwrap();
12467        assert_eq!(patches.len(), 1);
12468        apply_repair_patches(&mut corrupted, &patches);
12469
12470        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12471        repaired.verify().unwrap();
12472        assert!(repaired.repair_patches().unwrap().is_empty());
12473    }
12474
12475    #[test]
12476    fn recovers_physical_odd_block_size_from_cmra_authority() {
12477        let archive = write_archive(
12478            &[RegularFile::new("odd-block.txt", b"payload")],
12479            &master_key(),
12480            small_block_recovery_options(),
12481        )
12482        .unwrap();
12483        let mut malformed = archive.bytes;
12484        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
12485        let block_size_offset = volume_header.crypto_header_offset as usize + 24;
12486        malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
12487
12488        let opened = open_archive(&malformed, &master_key()).unwrap();
12489        assert_ne!(opened.crypto_header.block_size, 4097);
12490        assert_eq!(
12491            opened.extract_file("odd-block.txt").unwrap(),
12492            Some(b"payload".to_vec())
12493        );
12494        opened.verify().unwrap();
12495    }
12496
12497    #[test]
12498    fn repairs_structurally_malformed_payload_block_slots() {
12499        let payload = pseudo_random_bytes(12_000);
12500        let archive = write_archive(
12501            &[RegularFile::new("structural-block.bin", &payload)],
12502            &master_key(),
12503            small_block_recovery_options(),
12504        )
12505        .unwrap();
12506        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12507
12508        let mut bad_magic = archive.bytes.clone();
12509        corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
12510        assert_eq!(
12511            open_archive(&bad_magic, &master_key())
12512                .unwrap()
12513                .extract_file("structural-block.bin")
12514                .unwrap(),
12515            Some(payload.clone())
12516        );
12517
12518        let mut bad_reserved = archive.bytes;
12519        corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
12520        assert_eq!(
12521            open_archive(&bad_reserved, &master_key())
12522                .unwrap()
12523                .extract_file("structural-block.bin")
12524                .unwrap(),
12525            Some(payload)
12526        );
12527    }
12528
12529    #[test]
12530    fn repair_patches_restore_structurally_malformed_payload_block_slot() {
12531        let payload = pseudo_random_bytes(12_000);
12532        let archive = write_archive(
12533            &[RegularFile::new("structural-patch.bin", &payload)],
12534            &master_key(),
12535            small_block_recovery_options(),
12536        )
12537        .unwrap();
12538        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12539        let mut corrupted = archive.bytes.clone();
12540        corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
12541
12542        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12543        opened.verify().unwrap();
12544        assert_eq!(
12545            opened.extract_file("structural-patch.bin").unwrap(),
12546            Some(payload)
12547        );
12548        let patches = opened.repair_patches().unwrap();
12549        assert_eq!(patches.len(), 1);
12550        apply_repair_patches(&mut corrupted, &patches);
12551
12552        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12553        repaired.verify().unwrap();
12554        assert!(repaired.repair_patches().unwrap().is_empty());
12555    }
12556
12557    #[test]
12558    fn repairs_structurally_malformed_index_root_block_slot() {
12559        let archive = write_archive(
12560            &[RegularFile::new(
12561                "structural-index-root.txt",
12562                b"metadata repair",
12563            )],
12564            &master_key(),
12565            small_block_recovery_options(),
12566        )
12567        .unwrap();
12568        let index_root_slot =
12569            first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
12570        let mut corrupted = archive.bytes;
12571        corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
12572
12573        let opened = open_archive(&corrupted, &master_key()).unwrap();
12574        assert_eq!(
12575            opened.extract_file("structural-index-root.txt").unwrap(),
12576            Some(b"metadata repair".to_vec())
12577        );
12578        opened.verify().unwrap();
12579    }
12580
12581    #[test]
12582    fn rejects_parity_block_with_last_data_flag() {
12583        let archive = write_archive(
12584            &[RegularFile::new("parity-flag.txt", b"payload")],
12585            &master_key(),
12586            small_block_recovery_options(),
12587        )
12588        .unwrap();
12589        let parity_slot =
12590            first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
12591        let mut malformed = archive.bytes;
12592        mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
12593            record.flags = 0x01;
12594        });
12595
12596        assert_eq!(
12597            open_archive(&malformed, &master_key()).unwrap_err(),
12598            FormatError::ParityBlockHasLastDataFlag
12599        );
12600    }
12601
12602    #[test]
12603    fn rejects_missing_and_duplicate_payload_last_data_flags() {
12604        let payload = pseudo_random_bytes(12_000);
12605        let archive = write_archive(
12606            &[RegularFile::new("flags.bin", &payload)],
12607            &master_key(),
12608            small_block_recovery_options(),
12609        )
12610        .unwrap();
12611        let payload_slots = first_payload_data_run_slots(&archive.bytes);
12612        assert!(
12613            payload_slots.len() >= 2,
12614            "fixture must contain a multi-block payload object"
12615        );
12616
12617        let mut duplicate_last = archive.bytes.clone();
12618        mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
12619            record.flags = 0x01;
12620        });
12621        let opened = open_archive(&duplicate_last, &master_key()).unwrap();
12622        assert_eq!(
12623            opened.extract_file("flags.bin").unwrap_err(),
12624            FormatError::InvalidArchive("object last-data flag is not on the final data block")
12625        );
12626
12627        let mut missing_last = archive.bytes;
12628        mutate_block_record_at_slot(
12629            &mut missing_last,
12630            *payload_slots.last().unwrap(),
12631            |record| {
12632                record.flags = 0;
12633            },
12634        );
12635        let opened = open_archive(&missing_last, &master_key()).unwrap();
12636        assert_eq!(
12637            opened.extract_file("flags.bin").unwrap_err(),
12638            FormatError::InvalidArchive("object last-data flag is not on the final data block")
12639        );
12640    }
12641
12642    #[test]
12643    fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
12644        let files = [RegularFile::new(
12645            "footer-copy.txt",
12646            b"survives one bad footer",
12647        )];
12648        let archive = write_archive(
12649            &files,
12650            &master_key(),
12651            WriterOptions {
12652                stripe_width: 2,
12653                volume_loss_tolerance: 1,
12654                ..single_stream_options()
12655            },
12656        )
12657        .unwrap();
12658        let mut volumes = archive.volumes.clone();
12659        corrupt_manifest_footer_hmac(&mut volumes[0]);
12660
12661        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12662        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12663        assert_eq!(opened.manifest_footer.volume_index, 0);
12664        assert_eq!(opened.volume_header.volume_index, 0);
12665        assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
12666        assert_eq!(
12667            opened.extract_file("footer-copy.txt").unwrap(),
12668            Some(b"survives one bad footer".to_vec())
12669        );
12670        opened.verify().unwrap();
12671    }
12672
12673    #[test]
12674    fn manifest_footer_corruption_requires_trusted_sidecar() {
12675        let archive = write_archive(
12676            &[RegularFile::new("footer.txt", b"sidecar authority")],
12677            &master_key(),
12678            single_stream_options(),
12679        )
12680        .unwrap();
12681        let manifest_offset = terminal_material_offset(&archive.bytes);
12682        let mut corrupted = archive.bytes.clone();
12683        corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
12684        corrupt_v41_terminal_recovery(&mut corrupted);
12685
12686        assert!(open_archive(&corrupted, &master_key()).is_err());
12687
12688        let opened =
12689            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
12690                .unwrap();
12691        assert!(opened.volume_trailer.is_none());
12692        assert_eq!(
12693            opened.extract_file("footer.txt").unwrap(),
12694            Some(b"sidecar authority".to_vec())
12695        );
12696        opened.verify().unwrap();
12697    }
12698
12699    #[test]
12700    fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
12701        let archive = write_archive(
12702            &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
12703            &master_key(),
12704            single_stream_options(),
12705        )
12706        .unwrap();
12707        let strict_options = ReaderOptions {
12708            max_trailing_garbage_scan: 0,
12709            ..ReaderOptions::default()
12710        };
12711
12712        let manifest_offset = terminal_material_offset(&archive.bytes);
12713        for offset in [
12714            manifest_offset + 71,
12715            manifest_offset + MANIFEST_HMAC_COVERED_LEN,
12716        ] {
12717            let mut corrupted = archive.bytes.clone();
12718            corrupted[offset] ^= 0x01;
12719            open_archive(&corrupted, &master_key()).unwrap();
12720        }
12721
12722        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
12723        for offset in [
12724            trailer_offset + 75,
12725            trailer_offset + TRAILER_HMAC_COVERED_LEN,
12726        ] {
12727            let mut corrupted = archive.bytes.clone();
12728            corrupted[offset] ^= 0x01;
12729            OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
12730        }
12731
12732        let mut covered_sidecar = archive.bootstrap_sidecar.clone();
12733        let mut header =
12734            BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12735                .unwrap();
12736        header.manifest_footer_offset += 1;
12737        covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12738        assert_eq!(
12739            open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
12740                .unwrap_err(),
12741            FormatError::HmacMismatch {
12742                structure: "BootstrapSidecarHeader"
12743            }
12744        );
12745
12746        let mut tag_sidecar = archive.bootstrap_sidecar.clone();
12747        let mut header =
12748            BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12749        header.sidecar_hmac[0] ^= 1;
12750        tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12751        assert_eq!(
12752            open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
12753                .unwrap_err(),
12754            FormatError::HmacMismatch {
12755                structure: "BootstrapSidecarHeader"
12756            }
12757        );
12758
12759        let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
12760        let header =
12761            BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12762                .unwrap();
12763        let mut header_bytes = header.to_bytes();
12764        header_bytes[124] ^= 0x01;
12765        let crc = crc32c::crc32c(&header_bytes[..124]);
12766        header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
12767        non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12768        let opened = open_archive_with_bootstrap_sidecar(
12769            &archive.bytes,
12770            &non_covered_sidecar,
12771            &master_key(),
12772        )
12773        .unwrap();
12774        assert_eq!(
12775            opened.extract_file("hmac-boundary.txt").unwrap(),
12776            Some(b"boundary bytes".to_vec())
12777        );
12778    }
12779
12780    #[test]
12781    fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
12782        let archive = write_archive(
12783            &[RegularFile::new("volume-index.txt", b"identity")],
12784            &master_key(),
12785            single_stream_options(),
12786        )
12787        .unwrap();
12788
12789        let mut bad_trailer = archive.bytes.clone();
12790        rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
12791            trailer.volume_index = 1;
12792        });
12793        open_archive(&bad_trailer, &master_key()).unwrap();
12794
12795        let mut bad_manifest = archive.bytes;
12796        rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
12797            footer.volume_index = 1;
12798        });
12799        open_archive(&bad_manifest, &master_key()).unwrap();
12800    }
12801
12802    #[test]
12803    fn rejects_same_key_header_terminal_material_splice() {
12804        let first = write_archive(
12805            &[RegularFile::new("splice.txt", b"same shape")],
12806            &master_key(),
12807            single_stream_options(),
12808        )
12809        .unwrap();
12810        let second = write_archive(
12811            &[RegularFile::new("splice.txt", b"same shape")],
12812            &master_key(),
12813            single_stream_options(),
12814        )
12815        .unwrap();
12816        assert_ne!(first.archive_uuid, second.archive_uuid);
12817        assert_eq!(
12818            terminal_material_offset(&first.bytes),
12819            terminal_material_offset(&second.bytes)
12820        );
12821        assert_eq!(first.bytes.len(), second.bytes.len());
12822
12823        let terminal_offset = terminal_material_offset(&first.bytes);
12824        let mut spliced = first.bytes.clone();
12825        spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
12826
12827        assert_eq!(
12828            open_archive(&spliced, &master_key()).unwrap_err(),
12829            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
12830        );
12831    }
12832
12833    #[test]
12834    fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
12835        let kdf_params = crate::crypto::KdfParams::Argon2id {
12836            t_cost: 1,
12837            m_cost_kib: 8,
12838            parallelism: 1,
12839            salt: b"0123456789abcdef".to_vec(),
12840        };
12841        let archive = write_archive_with_kdf(
12842            &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
12843            &master_key(),
12844            single_stream_options(),
12845            &kdf_params,
12846        )
12847        .unwrap();
12848        let mut mutated = archive.bytes.clone();
12849        let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
12850        let subkeys = Subkeys::derive(
12851            &master_key(),
12852            &volume_header.archive_uuid,
12853            &volume_header.session_id,
12854        )
12855        .unwrap();
12856
12857        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
12858            let crypto_region = image
12859                .regions
12860                .iter_mut()
12861                .find(|region| region.region_type == 2)
12862                .unwrap();
12863            let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
12864            let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
12865            crypto_region.bytes[salt_start] ^= 0x01;
12866            let hmac = compute_hmac(
12867                HmacDomain::CryptoHeader,
12868                &subkeys.mac_key,
12869                &volume_header.archive_uuid,
12870                &volume_header.session_id,
12871                &crypto_region.bytes[..hmac_offset],
12872            );
12873            crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
12874        });
12875
12876        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12877        let locator = final_recovery_locator(&mutated);
12878        let crypto_start = volume_header.crypto_header_offset as usize;
12879        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12880        let parsed_crypto = CryptoHeader::parse(
12881            &mutated[crypto_start..crypto_end],
12882            volume_header.crypto_header_length,
12883        )
12884        .unwrap();
12885        assert_eq!(
12886            parse_locator_cmra_candidate(
12887                &mutated,
12888                final_offset,
12889                locator,
12890                KeyHoldingTerminalContext {
12891                    subkeys: &subkeys,
12892                    volume_header: &volume_header,
12893                    crypto_header: &parsed_crypto.fixed,
12894                    crypto_header_bytes: &mutated[crypto_start..crypto_end],
12895                },
12896            )
12897            .unwrap_err(),
12898            FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
12899        );
12900        assert!(open_archive(&mutated, &master_key()).is_err());
12901    }
12902
12903    #[test]
12904    fn recovers_physical_crypto_header_splice_from_cmra_authority() {
12905        let base = WriterOptions {
12906            archive_uuid: Some([0x11; 16]),
12907            session_id: Some([0x22; 16]),
12908            ..small_block_recovery_options()
12909        };
12910        let same_archive = WriterOptions {
12911            archive_uuid: Some([0x11; 16]),
12912            session_id: Some([0x33; 16]),
12913            ..small_block_recovery_options()
12914        };
12915
12916        let first = write_archive(
12917            &[RegularFile::new("splice.txt", b"same shape")],
12918            &master_key(),
12919            base,
12920        )
12921        .unwrap();
12922        let second = write_archive(
12923            &[RegularFile::new("splice.txt", b"same shape")],
12924            &master_key(),
12925            same_archive,
12926        )
12927        .unwrap();
12928
12929        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12930        let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
12931        let crypto_start = volume_header.crypto_header_offset as usize;
12932        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12933        let second_crypto_end = second_volume_header.crypto_header_offset as usize
12934            + second_volume_header.crypto_header_length as usize;
12935        assert_eq!(crypto_end, second_crypto_end);
12936
12937        let mut spliced = first.bytes.clone();
12938        spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
12939
12940        let opened = open_archive(&spliced, &master_key()).unwrap();
12941        assert_eq!(
12942            opened.crypto_header_bytes,
12943            first.bytes[crypto_start..crypto_end].to_vec()
12944        );
12945        assert_eq!(
12946            opened.extract_file("splice.txt").unwrap(),
12947            Some(b"same shape".to_vec())
12948        );
12949        opened.verify().unwrap();
12950    }
12951
12952    #[test]
12953    fn rejects_same_key_object_splice_with_session_mismatch() {
12954        let first = write_archive(
12955            &[RegularFile::new("splice.txt", b"same shape")],
12956            &master_key(),
12957            WriterOptions {
12958                archive_uuid: Some([0x11; 16]),
12959                session_id: Some([0x22; 16]),
12960                ..single_stream_options()
12961            },
12962        )
12963        .unwrap();
12964        let second = write_archive(
12965            &[RegularFile::new("splice.txt", b"same shape")],
12966            &master_key(),
12967            WriterOptions {
12968                archive_uuid: Some([0x11; 16]),
12969                session_id: Some([0x33; 16]),
12970                ..single_stream_options()
12971            },
12972        )
12973        .unwrap();
12974
12975        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12976        let crypto_end = volume_header.crypto_header_offset as usize
12977            + volume_header.crypto_header_length as usize;
12978        let terminal_offset = terminal_material_offset(&first.bytes);
12979        let second_terminal_offset = terminal_material_offset(&second.bytes);
12980        assert_eq!(terminal_offset, second_terminal_offset);
12981
12982        let mut spliced = first.bytes.clone();
12983        spliced[crypto_end..terminal_offset]
12984            .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
12985
12986        assert_eq!(
12987            open_archive(&spliced, &master_key()).unwrap_err(),
12988            FormatError::AeadFailure
12989        );
12990    }
12991
12992    #[test]
12993    fn rejects_authenticated_trailer_pointer_and_count_mutations() {
12994        let archive = write_archive(
12995            &[RegularFile::new(
12996                "trailer-range.txt",
12997                b"authenticated ranges",
12998            )],
12999            &master_key(),
13000            single_stream_options(),
13001        )
13002        .unwrap();
13003        let strict_options = ReaderOptions {
13004            max_trailing_garbage_scan: 0,
13005            ..ReaderOptions::default()
13006        };
13007        let bytes = archive.bytes;
13008        let manifest_offset = terminal_material_offset(&bytes);
13009        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
13010
13011        let mut wrong_footer_length = bytes.clone();
13012        rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
13013            trailer.manifest_footer_length = 42;
13014        });
13015        OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
13016            .unwrap();
13017
13018        for (label, offset) in [
13019            (
13020                "offset before trailer by 1",
13021                manifest_offset.saturating_sub(1),
13022            ),
13023            ("offset after trailer", manifest_offset + 1),
13024            ("offset at stream start", 0),
13025            ("offset at trailer", trailer_offset),
13026            ("offset beyond trailer", trailer_offset + 4),
13027        ] {
13028            let mut wrong_footer_offset = bytes.clone();
13029            rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13030                trailer.manifest_footer_offset = offset as u64;
13031            });
13032            open_archive(&wrong_footer_offset, &master_key())
13033                .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
13034        }
13035
13036        let mut wrong_bytes_written = bytes.clone();
13037        rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
13038            trailer.bytes_written += 1;
13039        });
13040        open_archive(&wrong_bytes_written, &master_key()).unwrap();
13041
13042        let mut wrong_block_count = bytes.clone();
13043        rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
13044            trailer.block_count += 1;
13045        });
13046        open_archive(&wrong_block_count, &master_key()).unwrap();
13047
13048        let mut wrong_footer_offset = bytes.clone();
13049        rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13050            trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
13051        });
13052        open_archive(&wrong_footer_offset, &master_key()).unwrap();
13053    }
13054
13055    #[test]
13056    fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
13057        let archive = write_archive(
13058            &[RegularFile::new(
13059                "trailer-trailing-scan.txt",
13060                b"trailer scan boundaries",
13061            )],
13062            &master_key(),
13063            single_stream_options(),
13064        )
13065        .unwrap();
13066        let options = ReaderOptions {
13067            max_trailing_garbage_scan: 8,
13068            ..ReaderOptions::default()
13069        };
13070
13071        let mut within_scan = archive.bytes.clone();
13072        within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
13073        let opened =
13074            OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
13075        assert_eq!(
13076            opened.extract_file("trailer-trailing-scan.txt").unwrap(),
13077            Some(b"trailer scan boundaries".to_vec())
13078        );
13079
13080        let mut beyond_scan = archive.bytes.clone();
13081        beyond_scan.resize(
13082            beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
13083            0xAA,
13084        );
13085        assert_eq!(
13086            OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
13087            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13088        );
13089    }
13090
13091    #[test]
13092    fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
13093        let archive = write_archive(
13094            &[RegularFile::new("index-root-size.txt", b"extent size")],
13095            &master_key(),
13096            single_stream_options(),
13097        )
13098        .unwrap();
13099        let mut malformed = archive.bytes;
13100        let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
13101            .expect("archive should contain IndexRootData");
13102        mutate_block_record_at_slot(&mut malformed, slot, |record| {
13103            record.payload[0] ^= 0x55;
13104        });
13105
13106        assert_eq!(
13107            open_archive(&malformed, &master_key()).unwrap_err(),
13108            FormatError::AeadFailure
13109        );
13110    }
13111
13112    #[test]
13113    fn rejects_block_record_at_wrong_stripe_position() {
13114        let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
13115        let archive = write_archive(
13116            &files,
13117            &master_key(),
13118            WriterOptions {
13119                stripe_width: 2,
13120                volume_loss_tolerance: 1,
13121                ..single_stream_options()
13122            },
13123        )
13124        .unwrap();
13125        let mut volumes = archive.volumes.clone();
13126        mutate_first_block_record(&mut volumes[0], |record| {
13127            record.block_index += 2;
13128        });
13129
13130        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
13131        assert_eq!(
13132            open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
13133            FormatError::InvalidArchive("BlockRecord index does not match volume position")
13134        );
13135    }
13136
13137    #[test]
13138    fn rejects_decreasing_block_record_index_in_required_region() {
13139        let archive = write_archive(
13140            &[RegularFile::new("alpha.txt", b"decreasing block index")],
13141            &master_key(),
13142            single_stream_options(),
13143        )
13144        .unwrap();
13145        assert!(block_record_slots(&archive.bytes).len() >= 2);
13146
13147        let mut malformed = archive.bytes;
13148        mutate_block_record_at_slot(&mut malformed, 1, |record| {
13149            record.block_index = 0;
13150        });
13151
13152        assert_eq!(
13153            open_archive(&malformed, &master_key()).unwrap_err(),
13154            FormatError::InvalidArchive("BlockRecord index does not match volume position")
13155        );
13156    }
13157
13158    #[test]
13159    fn rejects_duplicate_authenticated_volume_indexes() {
13160        let files = [RegularFile::new("alpha.txt", b"duplicates")];
13161        let archive = write_archive(
13162            &files,
13163            &master_key(),
13164            WriterOptions {
13165                stripe_width: 2,
13166                volume_loss_tolerance: 1,
13167                ..single_stream_options()
13168            },
13169        )
13170        .unwrap();
13171
13172        assert_eq!(
13173            open_archive_volumes(
13174                &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
13175                &master_key()
13176            )
13177            .unwrap_err(),
13178            FormatError::InvalidArchive("duplicate authenticated volume index")
13179        );
13180    }
13181
13182    #[test]
13183    fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
13184        let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
13185        let archive = write_archive(
13186            &files,
13187            &master_key(),
13188            WriterOptions {
13189                stripe_width: 2,
13190                volume_loss_tolerance: 1,
13191                ..single_stream_options()
13192            },
13193        )
13194        .unwrap();
13195        let mut conflicting = archive.volumes[0].clone();
13196        corrupt_first_block_record_payload(&mut conflicting);
13197
13198        assert_eq!(
13199            open_archive_volumes(
13200                &[archive.volumes[0].as_slice(), conflicting.as_slice()],
13201                &master_key()
13202            )
13203            .unwrap_err(),
13204            FormatError::InvalidArchive("duplicate authenticated volume index")
13205        );
13206    }
13207
13208    fn directory_hint_table_from_rows(
13209        hint_shard_index: u64,
13210        rows: &[(Vec<u8>, Vec<u32>)],
13211        shard_count: u32,
13212    ) -> DirectoryHintTable {
13213        let mut entries = Vec::new();
13214        let mut shard_row_indexes = Vec::new();
13215        let mut string_pool = Vec::new();
13216
13217        for (path, rows) in rows {
13218            let path_offset = if path.is_empty() {
13219                0
13220            } else {
13221                let offset = string_pool.len() as u64;
13222                string_pool.extend_from_slice(path);
13223                offset
13224            };
13225            let shard_list_start_index = shard_row_indexes.len() as u32;
13226            shard_row_indexes.extend_from_slice(rows);
13227            entries.push(DirectoryHintEntry {
13228                dir_hash: hash_prefix(path),
13229                path_offset,
13230                path_length: path.len() as u32,
13231                shard_list_start_index,
13232                shard_count: rows.len() as u32,
13233            });
13234        }
13235
13236        let table_bytes =
13237            directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
13238        let locating = DirectoryHintShardEntry {
13239            hint_shard_index,
13240            first_dir_hash: hash_prefix(&rows.first().unwrap().0),
13241            last_dir_hash: hash_prefix(&rows.last().unwrap().0),
13242            first_block_index: 0,
13243            data_block_count: 1,
13244            parity_block_count: 0,
13245            encrypted_size: 4096,
13246            decompressed_size: table_bytes.len() as u32,
13247            entry_count: rows.len() as u64,
13248        };
13249        DirectoryHintTable::parse(
13250            &table_bytes,
13251            &locating,
13252            shard_count,
13253            MetadataLimits::default(),
13254        )
13255        .unwrap()
13256    }
13257
13258    fn directory_hint_table_bytes(
13259        hint_shard_index: u64,
13260        entries: Vec<DirectoryHintEntry>,
13261        shard_row_indexes: Vec<u32>,
13262        string_pool: Vec<u8>,
13263    ) -> Vec<u8> {
13264        let header_len = DirectoryHintTableHeader {
13265            version: 1,
13266            hint_shard_index,
13267            entry_count: 0,
13268            entry_table_offset: 0,
13269            shard_list_offset: 0,
13270            string_pool_offset: 0,
13271            string_pool_size: 0,
13272        }
13273        .to_bytes()
13274        .len();
13275        let entry_len = entries
13276            .first()
13277            .map(|entry| entry.to_bytes().len())
13278            .unwrap_or(0);
13279        let shard_list_offset = if entries.is_empty() {
13280            0
13281        } else {
13282            header_len + entries.len() * entry_len
13283        };
13284        let string_pool_offset = if string_pool.is_empty() {
13285            0
13286        } else {
13287            shard_list_offset + shard_row_indexes.len() * 4
13288        };
13289
13290        let header = DirectoryHintTableHeader {
13291            version: 1,
13292            hint_shard_index,
13293            entry_count: entries.len() as u64,
13294            entry_table_offset: if entries.is_empty() {
13295                0
13296            } else {
13297                header_len as u64
13298            },
13299            shard_list_offset: shard_list_offset as u64,
13300            string_pool_offset: string_pool_offset as u64,
13301            string_pool_size: string_pool.len() as u64,
13302        };
13303
13304        let mut out = Vec::new();
13305        out.extend_from_slice(&header.to_bytes());
13306        for entry in entries {
13307            out.extend_from_slice(&entry.to_bytes());
13308        }
13309        for row in shard_row_indexes {
13310            out.extend_from_slice(&row.to_le_bytes());
13311        }
13312        out.extend_from_slice(&string_pool);
13313        out
13314    }
13315
13316    fn corrupt_first_block_record_payload(volume: &mut [u8]) {
13317        let (record_offset, _) = first_block_record(volume);
13318        volume[record_offset + 16] ^= 0x55;
13319    }
13320
13321    fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
13322        let (record_offset, _) = block_record_at_slot(volume, slot);
13323        volume[record_offset + 16] ^= 0x55;
13324    }
13325
13326    fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
13327        for patch in patches {
13328            let offset = patch.record_offset as usize;
13329            let end = offset + patch.record_bytes.len();
13330            volume[offset..end].copy_from_slice(&patch.record_bytes);
13331        }
13332    }
13333
13334    fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
13335        let (record_offset, _) = block_record_at_slot(volume, slot);
13336        volume[record_offset] ^= 0x55;
13337    }
13338
13339    fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
13340        let (record_offset, _) = block_record_at_slot(volume, slot);
13341        volume[record_offset + 14] = 0x01;
13342    }
13343
13344    fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
13345        let manifest_offset = terminal_material_offset(volume);
13346        volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
13347    }
13348
13349    fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
13350        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13351        CriticalRecoveryLocator::parse(
13352            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13353        )
13354        .unwrap()
13355    }
13356
13357    fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
13358        let locator = final_recovery_locator(volume);
13359        let tuple = CmraDecoderTuple::from(locator);
13360        assert!(parity_shard_count < tuple.parity_shard_count);
13361        let cmra_offset = locator.cmra_offset as usize;
13362        let shard_size = tuple.shard_size as usize;
13363        let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
13364        let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
13365        let mut header = CriticalMetadataRecoveryHeader::parse(
13366            &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
13367        )
13368        .unwrap();
13369        header.parity_shard_count = parity_shard_count;
13370
13371        let mut cmra =
13372            Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
13373        cmra.extend_from_slice(&header.to_bytes());
13374        let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
13375        for row in 0..kept_rows {
13376            let start = rows_start + row * row_len;
13377            cmra.extend_from_slice(&volume[start..start + row_len]);
13378        }
13379
13380        let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
13381        out.extend_from_slice(&volume[..cmra_offset]);
13382        out.extend_from_slice(&cmra);
13383        let mut mirror = locator;
13384        mirror.locator_sequence = 1;
13385        mirror.cmra_length = cmra.len() as u32;
13386        mirror.cmra_parity_shard_count = parity_shard_count;
13387        out.extend_from_slice(&mirror.to_bytes());
13388        let final_locator = CriticalRecoveryLocator {
13389            locator_sequence: 0,
13390            ..mirror
13391        };
13392        out.extend_from_slice(&final_locator.to_bytes());
13393        out
13394    }
13395
13396    fn rewrite_public_cmra_image(
13397        volume: &mut [u8],
13398        mutate: impl FnOnce(&mut CriticalMetadataImage),
13399    ) {
13400        rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
13401    }
13402
13403    fn rewrite_cmra_image(
13404        volume: &mut [u8],
13405        mode: CmraRecoveryMode,
13406        mutate: impl FnOnce(&mut CriticalMetadataImage),
13407    ) {
13408        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13409        let locator = final_recovery_locator(volume);
13410        let tuple = CmraDecoderTuple::from(locator);
13411        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
13412        let mut image = recovered.image;
13413        mutate(&mut image);
13414        refresh_critical_image_region_digests(&mut image);
13415        let image_bytes = image.to_bytes().unwrap();
13416        assert_eq!(image_bytes.len(), tuple.image_length as usize);
13417
13418        let shard_size = tuple.shard_size as usize;
13419        let data_shard_count = tuple.data_shard_count as usize;
13420        let parity_shard_count = tuple.parity_shard_count as usize;
13421        assert!(image_bytes.len() <= data_shard_count * shard_size);
13422
13423        let mut data_shards = Vec::with_capacity(data_shard_count);
13424        for idx in 0..data_shard_count {
13425            let start = idx * shard_size;
13426            let end = (start + shard_size).min(image_bytes.len());
13427            let mut payload = vec![0u8; shard_size];
13428            if start < image_bytes.len() {
13429                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
13430            }
13431            data_shards.push(payload);
13432        }
13433        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
13434        let image_sha256 = sha256_bytes(&image_bytes);
13435
13436        let header = CriticalMetadataRecoveryHeader {
13437            shard_size: tuple.shard_size,
13438            data_shard_count: tuple.data_shard_count,
13439            parity_shard_count: tuple.parity_shard_count,
13440            image_length: tuple.image_length,
13441            archive_uuid_hint: locator.archive_uuid_hint,
13442            session_id_hint: locator.session_id_hint,
13443            volume_index_hint: locator.volume_index_hint,
13444            image_sha256,
13445            header_crc32c: 0,
13446        };
13447        let mut cmra = Vec::new();
13448        cmra.extend_from_slice(&header.to_bytes());
13449        for (idx, payload) in data_shards.into_iter().enumerate() {
13450            let payload_len = if idx + 1 == data_shard_count {
13451                image_bytes.len() - idx * shard_size
13452            } else {
13453                shard_size
13454            };
13455            cmra.extend_from_slice(
13456                &CriticalMetadataRecoveryShard {
13457                    shard_index: idx as u16,
13458                    shard_role: 0,
13459                    shard_payload_length: payload_len as u32,
13460                    payload,
13461                    shard_crc32c: 0,
13462                }
13463                .to_bytes(shard_size)
13464                .unwrap(),
13465            );
13466        }
13467        for (idx, payload) in parity_shards.into_iter().enumerate() {
13468            cmra.extend_from_slice(
13469                &CriticalMetadataRecoveryShard {
13470                    shard_index: (data_shard_count + idx) as u16,
13471                    shard_role: 1,
13472                    shard_payload_length: shard_size as u32,
13473                    payload,
13474                    shard_crc32c: 0,
13475                }
13476                .to_bytes(shard_size)
13477                .unwrap(),
13478            );
13479        }
13480        assert_eq!(cmra.len() as u64, recovered.cmra_length);
13481        let cmra_offset = locator.cmra_offset as usize;
13482        volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
13483
13484        rewrite_locator_image_sha(volume, final_offset, image_sha256);
13485        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13486        rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
13487    }
13488
13489    fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
13490        image.volume_header_sha256 = sha256_bytes(
13491            &image
13492                .regions
13493                .iter()
13494                .find(|region| region.region_type == 1)
13495                .unwrap()
13496                .bytes,
13497        );
13498        image.crypto_header_sha256 = sha256_bytes(
13499            &image
13500                .regions
13501                .iter()
13502                .find(|region| region.region_type == 2)
13503                .unwrap()
13504                .bytes,
13505        );
13506        image.manifest_footer_sha256 = sha256_bytes(
13507            &image
13508                .regions
13509                .iter()
13510                .find(|region| region.region_type == 3)
13511                .unwrap()
13512                .bytes,
13513        );
13514        image.root_auth_footer_sha256 = image
13515            .regions
13516            .iter()
13517            .find(|region| region.region_type == 4)
13518            .map(|region| sha256_bytes(&region.bytes))
13519            .unwrap_or([0u8; 32]);
13520        image.volume_trailer_sha256 = sha256_bytes(
13521            &image
13522                .regions
13523                .iter()
13524                .find(|region| region.region_type == 5)
13525                .unwrap()
13526                .bytes,
13527        );
13528    }
13529
13530    fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
13531        let mut locator =
13532            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
13533                .unwrap();
13534        locator.cmra_image_sha256 = image_sha256;
13535        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
13536    }
13537
13538    fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
13539        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13540        let final_locator = CriticalRecoveryLocator::parse(
13541            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13542        )
13543        .unwrap();
13544        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13545        volume[final_locator.cmra_offset as usize] ^= 0x55;
13546        volume[mirror_offset] ^= 0x55;
13547        volume[final_offset] ^= 0x55;
13548    }
13549
13550    fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
13551        let (record_offset, record_len) = first_block_record(volume);
13552        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13553        let mut record = BlockRecord::parse(
13554            &volume[record_offset..record_offset + record_len],
13555            block_size,
13556        )
13557        .unwrap();
13558        mutate(&mut record);
13559        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13560    }
13561
13562    fn mutate_block_record_at_slot(
13563        volume: &mut [u8],
13564        slot: usize,
13565        mutate: impl FnOnce(&mut BlockRecord),
13566    ) {
13567        let (record_offset, record_len) = block_record_at_slot(volume, slot);
13568        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13569        let mut record = BlockRecord::parse(
13570            &volume[record_offset..record_offset + record_len],
13571            block_size,
13572        )
13573        .unwrap();
13574        mutate(&mut record);
13575        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13576    }
13577
13578    fn first_block_record(volume: &[u8]) -> (usize, usize) {
13579        block_record_at_slot(volume, 0)
13580    }
13581
13582    fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
13583        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13584        let crypto_start = volume_header.crypto_header_offset as usize;
13585        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13586        let crypto_header = CryptoHeader::parse(
13587            &volume[crypto_start..crypto_end],
13588            volume_header.crypto_header_length,
13589        )
13590        .unwrap();
13591        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13592        let record_offset = crypto_end + slot * record_len;
13593        assert!(volume.len() >= record_offset + record_len);
13594        (record_offset, record_len)
13595    }
13596
13597    fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
13598        block_record_slots(volume)
13599            .into_iter()
13600            .enumerate()
13601            .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13602    }
13603
13604    fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
13605        block_record_slots(volume)
13606            .into_iter()
13607            .enumerate()
13608            .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13609            .collect()
13610    }
13611
13612    fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
13613        let mut slots = Vec::new();
13614        for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
13615            if record.kind == BlockKind::PayloadData {
13616                slots.push(slot);
13617            } else if !slots.is_empty() {
13618                break;
13619            }
13620        }
13621        slots
13622    }
13623
13624    fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
13625        envelope_entries_for_path(opened, path)
13626            .into_iter()
13627            .map(|entry| entry.envelope_index)
13628            .collect()
13629    }
13630
13631    fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
13632        let normalized =
13633            normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
13634        let located = opened.locate_index_file(&normalized).unwrap().unwrap();
13635        let file = &located.shard.files[located.file_index];
13636        frame_range_for_file(&located.shard, file)
13637            .unwrap()
13638            .into_iter()
13639            .map(|frame| {
13640                located
13641                    .shard
13642                    .envelopes
13643                    .iter()
13644                    .find(|entry| entry.envelope_index == frame.envelope_index)
13645                    .unwrap()
13646                    .clone()
13647            })
13648            .collect()
13649    }
13650
13651    fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
13652        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13653        let crypto_start = volume_header.crypto_header_offset as usize;
13654        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13655        let crypto_header = CryptoHeader::parse(
13656            &volume[crypto_start..crypto_end],
13657            volume_header.crypto_header_length,
13658        )
13659        .unwrap();
13660        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13661        let manifest_offset = terminal_material_offset(volume);
13662        assert_eq!((manifest_offset - crypto_end) % record_len, 0);
13663        let record_count = (manifest_offset - crypto_end) / record_len;
13664        (0..record_count)
13665            .map(|slot| {
13666                let offset = crypto_end + slot * record_len;
13667                let record = BlockRecord::parse(
13668                    &volume[offset..offset + record_len],
13669                    record_len - BLOCK_RECORD_FRAMING_LEN,
13670                )
13671                .unwrap();
13672                (offset, record_len, record)
13673            })
13674            .collect()
13675    }
13676
13677    fn rewrite_manifest_footer(
13678        volume: &mut [u8],
13679        master_key: &MasterKey,
13680        mutate: impl FnOnce(&mut ManifestFooter),
13681    ) {
13682        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13683        let offset = terminal_material_offset(volume);
13684        let mut footer =
13685            ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
13686        mutate(&mut footer);
13687        footer.manifest_hmac = [0u8; 32];
13688        let mut footer_bytes = footer.to_bytes();
13689        let subkeys = Subkeys::derive(
13690            master_key,
13691            &volume_header.archive_uuid,
13692            &volume_header.session_id,
13693        )
13694        .unwrap();
13695        footer.manifest_hmac = compute_hmac(
13696            HmacDomain::ManifestFooter,
13697            &subkeys.mac_key,
13698            &volume_header.archive_uuid,
13699            &volume_header.session_id,
13700            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
13701        );
13702        footer_bytes = footer.to_bytes();
13703        volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
13704    }
13705
13706    fn rewrite_volume_trailer(
13707        volume: &mut [u8],
13708        master_key: &MasterKey,
13709        mutate: impl FnOnce(&mut VolumeTrailer),
13710    ) {
13711        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13712        let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
13713        let mut trailer =
13714            VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
13715        mutate(&mut trailer);
13716        trailer.trailer_hmac = [0u8; 32];
13717        let mut trailer_bytes = trailer.to_bytes();
13718        let subkeys = Subkeys::derive(
13719            master_key,
13720            &volume_header.archive_uuid,
13721            &volume_header.session_id,
13722        )
13723        .unwrap();
13724        trailer.trailer_hmac = compute_hmac(
13725            HmacDomain::VolumeTrailer,
13726            &subkeys.mac_key,
13727            &volume_header.archive_uuid,
13728            &volume_header.session_id,
13729            &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
13730        );
13731        trailer_bytes = trailer.to_bytes();
13732        volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
13733    }
13734
13735    fn rewrite_sidecar_header(
13736        sidecar: &mut [u8],
13737        master_key: &MasterKey,
13738        mutate: impl FnOnce(&mut BootstrapSidecarHeader),
13739    ) {
13740        let mut header =
13741            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13742        mutate(&mut header);
13743        write_signed_sidecar_header(sidecar, master_key, &mut header);
13744    }
13745
13746    fn write_signed_sidecar_header(
13747        sidecar: &mut [u8],
13748        master_key: &MasterKey,
13749        header: &mut BootstrapSidecarHeader,
13750    ) {
13751        header.sidecar_hmac = [0u8; 32];
13752        let mut header_bytes = header.to_bytes();
13753        let subkeys =
13754            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
13755        header.sidecar_hmac = compute_hmac(
13756            HmacDomain::BootstrapSidecar,
13757            &subkeys.mac_key,
13758            &header.archive_uuid,
13759            &header.session_id,
13760            &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
13761        );
13762        header_bytes = header.to_bytes();
13763        sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
13764    }
13765
13766    fn sparse_bootstrap_sidecar(
13767        source: &[u8],
13768        master_key: &MasterKey,
13769        include_manifest: bool,
13770        include_index_root: bool,
13771        include_dictionary: bool,
13772    ) -> Vec<u8> {
13773        let source_header =
13774            BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13775        let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
13776        let mut header = BootstrapSidecarHeader {
13777            archive_uuid: source_header.archive_uuid,
13778            session_id: source_header.session_id,
13779            flags: 0,
13780            manifest_footer_offset: 0,
13781            manifest_footer_length: 0,
13782            index_root_records_offset: 0,
13783            index_root_records_length: 0,
13784            dictionary_records_offset: 0,
13785            dictionary_records_length: 0,
13786            sidecar_hmac: [0u8; 32],
13787            header_crc32c: 0,
13788        };
13789
13790        if include_manifest {
13791            assert!(source_header.has_manifest_footer());
13792            let (offset, length) = append_sidecar_section(
13793                source,
13794                &mut sidecar,
13795                source_header.manifest_footer_offset,
13796                source_header.manifest_footer_length as u64,
13797            );
13798            header.flags |= 0x01;
13799            header.manifest_footer_offset = offset;
13800            header.manifest_footer_length = length as u32;
13801        }
13802        if include_index_root {
13803            assert!(source_header.has_index_root_records());
13804            let (offset, length) = append_sidecar_section(
13805                source,
13806                &mut sidecar,
13807                source_header.index_root_records_offset,
13808                source_header.index_root_records_length,
13809            );
13810            header.flags |= 0x02;
13811            header.index_root_records_offset = offset;
13812            header.index_root_records_length = length;
13813        }
13814        if include_dictionary {
13815            assert!(source_header.has_dictionary_records());
13816            let (offset, length) = append_sidecar_section(
13817                source,
13818                &mut sidecar,
13819                source_header.dictionary_records_offset,
13820                source_header.dictionary_records_length,
13821            );
13822            header.flags |= 0x04;
13823            header.dictionary_records_offset = offset;
13824            header.dictionary_records_length = length;
13825        }
13826
13827        write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
13828        sidecar
13829    }
13830
13831    fn append_sidecar_section(
13832        source: &[u8],
13833        sidecar: &mut Vec<u8>,
13834        source_offset: u64,
13835        length: u64,
13836    ) -> (u64, u64) {
13837        let source_offset = source_offset as usize;
13838        let length = length as usize;
13839        let offset = sidecar.len() as u64;
13840        sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
13841        (offset, length as u64)
13842    }
13843
13844    fn mutate_sidecar_manifest(
13845        sidecar: &mut [u8],
13846        master_key: &MasterKey,
13847        mutate: impl FnOnce(&mut ManifestFooter),
13848    ) {
13849        let header =
13850            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13851        let offset = header.manifest_footer_offset as usize;
13852        let mut footer =
13853            ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
13854        mutate(&mut footer);
13855        footer.manifest_hmac = [0u8; 32];
13856        let mut footer_bytes = footer.to_bytes();
13857        let subkeys =
13858            Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
13859        footer.manifest_hmac = compute_hmac(
13860            HmacDomain::ManifestFooter,
13861            &subkeys.mac_key,
13862            &footer.archive_uuid,
13863            &footer.session_id,
13864            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
13865        );
13866        footer_bytes = footer.to_bytes();
13867        sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
13868    }
13869
13870    fn mutate_sidecar_index_record(
13871        sidecar: &mut [u8],
13872        record_index: usize,
13873        mutate: impl FnOnce(&mut BlockRecord),
13874    ) {
13875        let header =
13876            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13877        let record_len = sidecar_record_len(sidecar);
13878        let offset = header.index_root_records_offset as usize + record_index * record_len;
13879        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13880        let mut record =
13881            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
13882        mutate(&mut record);
13883        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
13884    }
13885
13886    fn mutate_sidecar_dictionary_record(
13887        sidecar: &mut [u8],
13888        record_index: usize,
13889        mutate: impl FnOnce(&mut BlockRecord),
13890    ) {
13891        let header =
13892            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13893        let record_len = sidecar_record_len(sidecar);
13894        let offset = header.dictionary_records_offset as usize + record_index * record_len;
13895        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13896        let mut record =
13897            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
13898        mutate(&mut record);
13899        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
13900    }
13901
13902    fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
13903        let header =
13904            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13905        let record_len = sidecar_record_len(sidecar);
13906        let left_offset = header.index_root_records_offset as usize + left * record_len;
13907        let right_offset = header.index_root_records_offset as usize + right * record_len;
13908        for idx in 0..record_len {
13909            sidecar.swap(left_offset + idx, right_offset + idx);
13910        }
13911    }
13912
13913    fn sidecar_record_len(sidecar: &[u8]) -> usize {
13914        let header =
13915            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13916        let footer_offset = header.manifest_footer_offset as usize;
13917        let footer =
13918            ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
13919                .unwrap();
13920        let index_record_count = footer.index_root_data_block_count as usize
13921            + footer.index_root_parity_block_count as usize;
13922        header.index_root_records_length as usize / index_record_count
13923    }
13924
13925    fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
13926        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13927        assert_eq!(volume_header.volume_index, 0);
13928        assert_eq!(volume_header.stripe_width, 1);
13929        let crypto_start = volume_header.crypto_header_offset as usize;
13930        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13931        let crypto_header = CryptoHeader::parse(
13932            &volume[crypto_start..crypto_end],
13933            volume_header.crypto_header_length,
13934        )
13935        .unwrap();
13936        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13937        let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
13938        for offset in 0..record_count {
13939            let block_index = extent.first_block_index + offset;
13940            let record_offset = crypto_end + block_index as usize * record_len;
13941            volume[record_offset + 16] ^= 0x55;
13942        }
13943    }
13944
13945    fn terminal_material_offset(volume: &[u8]) -> usize {
13946        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13947        let crypto_start = volume_header.crypto_header_offset as usize;
13948        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13949        let crypto_header = CryptoHeader::parse(
13950            &volume[crypto_start..crypto_end],
13951            volume_header.crypto_header_length,
13952        )
13953        .unwrap();
13954        let (_, offset, _) = parse_stream_block_prefix(
13955            volume,
13956            crypto_end,
13957            crypto_header.fixed.block_size as usize,
13958            &volume_header,
13959        )
13960        .unwrap();
13961        offset
13962    }
13963
13964    #[derive(Debug)]
13965    struct TestObject {
13966        extent: ObjectExtent,
13967        records: Vec<BlockRecord>,
13968    }
13969
13970    #[derive(Debug)]
13971    struct TestFileMeta {
13972        path: Vec<u8>,
13973        frame_index: u64,
13974        tar_stream_offset: u64,
13975        member_group_size: u64,
13976        file_data_size: u64,
13977    }
13978
13979    fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
13980        let volume_header = test_volume_header();
13981        let crypto_header = test_crypto_header();
13982        let subkeys = Subkeys::derive(
13983            &master_key(),
13984            &volume_header.archive_uuid,
13985            &volume_header.session_id,
13986        )
13987        .unwrap();
13988        let mut next_block_index = 0u64;
13989        let mut blocks = BTreeMap::new();
13990
13991        let healthy = test_member(b"healthy.txt", b"healthy payload\n");
13992        let broken = test_member(b"broken.txt", b"broken payload\n");
13993        let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
13994
13995        let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
13996        let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
13997
13998        let healthy_payload = encrypt_test_object(
13999            &healthy_frame,
14000            &subkeys.enc_key,
14001            &subkeys.nonce_seed,
14002            b"envelope",
14003            0,
14004            BlockKind::PayloadData,
14005            &mut next_block_index,
14006            &crypto_header,
14007            &volume_header,
14008        );
14009        let broken_payload = encrypt_test_object(
14010            &broken_frame,
14011            &subkeys.enc_key,
14012            &subkeys.nonce_seed,
14013            b"envelope",
14014            1,
14015            BlockKind::PayloadData,
14016            &mut next_block_index,
14017            &crypto_header,
14018            &volume_header,
14019        );
14020        let broken_payload_block = broken_payload.extent.first_block_index;
14021        insert_records(&mut blocks, &healthy_payload.records);
14022        insert_records(&mut blocks, &broken_payload.records);
14023
14024        let frames = vec![
14025            FrameEntry {
14026                frame_index: 0,
14027                envelope_index: 0,
14028                offset_in_envelope: 0,
14029                compressed_size: healthy_frame.len() as u32,
14030                decompressed_size: healthy.len() as u32,
14031                flags: 0x0000_0003,
14032                tar_stream_offset: 0,
14033            },
14034            FrameEntry {
14035                frame_index: 1,
14036                envelope_index: 1,
14037                offset_in_envelope: 0,
14038                compressed_size: broken_frame.len() as u32,
14039                decompressed_size: broken.len() as u32,
14040                flags: 0x0000_0003,
14041                tar_stream_offset: healthy.len() as u64,
14042            },
14043        ];
14044        let envelopes = vec![
14045            EnvelopeEntry {
14046                envelope_index: 0,
14047                first_block_index: healthy_payload.extent.first_block_index,
14048                data_block_count: healthy_payload.extent.data_block_count,
14049                parity_block_count: 0,
14050                encrypted_size: healthy_payload.extent.encrypted_size,
14051                plaintext_size: healthy_frame.len() as u32,
14052                first_frame_index: 0,
14053                frame_count: 1,
14054            },
14055            EnvelopeEntry {
14056                envelope_index: 1,
14057                first_block_index: broken_payload.extent.first_block_index,
14058                data_block_count: broken_payload.extent.data_block_count,
14059                parity_block_count: 0,
14060                encrypted_size: broken_payload.extent.encrypted_size,
14061                plaintext_size: broken_frame.len() as u32,
14062                first_frame_index: 1,
14063                frame_count: 1,
14064            },
14065        ];
14066        let files = vec![
14067            TestFileMeta {
14068                path: b"healthy.txt".to_vec(),
14069                frame_index: 0,
14070                tar_stream_offset: 0,
14071                member_group_size: healthy.len() as u64,
14072                file_data_size: b"healthy payload\n".len() as u64,
14073            },
14074            TestFileMeta {
14075                path: b"broken.txt".to_vec(),
14076                frame_index: 1,
14077                tar_stream_offset: healthy.len() as u64,
14078                member_group_size: broken.len() as u64,
14079                file_data_size: b"broken payload\n".len() as u64,
14080            },
14081        ];
14082
14083        let (index_shard_plaintext, first_path_hash, last_path_hash) =
14084            build_test_index_shard(&files, &frames, &envelopes);
14085        let index_shard = encrypt_test_object(
14086            &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
14087            &subkeys.index_shard_key,
14088            &subkeys.index_nonce_seed,
14089            b"idxshard",
14090            0,
14091            BlockKind::IndexShardData,
14092            &mut next_block_index,
14093            &crypto_header,
14094            &volume_header,
14095        );
14096        insert_records(&mut blocks, &index_shard.records);
14097
14098        let shard_entry = ShardEntry {
14099            shard_index: 0,
14100            first_block_index: index_shard.extent.first_block_index,
14101            data_block_count: index_shard.extent.data_block_count,
14102            parity_block_count: 0,
14103            encrypted_size: index_shard.extent.encrypted_size,
14104            decompressed_size: index_shard_plaintext.len() as u32,
14105            file_count: files.len() as u32,
14106            first_path_hash,
14107            last_path_hash,
14108        };
14109        let mut root_header = IndexRootHeader::empty();
14110        root_header.frame_count = frames.len() as u64;
14111        root_header.envelope_count = envelopes.len() as u64;
14112        root_header.file_count = files.len() as u64;
14113        root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
14114            + broken_payload.extent.data_block_count as u64;
14115        root_header.tar_total_size = tar_stream.len() as u64;
14116        root_header.content_sha256 = sha256_bytes(&tar_stream);
14117        let index_root = IndexRoot {
14118            header: root_header,
14119            shards: vec![shard_entry],
14120            directory_hint_shards: Vec::new(),
14121        };
14122
14123        let index_root_plaintext = index_root.to_bytes();
14124        let index_root_object = encrypt_test_object(
14125            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14126            &subkeys.index_root_key,
14127            &subkeys.index_nonce_seed,
14128            b"idxroot",
14129            0,
14130            BlockKind::IndexRootData,
14131            &mut next_block_index,
14132            &crypto_header,
14133            &volume_header,
14134        );
14135        insert_records(&mut blocks, &index_root_object.records);
14136
14137        let archive_uuid = volume_header.archive_uuid;
14138        let session_id = volume_header.session_id;
14139        let opened = OpenedArchive {
14140            options: ReaderOptions::default(),
14141            observed_archive_bytes: 1_000_000,
14142            observed_volume_count: 1,
14143            subkeys,
14144            blocks,
14145            lazy_blocks: None,
14146            crypto_header_bytes: Vec::new(),
14147            volume_header,
14148            crypto_header,
14149            manifest_footer: ManifestFooter {
14150                archive_uuid,
14151                session_id,
14152                volume_index: 0,
14153                is_authoritative: 1,
14154                total_volumes: 1,
14155                index_root_first_block: index_root_object.extent.first_block_index,
14156                index_root_data_block_count: index_root_object.extent.data_block_count,
14157                index_root_parity_block_count: 0,
14158                index_root_encrypted_size: index_root_object.extent.encrypted_size,
14159                index_root_decompressed_size: index_root_plaintext.len() as u32,
14160                manifest_hmac: [0u8; 32],
14161            },
14162            volume_trailer: Some(VolumeTrailer {
14163                archive_uuid,
14164                session_id,
14165                volume_index: 0,
14166                block_count: next_block_index,
14167                bytes_written: 0,
14168                manifest_footer_offset: 0,
14169                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14170                closed_at_ns: 0,
14171                root_auth_footer_offset: 0,
14172                root_auth_footer_length: 0,
14173                root_auth_flags: 0,
14174                trailer_hmac: [0u8; 32],
14175            }),
14176            root_auth_footer: None,
14177            index_root,
14178            payload_dictionary: None,
14179        };
14180        (opened, broken_payload_block)
14181    }
14182
14183    fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
14184        let locating = opened.index_root.shards[0].clone();
14185        let mut shard = opened.load_index_shard(&locating).unwrap();
14186        mutate(&mut shard);
14187        let plaintext = shard.to_bytes();
14188        let mut next_block_index = opened
14189            .blocks
14190            .keys()
14191            .last()
14192            .copied()
14193            .map(|index| index + 1)
14194            .unwrap_or(0);
14195        let replacement = encrypt_test_object(
14196            &compress_zstd_frame(&plaintext, 1).unwrap(),
14197            &opened.subkeys.index_shard_key,
14198            &opened.subkeys.index_nonce_seed,
14199            b"idxshard",
14200            locating.shard_index,
14201            BlockKind::IndexShardData,
14202            &mut next_block_index,
14203            &opened.crypto_header,
14204            &opened.volume_header,
14205        );
14206        insert_records(&mut opened.blocks, &replacement.records);
14207        opened.index_root.shards[0] = ShardEntry {
14208            shard_index: locating.shard_index,
14209            first_block_index: replacement.extent.first_block_index,
14210            data_block_count: replacement.extent.data_block_count,
14211            parity_block_count: 0,
14212            encrypted_size: replacement.extent.encrypted_size,
14213            decompressed_size: plaintext.len() as u32,
14214            file_count: shard.files.len() as u32,
14215            first_path_hash: shard.files.first().unwrap().path_hash,
14216            last_path_hash: shard.files.last().unwrap().path_hash,
14217        };
14218    }
14219
14220    fn rewrite_as_single_healthy_file(
14221        opened: &mut OpenedArchive,
14222        mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
14223    ) {
14224        let healthy_path = b"healthy.txt";
14225        let healthy_payload = b"healthy payload\n";
14226        let healthy_member = test_member(healthy_path, healthy_payload);
14227        replace_first_index_shard(opened, |shard| {
14228            let file_index = (0..shard.files.len())
14229                .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
14230                .unwrap();
14231            let mut file = shard.files[file_index].clone();
14232            let frame = shard
14233                .frames
14234                .iter()
14235                .find(|entry| entry.frame_index == 0)
14236                .unwrap()
14237                .clone();
14238            let envelope = shard
14239                .envelopes
14240                .iter()
14241                .find(|entry| entry.envelope_index == 0)
14242                .unwrap()
14243                .clone();
14244            let mut path = healthy_path.to_vec();
14245
14246            file.path_offset = 0;
14247            file.path_length = path.len() as u32;
14248            file.first_frame_index = 0;
14249            file.frame_count = 1;
14250            file.offset_in_first_frame_plaintext = 0;
14251            file.tar_member_group_size = healthy_member.len() as u64;
14252            file.file_data_size = healthy_payload.len() as u64;
14253            file.flags = 0;
14254            mutate(&mut file, &mut path);
14255            file.path_offset = 0;
14256            file.path_length = path.len() as u32;
14257            file.path_hash = hash_prefix(&path);
14258
14259            shard.files = vec![file];
14260            shard.frames = vec![frame];
14261            shard.envelopes = vec![envelope];
14262            shard.string_pool = path;
14263        });
14264
14265        opened.index_root.header.file_count = 1;
14266        opened.index_root.header.frame_count = 1;
14267        opened.index_root.header.envelope_count = 1;
14268        opened.index_root.header.payload_block_count = 1;
14269        opened.index_root.header.tar_total_size = healthy_member.len() as u64;
14270        opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
14271    }
14272
14273    fn test_volume_header() -> VolumeHeader {
14274        VolumeHeader {
14275            format_version: FORMAT_VERSION,
14276            volume_format_rev: VOLUME_FORMAT_REV,
14277            volume_index: 0,
14278            stripe_width: 1,
14279            archive_uuid: [0x31; 16],
14280            session_id: [0x42; 16],
14281            crypto_header_offset: VOLUME_HEADER_LEN as u32,
14282            crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
14283            header_crc32c: 0,
14284        }
14285    }
14286
14287    fn test_crypto_header() -> CryptoHeaderFixed {
14288        CryptoHeaderFixed {
14289            length: CRYPTO_HEADER_FIXED_LEN as u32,
14290            compression_algo: CompressionAlgo::ZstdFramed,
14291            aead_algo: AeadAlgo::AesGcmSiv256,
14292            fec_algo: FecAlgo::ReedSolomonGF16,
14293            kdf_algo: KdfAlgo::Raw,
14294            chunk_size: 4096,
14295            envelope_target_size: 8192,
14296            block_size: 4096,
14297            fec_data_shards: 4,
14298            fec_parity_shards: 0,
14299            index_fec_data_shards: 4,
14300            index_fec_parity_shards: 0,
14301            index_root_fec_data_shards: 4,
14302            index_root_fec_parity_shards: 0,
14303            stripe_width: 1,
14304            volume_loss_tolerance: 0,
14305            bit_rot_buffer_pct: 0,
14306            has_dictionary: 0,
14307            max_path_length: 4096,
14308            expected_volume_size: 0,
14309        }
14310    }
14311
14312    #[allow(clippy::too_many_arguments)]
14313    fn encrypt_test_object(
14314        plaintext: &[u8],
14315        key: &[u8; 32],
14316        nonce_seed: &[u8; 32],
14317        domain: &[u8],
14318        counter: u64,
14319        data_kind: BlockKind,
14320        next_block_index: &mut u64,
14321        crypto_header: &CryptoHeaderFixed,
14322        volume_header: &VolumeHeader,
14323    ) -> TestObject {
14324        let block_size = crypto_header.block_size as usize;
14325        let encrypted = encrypt_padded_aead_object(
14326            AeadObjectContext {
14327                algo: crypto_header.aead_algo,
14328                key,
14329                nonce_seed,
14330                domain,
14331                archive_uuid: &volume_header.archive_uuid,
14332                session_id: &volume_header.session_id,
14333                counter,
14334            },
14335            block_size,
14336            plaintext,
14337        )
14338        .unwrap();
14339        assert_eq!(encrypted.len() % block_size, 0);
14340
14341        let first_block_index = *next_block_index;
14342        let data_block_count = encrypted.len() / block_size;
14343        let records = encrypted
14344            .chunks(block_size)
14345            .enumerate()
14346            .map(|(index, payload)| BlockRecord {
14347                block_index: first_block_index + index as u64,
14348                kind: data_kind,
14349                flags: if index + 1 == data_block_count {
14350                    0x01
14351                } else {
14352                    0
14353                },
14354                payload: payload.to_vec(),
14355                record_crc32c: 0,
14356            })
14357            .collect::<Vec<_>>();
14358        *next_block_index += data_block_count as u64;
14359
14360        TestObject {
14361            extent: ObjectExtent {
14362                first_block_index,
14363                data_block_count: data_block_count as u32,
14364                parity_block_count: 0,
14365                encrypted_size: encrypted.len() as u32,
14366            },
14367            records,
14368        }
14369    }
14370
14371    fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
14372        for record in records {
14373            assert!(blocks.insert(record.block_index, record.clone()).is_none());
14374        }
14375    }
14376
14377    #[allow(clippy::too_many_arguments)]
14378    fn build_metadata_object_from_payload(
14379        payload: &[u8],
14380        _subkeys: &Subkeys,
14381        volume_header: &VolumeHeader,
14382        crypto_header: &CryptoHeaderFixed,
14383        key: &[u8; 32],
14384        nonce_seed: &[u8; 32],
14385        domain: &[u8],
14386        counter: u64,
14387        data_kind: BlockKind,
14388        next_block_index: &mut u64,
14389    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14390        let compressed = compress_zstd_frame(payload, 1).unwrap();
14391        build_metadata_object_from_compressed(
14392            &compressed,
14393            key,
14394            nonce_seed,
14395            domain,
14396            counter,
14397            data_kind,
14398            next_block_index,
14399            crypto_header,
14400            volume_header,
14401        )
14402    }
14403
14404    #[allow(clippy::too_many_arguments)]
14405    fn build_metadata_object_from_compressed(
14406        compressed: &[u8],
14407        key: &[u8; 32],
14408        nonce_seed: &[u8; 32],
14409        domain: &[u8],
14410        counter: u64,
14411        data_kind: BlockKind,
14412        next_block_index: &mut u64,
14413        crypto_header: &CryptoHeaderFixed,
14414        volume_header: &VolumeHeader,
14415    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14416        let object = encrypt_test_object(
14417            compressed,
14418            key,
14419            nonce_seed,
14420            domain,
14421            counter,
14422            data_kind,
14423            next_block_index,
14424            crypto_header,
14425            volume_header,
14426        );
14427
14428        let mut blocks = BTreeMap::new();
14429        for record in object.records {
14430            blocks.insert(record.block_index, record);
14431        }
14432        (object.extent, blocks)
14433    }
14434
14435    #[allow(clippy::too_many_arguments)]
14436    fn assert_metadata_object_from_compressed(
14437        compressed: &[u8],
14438        decompressed_size: usize,
14439        _subkeys: &Subkeys,
14440        volume_header: &VolumeHeader,
14441        crypto_header: &CryptoHeaderFixed,
14442        key: &[u8; 32],
14443        nonce_seed: &[u8; 32],
14444        domain: &[u8],
14445        counter: u64,
14446        data_kind: BlockKind,
14447        parity_kind: BlockKind,
14448        class_data_shards: u16,
14449        class_parity_shards: u16,
14450        next_block_index: &mut u64,
14451        expected: FormatError,
14452    ) {
14453        let (extent, blocks) = build_metadata_object_from_compressed(
14454            compressed,
14455            key,
14456            nonce_seed,
14457            domain,
14458            counter,
14459            data_kind,
14460            next_block_index,
14461            crypto_header,
14462            volume_header,
14463        );
14464        let error = load_metadata_object_from_parts(
14465            &blocks,
14466            ObjectLoadContext {
14467                volume_header,
14468                crypto_header,
14469                extent,
14470                data_kind,
14471                parity_kind,
14472                key,
14473                nonce_seed,
14474                domain,
14475                counter,
14476                class_data_shard_max: class_data_shards,
14477                class_parity_shard_max: class_parity_shards,
14478            },
14479            decompressed_size as u32,
14480        )
14481        .unwrap_err();
14482        assert_eq!(error, expected);
14483    }
14484
14485    fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
14486        let record = blocks.get_mut(&block_index).unwrap();
14487        assert_eq!(record.kind, BlockKind::PayloadData);
14488        record.payload[0] ^= 0x55;
14489    }
14490
14491    fn build_test_index_shard(
14492        files: &[TestFileMeta],
14493        frames: &[FrameEntry],
14494        envelopes: &[EnvelopeEntry],
14495    ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
14496        let mut sorted = files
14497            .iter()
14498            .map(|file| (hash_prefix(&file.path), file))
14499            .collect::<Vec<_>>();
14500        sorted.sort_by(|left, right| {
14501            (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
14502                right.0,
14503                right.1.path.as_slice(),
14504                right.1.tar_stream_offset,
14505            ))
14506        });
14507
14508        let mut string_pool = Vec::new();
14509        let mut file_entries = Vec::with_capacity(sorted.len());
14510        for (path_hash, file) in &sorted {
14511            let path_offset = string_pool.len() as u32;
14512            string_pool.extend_from_slice(&file.path);
14513            file_entries.push(FileEntry {
14514                path_hash: *path_hash,
14515                path_offset,
14516                path_length: file.path.len() as u32,
14517                first_frame_index: file.frame_index,
14518                frame_count: 1,
14519                offset_in_first_frame_plaintext: 0,
14520                tar_member_group_size: file.member_group_size,
14521                file_data_size: file.file_data_size,
14522                flags: 0,
14523            });
14524        }
14525
14526        let header = IndexShardHeader {
14527            version: 1,
14528            shard_index: 0,
14529            file_count: file_entries.len() as u32,
14530            frame_count: frames.len() as u32,
14531            envelope_count: envelopes.len() as u32,
14532            file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
14533            frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
14534                as u32,
14535            envelope_table_offset: (INDEX_SHARD_HEADER_LEN
14536                + file_entries.len() * FILE_ENTRY_LEN
14537                + frames.len() * FRAME_ENTRY_LEN) as u32,
14538            string_pool_offset: (INDEX_SHARD_HEADER_LEN
14539                + file_entries.len() * FILE_ENTRY_LEN
14540                + frames.len() * FRAME_ENTRY_LEN
14541                + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
14542            string_pool_size: string_pool.len() as u32,
14543        };
14544
14545        let mut bytes = Vec::new();
14546        bytes.extend_from_slice(&header.to_bytes());
14547        for entry in &file_entries {
14548            bytes.extend_from_slice(&entry.to_bytes());
14549        }
14550        for entry in frames {
14551            bytes.extend_from_slice(&entry.to_bytes());
14552        }
14553        for entry in envelopes {
14554            bytes.extend_from_slice(&entry.to_bytes());
14555        }
14556        bytes.extend_from_slice(&string_pool);
14557
14558        (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
14559    }
14560
14561    fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
14562        let mut out = Vec::new();
14563        out.extend_from_slice(&test_tar_header(path, data.len() as u64));
14564        out.extend_from_slice(data);
14565        out.resize(out.len() + padding_to_512(data.len()), 0);
14566        out
14567    }
14568
14569    fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
14570        let mut header = [0u8; 512];
14571        header[..path.len()].copy_from_slice(path);
14572        write_test_tar_octal(&mut header[100..108], 0o644);
14573        write_test_tar_octal(&mut header[108..116], 0);
14574        write_test_tar_octal(&mut header[116..124], 0);
14575        write_test_tar_octal(&mut header[124..136], size);
14576        write_test_tar_octal(&mut header[136..148], 0);
14577        header[148..156].fill(b' ');
14578        header[156] = b'0';
14579        header[257..263].copy_from_slice(b"ustar\0");
14580        header[263..265].copy_from_slice(b"00");
14581        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
14582        write_test_tar_checksum(&mut header[148..156], checksum);
14583        header
14584    }
14585
14586    fn write_test_tar_octal(field: &mut [u8], value: u64) {
14587        let digits = format!("{value:o}");
14588        field.fill(0);
14589        let start = field.len() - 1 - digits.len();
14590        field[..start].fill(b'0');
14591        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
14592    }
14593
14594    fn write_test_tar_checksum(field: &mut [u8], value: u64) {
14595        let digits = format!("{value:06o}");
14596        field[0..6].copy_from_slice(digits.as_bytes());
14597        field[6] = 0;
14598        field[7] = b' ';
14599    }
14600
14601    fn padding_to_512(len: usize) -> usize {
14602        let remainder = len % 512;
14603        if remainder == 0 {
14604            0
14605        } else {
14606            512 - remainder
14607        }
14608    }
14609}