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
3107fn parse_seekable_volume(
3108    bytes: &[u8],
3109    master_key: &MasterKey,
3110    options: ReaderOptions,
3111) -> Result<ParsedSeekableVolume, FormatError> {
3112    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3113        return Err(FormatError::InvalidLength {
3114            structure: "archive",
3115            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3116            actual: bytes.len(),
3117        });
3118    }
3119
3120    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3121    let crypto_start = volume_header.crypto_header_offset as usize;
3122    let crypto_len = volume_header.crypto_header_length as usize;
3123    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3124    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3125    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3126    let subkeys = subkeys_for_open(
3127        Some(master_key),
3128        parsed_crypto.fixed.aead_algo,
3129        &volume_header.archive_uuid,
3130        &volume_header.session_id,
3131    )?;
3132    verify_integrity_tag(
3133        HmacDomain::CryptoHeader,
3134        parsed_crypto.fixed.aead_algo,
3135        Some(&subkeys.mac_key),
3136        &volume_header.archive_uuid,
3137        &volume_header.session_id,
3138        parsed_crypto.hmac_covered_bytes,
3139        &parsed_crypto.header_hmac,
3140    )?;
3141    parsed_crypto.validate_extension_semantics()?;
3142    validate_seekable_supported_volume(
3143        &volume_header,
3144        &parsed_crypto.fixed,
3145        &parsed_crypto.extensions,
3146    )?;
3147    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3148
3149    let terminal = locate_v41_terminal(
3150        bytes,
3151        KeyHoldingTerminalContext {
3152            subkeys: &subkeys,
3153            volume_header: &volume_header,
3154            crypto_header: &parsed_crypto.fixed,
3155            crypto_header_bytes: crypto_bytes,
3156        },
3157        options,
3158    )?;
3159    let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
3160    let volume_trailer = terminal.volume_trailer.clone();
3161    validate_trailer_identity(&volume_header, &volume_trailer)?;
3162
3163    let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
3164    let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
3165    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3166        if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
3167            || volume_trailer
3168                .root_auth_footer_offset
3169                .checked_add(volume_trailer.root_auth_footer_length as u64)
3170                .ok_or(FormatError::InvalidArchive(
3171                    "RootAuthFooter terminal boundary overflow",
3172                ))?
3173                != trailer_offset as u64
3174        {
3175            return Err(FormatError::InvalidArchive(
3176                "RootAuthFooter does not sit before selected trailer",
3177            ));
3178        }
3179    } else if manifest_end != trailer_offset {
3180        return Err(FormatError::InvalidArchive(
3181            "ManifestFooter does not end at selected trailer",
3182        ));
3183    }
3184    let manifest_bytes = &terminal.manifest_footer_bytes;
3185    let (manifest_footer, manifest_footer_error) = match parse_valid_manifest_footer(
3186        &volume_header,
3187        &parsed_crypto.fixed,
3188        &subkeys,
3189        manifest_bytes,
3190    ) {
3191        Ok(footer) => (Some(footer), None),
3192        Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3193        Err(err) => return Err(err),
3194    };
3195
3196    let block_region = parse_block_region(
3197        bytes,
3198        crypto_end,
3199        manifest_offset,
3200        parsed_crypto.fixed.block_size as usize,
3201        &volume_header,
3202        &volume_trailer,
3203    )?;
3204
3205    Ok(ParsedSeekableVolume {
3206        volume_header,
3207        crypto_header: parsed_crypto.fixed,
3208        crypto_header_bytes: crypto_bytes.to_vec(),
3209        subkeys,
3210        manifest_footer,
3211        manifest_footer_error,
3212        root_auth_footer: terminal.root_auth_footer,
3213        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3214        volume_trailer,
3215        blocks: block_region.blocks,
3216        erased_block_indices: block_region.erased_block_indices,
3217    })
3218}
3219
3220fn parse_seekable_read_at_volume(
3221    reader: Arc<dyn ArchiveReadAt>,
3222    master_key: &MasterKey,
3223    options: ReaderOptions,
3224) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3225    let observed_len = reader.len()?;
3226    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
3227        return Err(FormatError::InvalidLength {
3228            structure: "archive",
3229            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3230            actual: to_usize(observed_len, "archive")?,
3231        });
3232    }
3233
3234    let volume_header_bytes = read_at_vec(reader.as_ref(), 0, VOLUME_HEADER_LEN, "archive")?;
3235    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
3236    let crypto_start = volume_header.crypto_header_offset as u64;
3237    let crypto_len = volume_header.crypto_header_length as u64;
3238    let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
3239    let crypto_bytes = read_at_vec(
3240        reader.as_ref(),
3241        crypto_start,
3242        to_usize(crypto_len, "CryptoHeader")?,
3243        "CryptoHeader",
3244    )?;
3245    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
3246    let subkeys = subkeys_for_open(
3247        Some(master_key),
3248        parsed_crypto.fixed.aead_algo,
3249        &volume_header.archive_uuid,
3250        &volume_header.session_id,
3251    )?;
3252    verify_integrity_tag(
3253        HmacDomain::CryptoHeader,
3254        parsed_crypto.fixed.aead_algo,
3255        Some(&subkeys.mac_key),
3256        &volume_header.archive_uuid,
3257        &volume_header.session_id,
3258        parsed_crypto.hmac_covered_bytes,
3259        &parsed_crypto.header_hmac,
3260    )?;
3261    parsed_crypto.validate_extension_semantics()?;
3262    validate_seekable_supported_volume(
3263        &volume_header,
3264        &parsed_crypto.fixed,
3265        &parsed_crypto.extensions,
3266    )?;
3267    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3268
3269    let terminal = locate_v41_terminal_read_at(
3270        reader.as_ref(),
3271        observed_len,
3272        KeyHoldingTerminalContext {
3273            subkeys: &subkeys,
3274            volume_header: &volume_header,
3275            crypto_header: &parsed_crypto.fixed,
3276            crypto_header_bytes: &crypto_bytes,
3277        },
3278        options,
3279    )?;
3280    let volume_trailer = terminal.volume_trailer.clone();
3281    validate_trailer_identity(&volume_header, &volume_trailer)?;
3282
3283    let manifest_offset = volume_trailer.manifest_footer_offset;
3284    let manifest_end = checked_u64_add(
3285        manifest_offset,
3286        MANIFEST_FOOTER_LEN as u64,
3287        "ManifestFooter",
3288    )?;
3289    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3290        if volume_trailer.root_auth_footer_offset != manifest_end
3291            || volume_trailer
3292                .root_auth_footer_offset
3293                .checked_add(volume_trailer.root_auth_footer_length as u64)
3294                .ok_or(FormatError::InvalidArchive(
3295                    "RootAuthFooter terminal boundary overflow",
3296                ))?
3297                != terminal.image.volume_trailer_offset
3298        {
3299            return Err(FormatError::InvalidArchive(
3300                "RootAuthFooter does not sit before selected trailer",
3301            ));
3302        }
3303    } else if manifest_end != terminal.image.volume_trailer_offset {
3304        return Err(FormatError::InvalidArchive(
3305            "ManifestFooter does not end at selected trailer",
3306        ));
3307    }
3308    validate_seekable_block_region_layout(
3309        crypto_end,
3310        manifest_offset,
3311        parsed_crypto.fixed.block_size as usize,
3312        &volume_trailer,
3313    )?;
3314
3315    let manifest_bytes = &terminal.manifest_footer_bytes;
3316    let (manifest_footer, manifest_footer_error) = match parse_valid_manifest_footer(
3317        &volume_header,
3318        &parsed_crypto.fixed,
3319        &subkeys,
3320        manifest_bytes,
3321    ) {
3322        Ok(footer) => (Some(footer), None),
3323        Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3324        Err(err) => return Err(err),
3325    };
3326
3327    Ok(ParsedSeekableReadAtVolume {
3328        reader,
3329        volume_header,
3330        crypto_header: parsed_crypto.fixed,
3331        crypto_header_bytes: crypto_bytes,
3332        subkeys,
3333        manifest_footer,
3334        manifest_footer_error,
3335        root_auth_footer: terminal.root_auth_footer,
3336        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3337        volume_trailer,
3338        crypto_end,
3339    })
3340}
3341
3342#[derive(Debug)]
3343struct ParsedPublicNoKeyVolume {
3344    volume_header: VolumeHeader,
3345    crypto_header: CryptoHeaderFixed,
3346    root_auth_footer: RootAuthFooterV1,
3347    root_auth_footer_bytes: Vec<u8>,
3348    blocks: BTreeMap<u64, BlockRecord>,
3349}
3350
3351pub fn public_no_key_verify_volumes_with_options<F>(
3352    volumes: &[&[u8]],
3353    mut verifier: F,
3354    options: ReaderOptions,
3355) -> Result<PublicNoKeyVerification, FormatError>
3356where
3357    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3358{
3359    validate_reader_options(options)?;
3360    if volumes.is_empty() {
3361        return Err(FormatError::InvalidArchive("no volumes supplied"));
3362    }
3363    let mut parsed = Vec::with_capacity(volumes.len());
3364    for volume in volumes {
3365        parsed.push(parse_public_no_key_volume(volume, options)?);
3366    }
3367    let first = parsed
3368        .first()
3369        .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
3370    if parsed.len() != first.crypto_header.stripe_width as usize {
3371        return Err(FormatError::ReaderUnsupported(
3372            "public no-key verification requires a complete volume set",
3373        ));
3374    }
3375
3376    let mut seen_volume_indexes = BTreeSet::new();
3377    let mut blocks = BTreeMap::new();
3378    for volume in &parsed {
3379        if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
3380            || volume.volume_header.session_id != first.volume_header.session_id
3381            || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
3382        {
3383            return Err(FormatError::InvalidArchive(
3384                "public no-key volume global metadata differs",
3385            ));
3386        }
3387        if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
3388            return Err(FormatError::InvalidArchive(
3389                "public no-key RootAuthFooter copies differ",
3390            ));
3391        }
3392        if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
3393            return Err(FormatError::InvalidArchive(
3394                "duplicate public no-key volume index",
3395            ));
3396        }
3397        for (block_index, record) in &volume.blocks {
3398            if blocks.insert(*block_index, record.clone()).is_some() {
3399                return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
3400            }
3401        }
3402    }
3403    validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
3404
3405    let footer = &first.root_auth_footer;
3406    let mut data_leaves = blocks
3407        .values()
3408        .filter(|record| record.kind.is_data())
3409        .map(|record| DataBlockMerkleLeaf {
3410            block_index: record.block_index,
3411            kind: record.kind,
3412            flags: record.flags,
3413            payload: record.payload.clone(),
3414        })
3415        .collect::<Vec<_>>();
3416    data_leaves.sort_by_key(|leaf| leaf.block_index);
3417    let total_data_block_count = u64::try_from(data_leaves.len())
3418        .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
3419    let observed_data_root = data_block_merkle_root(&data_leaves);
3420    if total_data_block_count != footer.total_data_block_count
3421        || observed_data_root != footer.data_block_merkle_root
3422    {
3423        return Err(FormatError::InvalidArchive(
3424            "public no-key data-block commitment mismatch",
3425        ));
3426    }
3427    let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
3428    if archive_root != footer.archive_root {
3429        return Err(FormatError::InvalidArchive(
3430            "public no-key archive_root mismatch",
3431        ));
3432    }
3433    if !verifier(footer, &archive_root)? {
3434        return Err(FormatError::InvalidArchive(
3435            "public no-key authenticator verification failed",
3436        ));
3437    }
3438    Ok(PublicNoKeyVerification {
3439        archive_root,
3440        authenticator_id: footer.authenticator_id,
3441        signer_identity_type: footer.signer_identity_type,
3442        signer_identity_bytes: footer.signer_identity_bytes.clone(),
3443        total_data_block_count,
3444        diagnostics: vec![
3445            PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
3446            PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
3447            PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
3448        ],
3449    })
3450}
3451
3452fn parse_public_no_key_volume(
3453    bytes: &[u8],
3454    options: ReaderOptions,
3455) -> Result<ParsedPublicNoKeyVolume, FormatError> {
3456    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3457        return Err(FormatError::InvalidLength {
3458            structure: "archive",
3459            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3460            actual: bytes.len(),
3461        });
3462    }
3463    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3464    let crypto_start = volume_header.crypto_header_offset as usize;
3465    let crypto_len = volume_header.crypto_header_length as usize;
3466    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3467    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3468    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3469    parsed_crypto.validate_extension_semantics()?;
3470    validate_seekable_supported_volume(
3471        &volume_header,
3472        &parsed_crypto.fixed,
3473        &parsed_crypto.extensions,
3474    )?;
3475    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3476
3477    let terminal =
3478        locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
3479    let block_region = parse_public_block_observation(
3480        bytes,
3481        crypto_end,
3482        &terminal.image,
3483        parsed_crypto.fixed.block_size as usize,
3484        &volume_header,
3485    )?;
3486    Ok(ParsedPublicNoKeyVolume {
3487        volume_header,
3488        crypto_header: parsed_crypto.fixed,
3489        root_auth_footer: terminal.root_auth_footer,
3490        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3491        blocks: block_region,
3492    })
3493}
3494
3495fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3496    left.length == right.length
3497        && left.stripe_width == right.stripe_width
3498        && left.block_size == right.block_size
3499        && left.compression_algo == right.compression_algo
3500        && left.aead_algo == right.aead_algo
3501        && left.fec_algo == right.fec_algo
3502        && left.kdf_algo == right.kdf_algo
3503}
3504
3505fn recompute_public_archive_root(
3506    footer: &RootAuthFooterV1,
3507    crypto_header: &CryptoHeaderFixed,
3508) -> Result<[u8; 32], FormatError> {
3509    let descriptor_digest = root_auth_descriptor_digest(
3510        footer.authenticator_id,
3511        footer.signer_identity_type,
3512        &footer.signer_identity_bytes,
3513        u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3514            FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3515        })?,
3516        footer.footer_length()?,
3517    )?;
3518    let signer_digest =
3519        signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3520    if signer_digest != footer.signer_identity_digest {
3521        return Err(FormatError::InvalidArchive(
3522            "public no-key signer identity digest mismatch",
3523        ));
3524    }
3525    Ok(archive_root(ArchiveRootInputs {
3526        archive_uuid: footer.archive_uuid,
3527        session_id: footer.session_id,
3528        format_version: FORMAT_VERSION,
3529        volume_format_rev: VOLUME_FORMAT_REV,
3530        compression_algo: crypto_header.compression_algo,
3531        aead_algo: crypto_header.aead_algo,
3532        fec_algo: crypto_header.fec_algo,
3533        kdf_algo: crypto_header.kdf_algo,
3534        critical_metadata_digest: footer.critical_metadata_digest,
3535        index_digest: footer.index_digest,
3536        fec_layout_digest: footer.fec_layout_digest,
3537        total_data_block_count: footer.total_data_block_count,
3538        data_block_merkle_root: footer.data_block_merkle_root,
3539        root_auth_descriptor_digest: descriptor_digest,
3540        signer_identity_digest: signer_digest,
3541    }))
3542}
3543
3544fn parse_valid_manifest_footer(
3545    volume_header: &VolumeHeader,
3546    crypto_header: &CryptoHeaderFixed,
3547    subkeys: &Subkeys,
3548    manifest_bytes: &[u8],
3549) -> Result<ManifestFooter, FormatError> {
3550    let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3551    validate_manifest_footer(
3552        volume_header,
3553        crypto_header,
3554        &manifest_footer,
3555        subkeys,
3556        manifest_bytes,
3557    )?;
3558    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
3559    Ok(manifest_footer)
3560}
3561
3562fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3563    matches!(
3564        error,
3565        FormatError::BadMagic {
3566            structure: "ManifestFooter",
3567        } | FormatError::NonZeroReserved {
3568            structure: "ManifestFooter",
3569        } | FormatError::InvalidAuthoritativeFlag(_)
3570            | FormatError::HmacMismatch {
3571                structure: "ManifestFooter",
3572            }
3573            | FormatError::IntegrityDigestMismatch {
3574                structure: "ManifestFooter",
3575            }
3576    )
3577}
3578
3579fn validate_seekable_supported_volume(
3580    volume_header: &VolumeHeader,
3581    crypto_header: &CryptoHeaderFixed,
3582    extensions: &[ExtensionTlv<'_>],
3583) -> Result<(), FormatError> {
3584    reject_unsupported_raw_stream_profile(extensions)?;
3585    if crypto_header.stripe_width != volume_header.stripe_width {
3586        return Err(FormatError::InvalidArchive(
3587            "VolumeHeader and CryptoHeader stripe_width differ",
3588        ));
3589    }
3590    Ok(())
3591}
3592
3593pub(crate) fn validate_crypto_class_parity_exactness(
3594    crypto_header: &CryptoHeaderFixed,
3595) -> Result<(), FormatError> {
3596    let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3597    if crypto_header.fec_parity_shards as u32 != fec {
3598        return Err(FormatError::InvalidArchive(
3599            "fec_parity_shards does not match v41 compute_parity",
3600        ));
3601    }
3602    let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3603    if crypto_header.index_fec_parity_shards as u32 != index {
3604        return Err(FormatError::InvalidArchive(
3605            "index_fec_parity_shards does not match v41 compute_parity",
3606        ));
3607    }
3608    let index_root = required_object_parity(
3609        crypto_header.index_root_fec_data_shards as u64,
3610        crypto_header,
3611    )?;
3612    if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3613        return Err(FormatError::InvalidArchive(
3614            "index_root_fec_parity_shards does not match v41 compute_parity",
3615        ));
3616    }
3617    Ok(())
3618}
3619
3620fn validate_volume_set_member(
3621    first: &ParsedSeekableVolume,
3622    candidate: &ParsedSeekableVolume,
3623) -> Result<(), FormatError> {
3624    validate_volume_set_member_metadata(
3625        &first.volume_header,
3626        &first.crypto_header,
3627        &first.crypto_header_bytes,
3628        &candidate.volume_header,
3629        &candidate.crypto_header,
3630        &candidate.crypto_header_bytes,
3631    )
3632}
3633
3634fn validate_volume_set_member_metadata(
3635    first_volume_header: &VolumeHeader,
3636    first_crypto_header: &CryptoHeaderFixed,
3637    first_crypto_header_bytes: &[u8],
3638    candidate_volume_header: &VolumeHeader,
3639    candidate_crypto_header: &CryptoHeaderFixed,
3640    candidate_crypto_header_bytes: &[u8],
3641) -> Result<(), FormatError> {
3642    if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3643        || candidate_volume_header.session_id != first_volume_header.session_id
3644    {
3645        return Err(FormatError::InvalidArchive(
3646            "mixed archive or session IDs in volume set",
3647        ));
3648    }
3649    if candidate_crypto_header_bytes != first_crypto_header_bytes
3650        || candidate_crypto_header != first_crypto_header
3651    {
3652        return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3653    }
3654    Ok(())
3655}
3656
3657pub(crate) fn manifest_bootstrap_fields_match(
3658    left: &ManifestFooter,
3659    right: &ManifestFooter,
3660) -> bool {
3661    left.archive_uuid == right.archive_uuid
3662        && left.session_id == right.session_id
3663        && left.is_authoritative == right.is_authoritative
3664        && left.total_volumes == right.total_volumes
3665        && left.index_root_first_block == right.index_root_first_block
3666        && left.index_root_data_block_count == right.index_root_data_block_count
3667        && left.index_root_parity_block_count == right.index_root_parity_block_count
3668        && left.index_root_encrypted_size == right.index_root_encrypted_size
3669        && left.index_root_decompressed_size == right.index_root_decompressed_size
3670}
3671
3672fn validate_complete_global_block_coverage(
3673    blocks: &BTreeMap<u64, BlockRecord>,
3674    erased_block_indices: &BTreeSet<u64>,
3675) -> Result<(), FormatError> {
3676    let mut expected = 0u64;
3677    let mut block_iter = blocks.keys().copied().peekable();
3678    let mut erasure_iter = erased_block_indices.iter().copied().peekable();
3679
3680    loop {
3681        let next_block = block_iter.peek().copied();
3682        let next_erasure = erasure_iter.peek().copied();
3683        let next = match (next_block, next_erasure) {
3684            (Some(block), Some(erasure)) if block == erasure => {
3685                return Err(FormatError::InvalidArchive(
3686                    "BlockRecord index is both present and erased",
3687                ));
3688            }
3689            (Some(block), Some(erasure)) => block.min(erasure),
3690            (Some(block), None) => block,
3691            (None, Some(erasure)) => erasure,
3692            (None, None) => return Ok(()),
3693        };
3694
3695        if next != expected {
3696            return Err(FormatError::InvalidArchive(
3697                "complete volume set has missing global blocks",
3698            ));
3699        }
3700        if next_block == Some(next) {
3701            block_iter.next();
3702        }
3703        if next_erasure == Some(next) {
3704            erasure_iter.next();
3705        }
3706        expected = expected
3707            .checked_add(1)
3708            .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
3709    }
3710}
3711
3712#[derive(Debug)]
3713struct V41Terminal {
3714    image: CriticalMetadataImage,
3715    manifest_footer_bytes: Vec<u8>,
3716    root_auth_footer_bytes: Option<Vec<u8>>,
3717    root_auth_footer: Option<RootAuthFooterV1>,
3718    volume_trailer: VolumeTrailer,
3719}
3720
3721pub(crate) struct SequentialTerminalMaterial {
3722    pub(crate) manifest_footer: ManifestFooter,
3723    pub(crate) volume_trailer: VolumeTrailer,
3724    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
3725}
3726
3727#[derive(Debug)]
3728struct V41PublicTerminal {
3729    image: CriticalMetadataImage,
3730    root_auth_footer_bytes: Vec<u8>,
3731    root_auth_footer: RootAuthFooterV1,
3732}
3733
3734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3735struct CmraDecoderTuple {
3736    shard_size: u32,
3737    data_shard_count: u16,
3738    parity_shard_count: u16,
3739    image_length: u32,
3740    image_sha256: [u8; 32],
3741}
3742
3743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3744struct CmraIdentityHints {
3745    archive_uuid: [u8; 16],
3746    session_id: [u8; 16],
3747    volume_index: u32,
3748}
3749
3750impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
3751    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3752        Self {
3753            shard_size: header.shard_size,
3754            data_shard_count: header.data_shard_count,
3755            parity_shard_count: header.parity_shard_count,
3756            image_length: header.image_length,
3757            image_sha256: header.image_sha256,
3758        }
3759    }
3760}
3761
3762impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
3763    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
3764        Self {
3765            archive_uuid: header.archive_uuid_hint,
3766            session_id: header.session_id_hint,
3767            volume_index: header.volume_index_hint,
3768        }
3769    }
3770}
3771
3772impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
3773    fn from(locator: CriticalRecoveryLocator) -> Self {
3774        Self {
3775            shard_size: locator.cmra_shard_size,
3776            data_shard_count: locator.cmra_data_shard_count,
3777            parity_shard_count: locator.cmra_parity_shard_count,
3778            image_length: locator.cmra_image_length,
3779            image_sha256: locator.cmra_image_sha256,
3780        }
3781    }
3782}
3783
3784impl From<CriticalRecoveryLocator> for CmraIdentityHints {
3785    fn from(locator: CriticalRecoveryLocator) -> Self {
3786        Self {
3787            archive_uuid: locator.archive_uuid_hint,
3788            session_id: locator.session_id_hint,
3789            volume_index: locator.volume_index_hint,
3790        }
3791    }
3792}
3793
3794#[derive(Debug)]
3795struct RecoveredCmra {
3796    image: CriticalMetadataImage,
3797    tuple: CmraDecoderTuple,
3798    header_hints: Option<CmraIdentityHints>,
3799    cmra_length: u64,
3800}
3801
3802#[derive(Debug)]
3803struct TerminalCandidate {
3804    terminal: V41Terminal,
3805    anchor: usize,
3806    locator_sequence: Option<u32>,
3807    cmra_offset: u64,
3808    cmra_length: u64,
3809}
3810
3811#[derive(Debug)]
3812struct PublicTerminalCandidate {
3813    terminal: V41PublicTerminal,
3814    anchor: usize,
3815    cmra_offset: u64,
3816    cmra_length: u64,
3817}
3818
3819#[derive(Debug, Clone, Copy)]
3820enum CmraRecoveryMode {
3821    KeyHolding,
3822    PublicNoKey,
3823}
3824
3825#[derive(Clone, Copy)]
3826pub(crate) struct KeyHoldingTerminalContext<'a> {
3827    pub(crate) subkeys: &'a Subkeys,
3828    pub(crate) volume_header: &'a VolumeHeader,
3829    pub(crate) crypto_header: &'a CryptoHeaderFixed,
3830    pub(crate) crypto_header_bytes: &'a [u8],
3831}
3832
3833fn locate_v41_terminal(
3834    bytes: &[u8],
3835    context: KeyHoldingTerminalContext<'_>,
3836    options: ReaderOptions,
3837) -> Result<V41Terminal, FormatError> {
3838    locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
3839}
3840
3841fn locate_v41_terminal_read_at(
3842    reader: &dyn ArchiveReadAt,
3843    len: u64,
3844    context: KeyHoldingTerminalContext<'_>,
3845    options: ReaderOptions,
3846) -> Result<V41Terminal, FormatError> {
3847    let mut candidates = Vec::new();
3848    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
3849        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
3850        collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
3851    }
3852    if len >= LOCATOR_PAIR_LEN as u64 {
3853        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
3854        collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
3855    }
3856
3857    if candidates.is_empty() {
3858        let scan = max_critical_recovery_scan(options)? as u64;
3859        let scan_start = len.saturating_sub(scan);
3860        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
3861        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
3862        let mut offset = tail.len().saturating_sub(4);
3863        while offset < tail.len() {
3864            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
3865            if tail.get(offset..offset + 4) == Some(b"TZCL") {
3866                collect_v41_locator_candidate_read_at(
3867                    reader,
3868                    absolute_offset,
3869                    2,
3870                    context,
3871                    &mut candidates,
3872                );
3873            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
3874                if let Ok(candidate) =
3875                    parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
3876                {
3877                    candidates.push(candidate);
3878                }
3879            }
3880            if offset == 0 {
3881                break;
3882            }
3883            offset -= 1;
3884        }
3885    }
3886
3887    choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
3888}
3889
3890fn locate_v41_terminal_candidate(
3891    bytes: &[u8],
3892    context: KeyHoldingTerminalContext<'_>,
3893    options: ReaderOptions,
3894) -> Result<TerminalCandidate, FormatError> {
3895    let mut candidates = Vec::new();
3896    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
3897        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
3898        collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
3899    }
3900    if bytes.len() >= LOCATOR_PAIR_LEN {
3901        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
3902        collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
3903    }
3904
3905    if candidates.is_empty() {
3906        let scan = max_critical_recovery_scan(options)?;
3907        let scan_start = bytes.len().saturating_sub(scan);
3908        let mut offset = bytes.len().saturating_sub(4);
3909        while offset >= scan_start {
3910            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
3911                collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
3912            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
3913                if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
3914                    candidates.push(candidate);
3915                }
3916            }
3917            if offset == 0 {
3918                break;
3919            }
3920            offset -= 1;
3921        }
3922    }
3923
3924    choose_v41_terminal_candidate(candidates)
3925}
3926
3927fn locate_v41_public_terminal(
3928    bytes: &[u8],
3929    volume_header: &VolumeHeader,
3930    crypto_header: &CryptoHeaderFixed,
3931    options: ReaderOptions,
3932) -> Result<V41PublicTerminal, FormatError> {
3933    let mut candidates = Vec::new();
3934    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
3935        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
3936        collect_v41_public_locator_candidate(
3937            bytes,
3938            final_offset,
3939            0,
3940            volume_header,
3941            crypto_header,
3942            &mut candidates,
3943        );
3944    }
3945    if bytes.len() >= LOCATOR_PAIR_LEN {
3946        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
3947        collect_v41_public_locator_candidate(
3948            bytes,
3949            mirror_offset,
3950            1,
3951            volume_header,
3952            crypto_header,
3953            &mut candidates,
3954        );
3955    }
3956
3957    if candidates.is_empty() {
3958        let scan = max_critical_recovery_scan(options)?;
3959        let scan_start = bytes.len().saturating_sub(scan);
3960        let mut offset = bytes.len().saturating_sub(4);
3961        while offset >= scan_start {
3962            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
3963                collect_v41_public_locator_candidate(
3964                    bytes,
3965                    offset,
3966                    2,
3967                    volume_header,
3968                    crypto_header,
3969                    &mut candidates,
3970                );
3971            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
3972                if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
3973                    bytes,
3974                    offset,
3975                    volume_header,
3976                    crypto_header,
3977                ) {
3978                    candidates.push(candidate);
3979                }
3980            }
3981            if offset == 0 {
3982                break;
3983            }
3984            offset -= 1;
3985        }
3986    }
3987
3988    choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
3989}
3990
3991fn collect_v41_locator_candidate(
3992    bytes: &[u8],
3993    offset: usize,
3994    expected_sequence: u32,
3995    context: KeyHoldingTerminalContext<'_>,
3996    candidates: &mut Vec<TerminalCandidate>,
3997) {
3998    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
3999        return;
4000    };
4001    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4002        return;
4003    };
4004    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4005        return;
4006    }
4007    if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
4008        candidates.push(candidate);
4009    }
4010}
4011
4012fn collect_v41_locator_candidate_read_at(
4013    reader: &dyn ArchiveReadAt,
4014    offset: u64,
4015    expected_sequence: u32,
4016    context: KeyHoldingTerminalContext<'_>,
4017    candidates: &mut Vec<TerminalCandidate>,
4018) {
4019    let Ok(raw) = read_at_vec(
4020        reader,
4021        offset,
4022        CRITICAL_RECOVERY_LOCATOR_LEN,
4023        "CriticalRecoveryLocator",
4024    ) else {
4025        return;
4026    };
4027    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4028        return;
4029    };
4030    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4031        return;
4032    }
4033    if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
4034        candidates.push(candidate);
4035    }
4036}
4037
4038fn collect_v41_public_locator_candidate(
4039    bytes: &[u8],
4040    offset: usize,
4041    expected_sequence: u32,
4042    volume_header: &VolumeHeader,
4043    crypto_header: &CryptoHeaderFixed,
4044    candidates: &mut Vec<PublicTerminalCandidate>,
4045) {
4046    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4047        return;
4048    };
4049    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4050        return;
4051    };
4052    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4053        return;
4054    }
4055    if let Ok(candidate) =
4056        parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
4057    {
4058        candidates.push(candidate);
4059    }
4060}
4061
4062fn choose_v41_terminal_candidate(
4063    mut candidates: Vec<TerminalCandidate>,
4064) -> Result<TerminalCandidate, FormatError> {
4065    candidates.sort_by_key(|candidate| candidate.anchor);
4066    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4067        "no valid v41 CMRA candidate found",
4068    ))?;
4069    if let Some(previous) = candidates.last() {
4070        if previous.anchor == winner.anchor
4071            && (previous.cmra_offset != winner.cmra_offset
4072                || previous.cmra_length != winner.cmra_length)
4073        {
4074            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4075        }
4076    }
4077    Ok(winner)
4078}
4079
4080fn choose_v41_public_terminal_candidate(
4081    mut candidates: Vec<PublicTerminalCandidate>,
4082) -> Result<PublicTerminalCandidate, FormatError> {
4083    candidates.sort_by_key(|candidate| candidate.anchor);
4084    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4085        "no valid v41 public CMRA candidate found",
4086    ))?;
4087    if let Some(previous) = candidates.last() {
4088        if previous.anchor == winner.anchor
4089            && (previous.cmra_offset != winner.cmra_offset
4090                || previous.cmra_length != winner.cmra_length)
4091        {
4092            return Err(FormatError::InvalidArchive(
4093                "ambiguous v41 public CMRA candidates",
4094            ));
4095        }
4096    }
4097    Ok(winner)
4098}
4099
4100fn parse_locator_cmra_candidate(
4101    bytes: &[u8],
4102    locator_offset: usize,
4103    locator: CriticalRecoveryLocator,
4104    context: KeyHoldingTerminalContext<'_>,
4105) -> Result<TerminalCandidate, FormatError> {
4106    let tuple = CmraDecoderTuple::from(locator);
4107    validate_cmra_decoder_tuple(tuple)?;
4108    let expected_cmra_length = cmra_serialized_length(tuple)?;
4109    if locator.cmra_length as u64 != expected_cmra_length {
4110        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4111    }
4112    validate_locator_position(locator_offset, locator)?;
4113    let recovered = recover_cmra(
4114        bytes,
4115        locator.cmra_offset,
4116        Some(tuple),
4117        CmraRecoveryMode::KeyHolding,
4118    )?;
4119    if recovered.tuple != tuple {
4120        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4121    }
4122    if expected_cmra_length != recovered.cmra_length {
4123        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4124    }
4125    validate_locator_image_boundary(locator, &recovered.image)?;
4126    validate_cmra_identity_hints(
4127        recovered.header_hints,
4128        Some(CmraIdentityHints::from(locator)),
4129        &recovered.image,
4130    )?;
4131    let terminal = validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context)?;
4132    Ok(TerminalCandidate {
4133        terminal,
4134        anchor: locator_offset
4135            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4136            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4137        locator_sequence: Some(locator.locator_sequence),
4138        cmra_offset: locator.cmra_offset,
4139        cmra_length: recovered.cmra_length,
4140    })
4141}
4142
4143fn parse_locator_cmra_candidate_read_at(
4144    reader: &dyn ArchiveReadAt,
4145    locator_offset: u64,
4146    locator: CriticalRecoveryLocator,
4147    context: KeyHoldingTerminalContext<'_>,
4148) -> Result<TerminalCandidate, FormatError> {
4149    let tuple = CmraDecoderTuple::from(locator);
4150    validate_cmra_decoder_tuple(tuple)?;
4151    let expected_cmra_length = cmra_serialized_length(tuple)?;
4152    if locator.cmra_length as u64 != expected_cmra_length {
4153        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4154    }
4155    validate_locator_position(
4156        to_usize(locator_offset, "CriticalRecoveryLocator")?,
4157        locator,
4158    )?;
4159    let recovered = recover_cmra_read_at(
4160        reader,
4161        locator.cmra_offset,
4162        Some(tuple),
4163        CmraRecoveryMode::KeyHolding,
4164    )?;
4165    if recovered.tuple != tuple {
4166        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4167    }
4168    if expected_cmra_length != recovered.cmra_length {
4169        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4170    }
4171    validate_locator_image_boundary(locator, &recovered.image)?;
4172    validate_cmra_identity_hints(
4173        recovered.header_hints,
4174        Some(CmraIdentityHints::from(locator)),
4175        &recovered.image,
4176    )?;
4177    let terminal =
4178        validate_recovered_terminal_read_at(recovered.image, recovered.tuple, reader, context)?;
4179    Ok(TerminalCandidate {
4180        terminal,
4181        anchor: to_usize(
4182            checked_u64_add(
4183                locator_offset,
4184                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4185                "locator anchor overflow",
4186            )?,
4187            "locator anchor overflow",
4188        )?,
4189        locator_sequence: Some(locator.locator_sequence),
4190        cmra_offset: locator.cmra_offset,
4191        cmra_length: recovered.cmra_length,
4192    })
4193}
4194
4195fn parse_public_locator_cmra_candidate(
4196    bytes: &[u8],
4197    locator_offset: usize,
4198    locator: CriticalRecoveryLocator,
4199    volume_header: &VolumeHeader,
4200    crypto_header: &CryptoHeaderFixed,
4201) -> Result<PublicTerminalCandidate, FormatError> {
4202    let tuple = CmraDecoderTuple::from(locator);
4203    validate_cmra_decoder_tuple(tuple)?;
4204    let expected_cmra_length = cmra_serialized_length(tuple)?;
4205    if locator.cmra_length as u64 != expected_cmra_length {
4206        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4207    }
4208    validate_locator_position(locator_offset, locator)?;
4209    let recovered = recover_cmra(
4210        bytes,
4211        locator.cmra_offset,
4212        Some(tuple),
4213        CmraRecoveryMode::PublicNoKey,
4214    )?;
4215    if recovered.tuple != tuple {
4216        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4217    }
4218    if expected_cmra_length != recovered.cmra_length {
4219        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4220    }
4221    validate_locator_image_boundary(locator, &recovered.image)?;
4222    validate_cmra_identity_hints(
4223        recovered.header_hints,
4224        Some(CmraIdentityHints::from(locator)),
4225        &recovered.image,
4226    )?;
4227    let terminal =
4228        validate_recovered_public_terminal(recovered.image, bytes, volume_header, crypto_header)?;
4229    Ok(PublicTerminalCandidate {
4230        terminal,
4231        anchor: locator_offset
4232            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4233            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4234        cmra_offset: locator.cmra_offset,
4235        cmra_length: recovered.cmra_length,
4236    })
4237}
4238
4239fn parse_locatorless_cmra_candidate(
4240    bytes: &[u8],
4241    cmra_offset: usize,
4242    context: KeyHoldingTerminalContext<'_>,
4243) -> Result<TerminalCandidate, FormatError> {
4244    let recovered = recover_cmra(
4245        bytes,
4246        cmra_offset as u64,
4247        None,
4248        CmraRecoveryMode::KeyHolding,
4249    )?;
4250    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4251        return Err(FormatError::InvalidArchive(
4252            "locatorless CMRA boundary mismatch",
4253        ));
4254    }
4255    if recovered
4256        .image
4257        .volume_trailer_offset
4258        .checked_add(VOLUME_TRAILER_LEN as u64)
4259        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4260        != cmra_offset as u64
4261    {
4262        return Err(FormatError::InvalidArchive(
4263            "locatorless trailer boundary mismatch",
4264        ));
4265    }
4266    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4267    let terminal = validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context)?;
4268    Ok(TerminalCandidate {
4269        terminal,
4270        anchor: cmra_offset
4271            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4272            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4273        locator_sequence: None,
4274        cmra_offset: cmra_offset as u64,
4275        cmra_length: recovered.cmra_length,
4276    })
4277}
4278
4279fn parse_locatorless_cmra_candidate_read_at(
4280    reader: &dyn ArchiveReadAt,
4281    cmra_offset: u64,
4282    context: KeyHoldingTerminalContext<'_>,
4283) -> Result<TerminalCandidate, FormatError> {
4284    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4285    if recovered.image.body_bytes_before_cmra != cmra_offset {
4286        return Err(FormatError::InvalidArchive(
4287            "locatorless CMRA boundary mismatch",
4288        ));
4289    }
4290    if recovered
4291        .image
4292        .volume_trailer_offset
4293        .checked_add(VOLUME_TRAILER_LEN as u64)
4294        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4295        != cmra_offset
4296    {
4297        return Err(FormatError::InvalidArchive(
4298            "locatorless trailer boundary mismatch",
4299        ));
4300    }
4301    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4302    let terminal =
4303        validate_recovered_terminal_read_at(recovered.image, recovered.tuple, reader, context)?;
4304    Ok(TerminalCandidate {
4305        terminal,
4306        anchor: to_usize(
4307            checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
4308            "CMRA anchor overflow",
4309        )?,
4310        locator_sequence: None,
4311        cmra_offset,
4312        cmra_length: recovered.cmra_length,
4313    })
4314}
4315
4316fn parse_public_locatorless_cmra_candidate(
4317    bytes: &[u8],
4318    cmra_offset: usize,
4319    volume_header: &VolumeHeader,
4320    crypto_header: &CryptoHeaderFixed,
4321) -> Result<PublicTerminalCandidate, FormatError> {
4322    let recovered = recover_cmra(
4323        bytes,
4324        cmra_offset as u64,
4325        None,
4326        CmraRecoveryMode::PublicNoKey,
4327    )?;
4328    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4329        return Err(FormatError::InvalidArchive(
4330            "locatorless CMRA boundary mismatch",
4331        ));
4332    }
4333    if recovered
4334        .image
4335        .volume_trailer_offset
4336        .checked_add(VOLUME_TRAILER_LEN as u64)
4337        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4338        != cmra_offset as u64
4339    {
4340        return Err(FormatError::InvalidArchive(
4341            "locatorless trailer boundary mismatch",
4342        ));
4343    }
4344    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4345    let terminal =
4346        validate_recovered_public_terminal(recovered.image, bytes, volume_header, crypto_header)?;
4347    Ok(PublicTerminalCandidate {
4348        terminal,
4349        anchor: cmra_offset
4350            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4351            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4352        cmra_offset: cmra_offset as u64,
4353        cmra_length: recovered.cmra_length,
4354    })
4355}
4356
4357fn validate_locator_position(
4358    locator_offset: usize,
4359    locator: CriticalRecoveryLocator,
4360) -> Result<(), FormatError> {
4361    if locator.cmra_offset != locator.body_bytes_before_cmra {
4362        return Err(FormatError::InvalidArchive(
4363            "locator CMRA boundary mismatch",
4364        ));
4365    }
4366    if locator
4367        .volume_trailer_offset
4368        .checked_add(VOLUME_TRAILER_LEN as u64)
4369        .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
4370        != locator.cmra_offset
4371    {
4372        return Err(FormatError::InvalidArchive(
4373            "locator trailer boundary mismatch",
4374        ));
4375    }
4376    let expected_offset = match locator.locator_sequence {
4377        1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
4378        0 => locator
4379            .cmra_offset
4380            .checked_add(locator.cmra_length as u64)
4381            .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
4382        _ => None,
4383    }
4384    .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
4385    if expected_offset != locator_offset as u64 {
4386        return Err(FormatError::InvalidArchive(
4387            "locator position does not match sequence",
4388        ));
4389    }
4390    Ok(())
4391}
4392
4393fn validate_locator_image_boundary(
4394    locator: CriticalRecoveryLocator,
4395    image: &CriticalMetadataImage,
4396) -> Result<(), FormatError> {
4397    if locator.volume_trailer_offset != image.volume_trailer_offset
4398        || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
4399        || image
4400            .volume_trailer_offset
4401            .checked_add(VOLUME_TRAILER_LEN as u64)
4402            .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
4403            != locator.cmra_offset
4404    {
4405        return Err(FormatError::InvalidArchive(
4406            "locator and CMRA image boundaries differ",
4407        ));
4408    }
4409    Ok(())
4410}
4411
4412fn validate_cmra_identity_hints(
4413    header_hints: Option<CmraIdentityHints>,
4414    locator_hints: Option<CmraIdentityHints>,
4415    image: &CriticalMetadataImage,
4416) -> Result<(), FormatError> {
4417    if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
4418        if header != locator {
4419            return Err(FormatError::InvalidArchive(
4420                "CMRA header and locator identity hints differ",
4421            ));
4422        }
4423    }
4424    for hints in [header_hints, locator_hints].into_iter().flatten() {
4425        if hints.archive_uuid != image.archive_uuid
4426            || hints.session_id != image.session_id
4427            || hints.volume_index != image.volume_index
4428        {
4429            return Err(FormatError::InvalidArchive(
4430                "CMRA identity hints do not match recovered image",
4431            ));
4432        }
4433    }
4434    Ok(())
4435}
4436
4437fn recover_cmra(
4438    bytes: &[u8],
4439    cmra_offset: u64,
4440    locator_tuple: Option<CmraDecoderTuple>,
4441    mode: CmraRecoveryMode,
4442) -> Result<RecoveredCmra, FormatError> {
4443    let offset = to_usize(cmra_offset, "CMRA")?;
4444    let header_bytes = slice(
4445        bytes,
4446        offset,
4447        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
4448        "CriticalMetadataRecoveryHeader",
4449    )?;
4450    let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
4451    validate_cmra_decoder_tuple(tuple)?;
4452    let cmra_length = cmra_serialized_length(tuple)?;
4453    let cmra_len = to_usize(cmra_length, "CMRA")?;
4454    let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
4455    recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
4456}
4457
4458fn recover_cmra_read_at(
4459    reader: &dyn ArchiveReadAt,
4460    cmra_offset: u64,
4461    locator_tuple: Option<CmraDecoderTuple>,
4462    mode: CmraRecoveryMode,
4463) -> Result<RecoveredCmra, FormatError> {
4464    let header_bytes = read_at_vec(
4465        reader,
4466        cmra_offset,
4467        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
4468        "CriticalMetadataRecoveryHeader",
4469    )?;
4470    let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
4471    validate_cmra_decoder_tuple(tuple)?;
4472    let cmra_length = cmra_serialized_length(tuple)?;
4473    let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
4474    recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
4475}
4476
4477fn recover_cmra_header_tuple(
4478    header_bytes: &[u8],
4479    locator_tuple: Option<CmraDecoderTuple>,
4480) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
4481    let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
4482    Ok(match (parsed_header, locator_tuple) {
4483        (Ok(header), Some(locator_tuple)) => {
4484            let header_tuple = CmraDecoderTuple::from(header);
4485            if header_tuple != locator_tuple {
4486                return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4487            }
4488            (locator_tuple, Some(CmraIdentityHints::from(header)))
4489        }
4490        (Ok(header), None) => (
4491            CmraDecoderTuple::from(header),
4492            Some(CmraIdentityHints::from(header)),
4493        ),
4494        (
4495            Err(FormatError::BadCrc {
4496                structure: "CriticalMetadataRecoveryHeader",
4497            }),
4498            Some(tuple),
4499        ) => (tuple, None),
4500        (Err(err), _) => return Err(err),
4501    })
4502}
4503
4504fn recover_cmra_from_bytes(
4505    cmra_bytes: &[u8],
4506    tuple: CmraDecoderTuple,
4507    header_hints: Option<CmraIdentityHints>,
4508    cmra_length: u64,
4509    mode: CmraRecoveryMode,
4510) -> Result<RecoveredCmra, FormatError> {
4511    let shard_size = tuple.shard_size as usize;
4512    let mut data_shards = vec![None; tuple.data_shard_count as usize];
4513    let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
4514    let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
4515    for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
4516        let raw = slice(
4517            cmra_bytes,
4518            cursor,
4519            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
4520            "CriticalMetadataRecoveryShard",
4521        )?;
4522        let shard = match CriticalMetadataRecoveryShard::parse(raw, shard_size) {
4523            Ok(shard) => Some(shard),
4524            Err(FormatError::BadCrc {
4525                structure: "CriticalMetadataRecoveryShard",
4526            }) => None,
4527            Err(err) => return Err(err),
4528        };
4529        if let Some(shard) = shard {
4530            validate_cmra_shard(&shard, idx, tuple)?;
4531            if shard.shard_role == 0 {
4532                let data_slot = data_shards
4533                    .get_mut(idx)
4534                    .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
4535                *data_slot = Some(shard.payload);
4536            } else {
4537                let parity_idx = idx - tuple.data_shard_count as usize;
4538                let parity_slot =
4539                    parity_shards
4540                        .get_mut(parity_idx)
4541                        .ok_or(FormatError::InvalidArchive(
4542                            "CMRA parity shard out of range",
4543                        ))?;
4544                *parity_slot = Some(shard.payload);
4545            }
4546        }
4547        cursor = checked_add(
4548            cursor,
4549            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
4550            "CriticalMetadataRecoveryShard",
4551        )?;
4552    }
4553    let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
4554    let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
4555    for shard in repaired {
4556        image_bytes.extend_from_slice(&shard);
4557    }
4558    image_bytes.truncate(tuple.image_length as usize);
4559    if sha256_bytes(&image_bytes) != tuple.image_sha256 {
4560        return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
4561    }
4562    let image = CriticalMetadataImage::parse(&image_bytes)?;
4563    validate_critical_metadata_image(&image, mode)?;
4564    Ok(RecoveredCmra {
4565        image,
4566        tuple,
4567        header_hints,
4568        cmra_length,
4569    })
4570}
4571
4572fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
4573    let shard_size = tuple.shard_size as u64;
4574    if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
4575        return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
4576    }
4577    let image_length = tuple.image_length as u64;
4578    let min = critical_image_min();
4579    let cap = critical_image_cap()?;
4580    if image_length < min || image_length > cap {
4581        return Err(FormatError::InvalidArchive(
4582            "CMRA image_length is outside bounds",
4583        ));
4584    }
4585    let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
4586    if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
4587        return Err(FormatError::InvalidArchive(
4588            "CMRA data_shard_count does not match image length",
4589        ));
4590    }
4591    let max_parity = 2u64.max(ceil_div_u64(
4592        checked_u64_mul(
4593            expected_data_shards,
4594            READER_MAX_CMRA_PARITY_PCT as u64,
4595            "CMRA parity overflow",
4596        )?,
4597        100,
4598    )?);
4599    if tuple.parity_shard_count as u64 > max_parity {
4600        return Err(FormatError::ReaderResourceLimitExceeded {
4601            field: "CMRA parity shard count",
4602            cap: max_parity,
4603            actual: tuple.parity_shard_count as u64,
4604        });
4605    }
4606    let total = expected_data_shards
4607        .checked_add(tuple.parity_shard_count as u64)
4608        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
4609    if total > 65_535 {
4610        return Err(FormatError::FecTooManyShards(total as usize));
4611    }
4612    Ok(())
4613}
4614
4615fn validate_cmra_writer_parity_lower_bound(
4616    tuple: CmraDecoderTuple,
4617    bit_rot_buffer_pct: u8,
4618) -> Result<(), FormatError> {
4619    let min_parity = 2u64.max(ceil_div_u64(
4620        checked_u64_mul(
4621            tuple.data_shard_count as u64,
4622            bit_rot_buffer_pct as u64,
4623            "CMRA parity lower-bound overflow",
4624        )?,
4625        100,
4626    )?);
4627    if (tuple.parity_shard_count as u64) < min_parity {
4628        return Err(FormatError::InvalidArchive(
4629            "CMRA parity shard count is below authenticated bit-rot lower bound",
4630        ));
4631    }
4632    Ok(())
4633}
4634
4635fn validate_cmra_shard(
4636    shard: &CriticalMetadataRecoveryShard,
4637    serialized_idx: usize,
4638    tuple: CmraDecoderTuple,
4639) -> Result<(), FormatError> {
4640    if shard.shard_index as usize != serialized_idx {
4641        return Err(FormatError::InvalidArchive(
4642            "CMRA shards are not in canonical order",
4643        ));
4644    }
4645    let data_count = tuple.data_shard_count as usize;
4646    let shard_size = tuple.shard_size as usize;
4647    if serialized_idx < data_count {
4648        if shard.shard_role != 0 {
4649            return Err(FormatError::InvalidArchive(
4650                "CMRA data shard has wrong role",
4651            ));
4652        }
4653        let expected_len = if serialized_idx + 1 == data_count {
4654            let used = tuple.image_length as usize - serialized_idx * shard_size;
4655            if used == 0 {
4656                shard_size
4657            } else {
4658                used
4659            }
4660        } else {
4661            shard_size
4662        };
4663        if shard.shard_payload_length as usize != expected_len {
4664            return Err(FormatError::InvalidArchive(
4665                "CMRA data shard payload length is non-canonical",
4666            ));
4667        }
4668        if serialized_idx + 1 == data_count
4669            && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
4670        {
4671            return Err(FormatError::InvalidArchive(
4672                "CMRA final data shard padding is non-zero",
4673            ));
4674        }
4675    } else {
4676        if shard.shard_role != 1 {
4677            return Err(FormatError::InvalidArchive(
4678                "CMRA parity shard has wrong role",
4679            ));
4680        }
4681        if shard.shard_payload_length as usize != shard_size {
4682            return Err(FormatError::InvalidArchive(
4683                "CMRA parity shard payload length is non-canonical",
4684            ));
4685        }
4686    }
4687    Ok(())
4688}
4689
4690fn validate_critical_metadata_image(
4691    image: &CriticalMetadataImage,
4692    mode: CmraRecoveryMode,
4693) -> Result<(), FormatError> {
4694    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
4695    if image.volume_header_offset != 0
4696        || image.volume_header_length != VOLUME_HEADER_LEN as u32
4697        || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
4698        || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
4699        || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
4700        || image.body_bytes_before_cmra
4701            != image
4702                .volume_trailer_offset
4703                .checked_add(VOLUME_TRAILER_LEN as u64)
4704                .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
4705    {
4706        return Err(FormatError::InvalidArchive(
4707            "CriticalMetadataImage fixed layout is invalid",
4708        ));
4709    }
4710    if root_auth_present {
4711        if image.root_auth_footer_offset == 0
4712            || image.root_auth_footer_length == 0
4713            || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
4714        {
4715            return Err(FormatError::InvalidArchive(
4716                "CriticalMetadataImage root-auth range is invalid",
4717            ));
4718        }
4719    } else if image.root_auth_footer_offset != 0
4720        || image.root_auth_footer_length != 0
4721        || image.root_auth_footer_sha256 != [0u8; 32]
4722    {
4723        return Err(FormatError::InvalidArchive(
4724            "CriticalMetadataImage root-auth fields must be zero when absent",
4725        ));
4726    }
4727    let block_record_len = image_block_record_len_from_region(image)?;
4728    let block_record_len_u64 = u64::try_from(block_record_len)
4729        .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
4730    match mode {
4731        CmraRecoveryMode::KeyHolding => {
4732            let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
4733                FormatError::InvalidArchive("BlockRecord region length overflow"),
4734            )?;
4735            if image.block_records_length != expected_len {
4736                return Err(FormatError::InvalidArchive(
4737                    "CriticalMetadataImage terminal equations are invalid",
4738                ));
4739            }
4740        }
4741        CmraRecoveryMode::PublicNoKey => {
4742            if image.block_records_length % block_record_len_u64 != 0 {
4743                return Err(FormatError::InvalidArchive(
4744                    "CriticalMetadataImage BlockRecord region is not aligned",
4745                ));
4746            }
4747        }
4748    }
4749    if image.block_records_offset
4750        != image
4751            .crypto_header_offset
4752            .checked_add(image.crypto_header_length as u64)
4753            .ok_or(FormatError::InvalidArchive(
4754                "CryptoHeader boundary overflow",
4755            ))?
4756        || image.manifest_footer_offset
4757            != image
4758                .block_records_offset
4759                .checked_add(image.block_records_length)
4760                .ok_or(FormatError::InvalidArchive(
4761                    "ManifestFooter boundary overflow",
4762                ))?
4763    {
4764        return Err(FormatError::InvalidArchive(
4765            "CriticalMetadataImage terminal equations are invalid",
4766        ));
4767    }
4768    let manifest_end = image
4769        .manifest_footer_offset
4770        .checked_add(MANIFEST_FOOTER_LEN as u64)
4771        .ok_or(FormatError::InvalidArchive(
4772            "RootAuthFooter boundary overflow",
4773        ))?;
4774    if root_auth_present {
4775        if image.root_auth_footer_offset != manifest_end
4776            || image
4777                .root_auth_footer_offset
4778                .checked_add(image.root_auth_footer_length as u64)
4779                .ok_or(FormatError::InvalidArchive(
4780                    "VolumeTrailer boundary overflow",
4781                ))?
4782                != image.volume_trailer_offset
4783        {
4784            return Err(FormatError::InvalidArchive(
4785                "CriticalMetadataImage root-auth terminal equations are invalid",
4786            ));
4787        }
4788    } else if image.volume_trailer_offset != manifest_end {
4789        return Err(FormatError::InvalidArchive(
4790            "CriticalMetadataImage unsigned terminal equations are invalid",
4791        ));
4792    }
4793    let expected_types: &[u16] = if root_auth_present {
4794        &[1, 2, 3, 4, 5]
4795    } else {
4796        &[1, 2, 3, 5]
4797    };
4798    if image.regions.len() != expected_types.len()
4799        || image
4800            .regions
4801            .iter()
4802            .map(|region| region.region_type)
4803            .ne(expected_types.iter().copied())
4804    {
4805        return Err(FormatError::InvalidArchive(
4806            "CriticalMetadataImage regions are not canonical",
4807        ));
4808    }
4809    validate_image_region(
4810        image,
4811        1,
4812        image.volume_header_offset,
4813        image.volume_header_length,
4814    )?;
4815    validate_image_region(
4816        image,
4817        2,
4818        image.crypto_header_offset,
4819        image.crypto_header_length,
4820    )?;
4821    validate_image_region(
4822        image,
4823        3,
4824        image.manifest_footer_offset,
4825        image.manifest_footer_length,
4826    )?;
4827    if root_auth_present {
4828        validate_image_region(
4829            image,
4830            4,
4831            image.root_auth_footer_offset,
4832            image.root_auth_footer_length,
4833        )?;
4834    }
4835    validate_image_region(
4836        image,
4837        5,
4838        image.volume_trailer_offset,
4839        image.volume_trailer_length,
4840    )?;
4841    if sha256_region(image, 1)? != image.volume_header_sha256
4842        || sha256_region(image, 2)? != image.crypto_header_sha256
4843        || sha256_region(image, 3)? != image.manifest_footer_sha256
4844        || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
4845        || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
4846        || sha256_region(image, 5)? != image.volume_trailer_sha256
4847    {
4848        return Err(FormatError::InvalidArchive(
4849            "CriticalMetadataImage region digest mismatch",
4850        ));
4851    }
4852    Ok(())
4853}
4854
4855fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
4856    let crypto_region = image
4857        .region(2)
4858        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4859    let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4860    crypto.fixed.validate_supported_profile()?;
4861    Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
4862}
4863
4864fn validate_image_region(
4865    image: &CriticalMetadataImage,
4866    region_type: u16,
4867    offset: u64,
4868    length: u32,
4869) -> Result<(), FormatError> {
4870    let region = image
4871        .region(region_type)
4872        .ok_or(FormatError::InvalidArchive(
4873            "missing CriticalMetadataImage region",
4874        ))?;
4875    if region.offset != offset || region.bytes.len() != length as usize {
4876        return Err(FormatError::InvalidArchive(
4877            "CriticalMetadataImage region range mismatch",
4878        ));
4879    }
4880    Ok(())
4881}
4882
4883fn validate_image_identity(
4884    image: &CriticalMetadataImage,
4885    volume_header: &VolumeHeader,
4886    crypto_header: &CryptoHeaderFixed,
4887) -> Result<(), FormatError> {
4888    if image.archive_uuid != volume_header.archive_uuid
4889        || image.session_id != volume_header.session_id
4890        || image.volume_index != volume_header.volume_index
4891        || image.stripe_width != volume_header.stripe_width
4892        || image.stripe_width != crypto_header.stripe_width
4893    {
4894        return Err(FormatError::InvalidArchive(
4895            "CriticalMetadataImage identity does not match selected volume",
4896        ));
4897    }
4898    Ok(())
4899}
4900
4901fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
4902    Ok(sha256_bytes(
4903        &image
4904            .region(region_type)
4905            .ok_or(FormatError::InvalidArchive(
4906                "missing CriticalMetadataImage region",
4907            ))?
4908            .bytes,
4909    ))
4910}
4911
4912fn validate_recovered_terminal(
4913    image: CriticalMetadataImage,
4914    tuple: CmraDecoderTuple,
4915    bytes: &[u8],
4916    context: KeyHoldingTerminalContext<'_>,
4917) -> Result<V41Terminal, FormatError> {
4918    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
4919    let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
4920    validate_recovered_terminal_inner(image, tuple, cmra_boundary_magic_ok, context)
4921}
4922
4923fn validate_recovered_terminal_read_at(
4924    image: CriticalMetadataImage,
4925    tuple: CmraDecoderTuple,
4926    reader: &dyn ArchiveReadAt,
4927    context: KeyHoldingTerminalContext<'_>,
4928) -> Result<V41Terminal, FormatError> {
4929    let mut magic = [0u8; 4];
4930    reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
4931    validate_recovered_terminal_inner(image, tuple, magic == *b"TZCR", context)
4932}
4933
4934fn validate_recovered_terminal_inner(
4935    image: CriticalMetadataImage,
4936    tuple: CmraDecoderTuple,
4937    cmra_boundary_magic_ok: bool,
4938    context: KeyHoldingTerminalContext<'_>,
4939) -> Result<V41Terminal, FormatError> {
4940    let subkeys = context.subkeys;
4941    let volume_header = context.volume_header;
4942    let crypto_header = context.crypto_header;
4943    let volume_header_region = image
4944        .region(1)
4945        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
4946    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
4947    if &recovered_volume_header != volume_header {
4948        return Err(FormatError::InvalidArchive(
4949            "CMRA VolumeHeader differs from parsed VolumeHeader",
4950        ));
4951    }
4952    validate_image_identity(&image, volume_header, crypto_header)?;
4953    let crypto_region = image
4954        .region(2)
4955        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
4956    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
4957    if recovered_crypto.fixed != *crypto_header {
4958        return Err(FormatError::InvalidArchive(
4959            "CMRA CryptoHeader differs from parsed CryptoHeader",
4960        ));
4961    }
4962    let recovered_pre_hmac_len = crypto_region
4963        .bytes
4964        .len()
4965        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
4966        .ok_or(FormatError::InvalidArchive(
4967            "CMRA CryptoHeader is too short",
4968        ))?;
4969    let parsed_pre_hmac_len = context
4970        .crypto_header_bytes
4971        .len()
4972        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
4973        .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
4974    if recovered_pre_hmac_len != parsed_pre_hmac_len
4975        || crypto_region.bytes[..recovered_pre_hmac_len]
4976            != context.crypto_header_bytes[..parsed_pre_hmac_len]
4977    {
4978        return Err(FormatError::InvalidArchive(
4979            "CMRA CryptoHeader differs from parsed CryptoHeader",
4980        ));
4981    }
4982    verify_integrity_tag(
4983        HmacDomain::CryptoHeader,
4984        recovered_crypto.fixed.aead_algo,
4985        Some(&subkeys.mac_key),
4986        &volume_header.archive_uuid,
4987        &volume_header.session_id,
4988        recovered_crypto.hmac_covered_bytes,
4989        &recovered_crypto.header_hmac,
4990    )?;
4991    validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
4992    recovered_crypto.validate_extension_semantics()?;
4993
4994    let manifest_region = image
4995        .region(3)
4996        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
4997    let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
4998    validate_manifest_footer(
4999        volume_header,
5000        crypto_header,
5001        &manifest_footer,
5002        subkeys,
5003        &manifest_region.bytes,
5004    )?;
5005    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5006
5007    let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
5008        let root_auth_region = image
5009            .region(4)
5010            .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5011        let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5012        if footer.archive_uuid != volume_header.archive_uuid
5013            || footer.session_id != volume_header.session_id
5014            || footer.footer_length()? != image.root_auth_footer_length
5015        {
5016            return Err(FormatError::InvalidArchive(
5017                "RootAuthFooter identity or length does not match terminal image",
5018            ));
5019        }
5020        Some(footer)
5021    } else {
5022        None
5023    };
5024
5025    let trailer_region = image
5026        .region(5)
5027        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5028    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5029    verify_integrity_tag(
5030        HmacDomain::VolumeTrailer,
5031        crypto_header.aead_algo,
5032        Some(&subkeys.mac_key),
5033        &volume_header.archive_uuid,
5034        &volume_header.session_id,
5035        &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
5036        &trailer.trailer_hmac,
5037    )?;
5038    validate_trailer_identity(volume_header, &trailer)?;
5039    validate_v41_trailer_equations(&image, &trailer)?;
5040
5041    if !cmra_boundary_magic_ok {
5042        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5043    }
5044
5045    let manifest_footer_bytes = manifest_region.bytes.clone();
5046    let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
5047    Ok(V41Terminal {
5048        image,
5049        manifest_footer_bytes,
5050        root_auth_footer_bytes,
5051        root_auth_footer,
5052        volume_trailer: trailer,
5053    })
5054}
5055
5056fn validate_recovered_public_terminal(
5057    image: CriticalMetadataImage,
5058    bytes: &[u8],
5059    volume_header: &VolumeHeader,
5060    crypto_header: &CryptoHeaderFixed,
5061) -> Result<V41PublicTerminal, FormatError> {
5062    if image.layout_flags & 0x0000_0001 == 0 {
5063        return Err(FormatError::ReaderUnsupported(
5064            "public no-key verification requires root-auth",
5065        ));
5066    }
5067    let volume_header_region = image
5068        .region(1)
5069        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5070    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5071    if &recovered_volume_header != volume_header {
5072        return Err(FormatError::InvalidArchive(
5073            "CMRA VolumeHeader differs from parsed VolumeHeader",
5074        ));
5075    }
5076    validate_image_identity(&image, volume_header, crypto_header)?;
5077    let crypto_region = image
5078        .region(2)
5079        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5080    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5081    if recovered_crypto.fixed != *crypto_header {
5082        return Err(FormatError::InvalidArchive(
5083            "CMRA CryptoHeader differs from parsed CryptoHeader",
5084        ));
5085    }
5086    recovered_crypto.validate_extension_semantics()?;
5087
5088    image
5089        .region(3)
5090        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5091
5092    let root_auth_region = image
5093        .region(4)
5094        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5095    let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5096    if root_auth_footer.archive_uuid != volume_header.archive_uuid
5097        || root_auth_footer.session_id != volume_header.session_id
5098        || root_auth_footer.footer_length()? != image.root_auth_footer_length
5099    {
5100        return Err(FormatError::InvalidArchive(
5101            "public RootAuthFooter identity or length does not match terminal image",
5102        ));
5103    }
5104
5105    let trailer_region = image
5106        .region(5)
5107        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5108    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5109    validate_trailer_identity(volume_header, &trailer)?;
5110    validate_v41_public_trailer_profile(&image, &trailer)?;
5111
5112    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5113    if bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
5114        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5115    }
5116
5117    let root_auth_footer_bytes = root_auth_region.bytes.clone();
5118    Ok(V41PublicTerminal {
5119        image,
5120        root_auth_footer_bytes,
5121        root_auth_footer,
5122    })
5123}
5124
5125fn validate_v41_trailer_equations(
5126    image: &CriticalMetadataImage,
5127    trailer: &VolumeTrailer,
5128) -> Result<(), FormatError> {
5129    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5130    if trailer.bytes_written != image.volume_trailer_offset
5131        || trailer.manifest_footer_offset != image.manifest_footer_offset
5132        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5133        || trailer.block_count != image.block_count
5134    {
5135        return Err(FormatError::InvalidArchive(
5136            "VolumeTrailer does not match v41 terminal layout",
5137        ));
5138    }
5139    if root_auth_present {
5140        if trailer.root_auth_flags != 0x0000_0001
5141            || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5142            || trailer.root_auth_footer_length != image.root_auth_footer_length
5143            || image.root_auth_footer_offset
5144                != image
5145                    .manifest_footer_offset
5146                    .checked_add(MANIFEST_FOOTER_LEN as u64)
5147                    .ok_or(FormatError::InvalidArchive(
5148                        "RootAuthFooter trailer boundary overflow",
5149                    ))?
5150            || image
5151                .root_auth_footer_offset
5152                .checked_add(image.root_auth_footer_length as u64)
5153                .ok_or(FormatError::InvalidArchive(
5154                    "RootAuthFooter trailer boundary overflow",
5155                ))?
5156                != image.volume_trailer_offset
5157        {
5158            return Err(FormatError::InvalidArchive(
5159                "VolumeTrailer root-auth fields do not match v41 terminal layout",
5160            ));
5161        }
5162    } else if trailer.root_auth_footer_offset != 0
5163        || trailer.root_auth_footer_length != 0
5164        || trailer.root_auth_flags != 0
5165    {
5166        return Err(FormatError::InvalidArchive(
5167            "VolumeTrailer root-auth fields must be zero when absent",
5168        ));
5169    }
5170    Ok(())
5171}
5172
5173fn validate_v41_public_trailer_profile(
5174    image: &CriticalMetadataImage,
5175    trailer: &VolumeTrailer,
5176) -> Result<(), FormatError> {
5177    if trailer.bytes_written != image.volume_trailer_offset
5178        || trailer.manifest_footer_offset != image.manifest_footer_offset
5179        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5180    {
5181        return Err(FormatError::InvalidArchive(
5182            "VolumeTrailer does not match v41 public terminal layout",
5183        ));
5184    }
5185    if trailer.root_auth_flags != 0x0000_0001
5186        || trailer.root_auth_footer_offset == 0
5187        || trailer.root_auth_footer_length == 0
5188        || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5189        || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5190        || trailer.root_auth_footer_length != image.root_auth_footer_length
5191        || image.root_auth_footer_offset
5192            != image
5193                .manifest_footer_offset
5194                .checked_add(MANIFEST_FOOTER_LEN as u64)
5195                .ok_or(FormatError::InvalidArchive(
5196                    "RootAuthFooter trailer boundary overflow",
5197                ))?
5198        || image
5199            .root_auth_footer_offset
5200            .checked_add(image.root_auth_footer_length as u64)
5201            .ok_or(FormatError::InvalidArchive(
5202                "RootAuthFooter trailer boundary overflow",
5203            ))?
5204            != image.volume_trailer_offset
5205    {
5206        return Err(FormatError::InvalidArchive(
5207            "VolumeTrailer root-auth fields do not match v41 public terminal layout",
5208        ));
5209    }
5210    Ok(())
5211}
5212
5213fn critical_image_min() -> u64 {
5214    const MIN_CRYPTO_HEADER_LEN: u64 = 116;
5215    CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
5216        + 4 * SERIALIZED_REGION_HEADER_LEN as u64
5217        + VOLUME_HEADER_LEN as u64
5218        + MIN_CRYPTO_HEADER_LEN
5219        + MANIFEST_FOOTER_LEN as u64
5220        + VOLUME_TRAILER_LEN as u64
5221        + IMAGE_CRC_LEN as u64
5222}
5223
5224fn critical_image_cap() -> Result<u64, FormatError> {
5225    [
5226        CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
5227        5 * SERIALIZED_REGION_HEADER_LEN as u64,
5228        VOLUME_HEADER_LEN as u64,
5229        READER_MAX_CRYPTO_HEADER_LEN as u64,
5230        MANIFEST_FOOTER_LEN as u64,
5231        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5232        VOLUME_TRAILER_LEN as u64,
5233        IMAGE_CRC_LEN as u64,
5234    ]
5235    .into_iter()
5236    .try_fold(0u64, |total, value| {
5237        total
5238            .checked_add(value)
5239            .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
5240    })
5241}
5242
5243fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
5244    let shard_total = (tuple.data_shard_count as u64)
5245        .checked_add(tuple.parity_shard_count as u64)
5246        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5247    let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
5248        .checked_add(tuple.shard_size as u64)
5249        .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
5250    checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
5251        .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
5252        .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
5253}
5254
5255fn cmra_worst_case_cap() -> Result<u64, FormatError> {
5256    let cap = critical_image_cap()?;
5257    let mut worst = 0u64;
5258    let mut shard_size = 512u64;
5259    while shard_size <= 4096 {
5260        let data = ceil_div_u64(cap, shard_size)?;
5261        let parity = 2u64.max(ceil_div_u64(
5262            checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
5263            100,
5264        )?);
5265        let tuple = CmraDecoderTuple {
5266            shard_size: shard_size as u32,
5267            data_shard_count: u16::try_from(data)
5268                .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
5269            parity_shard_count: u16::try_from(parity)
5270                .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
5271            image_length: u32::try_from(cap)
5272                .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
5273            image_sha256: [0u8; 32],
5274        };
5275        worst = worst.max(cmra_serialized_length(tuple)?);
5276        shard_size += 2;
5277    }
5278    Ok(worst)
5279}
5280
5281pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
5282    let total = [
5283        MANIFEST_FOOTER_LEN as u64,
5284        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
5285        VOLUME_TRAILER_LEN as u64,
5286        cmra_worst_case_cap()?,
5287        LOCATOR_PAIR_LEN as u64,
5288    ]
5289    .into_iter()
5290    .try_fold(0u64, |sum, value| {
5291        sum.checked_add(value)
5292            .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
5293    })?;
5294    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
5295}
5296
5297fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
5298    let worst = cmra_worst_case_cap()?;
5299    let total = options
5300        .max_trailing_garbage_scan
5301        .try_into()
5302        .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5303        .and_then(|scan: u64| {
5304            scan.checked_add(worst)
5305                .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
5306                .ok_or(FormatError::InvalidArchive("scan cap overflow"))
5307        })?;
5308    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
5309}
5310
5311fn validate_bootstrap_single_volume_input(
5312    volume_header: &VolumeHeader,
5313    crypto_header: &CryptoHeaderFixed,
5314) -> Result<(), FormatError> {
5315    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
5316        return Err(FormatError::ReaderUnsupported(
5317            "bootstrap sidecar reader supports only single-volume archive input",
5318        ));
5319    }
5320    if crypto_header.stripe_width != volume_header.stripe_width {
5321        return Err(FormatError::InvalidArchive(
5322            "VolumeHeader and CryptoHeader stripe_width differ",
5323        ));
5324    }
5325    Ok(())
5326}
5327
5328#[derive(Debug)]
5329struct ParsedBootstrapSidecar {
5330    manifest_footer: Option<ManifestFooter>,
5331    index_root_records_section: Option<(u64, u64)>,
5332    dictionary_records_section: Option<(u64, u64)>,
5333}
5334
5335pub(crate) struct NonSeekableBootstrapMaterial {
5336    pub(crate) manifest_footer: ManifestFooter,
5337    pub(crate) payload_dictionary: Option<Vec<u8>>,
5338}
5339
5340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5341enum BootstrapSidecarUse {
5342    SeekableAssist,
5343    NonSeekableRandomAccess,
5344}
5345
5346impl ParsedBootstrapSidecar {
5347    fn require_sections_for(
5348        &self,
5349        sidecar_use: BootstrapSidecarUse,
5350        crypto_header: &CryptoHeaderFixed,
5351    ) -> Result<(), FormatError> {
5352        if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
5353            if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
5354                return Err(FormatError::ReaderUnsupported(
5355                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
5356                ));
5357            }
5358            if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
5359                return Err(FormatError::ReaderUnsupported(
5360                    "dictionary bootstrap required",
5361                ));
5362            }
5363        }
5364        Ok(())
5365    }
5366}
5367
5368pub(crate) fn parse_non_seekable_bootstrap_material(
5369    bootstrap_sidecar: &[u8],
5370    volume_header: &VolumeHeader,
5371    crypto_header: &CryptoHeaderFixed,
5372    subkeys: &Subkeys,
5373) -> Result<NonSeekableBootstrapMaterial, FormatError> {
5374    validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
5375    let sidecar =
5376        parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
5377    sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
5378    let manifest_footer = sidecar
5379        .manifest_footer
5380        .clone()
5381        .ok_or(FormatError::ReaderUnsupported(
5382            "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
5383        ))?;
5384
5385    let mut blocks = BTreeMap::new();
5386    let (offset, length) =
5387        sidecar
5388            .index_root_records_section
5389            .ok_or(FormatError::ReaderUnsupported(
5390                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
5391            ))?;
5392    let index_root_records = parse_sidecar_block_records(
5393        bootstrap_sidecar,
5394        crypto_header.block_size as usize,
5395        SidecarBlockRecordsSection {
5396            offset,
5397            length,
5398            extent: index_root_extent_from_manifest(&manifest_footer),
5399            data_kind: BlockKind::IndexRootData,
5400            parity_kind: BlockKind::IndexRootParity,
5401            structure: "IndexRoot",
5402        },
5403    )?;
5404    insert_sidecar_records(&mut blocks, index_root_records)?;
5405
5406    let limits = metadata_limits(crypto_header);
5407    let index_root_plaintext = load_metadata_object_from_parts(
5408        &blocks,
5409        ObjectLoadContext::index_root(
5410            volume_header,
5411            crypto_header,
5412            subkeys,
5413            index_root_extent_from_manifest(&manifest_footer),
5414        ),
5415        manifest_footer.index_root_decompressed_size,
5416    )?;
5417    let index_root = IndexRoot::parse(
5418        &index_root_plaintext,
5419        crypto_header.has_dictionary != 0,
5420        limits,
5421    )?;
5422
5423    if crypto_header.has_dictionary != 0 {
5424        let (offset, length) =
5425            sidecar
5426                .dictionary_records_section
5427                .ok_or(FormatError::ReaderUnsupported(
5428                    "dictionary bootstrap required",
5429                ))?;
5430        let dictionary_records = parse_sidecar_block_records(
5431            bootstrap_sidecar,
5432            crypto_header.block_size as usize,
5433            SidecarBlockRecordsSection {
5434                offset,
5435                length,
5436                extent: dictionary_extent_from_index_root(&index_root)?,
5437                data_kind: BlockKind::DictionaryData,
5438                parity_kind: BlockKind::DictionaryParity,
5439                structure: "dictionary",
5440            },
5441        )?;
5442        insert_sidecar_records(&mut blocks, dictionary_records)?;
5443    }
5444    let payload_dictionary =
5445        load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
5446
5447    Ok(NonSeekableBootstrapMaterial {
5448        manifest_footer,
5449        payload_dictionary,
5450    })
5451}
5452
5453fn parse_bootstrap_sidecar(
5454    bytes: &[u8],
5455    volume_header: &VolumeHeader,
5456    crypto_header: &CryptoHeaderFixed,
5457    subkeys: &Subkeys,
5458) -> Result<ParsedBootstrapSidecar, FormatError> {
5459    let header_bytes = slice(
5460        bytes,
5461        0,
5462        BOOTSTRAP_SIDECAR_HEADER_LEN,
5463        "BootstrapSidecarHeader",
5464    )?;
5465    let header = BootstrapSidecarHeader::parse(header_bytes)?;
5466    if header.archive_uuid != volume_header.archive_uuid
5467        || header.session_id != volume_header.session_id
5468    {
5469        return Err(FormatError::InvalidArchive(
5470            "bootstrap sidecar identity does not match VolumeHeader",
5471        ));
5472    }
5473    verify_integrity_tag(
5474        HmacDomain::BootstrapSidecar,
5475        crypto_header.aead_algo,
5476        Some(&subkeys.mac_key),
5477        &volume_header.archive_uuid,
5478        &volume_header.session_id,
5479        &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
5480        &header.sidecar_hmac,
5481    )?;
5482    header.validate_packed_layout(bytes.len() as u64)?;
5483    validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
5484
5485    if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
5486        return Err(FormatError::InvalidArchive(
5487            "bootstrap sidecar has dictionary records while has_dictionary is false",
5488        ));
5489    }
5490
5491    let manifest_footer = if header.has_manifest_footer() {
5492        let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
5493        let manifest_bytes = slice(
5494            bytes,
5495            manifest_offset,
5496            MANIFEST_FOOTER_LEN,
5497            "ManifestFooter",
5498        )?;
5499        let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5500        validate_sidecar_manifest_footer(
5501            volume_header,
5502            crypto_header,
5503            &manifest_footer,
5504            subkeys,
5505            manifest_bytes,
5506        )?;
5507        manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5508        Some(manifest_footer)
5509    } else {
5510        None
5511    };
5512
5513    Ok(ParsedBootstrapSidecar {
5514        manifest_footer,
5515        index_root_records_section: header.has_index_root_records().then_some((
5516            header.index_root_records_offset,
5517            header.index_root_records_length,
5518        )),
5519        dictionary_records_section: header.has_dictionary_records().then_some((
5520            header.dictionary_records_offset,
5521            header.dictionary_records_length,
5522        )),
5523    })
5524}
5525
5526fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
5527    ObjectExtent {
5528        first_block_index: manifest_footer.index_root_first_block,
5529        data_block_count: manifest_footer.index_root_data_block_count,
5530        parity_block_count: manifest_footer.index_root_parity_block_count,
5531        encrypted_size: manifest_footer.index_root_encrypted_size,
5532    }
5533}
5534
5535fn insert_sidecar_records(
5536    blocks: &mut BTreeMap<u64, BlockRecord>,
5537    records: Vec<BlockRecord>,
5538) -> Result<(), FormatError> {
5539    for record in records {
5540        if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
5541            if existing != record {
5542                return Err(FormatError::InvalidArchive(
5543                    "bootstrap sidecar conflicts with volume BlockRecord",
5544                ));
5545            }
5546        }
5547    }
5548    Ok(())
5549}
5550
5551fn validate_sidecar_manifest_footer(
5552    volume_header: &VolumeHeader,
5553    crypto_header: &CryptoHeaderFixed,
5554    footer: &ManifestFooter,
5555    subkeys: &Subkeys,
5556    raw: &[u8],
5557) -> Result<(), FormatError> {
5558    if footer.archive_uuid != volume_header.archive_uuid
5559        || footer.session_id != volume_header.session_id
5560    {
5561        return Err(FormatError::InvalidArchive(
5562            "sidecar ManifestFooter identity does not match VolumeHeader",
5563        ));
5564    }
5565    if footer.volume_index != 0 {
5566        return Err(FormatError::InvalidArchive(
5567            "sidecar ManifestFooter volume_index must be zero",
5568        ));
5569    }
5570    if footer.total_volumes != crypto_header.stripe_width {
5571        return Err(FormatError::InvalidArchive(
5572            "sidecar ManifestFooter total_volumes does not match stripe_width",
5573        ));
5574    }
5575    if footer.is_authoritative != 1 {
5576        return Err(FormatError::InvalidArchive(
5577            "sidecar ManifestFooter is not authoritative",
5578        ));
5579    }
5580    verify_integrity_tag(
5581        HmacDomain::ManifestFooter,
5582        crypto_header.aead_algo,
5583        Some(&subkeys.mac_key),
5584        &volume_header.archive_uuid,
5585        &volume_header.session_id,
5586        &raw[..MANIFEST_HMAC_COVERED_LEN],
5587        &footer.manifest_hmac,
5588    )
5589}
5590
5591fn validate_sidecar_size_cap(
5592    header: &BootstrapSidecarHeader,
5593    crypto_header: &CryptoHeaderFixed,
5594    file_size: u64,
5595) -> Result<(), FormatError> {
5596    let record_len = checked_u64_add(
5597        crypto_header.block_size as u64,
5598        BLOCK_RECORD_FRAMING_LEN as u64,
5599        "bootstrap sidecar cap overflow",
5600    )?;
5601    let max_index_records = crypto_header.index_root_fec_data_shards as u64
5602        + crypto_header.index_root_fec_parity_shards as u64;
5603    let max_record_section_bytes = checked_u64_mul(
5604        max_index_records,
5605        record_len,
5606        "bootstrap sidecar cap overflow",
5607    )?;
5608    if header.index_root_records_length % record_len != 0 {
5609        return Err(FormatError::InvalidArchive(
5610            "bootstrap sidecar IndexRoot records length is not aligned",
5611        ));
5612    }
5613    if header.index_root_records_length / record_len > max_index_records {
5614        return Err(FormatError::InvalidArchive(
5615            "bootstrap sidecar IndexRoot records exceed resource cap",
5616        ));
5617    }
5618    if header.dictionary_records_length % record_len != 0 {
5619        return Err(FormatError::InvalidArchive(
5620            "bootstrap sidecar dictionary records length is not aligned",
5621        ));
5622    }
5623    if header.dictionary_records_length / record_len > max_index_records {
5624        return Err(FormatError::InvalidArchive(
5625            "bootstrap sidecar dictionary records exceed resource cap",
5626        ));
5627    }
5628
5629    let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
5630    if header.has_manifest_footer() {
5631        cap = cap
5632            .checked_add(MANIFEST_FOOTER_LEN as u64)
5633            .ok_or(FormatError::InvalidArchive(
5634                "bootstrap sidecar cap overflow",
5635            ))?;
5636    }
5637    if header.has_index_root_records() {
5638        cap = checked_u64_add(
5639            cap,
5640            max_record_section_bytes,
5641            "bootstrap sidecar cap overflow",
5642        )?;
5643    }
5644    if header.has_dictionary_records() {
5645        cap = checked_u64_add(
5646            cap,
5647            max_record_section_bytes,
5648            "bootstrap sidecar cap overflow",
5649        )?;
5650    }
5651    if file_size > cap {
5652        return Err(FormatError::InvalidArchive(
5653            "bootstrap sidecar exceeds resource cap",
5654        ));
5655    }
5656    Ok(())
5657}
5658
5659#[derive(Debug, Clone, Copy)]
5660struct SidecarBlockRecordsSection {
5661    offset: u64,
5662    length: u64,
5663    extent: ObjectExtent,
5664    data_kind: BlockKind,
5665    parity_kind: BlockKind,
5666    structure: &'static str,
5667}
5668
5669fn parse_sidecar_block_records(
5670    sidecar_bytes: &[u8],
5671    block_size: usize,
5672    section: SidecarBlockRecordsSection,
5673) -> Result<Vec<BlockRecord>, FormatError> {
5674    let record_len = block_size
5675        .checked_add(BLOCK_RECORD_FRAMING_LEN)
5676        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5677    if section.length % record_len as u64 != 0 {
5678        return Err(FormatError::InvalidArchive(
5679            "sidecar BlockRecord section is not aligned",
5680        ));
5681    }
5682    let expected_count =
5683        section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
5684    let actual_count = usize::try_from(section.length / record_len as u64)
5685        .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
5686    if actual_count != expected_count {
5687        return Err(FormatError::InvalidArchive(
5688            "sidecar BlockRecord section does not match declared extent",
5689        ));
5690    }
5691    let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
5692    let raw = slice(
5693        sidecar_bytes,
5694        start,
5695        to_usize(section.length, "BootstrapSidecarHeader")?,
5696        "BootstrapSidecarHeader",
5697    )?;
5698    let mut records = Vec::with_capacity(expected_count);
5699
5700    for idx in 0..expected_count {
5701        let record = BlockRecord::parse(
5702            slice(raw, idx * record_len, record_len, "BlockRecord")?,
5703            block_size,
5704        )?;
5705        let expected_block_index = checked_u64_add(
5706            section.extent.first_block_index,
5707            idx as u64,
5708            section.structure,
5709        )?;
5710        if record.block_index != expected_block_index {
5711            return Err(FormatError::InvalidArchive(
5712                "sidecar BlockRecord section has missing or duplicate blocks",
5713            ));
5714        }
5715        let expected_kind = if idx < section.extent.data_block_count as usize {
5716            section.data_kind
5717        } else {
5718            section.parity_kind
5719        };
5720        if record.kind != expected_kind {
5721            return Err(FormatError::InvalidArchive(
5722                "sidecar BlockRecord section has wrong kind",
5723            ));
5724        }
5725        let should_be_last = idx + 1 == section.extent.data_block_count as usize;
5726        if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
5727        {
5728            return Err(FormatError::InvalidArchive(
5729                "sidecar BlockRecord section has wrong last-data flag",
5730            ));
5731        }
5732        records.push(record);
5733    }
5734
5735    Ok(records)
5736}
5737
5738fn validate_trailer_identity(
5739    volume_header: &VolumeHeader,
5740    trailer: &VolumeTrailer,
5741) -> Result<(), FormatError> {
5742    if trailer.archive_uuid != volume_header.archive_uuid
5743        || trailer.session_id != volume_header.session_id
5744        || trailer.volume_index != volume_header.volume_index
5745    {
5746        return Err(FormatError::InvalidArchive(
5747            "VolumeTrailer identity does not match VolumeHeader",
5748        ));
5749    }
5750    Ok(())
5751}
5752
5753fn validate_manifest_footer(
5754    volume_header: &VolumeHeader,
5755    crypto_header: &CryptoHeaderFixed,
5756    footer: &ManifestFooter,
5757    subkeys: &Subkeys,
5758    raw: &[u8],
5759) -> Result<(), FormatError> {
5760    if footer.archive_uuid != volume_header.archive_uuid
5761        || footer.session_id != volume_header.session_id
5762        || footer.volume_index != volume_header.volume_index
5763    {
5764        return Err(FormatError::InvalidArchive(
5765            "ManifestFooter identity does not match VolumeHeader",
5766        ));
5767    }
5768    if footer.total_volumes != volume_header.stripe_width {
5769        return Err(FormatError::InvalidArchive(
5770            "ManifestFooter total_volumes does not match stripe_width",
5771        ));
5772    }
5773    if footer.is_authoritative != 1 {
5774        return Err(FormatError::InvalidArchive(
5775            "ManifestFooter is not authoritative",
5776        ));
5777    }
5778    verify_integrity_tag(
5779        HmacDomain::ManifestFooter,
5780        crypto_header.aead_algo,
5781        Some(&subkeys.mac_key),
5782        &volume_header.archive_uuid,
5783        &volume_header.session_id,
5784        &raw[..MANIFEST_HMAC_COVERED_LEN],
5785        &footer.manifest_hmac,
5786    )
5787}
5788
5789#[derive(Debug)]
5790struct ParsedBlockRegion {
5791    blocks: BTreeMap<u64, BlockRecord>,
5792    erased_block_indices: BTreeSet<u64>,
5793}
5794
5795fn parse_block_region(
5796    bytes: &[u8],
5797    start: usize,
5798    end: usize,
5799    block_size: usize,
5800    volume_header: &VolumeHeader,
5801    trailer: &VolumeTrailer,
5802) -> Result<ParsedBlockRegion, FormatError> {
5803    if end < start {
5804        return Err(FormatError::InvalidArchive(
5805            "ManifestFooter starts before BlockRecord region",
5806        ));
5807    }
5808    let record_len = block_size
5809        .checked_add(BLOCK_RECORD_FRAMING_LEN)
5810        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5811    let region_len = end - start;
5812    if region_len % record_len != 0 {
5813        return Err(FormatError::InvalidArchive(
5814            "BlockRecord region length is not aligned",
5815        ));
5816    }
5817    let observed_count = region_len / record_len;
5818    if observed_count as u64 != trailer.block_count {
5819        return Err(FormatError::InvalidArchive(
5820            "VolumeTrailer block_count does not match BlockRecord region",
5821        ));
5822    }
5823
5824    let mut blocks = BTreeMap::new();
5825    let mut erased_block_indices = BTreeSet::new();
5826    for idx in 0..observed_count {
5827        let offset = start + idx * record_len;
5828        let expected_block_index = checked_u64_add(
5829            volume_header.volume_index as u64,
5830            checked_u64_mul(
5831                idx as u64,
5832                volume_header.stripe_width as u64,
5833                "BlockRecord index overflow",
5834            )?,
5835            "BlockRecord index overflow",
5836        )?;
5837        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5838        match BlockRecord::parse(raw, block_size) {
5839            Ok(record) => {
5840                if record.block_index != expected_block_index {
5841                    return Err(FormatError::InvalidArchive(
5842                        "BlockRecord index does not match volume position",
5843                    ));
5844                }
5845                if blocks.insert(record.block_index, record).is_some() {
5846                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5847                }
5848            }
5849            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
5850                if !erased_block_indices.insert(expected_block_index) {
5851                    return Err(FormatError::InvalidArchive(
5852                        "duplicate erased BlockRecord index",
5853                    ));
5854                }
5855            }
5856            Err(err) => return Err(err),
5857        }
5858    }
5859
5860    Ok(ParsedBlockRegion {
5861        blocks,
5862        erased_block_indices,
5863    })
5864}
5865
5866fn validate_seekable_block_region_layout(
5867    start: u64,
5868    end: u64,
5869    block_size: usize,
5870    trailer: &VolumeTrailer,
5871) -> Result<(), FormatError> {
5872    if end < start {
5873        return Err(FormatError::InvalidArchive(
5874            "ManifestFooter starts before BlockRecord region",
5875        ));
5876    }
5877    let record_len = block_record_len(block_size)?;
5878    let region_len = end - start;
5879    if region_len % record_len != 0 {
5880        return Err(FormatError::InvalidArchive(
5881            "BlockRecord region length is not aligned",
5882        ));
5883    }
5884    let observed_count = region_len / record_len;
5885    if observed_count != trailer.block_count {
5886        return Err(FormatError::InvalidArchive(
5887            "VolumeTrailer block_count does not match BlockRecord region",
5888        ));
5889    }
5890    Ok(())
5891}
5892
5893fn parse_public_block_observation(
5894    bytes: &[u8],
5895    start: usize,
5896    image: &CriticalMetadataImage,
5897    block_size: usize,
5898    volume_header: &VolumeHeader,
5899) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
5900    let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
5901    if start != image_start {
5902        return Err(FormatError::InvalidArchive(
5903            "public BlockRecord observation start mismatch",
5904        ));
5905    }
5906    let scan_limit_u64 = image
5907        .block_records_offset
5908        .checked_add(image.block_records_length)
5909        .ok_or(FormatError::InvalidArchive(
5910            "public BlockRecord observation limit overflow",
5911        ))?;
5912    if scan_limit_u64 != image.manifest_footer_offset {
5913        return Err(FormatError::InvalidArchive(
5914            "public BlockRecord observation limit mismatch",
5915        ));
5916    }
5917    let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
5918    if scan_limit < start {
5919        return Err(FormatError::InvalidArchive(
5920            "public BlockRecord observation limit before start",
5921        ));
5922    }
5923    let record_len = block_size
5924        .checked_add(BLOCK_RECORD_FRAMING_LEN)
5925        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
5926    let region_len = scan_limit - start;
5927    if region_len % record_len != 0 {
5928        return Err(FormatError::InvalidArchive(
5929            "public BlockRecord observation window is not aligned",
5930        ));
5931    }
5932
5933    let mut blocks = BTreeMap::new();
5934    let mut offset = start;
5935    let mut observed_slot = 0u64;
5936    while offset < scan_limit {
5937        let magic_end = checked_add(offset, 4, "BlockRecord")?;
5938        if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
5939            break;
5940        }
5941        let record_end = checked_add(offset, record_len, "BlockRecord")?;
5942        if record_end > scan_limit {
5943            return Err(FormatError::InvalidArchive(
5944                "public BlockRecord observation slot is incomplete",
5945            ));
5946        }
5947        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
5948        let record = BlockRecord::parse(raw, block_size)?;
5949        let expected_block_index = checked_u64_add(
5950            volume_header.volume_index as u64,
5951            checked_u64_mul(
5952                observed_slot,
5953                volume_header.stripe_width as u64,
5954                "BlockRecord index overflow",
5955            )?,
5956            "BlockRecord index overflow",
5957        )?;
5958        if record.block_index != expected_block_index {
5959            return Err(FormatError::InvalidArchive(
5960                "public BlockRecord index does not match volume position",
5961            ));
5962        }
5963        if blocks.insert(record.block_index, record).is_some() {
5964            return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5965        }
5966        offset = record_end;
5967        observed_slot = observed_slot
5968            .checked_add(1)
5969            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
5970    }
5971
5972    let mut scan = if offset < scan_limit {
5973        checked_add(offset, record_len, "BlockRecord")?
5974    } else {
5975        scan_limit
5976    };
5977    while scan < scan_limit {
5978        let magic_end = checked_add(scan, 4, "BlockRecord")?;
5979        let record_end = checked_add(scan, record_len, "BlockRecord")?;
5980        if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
5981            let raw = slice(bytes, scan, record_len, "BlockRecord")?;
5982            if BlockRecord::parse(raw, block_size).is_ok() {
5983                return Err(FormatError::InvalidArchive(
5984                    "public observation has ambiguous extra BlockRecord",
5985                ));
5986            }
5987        }
5988        scan = record_end;
5989    }
5990
5991    Ok(blocks)
5992}
5993
5994pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
5995    matches!(
5996        error,
5997        FormatError::BadCrc {
5998            structure: "BlockRecord",
5999        }
6000    )
6001}
6002
6003fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
6004    let len = block_size
6005        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6006        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6007    u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
6008}
6009
6010fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
6011    lhs.checked_mul(rhs)
6012        .ok_or(FormatError::InvalidArchive(reason))
6013}
6014
6015fn parse_stream_block_prefix(
6016    bytes: &[u8],
6017    start: usize,
6018    block_size: usize,
6019    volume_header: &VolumeHeader,
6020) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
6021    let record_len = block_size
6022        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6023        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6024    let mut blocks = BTreeMap::new();
6025    let mut offset = start;
6026    let mut observed_block_count = 0u64;
6027
6028    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6029        let expected_block_index =
6030            expected_stream_block_index(volume_header, observed_block_count)?;
6031        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6032        match BlockRecord::parse(raw, block_size) {
6033            Ok(record) => {
6034                if record.block_index != expected_block_index {
6035                    return Err(FormatError::InvalidArchive(
6036                        "BlockRecord index does not match stream position",
6037                    ));
6038                }
6039                if blocks.insert(record.block_index, record).is_some() {
6040                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6041                }
6042            }
6043            Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
6044            Err(err) => return Err(err),
6045        }
6046        offset = checked_add(offset, record_len, "BlockRecord")?;
6047        observed_block_count = observed_block_count
6048            .checked_add(1)
6049            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6050    }
6051
6052    Ok((blocks, offset, observed_block_count))
6053}
6054
6055pub(crate) fn expected_stream_block_index(
6056    volume_header: &VolumeHeader,
6057    observed_block_count: u64,
6058) -> Result<u64, FormatError> {
6059    checked_u64_add(
6060        volume_header.volume_index as u64,
6061        checked_u64_mul(
6062            observed_block_count,
6063            volume_header.stripe_width as u64,
6064            "BlockRecord index overflow",
6065        )?,
6066        "BlockRecord index overflow",
6067    )
6068}
6069
6070fn parse_sequential_block_or_erasure(
6071    bytes: &[u8],
6072    offset: usize,
6073    record_len: usize,
6074    block_size: usize,
6075    volume_header: &VolumeHeader,
6076    observed_block_count: u64,
6077) -> Result<Option<BlockRecord>, FormatError> {
6078    let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
6079    let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6080    match BlockRecord::parse(raw, block_size) {
6081        Ok(record) => {
6082            if record.block_index != expected_block_index {
6083                return Err(FormatError::InvalidArchive(
6084                    "BlockRecord index does not match stream position",
6085                ));
6086            }
6087            Ok(Some(record))
6088        }
6089        Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
6090        Err(err) => Err(err),
6091    }
6092}
6093
6094fn parse_terminal_material(
6095    bytes: &[u8],
6096    manifest_offset: usize,
6097    observed_block_count: u64,
6098    context: KeyHoldingTerminalContext<'_>,
6099    options: ReaderOptions,
6100) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
6101    let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
6102    if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
6103        return Err(FormatError::InvalidArchive(
6104            "sequential terminal does not end at EOF",
6105        ));
6106    }
6107    let terminal = candidate.terminal;
6108    if terminal.image.manifest_footer_offset != manifest_offset as u64 {
6109        return Err(FormatError::InvalidArchive(
6110            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6111        ));
6112    }
6113    if terminal.volume_trailer.block_count != observed_block_count {
6114        return Err(FormatError::InvalidArchive(
6115            "VolumeTrailer block_count does not match observed stream",
6116        ));
6117    }
6118    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6119    Ok((
6120        manifest_footer,
6121        terminal.volume_trailer,
6122        terminal.root_auth_footer,
6123    ))
6124}
6125
6126pub(crate) fn parse_terminal_material_read_at(
6127    reader: &dyn ArchiveReadAt,
6128    input_len: u64,
6129    manifest_offset: u64,
6130    observed_block_count: u64,
6131    context: KeyHoldingTerminalContext<'_>,
6132) -> Result<SequentialTerminalMaterial, FormatError> {
6133    let mut candidates = Vec::new();
6134    if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
6135        collect_v41_locator_candidate_read_at(
6136            reader,
6137            input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6138            0,
6139            context,
6140            &mut candidates,
6141        );
6142    }
6143    if input_len >= LOCATOR_PAIR_LEN as u64 {
6144        collect_v41_locator_candidate_read_at(
6145            reader,
6146            input_len - LOCATOR_PAIR_LEN as u64,
6147            1,
6148            context,
6149            &mut candidates,
6150        );
6151    }
6152
6153    let candidate = choose_v41_terminal_candidate(candidates)?;
6154    if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
6155        return Err(FormatError::InvalidArchive(
6156            "sequential terminal does not end at EOF",
6157        ));
6158    }
6159    let terminal = candidate.terminal;
6160    if terminal.image.manifest_footer_offset != manifest_offset {
6161        return Err(FormatError::InvalidArchive(
6162            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6163        ));
6164    }
6165    if terminal.volume_trailer.block_count != observed_block_count {
6166        return Err(FormatError::InvalidArchive(
6167            "VolumeTrailer block_count does not match observed stream",
6168        ));
6169    }
6170    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6171    Ok(SequentialTerminalMaterial {
6172        manifest_footer,
6173        volume_trailer: terminal.volume_trailer,
6174        root_auth_footer: terminal.root_auth_footer,
6175    })
6176}
6177
6178fn terminal_candidate_reaches_eof(
6179    candidate: &TerminalCandidate,
6180    input_len: usize,
6181) -> Result<bool, FormatError> {
6182    let expected_end =
6183        match candidate.locator_sequence {
6184            Some(0) => candidate.anchor,
6185            Some(1) => candidate
6186                .anchor
6187                .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6188                .ok_or(FormatError::InvalidArchive(
6189                    "terminal EOF boundary overflow",
6190                ))?,
6191            None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
6192                FormatError::InvalidArchive("terminal EOF boundary overflow"),
6193            )?,
6194            Some(_) => {
6195                return Err(FormatError::InvalidArchive(
6196                    "invalid terminal locator sequence",
6197                ))
6198            }
6199        };
6200    Ok(expected_end == input_len)
6201}
6202
6203#[derive(Debug, Default)]
6204struct PendingSequentialEnvelope {
6205    data_shards: Vec<Option<Vec<u8>>>,
6206    parity_shards: Vec<Option<Vec<u8>>>,
6207    saw_last_data: bool,
6208    awaiting_tentative_parity: bool,
6209}
6210
6211impl PendingSequentialEnvelope {
6212    fn is_empty(&self) -> bool {
6213        self.data_shards.is_empty() && self.parity_shards.is_empty()
6214    }
6215}
6216
6217fn handle_sequential_payload_erasure(
6218    pending: &mut PendingSequentialEnvelope,
6219    crypto_header: &CryptoHeaderFixed,
6220    metadata_seen: bool,
6221) -> Result<(), FormatError> {
6222    if metadata_seen || pending.saw_last_data {
6223        return Err(FormatError::BadCrc {
6224            structure: "BlockRecord",
6225        });
6226    }
6227    if !sequential_payload_parity_is_guaranteed(crypto_header) {
6228        return Err(FormatError::BadCrc {
6229            structure: "BlockRecord",
6230        });
6231    }
6232    pending.data_shards.push(None);
6233    pending.awaiting_tentative_parity = true;
6234    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
6235        return Err(FormatError::InvalidArchive(
6236            "sequential payload envelope exceeds data-shard cap",
6237        ));
6238    }
6239    Ok(())
6240}
6241
6242fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
6243    crypto_header.fec_parity_shards > 0
6244        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
6245}
6246
6247fn sequential_extract_tar_stream_with_options(
6248    bytes: &[u8],
6249    master_key: &MasterKey,
6250    options: ReaderOptions,
6251) -> Result<Vec<u8>, FormatError> {
6252    validate_reader_options(options)?;
6253    if bytes.len() < VOLUME_HEADER_LEN {
6254        return Err(FormatError::InvalidLength {
6255            structure: "archive",
6256            expected: VOLUME_HEADER_LEN,
6257            actual: bytes.len(),
6258        });
6259    }
6260
6261    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
6262    let crypto_start = volume_header.crypto_header_offset as usize;
6263    let crypto_len = volume_header.crypto_header_length as usize;
6264    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
6265    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
6266    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
6267    let subkeys = subkeys_for_open(
6268        Some(master_key),
6269        parsed_crypto.fixed.aead_algo,
6270        &volume_header.archive_uuid,
6271        &volume_header.session_id,
6272    )?;
6273    verify_integrity_tag(
6274        HmacDomain::CryptoHeader,
6275        parsed_crypto.fixed.aead_algo,
6276        Some(&subkeys.mac_key),
6277        &volume_header.archive_uuid,
6278        &volume_header.session_id,
6279        parsed_crypto.hmac_covered_bytes,
6280        &parsed_crypto.header_hmac,
6281    )?;
6282    parsed_crypto.validate_extension_semantics()?;
6283    validate_sequential_supported_volume(
6284        &volume_header,
6285        &parsed_crypto.fixed,
6286        &parsed_crypto.extensions,
6287    )?;
6288    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
6289
6290    let block_size = parsed_crypto.fixed.block_size as usize;
6291    let record_len = block_size
6292        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6293        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6294    let mut offset = crypto_end;
6295    let mut observed_block_count = 0u64;
6296    let mut metadata_seen = false;
6297    let mut pending = PendingSequentialEnvelope::default();
6298    let mut next_envelope_index = 0u64;
6299    let mut tar_stream = Vec::new();
6300    let max_tar_stream_size = options.max_verify_tar_size;
6301    let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
6302    let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
6303    let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
6304        parsed_crypto.fixed.max_path_length,
6305        total_extraction_cap,
6306    );
6307
6308    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6309        let record = parse_sequential_block_or_erasure(
6310            bytes,
6311            offset,
6312            record_len,
6313            block_size,
6314            &volume_header,
6315            observed_block_count,
6316        )?;
6317        observed_block_count = observed_block_count
6318            .checked_add(1)
6319            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6320        let Some(record) = record else {
6321            handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
6322            offset = checked_add(offset, record_len, "BlockRecord")?;
6323            continue;
6324        };
6325
6326        match record.kind {
6327            BlockKind::PayloadData => {
6328                if metadata_seen {
6329                    return Err(FormatError::InvalidArchive(
6330                        "payload BlockRecord appears after metadata",
6331                    ));
6332                }
6333                if pending.awaiting_tentative_parity {
6334                    return Err(FormatError::InvalidArchive(
6335                        "sequential payload envelope boundary is ambiguous after CRC erasure",
6336                    ));
6337                }
6338                if pending.saw_last_data {
6339                    finalize_sequential_envelope(
6340                        &mut pending,
6341                        SequentialEnvelopeDecodeContext {
6342                            crypto_header: &parsed_crypto.fixed,
6343                            subkeys: &subkeys,
6344                            volume_header: &volume_header,
6345                            next_envelope_index: &mut next_envelope_index,
6346                            tar_stream: &mut tar_stream,
6347                            max_tar_stream_size,
6348                            tar_stream_total_validator: &mut tar_stream_total_validator,
6349                        },
6350                    )?;
6351                }
6352                let is_last_data = record.is_last_data();
6353                pending.data_shards.push(Some(record.payload));
6354                if is_last_data {
6355                    pending.saw_last_data = true;
6356                }
6357                if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
6358                    return Err(FormatError::InvalidArchive(
6359                        "sequential payload envelope exceeds data-shard cap",
6360                    ));
6361                }
6362            }
6363            BlockKind::PayloadParity => {
6364                if metadata_seen {
6365                    return Err(FormatError::InvalidArchive(
6366                        "payload parity BlockRecord appears after metadata",
6367                    ));
6368                }
6369                if pending.awaiting_tentative_parity {
6370                    pending.awaiting_tentative_parity = false;
6371                    pending.saw_last_data = true;
6372                } else if pending.data_shards.is_empty() || !pending.saw_last_data {
6373                    return Err(FormatError::InvalidArchive(
6374                        "payload parity appears before envelope data is complete",
6375                    ));
6376                }
6377                pending.parity_shards.push(Some(record.payload));
6378                if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
6379                    return Err(FormatError::InvalidArchive(
6380                        "sequential payload envelope exceeds parity-shard cap",
6381                    ));
6382                }
6383            }
6384            _ => {
6385                if !pending.is_empty() {
6386                    finalize_sequential_envelope(
6387                        &mut pending,
6388                        SequentialEnvelopeDecodeContext {
6389                            crypto_header: &parsed_crypto.fixed,
6390                            subkeys: &subkeys,
6391                            volume_header: &volume_header,
6392                            next_envelope_index: &mut next_envelope_index,
6393                            tar_stream: &mut tar_stream,
6394                            max_tar_stream_size,
6395                            tar_stream_total_validator: &mut tar_stream_total_validator,
6396                        },
6397                    )?;
6398                }
6399                metadata_seen = true;
6400            }
6401        }
6402
6403        offset = checked_add(offset, record_len, "BlockRecord")?;
6404    }
6405
6406    if !pending.is_empty() {
6407        finalize_sequential_envelope(
6408            &mut pending,
6409            SequentialEnvelopeDecodeContext {
6410                crypto_header: &parsed_crypto.fixed,
6411                subkeys: &subkeys,
6412                volume_header: &volume_header,
6413                next_envelope_index: &mut next_envelope_index,
6414                tar_stream: &mut tar_stream,
6415                max_tar_stream_size,
6416                tar_stream_total_validator: &mut tar_stream_total_validator,
6417            },
6418        )?;
6419    }
6420
6421    parse_terminal_material(
6422        bytes,
6423        offset,
6424        observed_block_count,
6425        KeyHoldingTerminalContext {
6426            subkeys: &subkeys,
6427            volume_header: &volume_header,
6428            crypto_header: &parsed_crypto.fixed,
6429            crypto_header_bytes: crypto_bytes,
6430        },
6431        options,
6432    )?;
6433    // This public helper is intentionally whole-buffer: decoded payload bytes
6434    // stay internal until terminal ManifestFooter and VolumeTrailer HMACs pass.
6435    validate_tar_stream_total_extraction_size(
6436        &tar_stream,
6437        parsed_crypto.fixed.max_path_length,
6438        total_extraction_cap,
6439    )?;
6440    Ok(tar_stream)
6441}
6442
6443fn validate_sequential_supported_volume(
6444    volume_header: &VolumeHeader,
6445    crypto_header: &CryptoHeaderFixed,
6446    extensions: &[ExtensionTlv<'_>],
6447) -> Result<(), FormatError> {
6448    reject_unsupported_raw_stream_profile(extensions)?;
6449    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
6450        return Err(FormatError::ReaderUnsupported(
6451            "sequential reader supports only single-volume archive input",
6452        ));
6453    }
6454    if crypto_header.stripe_width != volume_header.stripe_width {
6455        return Err(FormatError::InvalidArchive(
6456            "VolumeHeader and CryptoHeader stripe_width differ",
6457        ));
6458    }
6459    if crypto_header.has_dictionary != 0 {
6460        return Err(FormatError::ReaderUnsupported(
6461            "dictionary bootstrap required for non-seekable sequential extraction",
6462        ));
6463    }
6464    Ok(())
6465}
6466
6467struct SequentialEnvelopeDecodeContext<'a> {
6468    crypto_header: &'a CryptoHeaderFixed,
6469    subkeys: &'a Subkeys,
6470    volume_header: &'a VolumeHeader,
6471    next_envelope_index: &'a mut u64,
6472    tar_stream: &'a mut Vec<u8>,
6473    max_tar_stream_size: usize,
6474    tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
6475}
6476
6477fn finalize_sequential_envelope(
6478    pending: &mut PendingSequentialEnvelope,
6479    context: SequentialEnvelopeDecodeContext<'_>,
6480) -> Result<(), FormatError> {
6481    if !pending.saw_last_data {
6482        return Err(FormatError::InvalidArchive(
6483            "sequential payload envelope is missing last-data flag",
6484        ));
6485    }
6486    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
6487        return Err(FormatError::InvalidArchive(
6488            "sequential payload envelope exceeds data-shard cap",
6489        ));
6490    }
6491    if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
6492        return Err(FormatError::InvalidArchive(
6493            "sequential payload envelope exceeds parity-shard cap",
6494        ));
6495    }
6496    let required_parity =
6497        required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
6498    if pending.parity_shards.len() < required_parity as usize {
6499        return Err(FormatError::InvalidArchive(
6500            "sequential payload envelope has insufficient parity for recovery settings",
6501        ));
6502    }
6503
6504    let repaired = repair_data_gf16(
6505        &pending.data_shards,
6506        &pending.parity_shards,
6507        context.crypto_header.block_size as usize,
6508    )?;
6509    let mut encrypted =
6510        Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
6511    for shard in repaired {
6512        encrypted.extend_from_slice(&shard);
6513    }
6514    let plaintext = decrypt_padded_aead_object(
6515        AeadObjectContext {
6516            algo: context.crypto_header.aead_algo,
6517            key: &context.subkeys.enc_key,
6518            nonce_seed: &context.subkeys.nonce_seed,
6519            domain: b"envelope",
6520            archive_uuid: &context.volume_header.archive_uuid,
6521            session_id: &context.volume_header.session_id,
6522            counter: *context.next_envelope_index,
6523        },
6524        &encrypted,
6525    )?;
6526    decode_concatenated_zstd_frames_with_cap(
6527        &plaintext,
6528        None,
6529        context.tar_stream,
6530        context.max_tar_stream_size,
6531        Some(context.tar_stream_total_validator),
6532    )?;
6533    *context.next_envelope_index = (*context.next_envelope_index)
6534        .checked_add(1)
6535        .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
6536    *pending = PendingSequentialEnvelope::default();
6537    Ok(())
6538}
6539
6540fn decode_concatenated_zstd_frames_with_cap(
6541    plaintext: &[u8],
6542    dictionary: Option<&[u8]>,
6543    output: &mut Vec<u8>,
6544    max_output_len: usize,
6545    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
6546) -> Result<(), FormatError> {
6547    let mut cursor = 0usize;
6548    while cursor < plaintext.len() {
6549        let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
6550            .map_err(|_| FormatError::InvalidZstdFrame)?;
6551        if frame_len == 0 {
6552            return Err(FormatError::InvalidZstdFrame);
6553        }
6554        let end = checked_add(cursor, frame_len, "zstd frame")?;
6555        validate_exact_zstd_frame(&plaintext[cursor..end])?;
6556        if let Some(dictionary) = dictionary {
6557            let mut decoder =
6558                zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
6559                    .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6560            read_zstd_frame_to_capped_output(
6561                &mut decoder,
6562                output,
6563                max_output_len,
6564                tar_stream_total_validator.as_deref_mut(),
6565            )?;
6566        } else {
6567            let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
6568                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6569            read_zstd_frame_to_capped_output(
6570                &mut decoder,
6571                output,
6572                max_output_len,
6573                tar_stream_total_validator.as_deref_mut(),
6574            )?;
6575        }
6576        cursor = end;
6577    }
6578    Ok(())
6579}
6580
6581fn read_zstd_frame_to_capped_output<R: Read>(
6582    decoder: &mut R,
6583    output: &mut Vec<u8>,
6584    max_output_len: usize,
6585    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
6586) -> Result<(), FormatError> {
6587    let mut buf = [0u8; 64 * 1024];
6588    loop {
6589        let read = decoder
6590            .read(&mut buf)
6591            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
6592        if read == 0 {
6593            return Ok(());
6594        }
6595        let next_len = output
6596            .len()
6597            .checked_add(read)
6598            .ok_or(FormatError::ReaderUnsupported(
6599                "sequential tar stream exceeds configured verification cap",
6600            ))?;
6601        if next_len > max_output_len {
6602            return Err(FormatError::ReaderUnsupported(
6603                "sequential tar stream exceeds configured verification cap",
6604            ));
6605        }
6606        output.extend_from_slice(&buf[..read]);
6607        if let Some(validator) = tar_stream_total_validator.as_mut() {
6608            validator.observe(output)?;
6609        }
6610    }
6611}
6612
6613fn load_archive_dictionary(
6614    blocks: &impl BlockProvider,
6615    subkeys: &Subkeys,
6616    volume_header: &VolumeHeader,
6617    crypto_header: &CryptoHeaderFixed,
6618    index_root: &IndexRoot,
6619) -> Result<Option<Vec<u8>>, FormatError> {
6620    if crypto_header.has_dictionary == 0 {
6621        return Ok(None);
6622    }
6623    let plaintext = load_metadata_object_from_parts(
6624        blocks,
6625        ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
6626        index_root.header.dictionary_decompressed_size,
6627    )?;
6628    Ok(Some(plaintext))
6629}
6630
6631#[derive(Clone, Copy)]
6632struct ObjectLoadContext<'a> {
6633    volume_header: &'a VolumeHeader,
6634    crypto_header: &'a CryptoHeaderFixed,
6635    extent: ObjectExtent,
6636    data_kind: BlockKind,
6637    parity_kind: BlockKind,
6638    key: &'a [u8; 32],
6639    nonce_seed: &'a [u8; 32],
6640    domain: &'a [u8],
6641    counter: u64,
6642    class_data_shard_max: u16,
6643    class_parity_shard_max: u16,
6644}
6645
6646impl<'a> ObjectLoadContext<'a> {
6647    fn index_root(
6648        volume_header: &'a VolumeHeader,
6649        crypto_header: &'a CryptoHeaderFixed,
6650        subkeys: &'a Subkeys,
6651        extent: ObjectExtent,
6652    ) -> Self {
6653        Self {
6654            volume_header,
6655            crypto_header,
6656            extent,
6657            data_kind: BlockKind::IndexRootData,
6658            parity_kind: BlockKind::IndexRootParity,
6659            key: &subkeys.index_root_key,
6660            nonce_seed: &subkeys.index_nonce_seed,
6661            domain: b"idxroot",
6662            counter: 0,
6663            class_data_shard_max: crypto_header.index_root_fec_data_shards,
6664            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
6665        }
6666    }
6667
6668    fn index_shard(
6669        volume_header: &'a VolumeHeader,
6670        crypto_header: &'a CryptoHeaderFixed,
6671        subkeys: &'a Subkeys,
6672        entry: &ShardEntry,
6673    ) -> Self {
6674        Self {
6675            volume_header,
6676            crypto_header,
6677            extent: ObjectExtent {
6678                first_block_index: entry.first_block_index,
6679                data_block_count: entry.data_block_count,
6680                parity_block_count: entry.parity_block_count,
6681                encrypted_size: entry.encrypted_size,
6682            },
6683            data_kind: BlockKind::IndexShardData,
6684            parity_kind: BlockKind::IndexShardParity,
6685            key: &subkeys.index_shard_key,
6686            nonce_seed: &subkeys.index_nonce_seed,
6687            domain: b"idxshard",
6688            counter: entry.shard_index,
6689            class_data_shard_max: crypto_header.index_fec_data_shards,
6690            class_parity_shard_max: crypto_header.index_fec_parity_shards,
6691        }
6692    }
6693
6694    fn directory_hint(
6695        volume_header: &'a VolumeHeader,
6696        crypto_header: &'a CryptoHeaderFixed,
6697        subkeys: &'a Subkeys,
6698        entry: &DirectoryHintShardEntry,
6699    ) -> Self {
6700        Self {
6701            volume_header,
6702            crypto_header,
6703            extent: ObjectExtent {
6704                first_block_index: entry.first_block_index,
6705                data_block_count: entry.data_block_count,
6706                parity_block_count: entry.parity_block_count,
6707                encrypted_size: entry.encrypted_size,
6708            },
6709            data_kind: BlockKind::DirectoryHintData,
6710            parity_kind: BlockKind::DirectoryHintParity,
6711            key: &subkeys.dir_hint_key,
6712            nonce_seed: &subkeys.index_nonce_seed,
6713            domain: b"dirhint",
6714            counter: entry.hint_shard_index,
6715            class_data_shard_max: crypto_header.index_fec_data_shards,
6716            class_parity_shard_max: crypto_header.index_fec_parity_shards,
6717        }
6718    }
6719
6720    fn dictionary(
6721        volume_header: &'a VolumeHeader,
6722        crypto_header: &'a CryptoHeaderFixed,
6723        subkeys: &'a Subkeys,
6724        index_root: &IndexRoot,
6725    ) -> Result<Self, FormatError> {
6726        Ok(Self {
6727            volume_header,
6728            crypto_header,
6729            extent: dictionary_extent_from_index_root(index_root)?,
6730            data_kind: BlockKind::DictionaryData,
6731            parity_kind: BlockKind::DictionaryParity,
6732            key: &subkeys.dictionary_key,
6733            nonce_seed: &subkeys.index_nonce_seed,
6734            domain: b"dict",
6735            counter: 0,
6736            class_data_shard_max: crypto_header.index_root_fec_data_shards,
6737            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
6738        })
6739    }
6740
6741    fn payload(
6742        volume_header: &'a VolumeHeader,
6743        crypto_header: &'a CryptoHeaderFixed,
6744        subkeys: &'a Subkeys,
6745        envelope: &EnvelopeEntry,
6746    ) -> Self {
6747        Self {
6748            volume_header,
6749            crypto_header,
6750            extent: ObjectExtent {
6751                first_block_index: envelope.first_block_index,
6752                data_block_count: envelope.data_block_count,
6753                parity_block_count: envelope.parity_block_count,
6754                encrypted_size: envelope.encrypted_size,
6755            },
6756            data_kind: BlockKind::PayloadData,
6757            parity_kind: BlockKind::PayloadParity,
6758            key: &subkeys.enc_key,
6759            nonce_seed: &subkeys.nonce_seed,
6760            domain: b"envelope",
6761            counter: envelope.envelope_index,
6762            class_data_shard_max: crypto_header.fec_data_shards,
6763            class_parity_shard_max: crypto_header.fec_parity_shards,
6764        }
6765    }
6766}
6767
6768fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
6769    if index_root.header.dictionary_data_block_count == 0
6770        || index_root.header.dictionary_encrypted_size == 0
6771        || index_root.header.dictionary_decompressed_size == 0
6772    {
6773        return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
6774    }
6775    Ok(ObjectExtent {
6776        first_block_index: index_root.header.dictionary_first_block,
6777        data_block_count: index_root.header.dictionary_data_block_count,
6778        parity_block_count: index_root.header.dictionary_parity_block_count,
6779        encrypted_size: index_root.header.dictionary_encrypted_size,
6780    })
6781}
6782
6783fn load_metadata_object_from_parts(
6784    blocks: &impl BlockProvider,
6785    context: ObjectLoadContext<'_>,
6786    decompressed_size: u32,
6787) -> Result<Vec<u8>, FormatError> {
6788    let compressed = load_decrypted_object_from_parts(blocks, context)?;
6789    decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
6790}
6791
6792fn load_decrypted_object_from_parts(
6793    blocks: &impl BlockProvider,
6794    context: ObjectLoadContext<'_>,
6795) -> Result<Vec<u8>, FormatError> {
6796    load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
6797}
6798
6799fn load_decrypted_object_from_parts_with_parity_policy(
6800    blocks: &impl BlockProvider,
6801    context: ObjectLoadContext<'_>,
6802    parity_policy: ParityReadPolicy,
6803) -> Result<Vec<u8>, FormatError> {
6804    let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
6805        blocks,
6806        context.crypto_header,
6807        context.extent,
6808        context.data_kind,
6809        context.parity_kind,
6810        context.class_data_shard_max,
6811        context.class_parity_shard_max,
6812        parity_policy,
6813    )?;
6814    let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
6815    for shard in repaired {
6816        encrypted.extend_from_slice(&shard);
6817    }
6818    if encrypted.len() != context.extent.encrypted_size as usize {
6819        return Err(FormatError::InvalidArchive(
6820            "object encrypted size does not match repaired shards",
6821        ));
6822    }
6823
6824    decrypt_padded_aead_object(
6825        AeadObjectContext {
6826            algo: context.crypto_header.aead_algo,
6827            key: context.key,
6828            nonce_seed: context.nonce_seed,
6829            domain: context.domain,
6830            archive_uuid: &context.volume_header.archive_uuid,
6831            session_id: &context.volume_header.session_id,
6832            counter: context.counter,
6833        },
6834        &encrypted,
6835    )
6836}
6837
6838fn load_repaired_object_data_shards_from_parts(
6839    blocks: &impl BlockProvider,
6840    crypto_header: &CryptoHeaderFixed,
6841    extent: ObjectExtent,
6842    data_kind: BlockKind,
6843    parity_kind: BlockKind,
6844    class_data_shard_max: u16,
6845    class_parity_shard_max: u16,
6846) -> Result<Vec<Vec<u8>>, FormatError> {
6847    load_repaired_object_data_shards_from_parts_with_parity_policy(
6848        blocks,
6849        crypto_header,
6850        extent,
6851        data_kind,
6852        parity_kind,
6853        class_data_shard_max,
6854        class_parity_shard_max,
6855        ParityReadPolicy::Always,
6856    )
6857}
6858
6859#[allow(clippy::too_many_arguments)]
6860fn load_repaired_object_data_shards_from_parts_with_parity_policy(
6861    blocks: &impl BlockProvider,
6862    crypto_header: &CryptoHeaderFixed,
6863    extent: ObjectExtent,
6864    data_kind: BlockKind,
6865    parity_kind: BlockKind,
6866    class_data_shard_max: u16,
6867    class_parity_shard_max: u16,
6868    parity_policy: ParityReadPolicy,
6869) -> Result<Vec<Vec<u8>>, FormatError> {
6870    validate_object_extent(
6871        extent,
6872        crypto_header,
6873        class_data_shard_max,
6874        class_parity_shard_max,
6875    )?;
6876    let block_size = crypto_header.block_size as usize;
6877    let data_count = extent.data_block_count as usize;
6878    let parity_count = extent.parity_block_count as usize;
6879    let mut data_shards = Vec::with_capacity(data_count);
6880    let mut parity_shards = Vec::with_capacity(parity_count);
6881
6882    for offset in 0..data_count {
6883        let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
6884        if let Some(record) = blocks.block(block_index)? {
6885            if record.kind != data_kind {
6886                return Err(FormatError::InvalidArchive(
6887                    "object data block has unexpected kind",
6888                ));
6889            }
6890            let should_be_last = offset + 1 == data_count;
6891            if record.is_last_data() != should_be_last {
6892                return Err(FormatError::InvalidArchive(
6893                    "object last-data flag is not on the final data block",
6894                ));
6895            }
6896            data_shards.push(Some(record.payload.clone()));
6897        } else {
6898            data_shards.push(None);
6899        }
6900    }
6901
6902    if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
6903        return repair_data_gf16(&data_shards, &[], block_size);
6904    }
6905
6906    for offset in 0..parity_count {
6907        let block_index = checked_u64_add(
6908            extent.first_block_index,
6909            data_count as u64 + offset as u64,
6910            "object",
6911        )?;
6912        if let Some(record) = blocks.block(block_index)? {
6913            if record.kind != parity_kind {
6914                return Err(FormatError::InvalidArchive(
6915                    "object parity block has unexpected kind",
6916                ));
6917            }
6918            if record.is_last_data() {
6919                return Err(FormatError::InvalidArchive(
6920                    "object parity block has last-data flag",
6921                ));
6922            }
6923            parity_shards.push(Some(record.payload.clone()));
6924        } else {
6925            parity_shards.push(None);
6926        }
6927    }
6928
6929    repair_data_gf16(&data_shards, &parity_shards, block_size)
6930}
6931
6932fn validate_object_extent(
6933    extent: ObjectExtent,
6934    crypto_header: &CryptoHeaderFixed,
6935    class_data_shard_max: u16,
6936    class_parity_shard_max: u16,
6937) -> Result<(), FormatError> {
6938    if extent.data_block_count == 0 || extent.encrypted_size == 0 {
6939        return Err(FormatError::InvalidArchive(
6940            "encrypted object has zero data blocks or size",
6941        ));
6942    }
6943    if extent.data_block_count > class_data_shard_max as u32 {
6944        return Err(FormatError::InvalidArchive(
6945            "encrypted object exceeds its class data-shard maximum",
6946        ));
6947    }
6948    if extent.parity_block_count > class_parity_shard_max as u32 {
6949        return Err(FormatError::InvalidArchive(
6950            "encrypted object exceeds its class parity-shard maximum",
6951        ));
6952    }
6953    let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
6954    if extent.parity_block_count != required_parity {
6955        return Err(FormatError::InvalidArchive(
6956            "encrypted object parity does not match v41 compute_parity",
6957        ));
6958    }
6959    let total = checked_u64_add(
6960        extent.data_block_count as u64,
6961        extent.parity_block_count as u64,
6962        "encrypted object shard count overflow",
6963    )?;
6964    if total > 65_535 {
6965        return Err(FormatError::FecTooManyShards(total as usize));
6966    }
6967    let expected = checked_u64_mul(
6968        extent.data_block_count as u64,
6969        crypto_header.block_size as u64,
6970        "encrypted object size overflow",
6971    )?;
6972    if expected != extent.encrypted_size as u64 {
6973        return Err(FormatError::InvalidArchive(
6974            "encrypted object size is not data_block_count * block_size",
6975        ));
6976    }
6977    if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
6978        return Err(FormatError::InvalidArchive(
6979            "encrypted object is too small for AEAD tag",
6980        ));
6981    }
6982    Ok(())
6983}
6984
6985pub(crate) fn required_object_parity(
6986    data_block_count: u64,
6987    crypto_header: &CryptoHeaderFixed,
6988) -> Result<u32, FormatError> {
6989    let min_parity =
6990        if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
6991            1
6992        } else {
6993            0
6994        };
6995    let mut parity = 0u64;
6996    for _ in 0..100 {
6997        let total = data_block_count
6998            .checked_add(parity)
6999            .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
7000        let by_volume = checked_u64_mul(
7001            crypto_header.volume_loss_tolerance as u64,
7002            ceil_div_u64(total, crypto_header.stripe_width as u64)?,
7003            "volume-loss parity overflow",
7004        )?;
7005        let by_bitrot = ceil_div_u64(
7006            checked_u64_mul(
7007                total,
7008                crypto_header.bit_rot_buffer_pct as u64,
7009                "bit-rot parity overflow",
7010            )?,
7011            100,
7012        )?;
7013        let next = by_volume
7014            .checked_add(by_bitrot)
7015            .ok_or(FormatError::InvalidArchive("parity overflow"))?
7016            .max(min_parity);
7017        if next == parity {
7018            return u32::try_from(next)
7019                .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
7020        }
7021        parity = next;
7022    }
7023    Err(FormatError::InvalidArchive(
7024        "parity calculation did not converge",
7025    ))
7026}
7027
7028fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
7029    if denominator == 0 {
7030        return Err(FormatError::InvalidArchive("division by zero"));
7031    }
7032    numerator
7033        .checked_add(denominator - 1)
7034        .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
7035        .map(|value| value / denominator)
7036}
7037
7038fn frame_range_for_file<'b>(
7039    shard: &'b IndexShard,
7040    file: &FileEntry,
7041) -> Result<Vec<&'b FrameEntry>, FormatError> {
7042    let mut frames = Vec::with_capacity(file.frame_count as usize);
7043    for offset in 0..file.frame_count as u64 {
7044        let frame_index =
7045            file.first_frame_index
7046                .checked_add(offset)
7047                .ok_or(FormatError::InvalidArchive(
7048                    "FileEntry frame range overflow",
7049                ))?;
7050        let frame = shard
7051            .frames
7052            .iter()
7053            .find(|entry| entry.frame_index == frame_index)
7054            .ok_or(FormatError::InvalidArchive(
7055                "FileEntry references missing FrameEntry",
7056            ))?;
7057        frames.push(frame);
7058    }
7059    Ok(frames)
7060}
7061
7062fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
7063    MetadataLimits {
7064        block_size: crypto_header.block_size,
7065        max_path_length: crypto_header.max_path_length,
7066        max_payload_data_shards: crypto_header.fec_data_shards,
7067        max_payload_parity_shards: crypto_header.fec_parity_shards,
7068        max_index_data_shards: crypto_header.index_fec_data_shards,
7069        max_index_parity_shards: crypto_header.index_fec_parity_shards,
7070        max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
7071        max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
7072        ..MetadataLimits::default()
7073    }
7074}
7075
7076fn verify_dense_keys<T>(
7077    entries: &BTreeMap<u64, T>,
7078    expected_count: u64,
7079    structure: &'static str,
7080) -> Result<(), FormatError> {
7081    if entries.len() as u64 != expected_count {
7082        return Err(FormatError::InvalidArchive(
7083            "decoded table count does not match IndexRoot",
7084        ));
7085    }
7086    for expected in 0..expected_count {
7087        if !entries.contains_key(&expected) {
7088            return Err(FormatError::InvalidMetadata {
7089                structure,
7090                reason: "global index coverage has a gap",
7091            });
7092        }
7093    }
7094    Ok(())
7095}
7096
7097fn validate_envelope_frame_coverage(
7098    frames: &BTreeMap<u64, FrameEntry>,
7099    envelopes: &BTreeMap<u64, EnvelopeEntry>,
7100) -> Result<(), FormatError> {
7101    let mut accounted_frames = BTreeSet::new();
7102    for envelope in envelopes.values() {
7103        let first = envelope.first_frame_index;
7104        let end =
7105            first
7106                .checked_add(envelope.frame_count as u64)
7107                .ok_or(FormatError::InvalidArchive(
7108                    "EnvelopeEntry frame range overflow",
7109                ))?;
7110        let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
7111        for frame_index in first..end {
7112            let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
7113                "EnvelopeEntry references missing FrameEntry",
7114            ))?;
7115            if frame.envelope_index != envelope.envelope_index {
7116                return Err(FormatError::InvalidArchive(
7117                    "FrameEntry envelope_index does not match containing EnvelopeEntry",
7118                ));
7119            }
7120            if !accounted_frames.insert(frame_index) {
7121                return Err(FormatError::InvalidArchive(
7122                    "FrameEntry is covered by multiple EnvelopeEntries",
7123                ));
7124            }
7125            let start = frame.offset_in_envelope as usize;
7126            let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
7127            if end > envelope.plaintext_size as usize {
7128                return Err(FormatError::InvalidArchive(
7129                    "FrameEntry exceeds EnvelopeEntry plaintext_size",
7130                ));
7131            }
7132            ranges.push((start, end));
7133        }
7134        validate_exact_coverage_ranges(
7135            &mut ranges,
7136            envelope.plaintext_size as usize,
7137            "EnvelopeEntry frame coverage has a gap or overlap",
7138        )?;
7139    }
7140
7141    for frame_index in frames.keys() {
7142        if !accounted_frames.contains(frame_index) {
7143            return Err(FormatError::InvalidArchive(
7144                "FrameEntry is not covered by any EnvelopeEntry",
7145            ));
7146        }
7147    }
7148    Ok(())
7149}
7150
7151fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
7152    let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
7153    for shard in shards {
7154        for (idx, file) in shard.files.iter().enumerate() {
7155            let path = shard
7156                .file_path(idx)
7157                .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
7158                .to_vec();
7159            let start = shard
7160                .tar_member_group_start(idx)
7161                .ok_or(FormatError::InvalidArchive(
7162                    "FileEntry tar member start is missing",
7163                ))?;
7164            let key = (file.path_hash, path, start);
7165            validate_global_file_table_key_step(previous.as_ref(), &key)?;
7166            previous = Some(key);
7167        }
7168    }
7169    Ok(())
7170}
7171
7172fn validate_global_file_table_key_step(
7173    previous: Option<&([u8; 8], Vec<u8>, u64)>,
7174    current: &([u8; 8], Vec<u8>, u64),
7175) -> Result<(), FormatError> {
7176    if let Some(previous) = previous {
7177        if previous >= current {
7178            return Err(FormatError::InvalidArchive(
7179                "global FileEntry rows are not sorted and unique",
7180            ));
7181        }
7182    }
7183    Ok(())
7184}
7185
7186fn validate_file_extent_coverage_ranges(
7187    extents: &[(u64, u64)],
7188    tar_len: u64,
7189) -> Result<(), FormatError> {
7190    let mut ranges = Vec::with_capacity(extents.len());
7191    for (start, len) in extents {
7192        let end = checked_u64_add(*start, *len, "FileEntry")?;
7193        if end > tar_len {
7194            return Err(FormatError::InvalidArchive(
7195                "FileEntry extent exceeds IndexRoot tar_total_size",
7196            ));
7197        }
7198        ranges.push((*start, end));
7199    }
7200    validate_exact_coverage_ranges_u64(
7201        &mut ranges,
7202        tar_len,
7203        "FileEntry extents do not cover tar stream exactly",
7204    )
7205}
7206
7207fn add_expected_directory_hint_rows(
7208    map: &mut DirectoryHintMap,
7209    shard_row_index: u32,
7210    path: &[u8],
7211    kind: TarEntryKind,
7212) {
7213    map.entry(Vec::new()).or_default().insert(shard_row_index);
7214    for (idx, byte) in path.iter().enumerate() {
7215        if *byte == b'/' {
7216            map.entry(path[..idx].to_vec())
7217                .or_default()
7218                .insert(shard_row_index);
7219        }
7220    }
7221    if kind == TarEntryKind::Directory {
7222        map.entry(path.to_vec())
7223            .or_default()
7224            .insert(shard_row_index);
7225    }
7226}
7227
7228fn validate_directory_hint_tables_against_expected(
7229    tables: &[DirectoryHintTable],
7230    expected: &DirectoryHintMap,
7231) -> Result<(), FormatError> {
7232    let mut actual = Vec::new();
7233    let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
7234
7235    for table in tables {
7236        for entry_index in 0..table.entries.len() {
7237            let path = table
7238                .entry_path(entry_index)
7239                .ok_or(FormatError::InvalidArchive(
7240                    "DirectoryHintEntry path is missing",
7241                ))?;
7242            let key = (hash_prefix(path), path.to_vec());
7243            if let Some(previous) = &previous_key {
7244                if previous >= &key {
7245                    return Err(FormatError::InvalidArchive(
7246                        "DirectoryHintEntry rows are not globally sorted",
7247                    ));
7248                }
7249            }
7250            previous_key = Some(key);
7251
7252            let rows =
7253                table
7254                    .shard_rows_for_entry(entry_index)
7255                    .ok_or(FormatError::InvalidArchive(
7256                        "DirectoryHintEntry shard rows are missing",
7257                    ))?;
7258            actual.push((path.to_vec(), rows.to_vec()));
7259        }
7260    }
7261
7262    if actual != sorted_directory_hint_rows(expected) {
7263        return Err(FormatError::InvalidArchive(
7264            "directory hint map does not match decoded files",
7265        ));
7266    }
7267    Ok(())
7268}
7269
7270fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
7271    let mut rows = map
7272        .iter()
7273        .map(|(path, shard_rows)| {
7274            (
7275                path.clone(),
7276                shard_rows.iter().copied().collect::<Vec<u32>>(),
7277            )
7278        })
7279        .collect::<Vec<_>>();
7280    rows.sort_by(|(left_path, _), (right_path, _)| {
7281        hash_prefix(left_path)
7282            .cmp(&hash_prefix(right_path))
7283            .then_with(|| left_path.cmp(right_path))
7284    });
7285    rows
7286}
7287
7288fn validate_exact_coverage_ranges(
7289    ranges: &mut [(usize, usize)],
7290    expected_end: usize,
7291    reason: &'static str,
7292) -> Result<(), FormatError> {
7293    ranges.sort_unstable();
7294    let mut cursor = 0usize;
7295    for (start, end) in ranges.iter().copied() {
7296        if start != cursor || end < start {
7297            return Err(FormatError::InvalidArchive(reason));
7298        }
7299        cursor = end;
7300    }
7301    if cursor != expected_end {
7302        return Err(FormatError::InvalidArchive(reason));
7303    }
7304    Ok(())
7305}
7306
7307fn validate_exact_coverage_ranges_u64(
7308    ranges: &mut [(u64, u64)],
7309    expected_end: u64,
7310    reason: &'static str,
7311) -> Result<(), FormatError> {
7312    ranges.sort_unstable();
7313    let mut cursor = 0u64;
7314    for (start, end) in ranges.iter().copied() {
7315        if start != cursor || end < start {
7316            return Err(FormatError::InvalidArchive(reason));
7317        }
7318        cursor = end;
7319    }
7320    if cursor != expected_end {
7321        return Err(FormatError::InvalidArchive(reason));
7322    }
7323    Ok(())
7324}
7325
7326fn object_block_range(
7327    first_block_index: u64,
7328    data_block_count: u32,
7329    parity_block_count: u32,
7330    structure: &'static str,
7331) -> Result<(u64, u64), FormatError> {
7332    let total = data_block_count as u64 + parity_block_count as u64;
7333    if total == 0 {
7334        return Err(FormatError::InvalidArchive(structure));
7335    }
7336    let end = checked_u64_add(first_block_index, total, structure)?;
7337    Ok((first_block_index, end))
7338}
7339
7340fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
7341    ranges.sort_unstable();
7342    for pair in ranges.windows(2) {
7343        if pair[0].1 > pair[1].0 {
7344            return Err(FormatError::InvalidArchive(
7345                "encrypted object block ranges overlap",
7346            ));
7347        }
7348    }
7349    Ok(())
7350}
7351
7352pub(crate) fn observed_archive_size(
7353    sizes: impl IntoIterator<Item = u64>,
7354) -> Result<u64, FormatError> {
7355    sizes.into_iter().try_fold(0u64, |sum, size| {
7356        sum.checked_add(size).ok_or(FormatError::InvalidArchive(
7357            "observed archive size overflow",
7358        ))
7359    })
7360}
7361
7362pub(crate) fn total_extraction_size_cap(
7363    options: ReaderOptions,
7364    observed_archive_bytes: u64,
7365) -> u64 {
7366    options
7367        .max_total_extraction_size
7368        .min(observed_archive_bytes.saturating_mul(10))
7369}
7370
7371fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
7372    std::str::from_utf8(bytes)
7373        .map(|path| path.to_owned())
7374        .map_err(|_| FormatError::UnsafeArchivePath)
7375}
7376
7377fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
7378    let mut bytes = [0u8; 104];
7379    bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
7380    bytes[36..40].fill(0);
7381    bytes
7382}
7383
7384fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
7385    let digest = Sha256::digest(bytes);
7386    let mut out = [0u8; 32];
7387    out.copy_from_slice(&digest);
7388    out
7389}
7390
7391fn slice<'b>(
7392    bytes: &'b [u8],
7393    offset: usize,
7394    len: usize,
7395    structure: &'static str,
7396) -> Result<&'b [u8], FormatError> {
7397    let end = checked_add(offset, len, structure)?;
7398    bytes.get(offset..end).ok_or(FormatError::InvalidLength {
7399        structure,
7400        expected: end,
7401        actual: bytes.len(),
7402    })
7403}
7404
7405fn read_at_vec(
7406    reader: &dyn ArchiveReadAt,
7407    offset: u64,
7408    len: usize,
7409    structure: &'static str,
7410) -> Result<Vec<u8>, FormatError> {
7411    let expected_end = offset
7412        .checked_add(len as u64)
7413        .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
7414    let observed_len = reader.len()?;
7415    if expected_end > observed_len {
7416        return Err(FormatError::InvalidLength {
7417            structure,
7418            expected: to_usize(expected_end, structure)?,
7419            actual: to_usize(observed_len, structure)?,
7420        });
7421    }
7422    let mut out = vec![0u8; len];
7423    reader.read_exact_at(offset, &mut out)?;
7424    Ok(out)
7425}
7426
7427fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
7428where
7429    T: Sync,
7430    U: Send,
7431    F: Fn(&T) -> Result<U, FormatError> + Sync,
7432{
7433    if jobs <= 1 || items.len() <= 1 {
7434        return items.iter().map(f).collect();
7435    }
7436    let worker_count = jobs.min(items.len());
7437    let chunk_size = items.len().div_ceil(worker_count);
7438    let mut out = Vec::with_capacity(items.len());
7439    thread::scope(|scope| {
7440        let handles = items
7441            .chunks(chunk_size)
7442            .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
7443            .collect::<Vec<_>>();
7444        for handle in handles {
7445            let mut chunk = handle
7446                .join()
7447                .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
7448            out.append(&mut chunk);
7449        }
7450        Ok(out)
7451    })
7452}
7453
7454fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
7455    lhs.checked_add(rhs)
7456        .ok_or(FormatError::InvalidArchive(structure))
7457}
7458
7459fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
7460    lhs.checked_add(rhs)
7461        .ok_or(FormatError::InvalidArchive(structure))
7462}
7463
7464fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
7465    usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
7466}
7467
7468#[cfg(test)]
7469mod tests {
7470    use std::fs;
7471
7472    use super::*;
7473    use crate::compression::compress_zstd_frame;
7474    use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
7475    use crate::fec::encode_parity_gf16;
7476    use crate::format::{
7477        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
7478        VOLUME_FORMAT_REV,
7479    };
7480    use crate::metadata::{
7481        DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
7482        ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
7483    };
7484    use crate::non_seekable_reader::{
7485        extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
7486        verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
7487        NonSeekableReaderOptions, SequentialRootAuthStatus,
7488    };
7489    use crate::writer::{
7490        write_archive, write_archive_with_dictionary, write_archive_with_kdf,
7491        write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
7492        WriterOptions,
7493    };
7494
7495    fn master_key() -> MasterKey {
7496        MasterKey::from_raw_key(&[0x42; 32]).unwrap()
7497    }
7498
7499    #[test]
7500    fn reader_defaults_use_available_parallelism_jobs() {
7501        let options = ReaderOptions::default();
7502
7503        assert_eq!(options.jobs, default_jobs());
7504        assert!(options.jobs >= 1);
7505    }
7506
7507    #[test]
7508    fn reader_options_reject_zero_jobs() {
7509        let err = OpenedArchive::open_with_options(
7510            &[],
7511            &master_key(),
7512            ReaderOptions {
7513                jobs: 0,
7514                ..ReaderOptions::default()
7515            },
7516        )
7517        .unwrap_err();
7518
7519        assert_eq!(
7520            err,
7521            FormatError::ReaderUnsupported("jobs must be at least 1")
7522        );
7523    }
7524
7525    const TEST_ROOT_AUTH_ID: u16 = 0xe001;
7526    const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
7527
7528    fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
7529        RootAuthWriterConfig {
7530            authenticator_id: TEST_ROOT_AUTH_ID,
7531            signer_identity_type: 0,
7532            signer_identity: &[],
7533            authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
7534        }
7535    }
7536
7537    fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
7538        request.archive_root.to_vec()
7539    }
7540
7541    fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
7542        footer.authenticator_id == TEST_ROOT_AUTH_ID
7543            && footer.signer_identity_type == 0
7544            && footer.signer_identity_bytes.is_empty()
7545            && footer.authenticator_value.as_slice() == archive_root
7546    }
7547
7548    fn dictionary() -> &'static [u8] {
7549        b"dir/dict.txt common words common words common words dictionary payload"
7550    }
7551
7552    #[derive(Clone)]
7553    struct CountingReadAt {
7554        bytes: std::sync::Arc<Vec<u8>>,
7555        reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
7556        denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
7557    }
7558
7559    impl CountingReadAt {
7560        fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
7561            Self {
7562                bytes: std::sync::Arc::new(bytes),
7563                reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
7564                denied_ranges: std::sync::Arc::new(denied_ranges),
7565            }
7566        }
7567
7568        fn reads(&self) -> Vec<(u64, u64)> {
7569            self.reads.lock().unwrap().clone()
7570        }
7571    }
7572
7573    impl ArchiveReadAt for CountingReadAt {
7574        fn len(&self) -> Result<u64, FormatError> {
7575            u64::try_from(self.bytes.as_ref().len())
7576                .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
7577        }
7578
7579        fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
7580            let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
7581            self.reads.lock().unwrap().push((offset, end));
7582            if self
7583                .denied_ranges
7584                .iter()
7585                .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
7586            {
7587                return Err(FormatError::InvalidArchive("denied test read"));
7588            }
7589            let start = to_usize(offset, "archive")?;
7590            let end_usize = checked_add(start, buf.len(), "archive")?;
7591            let source = self
7592                .bytes
7593                .get(start..end_usize)
7594                .ok_or(FormatError::InvalidLength {
7595                    structure: "archive",
7596                    expected: end_usize,
7597                    actual: self.bytes.as_ref().len(),
7598                })?;
7599            buf.copy_from_slice(source);
7600            Ok(())
7601        }
7602    }
7603
7604    fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
7605        left_start < right_end && right_start < left_end
7606    }
7607
7608    fn single_stream_options() -> WriterOptions {
7609        WriterOptions {
7610            stripe_width: 1,
7611            volume_loss_tolerance: 0,
7612            ..WriterOptions::default()
7613        }
7614    }
7615
7616    struct ChunkedReader {
7617        bytes: Vec<u8>,
7618        cursor: usize,
7619        max_chunk: usize,
7620    }
7621
7622    impl ChunkedReader {
7623        fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
7624            Self {
7625                bytes,
7626                cursor: 0,
7627                max_chunk,
7628            }
7629        }
7630    }
7631
7632    impl std::io::Read for ChunkedReader {
7633        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
7634            if self.cursor >= self.bytes.len() {
7635                return Ok(0);
7636            }
7637            let available = self.bytes.len() - self.cursor;
7638            let len = available.min(buf.len()).min(self.max_chunk);
7639            buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
7640            self.cursor += len;
7641            Ok(len)
7642        }
7643    }
7644
7645    #[test]
7646    fn global_file_table_key_step_rejects_distinct_path_regression() {
7647        let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
7648        let current = ([1u8; 8], b"a.txt".to_vec(), 0);
7649
7650        assert_eq!(
7651            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
7652            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7653        );
7654    }
7655
7656    #[test]
7657    fn global_file_table_key_step_rejects_duplicate_full_key() {
7658        let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
7659        let current = ([1u8; 8], b"a.txt".to_vec(), 7);
7660
7661        assert_eq!(
7662            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
7663            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
7664        );
7665    }
7666
7667    fn small_block_recovery_options() -> WriterOptions {
7668        WriterOptions {
7669            block_size: 4096,
7670            chunk_size: 32 * 1024,
7671            envelope_target_size: 32 * 1024,
7672            stripe_width: 1,
7673            volume_loss_tolerance: 0,
7674            bit_rot_buffer_pct: 1,
7675            fec_data_shards: 16,
7676            fec_parity_shards: 1,
7677            index_fec_data_shards: 4,
7678            index_fec_parity_shards: 1,
7679            index_root_fec_data_shards: 16,
7680            index_root_fec_parity_shards: 1,
7681            ..WriterOptions::default()
7682        }
7683    }
7684
7685    fn parity_rich_recovery_options() -> WriterOptions {
7686        WriterOptions {
7687            block_size: 4096,
7688            chunk_size: 32 * 1024,
7689            envelope_target_size: 32 * 1024,
7690            stripe_width: 1,
7691            volume_loss_tolerance: 0,
7692            bit_rot_buffer_pct: 40,
7693            fec_data_shards: 16,
7694            fec_parity_shards: 16,
7695            index_fec_data_shards: 4,
7696            index_fec_parity_shards: 4,
7697            index_root_fec_data_shards: 16,
7698            index_root_fec_parity_shards: 16,
7699            ..WriterOptions::default()
7700        }
7701    }
7702
7703    fn pseudo_random_bytes(len: usize) -> Vec<u8> {
7704        let mut state = 0x1234_5678u32;
7705        (0..len)
7706            .map(|_| {
7707                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
7708                (state >> 24) as u8
7709            })
7710            .collect()
7711    }
7712
7713    #[test]
7714    fn opens_lists_verifies_and_extracts_one_file_archive() {
7715        let archive = write_archive(
7716            &[RegularFile::new("dir/hello.txt", b"hello m7")],
7717            &master_key(),
7718            single_stream_options(),
7719        )
7720        .unwrap();
7721        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7722
7723        assert_eq!(
7724            opened.list_files().unwrap(),
7725            vec![ArchiveEntry {
7726                path: "dir/hello.txt".to_string(),
7727                file_data_size: 8,
7728                kind: TarEntryKind::Regular,
7729                mode: 0o644,
7730                mtime: 0,
7731                diagnostics: Vec::new(),
7732            }]
7733        );
7734        opened.verify().unwrap();
7735        assert_eq!(
7736            opened.extract_file("dir/hello.txt").unwrap(),
7737            Some(b"hello m7".to_vec())
7738        );
7739        assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
7740    }
7741
7742    #[test]
7743    fn root_auth_archive_round_trips_and_verifies_with_callback() {
7744        let archive = write_archive_with_root_auth(
7745            &[RegularFile::new("signed.txt", b"root-auth payload")],
7746            &master_key(),
7747            single_stream_options(),
7748            RootAuthWriterConfig {
7749                authenticator_id: 0x7777,
7750                signer_identity_type: 1,
7751                signer_identity: b"test signer",
7752                authenticator_value_length: 32,
7753            },
7754            |request| Ok(request.archive_root.to_vec()),
7755        )
7756        .unwrap();
7757
7758        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7759        opened.verify().unwrap();
7760        let verified = opened
7761            .verify_root_auth_with(|footer, archive_root| {
7762                Ok(footer.authenticator_value == archive_root.as_slice())
7763            })
7764            .unwrap();
7765
7766        assert_eq!(verified.authenticator_id, 0x7777);
7767        assert_eq!(verified.signer_identity_type, 1);
7768        assert_eq!(verified.signer_identity_bytes, b"test signer");
7769        assert_eq!(
7770            verified.archive_root,
7771            opened.root_auth_footer.as_ref().unwrap().archive_root
7772        );
7773        assert_eq!(
7774            verified.diagnostics,
7775            vec![
7776                RootAuthDiagnostic::RootAuthContentVerified,
7777                RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
7778                RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
7779                RootAuthDiagnostic::RecoveryMarginUnchecked,
7780            ]
7781        );
7782    }
7783
7784    #[test]
7785    fn root_auth_verification_requires_authenticator_success() {
7786        let archive = write_archive_with_root_auth(
7787            &[RegularFile::new("signed.txt", b"root-auth payload")],
7788            &master_key(),
7789            single_stream_options(),
7790            RootAuthWriterConfig {
7791                authenticator_id: 9,
7792                signer_identity_type: 1,
7793                signer_identity: b"test signer",
7794                authenticator_value_length: 32,
7795            },
7796            |request| Ok(request.archive_root.to_vec()),
7797        )
7798        .unwrap();
7799        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
7800
7801        assert_eq!(
7802            opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
7803            FormatError::InvalidArchive("root-auth authenticator verification failed")
7804        );
7805    }
7806
7807    #[test]
7808    fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
7809        let archive = write_archive_with_root_auth(
7810            &[RegularFile::new("public.txt", b"public commitment")],
7811            &master_key(),
7812            single_stream_options(),
7813            RootAuthWriterConfig {
7814                authenticator_id: 0x2222,
7815                signer_identity_type: 1,
7816                signer_identity: b"public verifier",
7817                authenticator_value_length: 32,
7818            },
7819            |request| Ok(request.archive_root.to_vec()),
7820        )
7821        .unwrap();
7822
7823        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
7824            Ok(footer.authenticator_value == archive_root.as_slice())
7825        })
7826        .unwrap();
7827
7828        assert_eq!(verified.authenticator_id, 0x2222);
7829        assert_eq!(verified.signer_identity_bytes, b"public verifier");
7830        assert!(verified.total_data_block_count > 0);
7831    }
7832
7833    #[test]
7834    fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
7835        let archive = write_archive_with_root_auth(
7836            &[RegularFile::new(
7837                "public-fields.txt",
7838                b"public source authority",
7839            )],
7840            &master_key(),
7841            single_stream_options(),
7842            RootAuthWriterConfig {
7843                authenticator_id: 0x2222,
7844                signer_identity_type: 1,
7845                signer_identity: b"public verifier",
7846                authenticator_value_length: 32,
7847            },
7848            |request| Ok(request.archive_root.to_vec()),
7849        )
7850        .unwrap();
7851        let mut bytes = archive.bytes.clone();
7852
7853        rewrite_public_cmra_image(&mut bytes, |image| {
7854            let manifest_region = image
7855                .regions
7856                .iter_mut()
7857                .find(|region| region.region_type == 3)
7858                .unwrap();
7859            manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
7860
7861            let trailer_region = image
7862                .regions
7863                .iter_mut()
7864                .find(|region| region.region_type == 5)
7865                .unwrap();
7866            let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
7867            trailer.block_count += 7;
7868            trailer_region.bytes = trailer.to_bytes().to_vec();
7869        });
7870
7871        public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
7872            Ok(footer.authenticator_value == archive_root.as_slice())
7873        })
7874        .unwrap();
7875    }
7876
7877    #[test]
7878    fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
7879        let archive = write_archive_with_root_auth(
7880            &[RegularFile::new(
7881                "public-crypto.txt",
7882                b"cross-volume public profile",
7883            )],
7884            &master_key(),
7885            WriterOptions {
7886                stripe_width: 2,
7887                volume_loss_tolerance: 0,
7888                ..WriterOptions::default()
7889            },
7890            RootAuthWriterConfig {
7891                authenticator_id: 0x3333,
7892                signer_identity_type: 1,
7893                signer_identity: b"public verifier",
7894                authenticator_value_length: 32,
7895            },
7896            |request| Ok(request.archive_root.to_vec()),
7897        )
7898        .unwrap();
7899        let mut volumes = archive.volumes.clone();
7900        let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
7901        let crypto_offset = volume_header.crypto_header_offset as usize;
7902        let expected_volume_size = 123_456_789u64;
7903        volumes[1][crypto_offset + 52..crypto_offset + 60]
7904            .copy_from_slice(&expected_volume_size.to_le_bytes());
7905        rewrite_public_cmra_image(&mut volumes[1], |image| {
7906            let crypto_region = image
7907                .regions
7908                .iter_mut()
7909                .find(|region| region.region_type == 2)
7910                .unwrap();
7911            crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
7912        });
7913
7914        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
7915        public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
7916            Ok(footer.authenticator_value == archive_root.as_slice())
7917        })
7918        .unwrap();
7919    }
7920
7921    #[test]
7922    fn locator_based_cmra_recovery_only_ignores_header_crc_failures() {
7923        let archive = write_archive_with_root_auth(
7924            &[RegularFile::new("cmra-header.txt", b"header fallback")],
7925            &master_key(),
7926            single_stream_options(),
7927            RootAuthWriterConfig {
7928                authenticator_id: 0x4444,
7929                signer_identity_type: 1,
7930                signer_identity: b"public verifier",
7931                authenticator_value_length: 32,
7932            },
7933            |request| Ok(request.archive_root.to_vec()),
7934        )
7935        .unwrap();
7936        let final_locator = final_recovery_locator(&archive.bytes);
7937
7938        let mut bad_crc = archive.bytes.clone();
7939        let crc_offset =
7940            final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
7941        bad_crc[crc_offset] ^= 0x55;
7942        public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
7943            Ok(footer.authenticator_value == archive_root.as_slice())
7944        })
7945        .unwrap();
7946
7947        let mut bad_magic = archive.bytes.clone();
7948        bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
7949        assert_eq!(
7950            public_no_key_verify_archive_with(&bad_magic, |_, _| Ok(true)).unwrap_err(),
7951            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7952        );
7953
7954        let mut bad_hint = archive.bytes.clone();
7955        bad_hint[crc_offset] ^= 0xAA;
7956        for offset in [
7957            bad_hint.len() - LOCATOR_PAIR_LEN,
7958            bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
7959        ] {
7960            let mut locator = CriticalRecoveryLocator::parse(
7961                &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
7962            )
7963            .unwrap();
7964            locator.volume_index_hint += 1;
7965            bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
7966                .copy_from_slice(&locator.to_bytes());
7967        }
7968        assert_eq!(
7969            public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
7970            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
7971        );
7972    }
7973
7974    #[test]
7975    fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
7976        let archive = write_archive(
7977            &[RegularFile::new(
7978                "cmra-floor.txt",
7979                b"authenticated CMRA floor",
7980            )],
7981            &master_key(),
7982            single_stream_options(),
7983        )
7984        .unwrap();
7985        let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
7986        let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
7987        let locator = CriticalRecoveryLocator::parse(
7988            &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
7989        )
7990        .unwrap();
7991        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
7992        let crypto_start = volume_header.crypto_header_offset as usize;
7993        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
7994        let crypto_header = CryptoHeader::parse(
7995            &malformed[crypto_start..crypto_end],
7996            volume_header.crypto_header_length,
7997        )
7998        .unwrap();
7999        let subkeys = Subkeys::derive(
8000            &master_key(),
8001            &volume_header.archive_uuid,
8002            &volume_header.session_id,
8003        )
8004        .unwrap();
8005
8006        assert_eq!(
8007            parse_locator_cmra_candidate(
8008                &malformed,
8009                final_offset,
8010                locator,
8011                KeyHoldingTerminalContext {
8012                    subkeys: &subkeys,
8013                    volume_header: &volume_header,
8014                    crypto_header: &crypto_header.fixed,
8015                    crypto_header_bytes: &malformed[crypto_start..crypto_end],
8016                },
8017            )
8018            .unwrap_err(),
8019            FormatError::InvalidArchive(
8020                "CMRA parity shard count is below authenticated bit-rot lower bound"
8021            )
8022        );
8023        assert!(open_archive(&malformed, &master_key()).is_err());
8024    }
8025
8026    #[test]
8027    fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
8028        let archive = write_archive(
8029            &[RegularFile::new(
8030                "locator-order.txt",
8031                b"locator tuple first",
8032            )],
8033            &master_key(),
8034            single_stream_options(),
8035        )
8036        .unwrap();
8037        let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8038        let mut locator = final_recovery_locator(&archive.bytes);
8039        locator.cmra_shard_size = 513;
8040        locator.body_bytes_before_cmra = locator.cmra_offset + 1;
8041        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8042        let crypto_start = volume_header.crypto_header_offset as usize;
8043        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8044        let crypto_header = CryptoHeader::parse(
8045            &archive.bytes[crypto_start..crypto_end],
8046            volume_header.crypto_header_length,
8047        )
8048        .unwrap();
8049        let subkeys = Subkeys::derive(
8050            &master_key(),
8051            &volume_header.archive_uuid,
8052            &volume_header.session_id,
8053        )
8054        .unwrap();
8055
8056        assert_eq!(
8057            parse_locator_cmra_candidate(
8058                &archive.bytes,
8059                final_offset,
8060                locator,
8061                KeyHoldingTerminalContext {
8062                    subkeys: &subkeys,
8063                    volume_header: &volume_header,
8064                    crypto_header: &crypto_header.fixed,
8065                    crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
8066                },
8067            )
8068            .unwrap_err(),
8069            FormatError::InvalidArchive("CMRA shard_size is invalid")
8070        );
8071    }
8072
8073    #[test]
8074    fn sequential_extract_rejects_bytes_after_terminal_locator() {
8075        let archive = write_archive(
8076            &[RegularFile::new("seq.txt", b"sequential EOF")],
8077            &master_key(),
8078            single_stream_options(),
8079        )
8080        .unwrap();
8081        let mut appended = archive.bytes.clone();
8082        appended.extend_from_slice(&[0xAA; 32]);
8083
8084        assert_eq!(
8085            sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
8086            FormatError::InvalidArchive("sequential terminal does not end at EOF")
8087        );
8088    }
8089
8090    #[test]
8091    fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
8092        let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
8093        let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
8094
8095        assert_eq!(
8096            validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
8097            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8098        );
8099    }
8100
8101    #[test]
8102    fn root_auth_verifies_key_holding_and_public_no_key_modes() {
8103        let archive = write_archive_with_root_auth(
8104            &[RegularFile::new("signed.txt", b"ed25519 payload")],
8105            &master_key(),
8106            single_stream_options(),
8107            test_root_auth_config(),
8108            |request| Ok(test_root_auth_value(request)),
8109        )
8110        .unwrap();
8111
8112        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8113        let root_auth = opened
8114            .verify_root_auth_with(|footer, archive_root| {
8115                Ok(test_root_auth_verifies(footer, archive_root))
8116            })
8117            .unwrap();
8118        assert_eq!(
8119            root_auth.archive_root,
8120            opened.root_auth_footer.as_ref().unwrap().archive_root
8121        );
8122
8123        let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8124            Ok(test_root_auth_verifies(footer, archive_root))
8125        })
8126        .unwrap();
8127        assert_eq!(public.archive_root, root_auth.archive_root);
8128        assert_eq!(
8129            public.diagnostics,
8130            vec![
8131                PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
8132                PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
8133                PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
8134            ]
8135        );
8136    }
8137
8138    #[test]
8139    fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
8140        let options = WriterOptions {
8141            block_size: 4096,
8142            chunk_size: 16 * 1024,
8143            envelope_target_size: 16 * 1024,
8144            stripe_width: 2,
8145            volume_loss_tolerance: 1,
8146            bit_rot_buffer_pct: 0,
8147            fec_data_shards: 16,
8148            fec_parity_shards: 1,
8149            index_fec_data_shards: 4,
8150            index_fec_parity_shards: 1,
8151            index_root_fec_data_shards: 16,
8152            index_root_fec_parity_shards: 1,
8153            ..WriterOptions::default()
8154        };
8155        let archive = write_archive_with_root_auth(
8156            &[RegularFile::new("missing-volume.txt", b"recover me")],
8157            &master_key(),
8158            options,
8159            test_root_auth_config(),
8160            |request| Ok(test_root_auth_value(request)),
8161        )
8162        .unwrap();
8163
8164        let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
8165        let root_auth = opened
8166            .verify_root_auth_with(|footer, archive_root| {
8167                Ok(test_root_auth_verifies(footer, archive_root))
8168            })
8169            .unwrap();
8170        assert!(root_auth
8171            .diagnostics
8172            .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
8173    }
8174
8175    #[test]
8176    fn public_no_key_rejects_unsigned_archives() {
8177        let archive = write_archive(
8178            &[RegularFile::new("plain.txt", b"unsigned")],
8179            &master_key(),
8180            single_stream_options(),
8181        )
8182        .unwrap();
8183
8184        assert_eq!(
8185            public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
8186            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
8187        );
8188    }
8189
8190    #[test]
8191    fn unsigned_archive_reports_root_auth_absent() {
8192        let archive = write_archive(
8193            &[RegularFile::new("plain.txt", b"unsigned")],
8194            &master_key(),
8195            single_stream_options(),
8196        )
8197        .unwrap();
8198        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8199
8200        assert_eq!(
8201            opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
8202            FormatError::ReaderUnsupported("root-auth footer is absent")
8203        );
8204    }
8205
8206    #[test]
8207    fn safe_extract_writes_regular_file_under_root() {
8208        let archive = write_archive(
8209            &[RegularFile::new("dir/hello.txt", b"safe m8")],
8210            &master_key(),
8211            single_stream_options(),
8212        )
8213        .unwrap();
8214        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8215        let tmp = tempfile::tempdir().unwrap();
8216
8217        opened
8218            .extract_file_to(
8219                "dir/hello.txt",
8220                tmp.path(),
8221                SafeExtractionOptions::default(),
8222            )
8223            .unwrap()
8224            .unwrap();
8225
8226        assert_eq!(
8227            std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
8228            b"safe m8"
8229        );
8230    }
8231
8232    #[test]
8233    fn seekable_extract_all_to_streams_unique_archive() {
8234        let archive = write_archive(
8235            &[
8236                RegularFile::new("alpha.txt", b"alpha"),
8237                RegularFile::new("dir/beta.txt", b"beta"),
8238            ],
8239            &master_key(),
8240            single_stream_options(),
8241        )
8242        .unwrap();
8243        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8244        let tmp = tempfile::tempdir().unwrap();
8245
8246        let diagnostics = opened
8247            .extract_all_to(tmp.path(), SafeExtractionOptions::default())
8248            .unwrap();
8249
8250        assert_eq!(diagnostics.len(), 2);
8251        assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
8252        assert_eq!(
8253            fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
8254            b"beta"
8255        );
8256    }
8257
8258    #[test]
8259    fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
8260        let archive = write_archive(
8261            &[
8262                RegularFile::new("same.txt", b"old"),
8263                RegularFile::new("same.txt", b"new"),
8264            ],
8265            &master_key(),
8266            single_stream_options(),
8267        )
8268        .unwrap();
8269        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8270        let tmp = tempfile::tempdir().unwrap();
8271
8272        assert_eq!(
8273            opened
8274                .extract_all_to(tmp.path(), SafeExtractionOptions::default())
8275                .unwrap_err(),
8276            FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
8277        );
8278    }
8279
8280    #[test]
8281    fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
8282        let archive = write_archive(
8283            &[
8284                RegularFile::new("same.txt", b"old"),
8285                RegularFile::new("same.txt", b"new"),
8286            ],
8287            &master_key(),
8288            single_stream_options(),
8289        )
8290        .unwrap();
8291        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8292        let tmp = tempfile::tempdir().unwrap();
8293
8294        let diagnostics = opened
8295            .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
8296            .unwrap();
8297
8298        assert_eq!(diagnostics.len(), 1);
8299        assert_eq!(diagnostics[0].0, "same.txt");
8300        assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
8301    }
8302
8303    #[test]
8304    fn safe_extract_rejects_overwriting_existing_file_by_default() {
8305        let archive = write_archive(
8306            &[RegularFile::new("hello.txt", b"new")],
8307            &master_key(),
8308            single_stream_options(),
8309        )
8310        .unwrap();
8311        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8312        let tmp = tempfile::tempdir().unwrap();
8313        std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
8314
8315        assert_eq!(
8316            opened
8317                .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
8318                .unwrap_err(),
8319            FormatError::UnsafeOverwrite
8320        );
8321        assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
8322    }
8323
8324    #[test]
8325    fn opens_and_verifies_empty_archive() {
8326        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
8327        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8328
8329        assert!(opened.list_files().unwrap().is_empty());
8330        opened.verify().unwrap();
8331    }
8332
8333    #[test]
8334    fn default_reader_options_allow_v36_trailing_garbage_scan() {
8335        let archive = write_archive(
8336            &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
8337            &master_key(),
8338            single_stream_options(),
8339        )
8340        .unwrap();
8341        let mut with_trailing_garbage = archive.bytes.clone();
8342        with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
8343
8344        let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
8345        assert_eq!(
8346            opened.extract_file("garbage-tolerant.txt").unwrap(),
8347            Some(b"still intact".to_vec())
8348        );
8349    }
8350
8351    #[test]
8352    fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
8353        assert_eq!(
8354            open_archive(
8355                &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
8356                &master_key()
8357            )
8358            .unwrap_err(),
8359            FormatError::InvalidLength {
8360                structure: "archive",
8361                expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
8362                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
8363            }
8364        );
8365
8366        let mut header = test_volume_header();
8367        header.crypto_header_length = 512;
8368        let mut unavailable_crypto = header.to_bytes().to_vec();
8369        unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
8370
8371        assert_eq!(
8372            open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
8373            FormatError::InvalidLength {
8374                structure: "CryptoHeader",
8375                expected: VOLUME_HEADER_LEN + 512,
8376                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
8377            }
8378        );
8379    }
8380
8381    #[test]
8382    fn seekable_open_rejects_in_bounds_noncanonical_crypto_header_offset() {
8383        let archive = write_archive(
8384            &[RegularFile::new("offset.txt", b"offset")],
8385            &master_key(),
8386            single_stream_options(),
8387        )
8388        .unwrap();
8389        let mut mutated = archive.bytes;
8390        let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
8391        header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
8392        mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8393
8394        assert_eq!(
8395            open_archive(&mutated, &master_key()).unwrap_err(),
8396            FormatError::NonCanonicalCryptoHeaderOffset(VOLUME_HEADER_LEN as u32 + 1)
8397        );
8398    }
8399
8400    #[test]
8401    fn rejects_wrong_key_before_metadata_release() {
8402        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
8403        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
8404
8405        assert_eq!(
8406            open_archive(&archive.bytes, &wrong).unwrap_err(),
8407            FormatError::HmacMismatch {
8408                structure: "CryptoHeader"
8409            }
8410        );
8411    }
8412
8413    #[test]
8414    fn rejects_payload_tamper_even_with_recomputed_block_crc() {
8415        let mut archive = write_archive(
8416            &[RegularFile::new("file.txt", b"authenticated")],
8417            &master_key(),
8418            single_stream_options(),
8419        )
8420        .unwrap()
8421        .bytes;
8422        let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
8423        let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
8424        let crypto = CryptoHeader::parse(
8425            &archive[VOLUME_HEADER_LEN..crypto_end],
8426            volume.crypto_header_length,
8427        )
8428        .unwrap();
8429        let block_size = crypto.fixed.block_size as usize;
8430        archive[crypto_end + 16] ^= 1;
8431        let crc_offset = crypto_end + 16 + block_size;
8432        let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
8433        archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
8434
8435        let opened = open_archive(&archive, &master_key()).unwrap();
8436        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
8437    }
8438
8439    #[test]
8440    fn list_and_extract_use_final_view_for_duplicate_paths() {
8441        let archive = write_archive(
8442            &[
8443                RegularFile::new("same.txt", b"old"),
8444                RegularFile::new("same.txt", b"newer"),
8445            ],
8446            &master_key(),
8447            single_stream_options(),
8448        )
8449        .unwrap();
8450        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8451
8452        assert_eq!(
8453            opened.list_index_entries().unwrap(),
8454            vec![ArchiveIndexEntry {
8455                path: "same.txt".to_string(),
8456                file_data_size: 5,
8457            }]
8458        );
8459        assert_eq!(
8460            opened.lookup_index_entry("same.txt").unwrap(),
8461            Some(ArchiveIndexEntry {
8462                path: "same.txt".to_string(),
8463                file_data_size: 5,
8464            })
8465        );
8466        assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
8467        assert_eq!(
8468            opened.list_files().unwrap(),
8469            vec![ArchiveEntry {
8470                path: "same.txt".to_string(),
8471                file_data_size: 5,
8472                kind: TarEntryKind::Regular,
8473                mode: 0o644,
8474                mtime: 0,
8475                diagnostics: Vec::new(),
8476            }]
8477        );
8478        assert_eq!(
8479            opened.extract_file("same.txt").unwrap(),
8480            Some(b"newer".to_vec())
8481        );
8482        opened.verify().unwrap();
8483    }
8484
8485    #[test]
8486    fn index_entries_do_not_decrypt_payload_envelopes() {
8487        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
8488        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
8489
8490        assert_eq!(
8491            opened.list_index_entries().unwrap(),
8492            vec![
8493                ArchiveIndexEntry {
8494                    path: "broken.txt".to_string(),
8495                    file_data_size: b"broken payload\n".len() as u64,
8496                },
8497                ArchiveIndexEntry {
8498                    path: "healthy.txt".to_string(),
8499                    file_data_size: b"healthy payload\n".len() as u64,
8500                },
8501            ]
8502        );
8503        assert_eq!(
8504            opened.lookup_index_entry("broken.txt").unwrap(),
8505            Some(ArchiveIndexEntry {
8506                path: "broken.txt".to_string(),
8507                file_data_size: b"broken payload\n".len() as u64,
8508            })
8509        );
8510        assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
8511    }
8512
8513    #[test]
8514    fn extract_file_does_not_decrypt_unselected_payload_envelope() {
8515        // This fixture corrupts only the unselected envelope, proving selected
8516        // extraction does not decrypt unrelated payload envelopes.
8517        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
8518        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
8519
8520        assert_eq!(
8521            opened.extract_file("healthy.txt").unwrap(),
8522            Some(b"healthy payload\n".to_vec())
8523        );
8524        assert_eq!(
8525            opened.extract_file("broken.txt").unwrap_err(),
8526            FormatError::AeadFailure
8527        );
8528        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
8529    }
8530
8531    #[test]
8532    fn seekable_extract_does_not_read_unselected_payload_envelope() {
8533        let healthy = pseudo_random_bytes(64 * 1024);
8534        let broken = pseudo_random_bytes(64 * 1024);
8535        let options = WriterOptions {
8536            block_size: 4096,
8537            chunk_size: 4096,
8538            envelope_target_size: 8192,
8539            stripe_width: 1,
8540            volume_loss_tolerance: 0,
8541            bit_rot_buffer_pct: 0,
8542            fec_data_shards: 4,
8543            fec_parity_shards: 0,
8544            index_fec_data_shards: 4,
8545            index_fec_parity_shards: 0,
8546            index_root_fec_data_shards: 4,
8547            index_root_fec_parity_shards: 0,
8548            ..WriterOptions::default()
8549        };
8550        let archive = write_archive(
8551            &[
8552                RegularFile::new("healthy.bin", &healthy),
8553                RegularFile::new("broken.bin", &broken),
8554            ],
8555            &master_key(),
8556            options,
8557        )
8558        .unwrap();
8559        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8560        let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
8561        let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
8562        let denied_block_indices = broken_envelopes
8563            .iter()
8564            .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
8565            .flat_map(|envelope| {
8566                let block_count =
8567                    envelope.data_block_count as u64 + envelope.parity_block_count as u64;
8568                envelope.first_block_index..envelope.first_block_index + block_count
8569            })
8570            .collect::<BTreeSet<_>>();
8571        assert!(
8572            !denied_block_indices.is_empty(),
8573            "fixture must place broken.bin in at least one unshared envelope"
8574        );
8575        let denied_ranges = block_record_slots(&archive.bytes)
8576            .into_iter()
8577            .filter_map(|(offset, len, record)| {
8578                denied_block_indices
8579                    .contains(&record.block_index)
8580                    .then_some((offset as u64, (offset + len) as u64))
8581            })
8582            .collect::<Vec<_>>();
8583        assert!(!denied_ranges.is_empty());
8584
8585        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
8586        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
8587
8588        assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
8589        for (read_start, read_end) in reader.reads() {
8590            assert!(
8591                denied_ranges
8592                    .iter()
8593                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
8594                "targeted extract read an unrelated payload BlockRecord range"
8595            );
8596        }
8597        assert_eq!(
8598            opened.extract_file("broken.bin").unwrap_err(),
8599            FormatError::InvalidArchive("denied test read")
8600        );
8601    }
8602
8603    #[test]
8604    fn extract_file_to_writer_streams_before_reading_later_envelopes() {
8605        struct FailOnFirstWrite;
8606
8607        impl std::io::Write for FailOnFirstWrite {
8608            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
8609                Err(std::io::Error::other("sink stopped"))
8610            }
8611
8612            fn flush(&mut self) -> std::io::Result<()> {
8613                Ok(())
8614            }
8615        }
8616
8617        let payload = pseudo_random_bytes(128 * 1024);
8618        let options = WriterOptions {
8619            block_size: 4096,
8620            chunk_size: 4096,
8621            envelope_target_size: 8192,
8622            stripe_width: 1,
8623            volume_loss_tolerance: 0,
8624            bit_rot_buffer_pct: 0,
8625            fec_data_shards: 4,
8626            fec_parity_shards: 0,
8627            index_fec_data_shards: 4,
8628            index_fec_parity_shards: 0,
8629            index_root_fec_data_shards: 4,
8630            index_root_fec_parity_shards: 0,
8631            ..WriterOptions::default()
8632        };
8633        let archive = write_archive(
8634            &[RegularFile::new("large.bin", &payload)],
8635            &master_key(),
8636            options,
8637        )
8638        .unwrap();
8639        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8640        let envelopes = envelope_entries_for_path(&eager, "large.bin");
8641        let first_envelope = envelopes
8642            .first()
8643            .expect("large fixture should have at least one envelope")
8644            .envelope_index;
8645        let later_envelope_blocks = envelopes
8646            .iter()
8647            .filter(|entry| entry.envelope_index != first_envelope)
8648            .flat_map(|entry| {
8649                let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
8650                entry.first_block_index..entry.first_block_index + block_count
8651            })
8652            .collect::<BTreeSet<_>>();
8653        assert!(
8654            !later_envelope_blocks.is_empty(),
8655            "fixture must span more than one payload envelope"
8656        );
8657        let denied_ranges = block_record_slots(&archive.bytes)
8658            .into_iter()
8659            .filter_map(|(offset, len, record)| {
8660                later_envelope_blocks
8661                    .contains(&record.block_index)
8662                    .then_some((offset as u64, (offset + len) as u64))
8663            })
8664            .collect::<Vec<_>>();
8665        assert!(!denied_ranges.is_empty());
8666
8667        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
8668        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
8669        let mut writer = FailOnFirstWrite;
8670
8671        let err = opened
8672            .extract_file_to_writer("large.bin", &mut writer)
8673            .unwrap_err();
8674        assert_eq!(err.to_string(), "extraction output write failed");
8675        for (read_start, read_end) in reader.reads() {
8676            assert!(
8677                denied_ranges
8678                    .iter()
8679                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
8680                "streaming writer read a later payload envelope before surfacing writer failure"
8681            );
8682        }
8683    }
8684
8685    #[test]
8686    fn extract_file_to_writer_writes_bounded_chunks() {
8687        struct ChunkRecorder {
8688            total: usize,
8689            max_write: usize,
8690            writes: usize,
8691        }
8692
8693        impl std::io::Write for ChunkRecorder {
8694            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
8695                self.total += buf.len();
8696                self.max_write = self.max_write.max(buf.len());
8697                self.writes += 1;
8698                Ok(buf.len())
8699            }
8700
8701            fn flush(&mut self) -> std::io::Result<()> {
8702                Ok(())
8703            }
8704        }
8705
8706        let payload = pseudo_random_bytes(128 * 1024);
8707        let options = WriterOptions {
8708            block_size: 4096,
8709            chunk_size: 4096,
8710            envelope_target_size: 8192,
8711            stripe_width: 1,
8712            volume_loss_tolerance: 0,
8713            bit_rot_buffer_pct: 0,
8714            fec_data_shards: 4,
8715            fec_parity_shards: 0,
8716            index_fec_data_shards: 4,
8717            index_fec_parity_shards: 0,
8718            index_root_fec_data_shards: 4,
8719            index_root_fec_parity_shards: 0,
8720            ..WriterOptions::default()
8721        };
8722        let archive = write_archive(
8723            &[RegularFile::new("large.bin", &payload)],
8724            &master_key(),
8725            options,
8726        )
8727        .unwrap();
8728        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8729        let mut writer = ChunkRecorder {
8730            total: 0,
8731            max_write: 0,
8732            writes: 0,
8733        };
8734
8735        opened
8736            .extract_file_to_writer("large.bin", &mut writer)
8737            .unwrap()
8738            .unwrap();
8739
8740        assert_eq!(writer.total, payload.len());
8741        assert!(writer.writes > 1);
8742        assert!(
8743            writer.max_write <= options.chunk_size as usize,
8744            "writer saw a {} byte chunk, larger than the {} byte frame target",
8745            writer.max_write,
8746            options.chunk_size
8747        );
8748    }
8749
8750    #[test]
8751    fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
8752        let payload = pseudo_random_bytes(128 * 1024);
8753        let options = WriterOptions {
8754            block_size: 4096,
8755            chunk_size: 4096,
8756            envelope_target_size: 8192,
8757            stripe_width: 1,
8758            volume_loss_tolerance: 0,
8759            bit_rot_buffer_pct: 0,
8760            fec_data_shards: 4,
8761            fec_parity_shards: 0,
8762            index_fec_data_shards: 4,
8763            index_fec_parity_shards: 0,
8764            index_root_fec_data_shards: 4,
8765            index_root_fec_parity_shards: 0,
8766            ..WriterOptions::default()
8767        };
8768        let archive = write_archive(
8769            &[RegularFile::new("large.bin", &payload)],
8770            &master_key(),
8771            options,
8772        )
8773        .unwrap();
8774        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
8775        let envelopes = envelope_entries_for_path(&eager, "large.bin");
8776        let last_envelope = envelopes
8777            .last()
8778            .expect("large fixture should have at least one envelope");
8779        assert_ne!(
8780            envelopes.first().unwrap().envelope_index,
8781            last_envelope.envelope_index,
8782            "fixture must span more than one payload envelope"
8783        );
8784        let corrupt_slot = block_record_slots(&archive.bytes)
8785            .into_iter()
8786            .enumerate()
8787            .find_map(|(slot, (_, _, record))| {
8788                (record.block_index == last_envelope.first_block_index).then_some(slot)
8789            })
8790            .unwrap();
8791        let mut corrupted = archive.bytes;
8792        corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
8793        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8794        let tmp = tempfile::tempdir().unwrap();
8795
8796        assert!(matches!(
8797            opened
8798                .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
8799                .unwrap_err(),
8800            FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
8801        ));
8802        assert!(!tmp.path().join("large.bin").exists());
8803        assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
8804    }
8805
8806    #[test]
8807    fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
8808        let archive = write_archive(
8809            &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
8810            &master_key(),
8811            single_stream_options(),
8812        )
8813        .unwrap();
8814        let opened = open_archive_with_bootstrap_sidecar(
8815            &archive.bytes,
8816            &archive.bootstrap_sidecar,
8817            &master_key(),
8818        )
8819        .unwrap();
8820
8821        assert_eq!(
8822            opened.list_files().unwrap(),
8823            vec![ArchiveEntry {
8824                path: "dir/sidecar.txt".to_string(),
8825                file_data_size: 13,
8826                kind: TarEntryKind::Regular,
8827                mode: 0o644,
8828                mtime: 0,
8829                diagnostics: Vec::new(),
8830            }]
8831        );
8832        assert_eq!(
8833            opened.extract_file("dir/sidecar.txt").unwrap(),
8834            Some(b"hello sidecar".to_vec())
8835        );
8836        opened.verify().unwrap();
8837    }
8838
8839    #[test]
8840    fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
8841        let archive = write_archive_with_dictionary(
8842            &[RegularFile::new(
8843                "dir/dict.txt",
8844                b"common words common words dictionary payload",
8845            )],
8846            &master_key(),
8847            single_stream_options(),
8848            dictionary(),
8849        )
8850        .unwrap();
8851        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8852
8853        assert_eq!(opened.crypto_header.has_dictionary, 1);
8854        assert!(opened.index_root.header.dictionary_data_block_count > 0);
8855        assert_eq!(
8856            opened.list_files().unwrap(),
8857            vec![ArchiveEntry {
8858                path: "dir/dict.txt".to_string(),
8859                file_data_size: 44,
8860                kind: TarEntryKind::Regular,
8861                mode: 0o644,
8862                mtime: 0,
8863                diagnostics: Vec::new(),
8864            }]
8865        );
8866        assert_eq!(
8867            opened.extract_file("dir/dict.txt").unwrap(),
8868            Some(b"common words common words dictionary payload".to_vec())
8869        );
8870        opened.verify().unwrap();
8871    }
8872
8873    #[test]
8874    fn dictionary_object_tamper_fails_before_payload_decompression() {
8875        let archive = write_archive_with_dictionary(
8876            &[RegularFile::new(
8877                "dir/dict.txt",
8878                b"common words common words dictionary payload",
8879            )],
8880            &master_key(),
8881            single_stream_options(),
8882            dictionary(),
8883        )
8884        .unwrap();
8885        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8886        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
8887        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
8888        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
8889        let dictionary_offset =
8890            crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
8891
8892        let mut tampered = archive.bytes.clone();
8893        tampered[dictionary_offset + 16] ^= 0x01;
8894        let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
8895        let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
8896        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
8897
8898        assert_eq!(
8899            open_archive(&tampered, &master_key()).unwrap_err(),
8900            FormatError::AeadFailure
8901        );
8902    }
8903
8904    #[test]
8905    fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
8906        let archive = write_archive_with_dictionary(
8907            &[RegularFile::new(
8908                "dict-sidecar.txt",
8909                b"common words common words sidecar payload",
8910            )],
8911            &master_key(),
8912            single_stream_options(),
8913            dictionary(),
8914        )
8915        .unwrap();
8916        let opened = open_non_seekable_archive(
8917            &archive.bytes,
8918            &master_key(),
8919            Some(&archive.bootstrap_sidecar),
8920        )
8921        .unwrap();
8922
8923        assert_eq!(
8924            opened.extract_file("dict-sidecar.txt").unwrap(),
8925            Some(b"common words common words sidecar payload".to_vec())
8926        );
8927        opened.verify().unwrap();
8928    }
8929
8930    #[test]
8931    fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
8932        let archive = write_archive(
8933            &[RegularFile::new(
8934                "sidecar-terminal.txt",
8935                b"sidecar authority",
8936            )],
8937            &master_key(),
8938            single_stream_options(),
8939        )
8940        .unwrap();
8941        let mut corrupted = archive.bytes.clone();
8942        corrupt_v41_terminal_recovery(&mut corrupted);
8943        assert!(open_archive(&corrupted, &master_key()).is_err());
8944
8945        let opened =
8946            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
8947                .unwrap();
8948
8949        assert!(opened.volume_trailer.is_none());
8950        assert_eq!(
8951            opened.extract_file("sidecar-terminal.txt").unwrap(),
8952            Some(b"sidecar authority".to_vec())
8953        );
8954        opened.verify().unwrap();
8955    }
8956
8957    #[test]
8958    fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
8959        let archive = write_archive_with_dictionary(
8960            &[RegularFile::new(
8961                "dict-no-terminal.txt",
8962                b"common words common words without terminal",
8963            )],
8964            &master_key(),
8965            single_stream_options(),
8966            dictionary(),
8967        )
8968        .unwrap();
8969        let terminal_offset = terminal_material_offset(&archive.bytes);
8970        let truncated = archive.bytes[..terminal_offset].to_vec();
8971        assert!(open_archive(&truncated, &master_key()).is_err());
8972
8973        let opened =
8974            open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
8975                .unwrap();
8976
8977        assert!(opened.volume_trailer.is_none());
8978        assert_eq!(
8979            opened.extract_file("dict-no-terminal.txt").unwrap(),
8980            Some(b"common words common words without terminal".to_vec())
8981        );
8982        opened.verify().unwrap();
8983    }
8984
8985    #[test]
8986    fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
8987        let archive = write_archive(
8988            &[RegularFile::new(
8989                "sidecar-erasure.txt",
8990                b"repair through sidecar",
8991            )],
8992            &master_key(),
8993            single_stream_options(),
8994        )
8995        .unwrap();
8996        let mut corrupted = archive.bytes.clone();
8997        corrupt_first_block_record_payload(&mut corrupted);
8998
8999        let opened = open_archive_with_bootstrap_sidecar(
9000            &corrupted,
9001            &archive.bootstrap_sidecar,
9002            &master_key(),
9003        )
9004        .unwrap();
9005        assert_eq!(
9006            opened.extract_file("sidecar-erasure.txt").unwrap(),
9007            Some(b"repair through sidecar".to_vec())
9008        );
9009    }
9010
9011    #[test]
9012    fn extraction_rejects_logical_payload_above_total_size_cap() {
9013        let archive = write_archive(
9014            &[RegularFile::new("cap.txt", b"payload")],
9015            &master_key(),
9016            single_stream_options(),
9017        )
9018        .unwrap();
9019        let options = ReaderOptions {
9020            max_total_extraction_size: 3,
9021            ..ReaderOptions::default()
9022        };
9023        let opened =
9024            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9025
9026        assert_eq!(
9027            opened.extract_file("cap.txt").unwrap_err(),
9028            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9029        );
9030    }
9031
9032    #[test]
9033    fn verify_does_not_apply_extraction_payload_cap() {
9034        let archive = write_archive(
9035            &[RegularFile::new("verify-cap.txt", b"payload")],
9036            &master_key(),
9037            single_stream_options(),
9038        )
9039        .unwrap();
9040        let options = ReaderOptions {
9041            max_total_extraction_size: 3,
9042            ..ReaderOptions::default()
9043        };
9044        let opened =
9045            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9046
9047        opened.verify().unwrap();
9048        assert_eq!(
9049            opened.extract_file("verify-cap.txt").unwrap_err(),
9050            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9051        );
9052    }
9053
9054    #[test]
9055    fn verify_streams_past_legacy_in_memory_tar_cap() {
9056        let data = vec![0x5a; 4096];
9057        let archive = write_archive(
9058            &[RegularFile::new("verify-large.txt", &data)],
9059            &master_key(),
9060            single_stream_options(),
9061        )
9062        .unwrap();
9063        let options = ReaderOptions {
9064            max_verify_tar_size: 1,
9065            ..ReaderOptions::default()
9066        };
9067        let opened =
9068            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
9069
9070        opened.verify().unwrap();
9071    }
9072
9073    #[test]
9074    fn dictionary_sidecar_requires_dictionary_record_section() {
9075        let archive = write_archive_with_dictionary(
9076            &[RegularFile::new("dict-missing.txt", b"common words")],
9077            &master_key(),
9078            single_stream_options(),
9079            dictionary(),
9080        )
9081        .unwrap();
9082        let header = BootstrapSidecarHeader::parse(
9083            &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
9084        )
9085        .unwrap();
9086        let mut missing_dictionary =
9087            archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
9088        rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
9089            header.flags &= !0x04;
9090            header.dictionary_records_offset = 0;
9091            header.dictionary_records_length = 0;
9092        });
9093
9094        assert_eq!(
9095            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
9096                .unwrap_err(),
9097            FormatError::ReaderUnsupported("dictionary bootstrap required")
9098        );
9099    }
9100
9101    #[test]
9102    fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
9103        let archive = write_archive_with_dictionary(
9104            &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
9105            &master_key(),
9106            single_stream_options(),
9107            dictionary(),
9108        )
9109        .unwrap();
9110
9111        let mut wrong_kind = archive.bootstrap_sidecar.clone();
9112        mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
9113            record.kind = BlockKind::IndexRootData;
9114        });
9115        assert_eq!(
9116            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
9117                .unwrap_err(),
9118            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
9119        );
9120
9121        let mut wrong_last = archive.bootstrap_sidecar.clone();
9122        mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
9123            record.flags = 0;
9124        });
9125        assert_eq!(
9126            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
9127                .unwrap_err(),
9128            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
9129        );
9130    }
9131
9132    #[test]
9133    fn non_seekable_random_access_requires_sidecar() {
9134        let archive = write_archive(
9135            &[RegularFile::new("file.txt", b"payload")],
9136            &master_key(),
9137            single_stream_options(),
9138        )
9139        .unwrap();
9140
9141        assert_eq!(
9142            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
9143            FormatError::ReaderUnsupported(
9144                "non-seekable random access requires a bootstrap sidecar"
9145            )
9146        );
9147        assert!(open_non_seekable_archive(
9148            &archive.bytes,
9149            &master_key(),
9150            Some(&archive.bootstrap_sidecar)
9151        )
9152        .is_ok());
9153    }
9154
9155    #[test]
9156    fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
9157        let archive = write_archive(
9158            &[RegularFile::new("sparse.txt", b"sparse sidecar")],
9159            &master_key(),
9160            single_stream_options(),
9161        )
9162        .unwrap();
9163        let index_root_only = sparse_bootstrap_sidecar(
9164            &archive.bootstrap_sidecar,
9165            &master_key(),
9166            false,
9167            true,
9168            false,
9169        );
9170
9171        assert_eq!(
9172            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
9173                .unwrap_err(),
9174            FormatError::ReaderUnsupported(
9175                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
9176            )
9177        );
9178    }
9179
9180    #[test]
9181    fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
9182        let archive = write_archive(
9183            &[RegularFile::new("sparse-index.txt", b"recover index root")],
9184            &master_key(),
9185            single_stream_options(),
9186        )
9187        .unwrap();
9188        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9189        let mut corrupted = archive.bytes.clone();
9190        corrupt_object_extent_records(
9191            &mut corrupted,
9192            index_root_extent_from_manifest(&opened.manifest_footer),
9193        );
9194        assert!(open_archive(&corrupted, &master_key()).is_err());
9195
9196        let index_root_only = sparse_bootstrap_sidecar(
9197            &archive.bootstrap_sidecar,
9198            &master_key(),
9199            false,
9200            true,
9201            false,
9202        );
9203        let recovered =
9204            open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
9205                .unwrap();
9206
9207        assert_eq!(
9208            recovered.extract_file("sparse-index.txt").unwrap(),
9209            Some(b"recover index root".to_vec())
9210        );
9211        recovered.verify().unwrap();
9212    }
9213
9214    #[test]
9215    fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
9216        let archive = write_archive_with_dictionary(
9217            &[RegularFile::new(
9218                "sparse-dict.txt",
9219                b"common words common words sparse dictionary",
9220            )],
9221            &master_key(),
9222            single_stream_options(),
9223            dictionary(),
9224        )
9225        .unwrap();
9226        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9227        let mut corrupted = archive.bytes.clone();
9228        corrupt_object_extent_records(
9229            &mut corrupted,
9230            dictionary_extent_from_index_root(&opened.index_root).unwrap(),
9231        );
9232        assert!(open_archive(&corrupted, &master_key()).is_err());
9233
9234        let dictionary_only = sparse_bootstrap_sidecar(
9235            &archive.bootstrap_sidecar,
9236            &master_key(),
9237            false,
9238            false,
9239            true,
9240        );
9241        assert_eq!(
9242            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
9243                .unwrap_err(),
9244            FormatError::ReaderUnsupported(
9245                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
9246            )
9247        );
9248
9249        let recovered =
9250            open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
9251                .unwrap();
9252        assert_eq!(
9253            recovered.extract_file("sparse-dict.txt").unwrap(),
9254            Some(b"common words common words sparse dictionary".to_vec())
9255        );
9256        recovered.verify().unwrap();
9257    }
9258
9259    #[test]
9260    fn sequential_extracts_dictionary_free_tar_stream() {
9261        let archive = write_archive(
9262            &[RegularFile::new("seq.txt", b"streaming")],
9263            &master_key(),
9264            single_stream_options(),
9265        )
9266        .unwrap();
9267
9268        let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
9269        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
9270        assert_eq!(member.path, b"seq.txt");
9271        assert_eq!(member.data, b"streaming");
9272    }
9273
9274    #[test]
9275    fn sequential_rejects_logical_payload_above_total_size_cap() {
9276        let archive = write_archive(
9277            &[RegularFile::new("seq-cap.txt", b"payload")],
9278            &master_key(),
9279            single_stream_options(),
9280        )
9281        .unwrap();
9282        let options = ReaderOptions {
9283            max_total_extraction_size: 3,
9284            ..ReaderOptions::default()
9285        };
9286
9287        assert_eq!(
9288            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
9289                .unwrap_err(),
9290            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9291        );
9292    }
9293
9294    #[test]
9295    fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
9296        let archive = write_archive(
9297            &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
9298            &master_key(),
9299            single_stream_options(),
9300        )
9301        .unwrap();
9302        let options = ReaderOptions {
9303            max_verify_tar_size: 512,
9304            ..ReaderOptions::default()
9305        };
9306
9307        assert_eq!(
9308            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
9309                .unwrap_err(),
9310            FormatError::ReaderUnsupported(
9311                "sequential tar stream exceeds configured verification cap"
9312            )
9313        );
9314    }
9315
9316    #[test]
9317    fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
9318        let archive = write_archive(
9319            &[RegularFile::new("seq-erasure.txt", b"stream repair")],
9320            &master_key(),
9321            single_stream_options(),
9322        )
9323        .unwrap();
9324        let mut corrupted = archive.bytes;
9325        corrupt_first_block_record_payload(&mut corrupted);
9326
9327        let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
9328        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
9329        assert_eq!(member.path, b"seq-erasure.txt");
9330        assert_eq!(member.data, b"stream repair");
9331    }
9332
9333    #[test]
9334    fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
9335        let archive = write_archive(
9336            &[RegularFile::new("seq-no-parity.txt", b"no repair")],
9337            &master_key(),
9338            WriterOptions {
9339                bit_rot_buffer_pct: 0,
9340                fec_parity_shards: 0,
9341                index_fec_parity_shards: 0,
9342                index_root_fec_parity_shards: 0,
9343                ..single_stream_options()
9344            },
9345        )
9346        .unwrap();
9347        let mut corrupted = archive.bytes;
9348        corrupt_first_block_record_payload(&mut corrupted);
9349
9350        assert_eq!(
9351            sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
9352            FormatError::BadCrc {
9353                structure: "BlockRecord"
9354            }
9355        );
9356    }
9357
9358    #[test]
9359    fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
9360        let archive = write_archive(
9361            &[RegularFile::new(
9362                "seq.txt",
9363                b"payload must not be returned after terminal auth failure",
9364            )],
9365            &master_key(),
9366            single_stream_options(),
9367        )
9368        .unwrap();
9369        let mut corrupted = archive.bytes;
9370        corrupt_v41_terminal_recovery(&mut corrupted);
9371
9372        match sequential_extract_tar_stream(&corrupted, &master_key()) {
9373            Ok(bytes) => panic!(
9374                "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
9375                bytes.len()
9376            ),
9377            Err(err) => assert_eq!(
9378                err,
9379                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
9380            ),
9381        }
9382    }
9383
9384    #[test]
9385    fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
9386        let archive = write_archive_with_dictionary(
9387            &[RegularFile::new(
9388                "seq-dict.txt",
9389                b"common words common words dictionary payload",
9390            )],
9391            &master_key(),
9392            single_stream_options(),
9393            b"common words dictionary",
9394        )
9395        .unwrap();
9396
9397        match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
9398            Ok(bytes) => panic!(
9399                "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
9400                bytes.len()
9401            ),
9402            Err(err) => assert_eq!(
9403                err,
9404                FormatError::ReaderUnsupported(
9405                    "dictionary bootstrap required for non-seekable sequential extraction"
9406                )
9407            ),
9408        }
9409    }
9410
9411    #[test]
9412    fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
9413        let archive = write_archive_with_dictionary(
9414            &[RegularFile::new(
9415                "seq-dict-open.txt",
9416                b"common words common words bootstrap required",
9417            )],
9418            &master_key(),
9419            single_stream_options(),
9420            b"common words bootstrap",
9421        )
9422        .unwrap();
9423
9424        assert_eq!(
9425            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
9426            FormatError::ReaderUnsupported(
9427                "non-seekable random access requires a bootstrap sidecar"
9428            )
9429        );
9430    }
9431
9432    #[test]
9433    fn sequential_zstd_stream_rejects_skippable_frame_segments() {
9434        let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
9435        let mut output = Vec::new();
9436
9437        assert_eq!(
9438            decode_concatenated_zstd_frames_with_cap(
9439                &skippable,
9440                None,
9441                &mut output,
9442                usize::MAX,
9443                None,
9444            )
9445            .unwrap_err(),
9446            FormatError::NotStandardZstdFrame
9447        );
9448        assert!(output.is_empty());
9449    }
9450
9451    #[test]
9452    fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
9453        let archive = write_archive(
9454            &[RegularFile::new("live.txt", b"stream verify")],
9455            &master_key(),
9456            single_stream_options(),
9457        )
9458        .unwrap();
9459
9460        let report =
9461            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9462
9463        assert_eq!(report.file_count, 1);
9464        assert_eq!(report.total_volumes, 1);
9465        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
9466        assert!(report.payload_block_count > 0);
9467    }
9468
9469    #[test]
9470    fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
9471        let archive = write_archive(
9472            &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
9473            &master_key(),
9474            single_stream_options(),
9475        )
9476        .unwrap();
9477
9478        let report =
9479            verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
9480                .unwrap();
9481
9482        assert_eq!(report.file_count, 1);
9483        assert_eq!(report.tar_total_size % 512, 0);
9484    }
9485
9486    #[test]
9487    fn live_non_seekable_verify_stream_accepts_empty_archive() {
9488        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9489
9490        let report =
9491            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9492
9493        assert_eq!(report.file_count, 0);
9494        assert_eq!(report.payload_block_count, 0);
9495        assert_eq!(report.tar_total_size, 0);
9496    }
9497
9498    #[test]
9499    fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
9500        let archive = write_archive_with_dictionary(
9501            &[RegularFile::new(
9502                "live-dict.txt",
9503                b"common words common words dictionary payload",
9504            )],
9505            &master_key(),
9506            single_stream_options(),
9507            b"common words dictionary",
9508        )
9509        .unwrap();
9510
9511        assert_eq!(
9512            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
9513                .unwrap_err(),
9514            FormatError::ReaderUnsupported(
9515                "dictionary bootstrap required for non-seekable sequential verification"
9516            )
9517        );
9518    }
9519
9520    #[test]
9521    fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
9522        let archive = write_archive_with_dictionary(
9523            &[RegularFile::new(
9524                "live-dict-sidecar.txt",
9525                b"common words common words dictionary payload",
9526            )],
9527            &master_key(),
9528            single_stream_options(),
9529            b"common words dictionary",
9530        )
9531        .unwrap();
9532
9533        let report = verify_non_seekable_stream_with_bootstrap_sidecar(
9534            std::io::Cursor::new(archive.bytes),
9535            &archive.bootstrap_sidecar,
9536            &master_key(),
9537            NonSeekableReaderOptions::default(),
9538        )
9539        .unwrap();
9540
9541        assert_eq!(report.file_count, 1);
9542        assert_eq!(report.total_volumes, 1);
9543    }
9544
9545    #[test]
9546    fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
9547        let archive = write_archive(
9548            &[RegularFile::new("tail-cap.txt", b"payload")],
9549            &master_key(),
9550            single_stream_options(),
9551        )
9552        .unwrap();
9553        let options = NonSeekableReaderOptions {
9554            max_terminal_tail_size: 8,
9555            ..NonSeekableReaderOptions::default()
9556        };
9557
9558        assert_eq!(
9559            verify_non_seekable_stream_with_options(
9560                std::io::Cursor::new(archive.bytes),
9561                &master_key(),
9562                options
9563            )
9564            .unwrap_err(),
9565            FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
9566        );
9567    }
9568
9569    #[test]
9570    fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
9571        let archive = write_archive(
9572            &[RegularFile::new("metadata-cap.txt", b"payload")],
9573            &master_key(),
9574            single_stream_options(),
9575        )
9576        .unwrap();
9577        let options = NonSeekableReaderOptions {
9578            max_retained_metadata_bytes: 1,
9579            ..NonSeekableReaderOptions::default()
9580        };
9581
9582        assert_eq!(
9583            verify_non_seekable_stream_with_options(
9584                std::io::Cursor::new(archive.bytes),
9585                &master_key(),
9586                options
9587            )
9588            .unwrap_err(),
9589            FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
9590        );
9591    }
9592
9593    #[test]
9594    fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
9595        let archive = write_archive(
9596            &[RegularFile::new("metadata-erasure.txt", b"payload")],
9597            &master_key(),
9598            single_stream_options(),
9599        )
9600        .unwrap();
9601        let mut corrupted = archive.bytes;
9602        let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
9603        corrupt_block_record_payload_at_slot(&mut corrupted, slot);
9604
9605        let report =
9606            verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
9607
9608        assert_eq!(report.file_count, 1);
9609    }
9610
9611    #[test]
9612    fn live_non_seekable_verify_rejects_member_count_above_cap() {
9613        let archive = write_archive(
9614            &[RegularFile::new("member-cap.txt", b"payload")],
9615            &master_key(),
9616            single_stream_options(),
9617        )
9618        .unwrap();
9619        let options = NonSeekableReaderOptions {
9620            max_streamed_member_count: 0,
9621            ..NonSeekableReaderOptions::default()
9622        };
9623
9624        assert_eq!(
9625            verify_non_seekable_stream_with_options(
9626                std::io::Cursor::new(archive.bytes),
9627                &master_key(),
9628                options
9629            )
9630            .unwrap_err(),
9631            FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
9632        );
9633    }
9634
9635    #[test]
9636    fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
9637        let archive = write_archive(
9638            &[RegularFile::new("live-total-cap.txt", b"payload")],
9639            &master_key(),
9640            single_stream_options(),
9641        )
9642        .unwrap();
9643        let mut options = NonSeekableReaderOptions::default();
9644        options.reader.max_total_extraction_size = 3;
9645
9646        assert_eq!(
9647            verify_non_seekable_stream_with_options(
9648                std::io::Cursor::new(archive.bytes),
9649                &master_key(),
9650                options
9651            )
9652            .unwrap_err(),
9653            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
9654        );
9655    }
9656
9657    #[test]
9658    fn live_non_seekable_verify_reports_root_auth_wire_only() {
9659        let archive = write_archive_with_root_auth(
9660            &[RegularFile::new("signed-live.txt", b"root-auth stream")],
9661            &master_key(),
9662            single_stream_options(),
9663            test_root_auth_config(),
9664            |request| Ok(test_root_auth_value(request)),
9665        )
9666        .unwrap();
9667
9668        let report =
9669            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
9670
9671        assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
9672    }
9673
9674    #[test]
9675    fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
9676        let archive = write_archive(
9677            &[
9678                RegularFile::new("alpha.txt", b"alpha"),
9679                RegularFile::new("nested/beta.txt", b"beta"),
9680            ],
9681            &master_key(),
9682            single_stream_options(),
9683        )
9684        .unwrap();
9685        let tmp = tempfile::tempdir().unwrap();
9686        let out = tmp.path().join("out");
9687
9688        let report = extract_non_seekable_stream_to_dir(
9689            std::io::Cursor::new(archive.bytes),
9690            &master_key(),
9691            &out,
9692            NonSeekableReaderOptions::default(),
9693            SafeExtractionOptions::default(),
9694        )
9695        .unwrap();
9696
9697        assert_eq!(report.verification.file_count, 2);
9698        assert_eq!(report.extracted_member_count, 2);
9699        assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
9700        assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
9701    }
9702
9703    #[test]
9704    fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
9705        let archive = write_archive(
9706            &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
9707            &master_key(),
9708            single_stream_options(),
9709        )
9710        .unwrap();
9711        let tmp = tempfile::tempdir().unwrap();
9712        let out = tmp.path().join("out");
9713
9714        extract_non_seekable_stream_to_dir(
9715            ChunkedReader::new(archive.bytes, 1),
9716            &master_key(),
9717            &out,
9718            NonSeekableReaderOptions::default(),
9719            SafeExtractionOptions::default(),
9720        )
9721        .unwrap();
9722
9723        assert_eq!(
9724            fs::read(out.join("tiny-extract.txt")).unwrap(),
9725            b"chunked extraction"
9726        );
9727    }
9728
9729    #[test]
9730    fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
9731        let archive = write_archive(
9732            &[RegularFile::new("late-fail.txt", b"must remain staged")],
9733            &master_key(),
9734            single_stream_options(),
9735        )
9736        .unwrap();
9737        let mut corrupted = archive.bytes;
9738        corrupt_v41_terminal_recovery(&mut corrupted);
9739        let tmp = tempfile::tempdir().unwrap();
9740        let out = tmp.path().join("out");
9741
9742        match extract_non_seekable_stream_to_dir(
9743            std::io::Cursor::new(corrupted),
9744            &master_key(),
9745            &out,
9746            NonSeekableReaderOptions::default(),
9747            SafeExtractionOptions::default(),
9748        )
9749        .unwrap_err()
9750        {
9751            ExtractError::Format(err) => assert_eq!(
9752                err,
9753                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
9754            ),
9755            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
9756        }
9757        assert!(!out.exists());
9758    }
9759
9760    #[test]
9761    fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
9762        let archive = write_archive(
9763            &[RegularFile::new("same.txt", b"new")],
9764            &master_key(),
9765            single_stream_options(),
9766        )
9767        .unwrap();
9768        let tmp = tempfile::tempdir().unwrap();
9769        let out = tmp.path().join("out");
9770        fs::create_dir(&out).unwrap();
9771        fs::write(out.join("same.txt"), b"old").unwrap();
9772
9773        match extract_non_seekable_stream_to_dir(
9774            std::io::Cursor::new(archive.bytes.clone()),
9775            &master_key(),
9776            &out,
9777            NonSeekableReaderOptions::default(),
9778            SafeExtractionOptions::default(),
9779        )
9780        .unwrap_err()
9781        {
9782            ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
9783            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
9784        }
9785        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
9786
9787        extract_non_seekable_stream_to_dir(
9788            std::io::Cursor::new(archive.bytes),
9789            &master_key(),
9790            &out,
9791            NonSeekableReaderOptions::default(),
9792            SafeExtractionOptions {
9793                overwrite_existing: true,
9794            },
9795        )
9796        .unwrap();
9797        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
9798    }
9799
9800    #[test]
9801    fn live_non_seekable_list_stream_matches_seekable_final_view() {
9802        let archive = write_archive(
9803            &[
9804                RegularFile::new("a.txt", b"a"),
9805                RegularFile::new("b.txt", b"bb"),
9806            ],
9807            &master_key(),
9808            single_stream_options(),
9809        )
9810        .unwrap();
9811        let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
9812        let expected = seekable.list_files().unwrap();
9813
9814        let report = list_non_seekable_stream(
9815            std::io::Cursor::new(archive.bytes),
9816            &master_key(),
9817            NonSeekableReaderOptions::default(),
9818        )
9819        .unwrap();
9820
9821        assert_eq!(report.verification.file_count, 2);
9822        assert_eq!(report.entries, expected);
9823    }
9824
9825    #[test]
9826    fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
9827        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9828        let mut bad_flags = archive.bootstrap_sidecar.clone();
9829        rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
9830            header.flags |= 0x08;
9831        });
9832        assert_eq!(
9833            open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
9834                .unwrap_err(),
9835            FormatError::UnknownBootstrapSidecarFlags(0x0b)
9836        );
9837
9838        let mut trailing = archive.bootstrap_sidecar.clone();
9839        trailing.push(0);
9840        assert_eq!(
9841            open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
9842                .unwrap_err(),
9843            FormatError::NonCanonicalBootstrapSidecarLayout
9844        );
9845    }
9846
9847    #[test]
9848    fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
9849        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9850        let mut wrong_volume = archive.bootstrap_sidecar.clone();
9851        mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
9852            footer.volume_index = 1;
9853        });
9854        assert_eq!(
9855            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
9856                .unwrap_err(),
9857            FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
9858        );
9859
9860        let mut non_authoritative = archive.bootstrap_sidecar.clone();
9861        mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
9862            footer.is_authoritative = 0;
9863        });
9864        assert_eq!(
9865            open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
9866                .unwrap_err(),
9867            FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
9868        );
9869    }
9870
9871    #[test]
9872    fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
9873        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9874        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9875        let crypto_start = volume_header.crypto_header_offset as usize;
9876        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9877        let crypto_header = CryptoHeader::parse(
9878            &archive.bytes[crypto_start..crypto_end],
9879            volume_header.crypto_header_length,
9880        )
9881        .unwrap();
9882        let subkeys = Subkeys::derive(
9883            &master_key(),
9884            &volume_header.archive_uuid,
9885            &volume_header.session_id,
9886        )
9887        .unwrap();
9888        let mut opened_header = volume_header;
9889        opened_header.volume_index = 1;
9890
9891        let parsed = parse_bootstrap_sidecar(
9892            &archive.bootstrap_sidecar,
9893            &opened_header,
9894            &crypto_header.fixed,
9895            &subkeys,
9896        )
9897        .unwrap();
9898
9899        assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
9900    }
9901
9902    #[test]
9903    fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
9904        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9905        let mut conflicting = archive.bootstrap_sidecar.clone();
9906        mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
9907            footer.index_root_first_block += 1;
9908        });
9909
9910        assert_eq!(
9911            open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
9912                .unwrap_err(),
9913            FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
9914        );
9915    }
9916
9917    #[test]
9918    fn sidecar_size_cap_counts_only_present_sparse_sections() {
9919        let mut crypto_header = test_crypto_header();
9920        crypto_header.has_dictionary = 1;
9921        crypto_header.index_root_fec_data_shards = 1;
9922        crypto_header.index_root_fec_parity_shards = 0;
9923        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9924        let header = BootstrapSidecarHeader {
9925            archive_uuid: [0x31; 16],
9926            session_id: [0x42; 16],
9927            flags: 0x04,
9928            manifest_footer_offset: 0,
9929            manifest_footer_length: 0,
9930            index_root_records_offset: 0,
9931            index_root_records_length: 0,
9932            dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9933            dictionary_records_length: record_len,
9934            sidecar_hmac: [0u8; 32],
9935            header_crc32c: 0,
9936        };
9937
9938        validate_sidecar_size_cap(
9939            &header,
9940            &crypto_header,
9941            BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
9942        )
9943        .unwrap();
9944        assert_eq!(
9945            validate_sidecar_size_cap(
9946                &header,
9947                &crypto_header,
9948                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
9949            )
9950            .unwrap_err(),
9951            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
9952        );
9953    }
9954
9955    #[test]
9956    fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
9957        let mut crypto_header = test_crypto_header();
9958        crypto_header.index_root_fec_data_shards = 1;
9959        crypto_header.index_root_fec_parity_shards = 0;
9960        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9961        let header = BootstrapSidecarHeader {
9962            archive_uuid: [0x31; 16],
9963            session_id: [0x42; 16],
9964            flags: 0x02,
9965            manifest_footer_offset: 0,
9966            manifest_footer_length: 0,
9967            index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
9968            index_root_records_length: record_len * 2,
9969            dictionary_records_offset: 0,
9970            dictionary_records_length: 0,
9971            sidecar_hmac: [0u8; 32],
9972            header_crc32c: 0,
9973        };
9974
9975        assert_eq!(
9976            validate_sidecar_size_cap(
9977                &header,
9978                &crypto_header,
9979                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
9980            )
9981            .unwrap_err(),
9982            FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
9983        );
9984    }
9985
9986    #[test]
9987    fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
9988        let mut crypto_header = test_crypto_header();
9989        crypto_header.block_size = u32::MAX;
9990        crypto_header.index_root_fec_data_shards = u16::MAX;
9991        crypto_header.index_root_fec_parity_shards = u16::MAX;
9992        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
9993        let max_records = crypto_header.index_root_fec_data_shards as u64
9994            + crypto_header.index_root_fec_parity_shards as u64;
9995        let max_section_len = max_records * record_len;
9996        let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
9997            + MANIFEST_FOOTER_LEN as u64
9998            + max_section_len
9999            + max_section_len;
10000        let header = BootstrapSidecarHeader {
10001            archive_uuid: [0x31; 16],
10002            session_id: [0x42; 16],
10003            flags: 0x01 | 0x02 | 0x04,
10004            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10005            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
10006            index_root_records_offset: 0,
10007            index_root_records_length: max_section_len,
10008            dictionary_records_offset: 0,
10009            dictionary_records_length: max_section_len,
10010            sidecar_hmac: [0u8; 32],
10011            header_crc32c: 0,
10012        };
10013
10014        validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
10015        assert_eq!(
10016            validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
10017            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10018        );
10019    }
10020
10021    #[test]
10022    fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
10023        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10024        let mut with_dictionary = archive.bootstrap_sidecar.clone();
10025        let header =
10026            BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
10027                .unwrap();
10028        let record_len = sidecar_record_len(&with_dictionary);
10029        let first_record = header.index_root_records_offset as usize;
10030        let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
10031        let dictionary_offset = with_dictionary.len() as u64;
10032        with_dictionary.extend_from_slice(&copied_record);
10033        rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
10034            header.flags |= 0x04;
10035            header.dictionary_records_offset = dictionary_offset;
10036            header.dictionary_records_length = record_len as u64;
10037        });
10038
10039        assert_eq!(
10040            open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
10041                .unwrap_err(),
10042            FormatError::InvalidArchive(
10043                "bootstrap sidecar has dictionary records while has_dictionary is false"
10044            )
10045        );
10046    }
10047
10048    #[test]
10049    fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
10050        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10051        let mut missing = archive.bootstrap_sidecar.clone();
10052        let record_len = sidecar_record_len(&missing);
10053        let new_len = missing.len() - record_len;
10054        missing.truncate(new_len);
10055        rewrite_sidecar_header(&mut missing, &master_key(), |header| {
10056            header.index_root_records_length -= record_len as u64;
10057        });
10058        assert_eq!(
10059            open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
10060                .unwrap_err(),
10061            FormatError::InvalidArchive(
10062                "sidecar BlockRecord section does not match declared extent"
10063            )
10064        );
10065
10066        let mut duplicate = archive.bootstrap_sidecar.clone();
10067        mutate_sidecar_index_record(&mut duplicate, 1, |record| {
10068            record.block_index -= 1;
10069        });
10070        assert_eq!(
10071            open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
10072                .unwrap_err(),
10073            FormatError::InvalidArchive(
10074                "sidecar BlockRecord section has missing or duplicate blocks"
10075            )
10076        );
10077
10078        let mut misordered = archive.bootstrap_sidecar.clone();
10079        swap_sidecar_index_records(&mut misordered, 0, 1);
10080        assert_eq!(
10081            open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
10082                .unwrap_err(),
10083            FormatError::InvalidArchive(
10084                "sidecar BlockRecord section has missing or duplicate blocks"
10085            )
10086        );
10087
10088        let mut wrong_kind = archive.bootstrap_sidecar.clone();
10089        mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
10090            record.kind = BlockKind::PayloadData;
10091        });
10092        assert_eq!(
10093            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
10094                .unwrap_err(),
10095            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
10096        );
10097
10098        let mut wrong_last = archive.bootstrap_sidecar.clone();
10099        mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
10100            record.flags = 0;
10101        });
10102        assert_eq!(
10103            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
10104                .unwrap_err(),
10105            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
10106        );
10107    }
10108
10109    #[test]
10110    fn verify_helper_rejects_envelope_frame_coverage_gap() {
10111        let frames = BTreeMap::from([(
10112            0,
10113            FrameEntry {
10114                frame_index: 0,
10115                envelope_index: 0,
10116                offset_in_envelope: 0,
10117                compressed_size: 10,
10118                decompressed_size: 512,
10119                flags: 0,
10120                tar_stream_offset: 0,
10121            },
10122        )]);
10123        let envelopes = BTreeMap::from([(
10124            0,
10125            EnvelopeEntry {
10126                envelope_index: 0,
10127                first_block_index: 0,
10128                data_block_count: 1,
10129                parity_block_count: 1,
10130                encrypted_size: 4096,
10131                plaintext_size: 11,
10132                first_frame_index: 0,
10133                frame_count: 1,
10134            },
10135        )]);
10136
10137        assert_eq!(
10138            validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
10139            FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
10140        );
10141    }
10142
10143    #[test]
10144    fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
10145        assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
10146        assert_eq!(
10147            validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
10148            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
10149        );
10150        assert_eq!(
10151            validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
10152            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
10153        );
10154    }
10155
10156    #[test]
10157    fn verify_rejects_authenticated_content_hash_mismatch() {
10158        let options = WriterOptions {
10159            index_root_fec_parity_shards: 0,
10160            ..single_stream_options()
10161        };
10162        let archive = write_archive(
10163            &[RegularFile::new("content-hash.txt", b"hash covered")],
10164            &master_key(),
10165            options,
10166        )
10167        .unwrap();
10168        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10169
10170        let mut root = opened.index_root.clone();
10171        root.header.content_sha256 = [0xa5; 32];
10172        let root_plaintext = root.to_bytes();
10173        IndexRoot::parse(
10174            &root_plaintext,
10175            false,
10176            metadata_limits(&opened.crypto_header),
10177        )
10178        .unwrap();
10179        assert_eq!(
10180            root_plaintext.len() as u32,
10181            opened.manifest_footer.index_root_decompressed_size
10182        );
10183
10184        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
10185        let mut next_block_index = opened.manifest_footer.index_root_first_block;
10186        let replacement = encrypt_test_object(
10187            &compressed_root,
10188            &opened.subkeys.index_root_key,
10189            &opened.subkeys.index_nonce_seed,
10190            b"idxroot",
10191            0,
10192            BlockKind::IndexRootData,
10193            &mut next_block_index,
10194            &opened.crypto_header,
10195            &opened.volume_header,
10196        );
10197        assert_eq!(
10198            replacement.extent.data_block_count,
10199            opened.manifest_footer.index_root_data_block_count
10200        );
10201        assert_eq!(
10202            replacement.extent.encrypted_size,
10203            opened.manifest_footer.index_root_encrypted_size
10204        );
10205
10206        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10207        let crypto_end = volume_header.crypto_header_offset as usize
10208            + volume_header.crypto_header_length as usize;
10209        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
10210        let mut malformed = archive.bytes.clone();
10211        for record in replacement.records {
10212            let offset = crypto_end + record.block_index as usize * record_len;
10213            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
10214        }
10215
10216        let reopened = open_archive(&malformed, &master_key()).unwrap();
10217        assert_eq!(
10218            reopened.verify().unwrap_err(),
10219            FormatError::InvalidArchive(
10220                "IndexRoot content_sha256 does not match decoded tar stream"
10221            )
10222        );
10223    }
10224
10225    #[test]
10226    fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
10227        let (mut path_mismatch, _) = multi_envelope_reader_fixture();
10228        rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
10229            path[0] = b'x';
10230        });
10231        assert_eq!(
10232            path_mismatch.verify().unwrap_err(),
10233            FormatError::InvalidArchive("tar member path does not match FileEntry path")
10234        );
10235
10236        let (mut size_mismatch, _) = multi_envelope_reader_fixture();
10237        rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
10238            file.file_data_size += 1;
10239        });
10240        assert_eq!(
10241            size_mismatch.verify().unwrap_err(),
10242            FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
10243        );
10244    }
10245
10246    #[test]
10247    fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
10248        let (mut opened, _) = multi_envelope_reader_fixture();
10249        let locating = opened.index_root.shards[0].clone();
10250        let mut duplicate = opened.load_index_shard(&locating).unwrap();
10251        duplicate.header.shard_index = 1;
10252        duplicate.frames[0].flags ^= 0x0000_0001;
10253        let duplicate_plaintext = duplicate.to_bytes();
10254        let mut next_block_index = opened
10255            .blocks
10256            .keys()
10257            .last()
10258            .copied()
10259            .map(|index| index + 1)
10260            .unwrap_or(0);
10261        let duplicate_object = encrypt_test_object(
10262            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
10263            &opened.subkeys.index_shard_key,
10264            &opened.subkeys.index_nonce_seed,
10265            b"idxshard",
10266            1,
10267            BlockKind::IndexShardData,
10268            &mut next_block_index,
10269            &opened.crypto_header,
10270            &opened.volume_header,
10271        );
10272        insert_records(&mut opened.blocks, &duplicate_object.records);
10273        opened.index_root.shards.push(ShardEntry {
10274            shard_index: 1,
10275            first_block_index: duplicate_object.extent.first_block_index,
10276            data_block_count: duplicate_object.extent.data_block_count,
10277            parity_block_count: 0,
10278            encrypted_size: duplicate_object.extent.encrypted_size,
10279            decompressed_size: duplicate_plaintext.len() as u32,
10280            file_count: locating.file_count,
10281            first_path_hash: locating.first_path_hash,
10282            last_path_hash: locating.last_path_hash,
10283        });
10284        opened.index_root.header.file_count += locating.file_count as u64;
10285
10286        assert_eq!(
10287            opened.verify().unwrap_err(),
10288            FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
10289        );
10290    }
10291
10292    #[test]
10293    fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
10294        let (mut opened, _) = multi_envelope_reader_fixture();
10295        let locating = opened.index_root.shards[0].clone();
10296        let mut duplicate = opened.load_index_shard(&locating).unwrap();
10297        duplicate.header.shard_index = 1;
10298        duplicate.envelopes[0].first_block_index += 1;
10299        let duplicate_plaintext = duplicate.to_bytes();
10300        let mut next_block_index = opened
10301            .blocks
10302            .keys()
10303            .last()
10304            .copied()
10305            .map(|index| index + 1)
10306            .unwrap_or(0);
10307        let duplicate_object = encrypt_test_object(
10308            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
10309            &opened.subkeys.index_shard_key,
10310            &opened.subkeys.index_nonce_seed,
10311            b"idxshard",
10312            1,
10313            BlockKind::IndexShardData,
10314            &mut next_block_index,
10315            &opened.crypto_header,
10316            &opened.volume_header,
10317        );
10318        insert_records(&mut opened.blocks, &duplicate_object.records);
10319        opened.index_root.shards.push(ShardEntry {
10320            shard_index: 1,
10321            first_block_index: duplicate_object.extent.first_block_index,
10322            data_block_count: duplicate_object.extent.data_block_count,
10323            parity_block_count: 0,
10324            encrypted_size: duplicate_object.extent.encrypted_size,
10325            decompressed_size: duplicate_plaintext.len() as u32,
10326            file_count: locating.file_count,
10327            first_path_hash: locating.first_path_hash,
10328            last_path_hash: locating.last_path_hash,
10329        });
10330        opened.index_root.header.file_count += locating.file_count as u64;
10331
10332        assert_eq!(
10333            opened.verify().unwrap_err(),
10334            FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
10335        );
10336    }
10337
10338    #[test]
10339    fn verify_rejects_non_contiguous_global_envelope_indexes() {
10340        let (mut opened, _) = multi_envelope_reader_fixture();
10341        replace_first_index_shard(&mut opened, |shard| {
10342            let frame = shard
10343                .frames
10344                .iter_mut()
10345                .find(|entry| entry.frame_index == 1)
10346                .unwrap();
10347            frame.envelope_index = 2;
10348
10349            let envelope = shard
10350                .envelopes
10351                .iter_mut()
10352                .find(|entry| entry.envelope_index == 1)
10353                .unwrap();
10354            envelope.envelope_index = 2;
10355        });
10356
10357        assert_eq!(
10358            opened.verify().unwrap_err(),
10359            FormatError::InvalidMetadata {
10360                structure: "EnvelopeEntry",
10361                reason: "global index coverage has a gap",
10362            }
10363        );
10364    }
10365
10366    #[test]
10367    fn verify_rejects_payload_object_extent_overlap() {
10368        let (mut opened, _) = multi_envelope_reader_fixture();
10369        replace_first_index_shard(&mut opened, |shard| {
10370            let first_block_index = shard.envelopes[0].first_block_index;
10371            shard.envelopes[1].first_block_index = first_block_index;
10372        });
10373
10374        assert_eq!(
10375            opened.verify().unwrap_err(),
10376            FormatError::InvalidArchive("encrypted object block ranges overlap")
10377        );
10378    }
10379
10380    #[test]
10381    fn verify_accepts_cross_shard_shared_envelope_frame_union() {
10382        let volume_header = test_volume_header();
10383        let crypto_header = test_crypto_header();
10384        let subkeys = Subkeys::derive(
10385            &master_key(),
10386            &volume_header.archive_uuid,
10387            &volume_header.session_id,
10388        )
10389        .unwrap();
10390        let mut next_block_index = 0u64;
10391        let mut blocks = BTreeMap::new();
10392
10393        let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
10394        let beta = test_member(b"beta.txt", b"beta cross shard\n");
10395        let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
10396        let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
10397        let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
10398        let envelope_plaintext =
10399            [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
10400        let payload = encrypt_test_object(
10401            &envelope_plaintext,
10402            &subkeys.enc_key,
10403            &subkeys.nonce_seed,
10404            b"envelope",
10405            0,
10406            BlockKind::PayloadData,
10407            &mut next_block_index,
10408            &crypto_header,
10409            &volume_header,
10410        );
10411        insert_records(&mut blocks, &payload.records);
10412
10413        let envelope = EnvelopeEntry {
10414            envelope_index: 0,
10415            first_block_index: payload.extent.first_block_index,
10416            data_block_count: payload.extent.data_block_count,
10417            parity_block_count: 0,
10418            encrypted_size: payload.extent.encrypted_size,
10419            plaintext_size: envelope_plaintext.len() as u32,
10420            first_frame_index: 0,
10421            frame_count: 2,
10422        };
10423        let frame0 = FrameEntry {
10424            frame_index: 0,
10425            envelope_index: 0,
10426            offset_in_envelope: 0,
10427            compressed_size: frame0_plaintext.len() as u32,
10428            decompressed_size: alpha.len() as u32,
10429            flags: 0x0000_0003,
10430            tar_stream_offset: 0,
10431        };
10432        let frame1 = FrameEntry {
10433            frame_index: 1,
10434            envelope_index: 0,
10435            offset_in_envelope: frame0_plaintext.len() as u32,
10436            compressed_size: frame1_plaintext.len() as u32,
10437            decompressed_size: beta.len() as u32,
10438            flags: 0x0000_0003,
10439            tar_stream_offset: alpha.len() as u64,
10440        };
10441
10442        let (shard0_plaintext, first0, last0) = build_test_index_shard(
10443            &[TestFileMeta {
10444                path: b"alpha.txt".to_vec(),
10445                frame_index: 0,
10446                tar_stream_offset: 0,
10447                member_group_size: alpha.len() as u64,
10448                file_data_size: b"alpha cross shard\n".len() as u64,
10449            }],
10450            &[frame0],
10451            std::slice::from_ref(&envelope),
10452        );
10453        let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
10454            &[TestFileMeta {
10455                path: b"beta.txt".to_vec(),
10456                frame_index: 1,
10457                tar_stream_offset: alpha.len() as u64,
10458                member_group_size: beta.len() as u64,
10459                file_data_size: b"beta cross shard\n".len() as u64,
10460            }],
10461            &[frame1],
10462            std::slice::from_ref(&envelope),
10463        );
10464        shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
10465
10466        let shard0 = encrypt_test_object(
10467            &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
10468            &subkeys.index_shard_key,
10469            &subkeys.index_nonce_seed,
10470            b"idxshard",
10471            0,
10472            BlockKind::IndexShardData,
10473            &mut next_block_index,
10474            &crypto_header,
10475            &volume_header,
10476        );
10477        let shard1 = encrypt_test_object(
10478            &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
10479            &subkeys.index_shard_key,
10480            &subkeys.index_nonce_seed,
10481            b"idxshard",
10482            1,
10483            BlockKind::IndexShardData,
10484            &mut next_block_index,
10485            &crypto_header,
10486            &volume_header,
10487        );
10488        insert_records(&mut blocks, &shard0.records);
10489        insert_records(&mut blocks, &shard1.records);
10490
10491        let index_root = IndexRoot {
10492            header: IndexRootHeader {
10493                frame_count: 2,
10494                envelope_count: 1,
10495                file_count: 2,
10496                payload_block_count: payload.extent.data_block_count as u64,
10497                tar_total_size: tar_stream.len() as u64,
10498                content_sha256: sha256_bytes(&tar_stream),
10499                ..IndexRootHeader::empty()
10500            },
10501            shards: vec![
10502                ShardEntry {
10503                    shard_index: 0,
10504                    first_block_index: shard0.extent.first_block_index,
10505                    data_block_count: shard0.extent.data_block_count,
10506                    parity_block_count: 0,
10507                    encrypted_size: shard0.extent.encrypted_size,
10508                    decompressed_size: shard0_plaintext.len() as u32,
10509                    file_count: 1,
10510                    first_path_hash: first0,
10511                    last_path_hash: last0,
10512                },
10513                ShardEntry {
10514                    shard_index: 1,
10515                    first_block_index: shard1.extent.first_block_index,
10516                    data_block_count: shard1.extent.data_block_count,
10517                    parity_block_count: 0,
10518                    encrypted_size: shard1.extent.encrypted_size,
10519                    decompressed_size: shard1_plaintext.len() as u32,
10520                    file_count: 1,
10521                    first_path_hash: first1,
10522                    last_path_hash: last1,
10523                },
10524            ],
10525            directory_hint_shards: Vec::new(),
10526        };
10527
10528        let index_root_plaintext = index_root.to_bytes();
10529        let index_root_object = encrypt_test_object(
10530            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
10531            &subkeys.index_root_key,
10532            &subkeys.index_nonce_seed,
10533            b"idxroot",
10534            0,
10535            BlockKind::IndexRootData,
10536            &mut next_block_index,
10537            &crypto_header,
10538            &volume_header,
10539        );
10540        insert_records(&mut blocks, &index_root_object.records);
10541
10542        let archive_uuid = volume_header.archive_uuid;
10543        let session_id = volume_header.session_id;
10544        let opened = OpenedArchive {
10545            options: ReaderOptions::default(),
10546            observed_archive_bytes: 1_000_000,
10547            observed_volume_count: 1,
10548            subkeys,
10549            blocks,
10550            lazy_blocks: None,
10551            crypto_header_bytes: Vec::new(),
10552            volume_header,
10553            crypto_header,
10554            manifest_footer: ManifestFooter {
10555                archive_uuid,
10556                session_id,
10557                volume_index: 0,
10558                is_authoritative: 1,
10559                total_volumes: 1,
10560                index_root_first_block: index_root_object.extent.first_block_index,
10561                index_root_data_block_count: index_root_object.extent.data_block_count,
10562                index_root_parity_block_count: 0,
10563                index_root_encrypted_size: index_root_object.extent.encrypted_size,
10564                index_root_decompressed_size: index_root_plaintext.len() as u32,
10565                manifest_hmac: [0u8; 32],
10566            },
10567            volume_trailer: Some(VolumeTrailer {
10568                archive_uuid,
10569                session_id,
10570                volume_index: 0,
10571                block_count: next_block_index,
10572                bytes_written: 0,
10573                manifest_footer_offset: 0,
10574                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
10575                closed_at_ns: 0,
10576                root_auth_footer_offset: 0,
10577                root_auth_footer_length: 0,
10578                root_auth_flags: 0,
10579                trailer_hmac: [0u8; 32],
10580            }),
10581            root_auth_footer: None,
10582            index_root,
10583            payload_dictionary: None,
10584        };
10585
10586        opened.verify().unwrap();
10587    }
10588
10589    #[test]
10590    fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
10591        let options = WriterOptions {
10592            index_root_fec_parity_shards: 0,
10593            ..single_stream_options()
10594        };
10595        let archive = write_archive(
10596            &[RegularFile::new("only.txt", b"only payload")],
10597            &master_key(),
10598            options,
10599        )
10600        .unwrap();
10601        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10602        assert!(opened.index_root.directory_hint_shards.is_empty());
10603
10604        let mut root = opened.index_root.clone();
10605        root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
10606        root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
10607        let root_plaintext = root.to_bytes();
10608        IndexRoot::parse(
10609            &root_plaintext,
10610            false,
10611            metadata_limits(&opened.crypto_header),
10612        )
10613        .unwrap();
10614        assert_eq!(
10615            root_plaintext.len() as u32,
10616            opened.manifest_footer.index_root_decompressed_size
10617        );
10618
10619        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
10620        let mut next_block_index = opened.manifest_footer.index_root_first_block;
10621        let replacement = encrypt_test_object(
10622            &compressed_root,
10623            &opened.subkeys.index_root_key,
10624            &opened.subkeys.index_nonce_seed,
10625            b"idxroot",
10626            0,
10627            BlockKind::IndexRootData,
10628            &mut next_block_index,
10629            &opened.crypto_header,
10630            &opened.volume_header,
10631        );
10632        assert_eq!(
10633            replacement.extent.first_block_index,
10634            opened.manifest_footer.index_root_first_block
10635        );
10636        assert_eq!(
10637            replacement.extent.data_block_count,
10638            opened.manifest_footer.index_root_data_block_count
10639        );
10640        assert_eq!(
10641            replacement.extent.encrypted_size,
10642            opened.manifest_footer.index_root_encrypted_size
10643        );
10644
10645        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10646        let crypto_end = volume_header.crypto_header_offset as usize
10647            + volume_header.crypto_header_length as usize;
10648        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
10649        let mut malformed = archive.bytes.clone();
10650        for record in replacement.records {
10651            let offset = crypto_end + record.block_index as usize * record_len;
10652            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
10653        }
10654
10655        let reopened = open_archive(&malformed, &master_key()).unwrap();
10656        assert_eq!(
10657            reopened.index_root.header.file_count,
10658            DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
10659        );
10660        assert!(reopened.index_root.directory_hint_shards.is_empty());
10661
10662        assert_eq!(
10663            reopened.verify().unwrap_err(),
10664            FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
10665        );
10666    }
10667
10668    #[test]
10669    fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
10670        let mut map = DirectoryHintMap::new();
10671        add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
10672        add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
10673
10674        assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
10675        assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
10676        assert_eq!(
10677            map.get(b"foo/bar".as_slice()),
10678            Some(&BTreeSet::from([2, 4]))
10679        );
10680        assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
10681        assert!(!map.contains_key(b"foobar".as_slice()));
10682    }
10683
10684    #[test]
10685    fn directory_hint_validation_requires_exact_global_map() {
10686        let mut expected = DirectoryHintMap::new();
10687        add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
10688        add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
10689        let rows = sorted_directory_hint_rows(&expected);
10690        let table = directory_hint_table_from_rows(7, &rows, 2);
10691
10692        validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
10693            .unwrap();
10694
10695        let mut missing_root = expected.clone();
10696        missing_root.remove(&Vec::new());
10697        let missing_root_rows = sorted_directory_hint_rows(&missing_root);
10698        let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
10699        assert_eq!(
10700            validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
10701                .unwrap_err(),
10702            FormatError::InvalidArchive("directory hint map does not match decoded files")
10703        );
10704
10705        let mut expected_missing_directory_entry = expected.clone();
10706        expected_missing_directory_entry
10707            .get_mut(b"foo".as_slice())
10708            .unwrap()
10709            .remove(&1);
10710        assert_eq!(
10711            validate_directory_hint_tables_against_expected(
10712                std::slice::from_ref(&table),
10713                &expected_missing_directory_entry,
10714            )
10715            .unwrap_err(),
10716            FormatError::InvalidArchive("directory hint map does not match decoded files")
10717        );
10718
10719        let mut extra = expected.clone();
10720        extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
10721        let extra_rows = sorted_directory_hint_rows(&extra);
10722        let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
10723        assert_eq!(
10724            validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
10725            FormatError::InvalidArchive("directory hint map does not match decoded files")
10726        );
10727    }
10728
10729    #[test]
10730    fn directory_hint_validation_rejects_global_order_mismatch() {
10731        let mut expected = DirectoryHintMap::new();
10732        expected.insert(Vec::new(), BTreeSet::from([0]));
10733        expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
10734        let rows = sorted_directory_hint_rows(&expected);
10735        let first = directory_hint_table_from_rows(8, &rows[..1], 1);
10736        let second = directory_hint_table_from_rows(9, &rows[1..], 1);
10737
10738        assert_eq!(
10739            validate_directory_hint_tables_against_expected(&[second, first], &expected)
10740                .unwrap_err(),
10741            FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
10742        );
10743    }
10744
10745    #[test]
10746    fn object_extent_rejects_parity_above_class_cap() {
10747        let crypto_header = CryptoHeaderFixed {
10748            length: 0,
10749            compression_algo: CompressionAlgo::ZstdFramed,
10750            aead_algo: AeadAlgo::AesGcmSiv256,
10751            fec_algo: FecAlgo::ReedSolomonGF16,
10752            kdf_algo: KdfAlgo::Raw,
10753            chunk_size: 1024,
10754            envelope_target_size: 4096,
10755            block_size: 4096,
10756            fec_data_shards: 1,
10757            fec_parity_shards: 1,
10758            index_fec_data_shards: 1,
10759            index_fec_parity_shards: 1,
10760            index_root_fec_data_shards: 1,
10761            index_root_fec_parity_shards: 1,
10762            stripe_width: 1,
10763            volume_loss_tolerance: 0,
10764            bit_rot_buffer_pct: 0,
10765            has_dictionary: 0,
10766            max_path_length: 4096,
10767            expected_volume_size: 0,
10768        };
10769        let extent = ObjectExtent {
10770            first_block_index: 0,
10771            data_block_count: 1,
10772            parity_block_count: 2,
10773            encrypted_size: 4096,
10774        };
10775
10776        assert_eq!(
10777            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
10778            FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
10779        );
10780    }
10781
10782    #[test]
10783    fn object_extent_rejects_parity_below_recoverability_requirement() {
10784        let crypto_header = CryptoHeaderFixed {
10785            length: 0,
10786            compression_algo: CompressionAlgo::ZstdFramed,
10787            aead_algo: AeadAlgo::AesGcmSiv256,
10788            fec_algo: FecAlgo::ReedSolomonGF16,
10789            kdf_algo: KdfAlgo::Raw,
10790            chunk_size: 1024,
10791            envelope_target_size: 4096,
10792            block_size: 4096,
10793            fec_data_shards: 1,
10794            fec_parity_shards: 1,
10795            index_fec_data_shards: 1,
10796            index_fec_parity_shards: 1,
10797            index_root_fec_data_shards: 1,
10798            index_root_fec_parity_shards: 1,
10799            stripe_width: 2,
10800            volume_loss_tolerance: 1,
10801            bit_rot_buffer_pct: 0,
10802            has_dictionary: 0,
10803            max_path_length: 4096,
10804            expected_volume_size: 0,
10805        };
10806        let extent = ObjectExtent {
10807            first_block_index: 0,
10808            data_block_count: 1,
10809            parity_block_count: 0,
10810            encrypted_size: 4096,
10811        };
10812
10813        assert_eq!(
10814            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
10815            FormatError::InvalidArchive(
10816                "encrypted object parity does not match v41 compute_parity"
10817            )
10818        );
10819    }
10820
10821    #[test]
10822    fn encrypted_object_extent_matrix_rejects_overlaps() {
10823        let (opened, _) = multi_envelope_reader_fixture();
10824        let loaded_shard = opened
10825            .load_index_shard(&opened.index_root.shards[0])
10826            .unwrap();
10827        let base_envelopes = loaded_shard
10828            .envelopes
10829            .iter()
10830            .map(|entry| (entry.envelope_index, entry.clone()))
10831            .collect::<BTreeMap<_, _>>();
10832        let payload_start = loaded_shard.envelopes[0].first_block_index;
10833        let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
10834
10835        let mut payload_overlap = base_envelopes.clone();
10836        payload_overlap
10837            .get_mut(&loaded_shard.envelopes[1].envelope_index)
10838            .unwrap()
10839            .first_block_index = payload_start;
10840        assert_eq!(
10841            opened
10842                .validate_encrypted_object_block_ranges(&payload_overlap)
10843                .unwrap_err(),
10844            overlap
10845        );
10846
10847        let mut shard_overlap = opened.clone();
10848        let shard = shard_overlap.index_root.shards[0].clone();
10849        shard_overlap.index_root.shards.push(ShardEntry {
10850            shard_index: 1,
10851            ..shard
10852        });
10853        assert_eq!(
10854            shard_overlap
10855                .validate_encrypted_object_block_ranges(&base_envelopes)
10856                .unwrap_err(),
10857            overlap
10858        );
10859
10860        let mut dictionary_overlap = opened.clone();
10861        dictionary_overlap.crypto_header.has_dictionary = 1;
10862        dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
10863        dictionary_overlap
10864            .index_root
10865            .header
10866            .dictionary_data_block_count = 1;
10867        dictionary_overlap
10868            .index_root
10869            .header
10870            .dictionary_parity_block_count = 0;
10871        dictionary_overlap
10872            .index_root
10873            .header
10874            .dictionary_encrypted_size = 4096;
10875        dictionary_overlap
10876            .index_root
10877            .header
10878            .dictionary_decompressed_size = 128;
10879        assert_eq!(
10880            dictionary_overlap
10881                .validate_encrypted_object_block_ranges(&base_envelopes)
10882                .unwrap_err(),
10883            overlap
10884        );
10885
10886        let mut hint_overlap = opened.clone();
10887        hint_overlap
10888            .index_root
10889            .directory_hint_shards
10890            .push(DirectoryHintShardEntry {
10891                hint_shard_index: 0,
10892                first_dir_hash: [0; 8],
10893                last_dir_hash: [0; 8],
10894                first_block_index: payload_start,
10895                data_block_count: 1,
10896                parity_block_count: 0,
10897                encrypted_size: 4096,
10898                decompressed_size: 128,
10899                entry_count: 1,
10900            });
10901        assert_eq!(
10902            hint_overlap
10903                .validate_encrypted_object_block_ranges(&base_envelopes)
10904                .unwrap_err(),
10905            overlap
10906        );
10907    }
10908
10909    #[test]
10910    fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
10911        let volume_header = test_volume_header();
10912        let crypto_header = test_crypto_header();
10913        let subkeys = Subkeys::derive(
10914            &master_key(),
10915            &volume_header.archive_uuid,
10916            &volume_header.session_id,
10917        )
10918        .unwrap();
10919        let mut next_block_index = 0u64;
10920
10921        let index_root_payload = b"index root metadata object";
10922        let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
10923        assert_metadata_object_from_compressed(
10924            &{
10925                let mut bytes = index_root_compressed.clone();
10926                bytes.push(0);
10927                bytes
10928            },
10929            index_root_payload.len(),
10930            &subkeys,
10931            &volume_header,
10932            &crypto_header,
10933            &subkeys.index_root_key,
10934            &subkeys.index_nonce_seed,
10935            b"idxroot",
10936            0,
10937            BlockKind::IndexRootData,
10938            BlockKind::IndexRootParity,
10939            crypto_header.index_root_fec_data_shards,
10940            crypto_header.index_root_fec_parity_shards,
10941            &mut next_block_index,
10942            FormatError::TrailingBytesAfterZstdFrame,
10943        );
10944        assert_metadata_object_from_compressed(
10945            &index_root_compressed,
10946            index_root_payload.len() + 1,
10947            &subkeys,
10948            &volume_header,
10949            &crypto_header,
10950            &subkeys.index_root_key,
10951            &subkeys.index_nonce_seed,
10952            b"idxroot",
10953            0,
10954            BlockKind::IndexRootData,
10955            BlockKind::IndexRootParity,
10956            crypto_header.index_root_fec_data_shards,
10957            crypto_header.index_root_fec_parity_shards,
10958            &mut next_block_index,
10959            FormatError::ZstdDecompressedSizeMismatch {
10960                expected: index_root_payload.len() + 1,
10961                actual: index_root_payload.len(),
10962            },
10963        );
10964
10965        let index_shard_payload = b"index shard metadata object";
10966        let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
10967        assert_metadata_object_from_compressed(
10968            &{
10969                let mut bytes = index_shard_compressed.clone();
10970                bytes.push(0);
10971                bytes
10972            },
10973            index_shard_payload.len(),
10974            &subkeys,
10975            &volume_header,
10976            &crypto_header,
10977            &subkeys.index_shard_key,
10978            &subkeys.index_nonce_seed,
10979            b"idxshard",
10980            1,
10981            BlockKind::IndexShardData,
10982            BlockKind::IndexShardParity,
10983            crypto_header.index_fec_data_shards,
10984            crypto_header.index_fec_parity_shards,
10985            &mut next_block_index,
10986            FormatError::TrailingBytesAfterZstdFrame,
10987        );
10988        assert_metadata_object_from_compressed(
10989            &index_shard_compressed,
10990            index_shard_payload.len() + 1,
10991            &subkeys,
10992            &volume_header,
10993            &crypto_header,
10994            &subkeys.index_shard_key,
10995            &subkeys.index_nonce_seed,
10996            b"idxshard",
10997            1,
10998            BlockKind::IndexShardData,
10999            BlockKind::IndexShardParity,
11000            crypto_header.index_fec_data_shards,
11001            crypto_header.index_fec_parity_shards,
11002            &mut next_block_index,
11003            FormatError::ZstdDecompressedSizeMismatch {
11004                expected: index_shard_payload.len() + 1,
11005                actual: index_shard_payload.len(),
11006            },
11007        );
11008
11009        let directory_hint_payload = b"directory hint metadata object";
11010        let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
11011        assert_metadata_object_from_compressed(
11012            &{
11013                let mut bytes = directory_hint_compressed.clone();
11014                bytes.push(0);
11015                bytes
11016            },
11017            directory_hint_payload.len(),
11018            &subkeys,
11019            &volume_header,
11020            &crypto_header,
11021            &subkeys.dir_hint_key,
11022            &subkeys.index_nonce_seed,
11023            b"dirhint",
11024            0,
11025            BlockKind::DirectoryHintData,
11026            BlockKind::DirectoryHintParity,
11027            crypto_header.index_fec_data_shards,
11028            crypto_header.index_fec_parity_shards,
11029            &mut next_block_index,
11030            FormatError::TrailingBytesAfterZstdFrame,
11031        );
11032        assert_metadata_object_from_compressed(
11033            &directory_hint_compressed,
11034            directory_hint_payload.len() + 1,
11035            &subkeys,
11036            &volume_header,
11037            &crypto_header,
11038            &subkeys.dir_hint_key,
11039            &subkeys.index_nonce_seed,
11040            b"dirhint",
11041            0,
11042            BlockKind::DirectoryHintData,
11043            BlockKind::DirectoryHintParity,
11044            crypto_header.index_fec_data_shards,
11045            crypto_header.index_fec_parity_shards,
11046            &mut next_block_index,
11047            FormatError::ZstdDecompressedSizeMismatch {
11048                expected: directory_hint_payload.len() + 1,
11049                actual: directory_hint_payload.len(),
11050            },
11051        );
11052
11053        let dictionary_payload = b"dictionary metadata object";
11054        let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
11055        assert_metadata_object_from_compressed(
11056            &{
11057                let mut bytes = dictionary_compressed.clone();
11058                bytes.push(0);
11059                bytes
11060            },
11061            dictionary_payload.len(),
11062            &subkeys,
11063            &volume_header,
11064            &crypto_header,
11065            &subkeys.dictionary_key,
11066            &subkeys.index_nonce_seed,
11067            b"dict",
11068            0,
11069            BlockKind::DictionaryData,
11070            BlockKind::DictionaryParity,
11071            crypto_header.index_root_fec_data_shards,
11072            crypto_header.index_root_fec_parity_shards,
11073            &mut next_block_index,
11074            FormatError::TrailingBytesAfterZstdFrame,
11075        );
11076        assert_metadata_object_from_compressed(
11077            &dictionary_compressed,
11078            dictionary_payload.len() + 1,
11079            &subkeys,
11080            &volume_header,
11081            &crypto_header,
11082            &subkeys.dictionary_key,
11083            &subkeys.index_nonce_seed,
11084            b"dict",
11085            0,
11086            BlockKind::DictionaryData,
11087            BlockKind::DictionaryParity,
11088            crypto_header.index_root_fec_data_shards,
11089            crypto_header.index_root_fec_parity_shards,
11090            &mut next_block_index,
11091            FormatError::ZstdDecompressedSizeMismatch {
11092                expected: dictionary_payload.len() + 1,
11093                actual: dictionary_payload.len(),
11094            },
11095        );
11096    }
11097
11098    #[test]
11099    fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
11100        let volume_header = test_volume_header();
11101        let crypto_header = test_crypto_header();
11102        let subkeys = Subkeys::derive(
11103            &master_key(),
11104            &volume_header.archive_uuid,
11105            &volume_header.session_id,
11106        )
11107        .unwrap();
11108        let mut next_block_index = 0u64;
11109
11110        let index_root_payload = b"index root metadata object";
11111        let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
11112            index_root_payload,
11113            &subkeys,
11114            &volume_header,
11115            &crypto_header,
11116            &subkeys.index_root_key,
11117            &subkeys.index_nonce_seed,
11118            b"idxroot",
11119            0,
11120            BlockKind::IndexRootData,
11121            &mut next_block_index,
11122        );
11123        let mut index_root_extent = index_root_extent;
11124        index_root_extent.encrypted_size = index_root_extent
11125            .encrypted_size
11126            .saturating_add(crypto_header.block_size);
11127        assert_eq!(
11128            load_metadata_object_from_parts(
11129                &index_root_records,
11130                ObjectLoadContext::index_root(
11131                    &volume_header,
11132                    &crypto_header,
11133                    &subkeys,
11134                    index_root_extent,
11135                ),
11136                index_root_payload.len() as u32,
11137            )
11138            .unwrap_err(),
11139            FormatError::InvalidArchive(
11140                "encrypted object size is not data_block_count * block_size"
11141            )
11142        );
11143
11144        let index_shard_payload = b"index shard metadata object";
11145        let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
11146            index_shard_payload,
11147            &subkeys,
11148            &volume_header,
11149            &crypto_header,
11150            &subkeys.index_shard_key,
11151            &subkeys.index_nonce_seed,
11152            b"idxshard",
11153            1,
11154            BlockKind::IndexShardData,
11155            &mut next_block_index,
11156        );
11157        let mut index_shard_extent = index_shard_extent;
11158        index_shard_extent.encrypted_size = index_shard_extent
11159            .encrypted_size
11160            .saturating_add(crypto_header.block_size);
11161        assert_eq!(
11162            load_metadata_object_from_parts(
11163                &index_shard_records,
11164                ObjectLoadContext {
11165                    volume_header: &volume_header,
11166                    crypto_header: &crypto_header,
11167                    extent: index_shard_extent,
11168                    data_kind: BlockKind::IndexShardData,
11169                    parity_kind: BlockKind::IndexShardParity,
11170                    key: &subkeys.index_shard_key,
11171                    nonce_seed: &subkeys.index_nonce_seed,
11172                    domain: b"idxshard",
11173                    counter: 1,
11174                    class_data_shard_max: crypto_header.index_fec_data_shards,
11175                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
11176                },
11177                index_shard_payload.len() as u32,
11178            )
11179            .unwrap_err(),
11180            FormatError::InvalidArchive(
11181                "encrypted object size is not data_block_count * block_size"
11182            )
11183        );
11184
11185        let directory_hint_payload = b"directory hint metadata object";
11186        let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
11187            directory_hint_payload,
11188            &subkeys,
11189            &volume_header,
11190            &crypto_header,
11191            &subkeys.dir_hint_key,
11192            &subkeys.index_nonce_seed,
11193            b"dirhint",
11194            0,
11195            BlockKind::DirectoryHintData,
11196            &mut next_block_index,
11197        );
11198        let mut directory_hint_extent = directory_hint_extent;
11199        directory_hint_extent.encrypted_size = directory_hint_extent
11200            .encrypted_size
11201            .saturating_add(crypto_header.block_size);
11202        assert_eq!(
11203            load_metadata_object_from_parts(
11204                &directory_hint_records,
11205                ObjectLoadContext {
11206                    volume_header: &volume_header,
11207                    crypto_header: &crypto_header,
11208                    extent: directory_hint_extent,
11209                    data_kind: BlockKind::DirectoryHintData,
11210                    parity_kind: BlockKind::DirectoryHintParity,
11211                    key: &subkeys.dir_hint_key,
11212                    nonce_seed: &subkeys.index_nonce_seed,
11213                    domain: b"dirhint",
11214                    counter: 0,
11215                    class_data_shard_max: crypto_header.index_fec_data_shards,
11216                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
11217                },
11218                directory_hint_payload.len() as u32,
11219            )
11220            .unwrap_err(),
11221            FormatError::InvalidArchive(
11222                "encrypted object size is not data_block_count * block_size"
11223            )
11224        );
11225
11226        let dictionary_payload = b"dictionary metadata object";
11227        let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
11228            dictionary_payload,
11229            &subkeys,
11230            &volume_header,
11231            &crypto_header,
11232            &subkeys.dictionary_key,
11233            &subkeys.index_nonce_seed,
11234            b"dict",
11235            0,
11236            BlockKind::DictionaryData,
11237            &mut next_block_index,
11238        );
11239        let mut dictionary_extent = dictionary_extent;
11240        dictionary_extent.encrypted_size = dictionary_extent
11241            .encrypted_size
11242            .saturating_add(crypto_header.block_size);
11243        assert_eq!(
11244            load_metadata_object_from_parts(
11245                &dictionary_records,
11246                ObjectLoadContext {
11247                    volume_header: &volume_header,
11248                    crypto_header: &crypto_header,
11249                    extent: dictionary_extent,
11250                    data_kind: BlockKind::DictionaryData,
11251                    parity_kind: BlockKind::DictionaryParity,
11252                    key: &subkeys.dictionary_key,
11253                    nonce_seed: &subkeys.index_nonce_seed,
11254                    domain: b"dict",
11255                    counter: 0,
11256                    class_data_shard_max: crypto_header.index_root_fec_data_shards,
11257                    class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
11258                },
11259                dictionary_payload.len() as u32,
11260            )
11261            .unwrap_err(),
11262            FormatError::InvalidArchive(
11263                "encrypted object size is not data_block_count * block_size"
11264            )
11265        );
11266    }
11267
11268    #[test]
11269    fn opens_complete_multi_volume_archive() {
11270        let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
11271        let archive = write_archive(
11272            &files,
11273            &master_key(),
11274            WriterOptions {
11275                stripe_width: 2,
11276                volume_loss_tolerance: 1,
11277                ..single_stream_options()
11278            },
11279        )
11280        .unwrap();
11281        assert_eq!(archive.volumes.len(), 2);
11282
11283        let volume_refs = archive
11284            .volumes
11285            .iter()
11286            .map(Vec::as_slice)
11287            .collect::<Vec<_>>();
11288        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11289
11290        assert_eq!(opened.volume_header.stripe_width, 2);
11291        assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
11292        assert_eq!(
11293            opened.extract_file("alpha.txt").unwrap(),
11294            Some(b"hello from volume stripes".to_vec())
11295        );
11296        opened.verify().unwrap();
11297    }
11298
11299    #[test]
11300    fn recovers_from_one_missing_volume_when_parity_allows() {
11301        let files = [RegularFile::new("alpha.txt", b"recover me")];
11302        let archive = write_archive(
11303            &files,
11304            &master_key(),
11305            WriterOptions {
11306                stripe_width: 2,
11307                volume_loss_tolerance: 1,
11308                ..single_stream_options()
11309            },
11310        )
11311        .unwrap();
11312
11313        let recovered =
11314            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
11315        assert_eq!(
11316            recovered.extract_file("alpha.txt").unwrap(),
11317            Some(b"recover me".to_vec())
11318        );
11319        recovered.verify().unwrap();
11320    }
11321
11322    #[test]
11323    fn recovers_from_crc_corrupted_block_when_parity_allows() {
11324        let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
11325        let archive = write_archive(
11326            &files,
11327            &master_key(),
11328            WriterOptions {
11329                stripe_width: 2,
11330                volume_loss_tolerance: 1,
11331                ..single_stream_options()
11332            },
11333        )
11334        .unwrap();
11335        let mut volumes = archive.volumes.clone();
11336        corrupt_first_block_record_payload(&mut volumes[0]);
11337
11338        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11339        let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11340
11341        assert_eq!(
11342            recovered.extract_file("alpha.txt").unwrap(),
11343            Some(b"repair corrupt block".to_vec())
11344        );
11345        recovered.verify().unwrap();
11346    }
11347
11348    #[test]
11349    fn rejects_multi_volume_count_mismatch_without_tolerance() {
11350        let files = [RegularFile::new("alpha.txt", b"count check")];
11351        let archive = write_archive(
11352            &files,
11353            &master_key(),
11354            WriterOptions {
11355                stripe_width: 3,
11356                volume_loss_tolerance: 0,
11357                ..single_stream_options()
11358            },
11359        )
11360        .unwrap();
11361
11362        assert_eq!(
11363            open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
11364            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
11365        );
11366    }
11367
11368    #[test]
11369    fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
11370        let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
11371        let archive = write_archive(
11372            &files,
11373            &master_key(),
11374            WriterOptions {
11375                stripe_width: 2,
11376                volume_loss_tolerance: 1,
11377                ..single_stream_options()
11378            },
11379        )
11380        .unwrap();
11381
11382        let mut bad_first = archive.volumes[0].clone();
11383        rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
11384            footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
11385        });
11386
11387        open_archive_volumes(
11388            &[bad_first.as_slice(), archive.volumes[1].as_slice()],
11389            &master_key(),
11390        )
11391        .unwrap();
11392    }
11393
11394    #[test]
11395    fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
11396        let files = [RegularFile::new("alpha.txt", b"repair meta root")];
11397        let archive = write_archive(
11398            &files,
11399            &master_key(),
11400            WriterOptions {
11401                stripe_width: 2,
11402                volume_loss_tolerance: 1,
11403                ..single_stream_options()
11404            },
11405        )
11406        .unwrap();
11407        let mut volumes = archive.volumes.clone();
11408
11409        let mut corrupted = false;
11410        for volume in &mut volumes {
11411            if let Some(slot) =
11412                block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
11413            {
11414                corrupt_block_record_payload_at_slot(volume, *slot);
11415                corrupted = true;
11416                break;
11417            }
11418        }
11419        assert!(corrupted, "expected an IndexRootData record");
11420
11421        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11422        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11423        assert_eq!(
11424            opened.extract_file("alpha.txt").unwrap(),
11425            Some(b"repair meta root".to_vec())
11426        );
11427        opened.verify().unwrap();
11428    }
11429
11430    #[test]
11431    fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
11432        let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
11433        let archive = write_archive(
11434            &files,
11435            &master_key(),
11436            WriterOptions {
11437                stripe_width: 2,
11438                volume_loss_tolerance: 1,
11439                ..single_stream_options()
11440            },
11441        )
11442        .unwrap();
11443        let mut volumes = archive.volumes.clone();
11444
11445        let mut corrupted = false;
11446        for volume in &mut volumes {
11447            if let Some(slot) =
11448                block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
11449            {
11450                corrupt_block_record_payload_at_slot(volume, *slot);
11451                corrupted = true;
11452                break;
11453            }
11454        }
11455        assert!(corrupted, "expected an IndexShardData record");
11456
11457        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11458        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11459        assert_eq!(
11460            opened.extract_file("alpha.txt").unwrap(),
11461            Some(b"repair meta shard".to_vec())
11462        );
11463        opened.verify().unwrap();
11464    }
11465
11466    #[test]
11467    fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
11468        let files = [RegularFile::new(
11469            "alpha.txt",
11470            b"bitrot parity is not volume loss",
11471        )];
11472        let archive = write_archive(
11473            &files,
11474            &master_key(),
11475            WriterOptions {
11476                stripe_width: 2,
11477                volume_loss_tolerance: 0,
11478                bit_rot_buffer_pct: 1,
11479                ..single_stream_options()
11480            },
11481        )
11482        .unwrap();
11483
11484        assert_eq!(
11485            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
11486            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
11487        );
11488    }
11489
11490    #[test]
11491    fn repairs_crc_erasure_only_within_parity_budget() {
11492        let payload = pseudo_random_bytes(12_000);
11493        let archive = write_archive(
11494            &[RegularFile::new("rot.bin", &payload)],
11495            &master_key(),
11496            small_block_recovery_options(),
11497        )
11498        .unwrap();
11499        let payload_slots = first_payload_data_run_slots(&archive.bytes);
11500        assert!(
11501            payload_slots.len() >= 2,
11502            "fixture must contain a multi-block payload object"
11503        );
11504
11505        let mut one_erasure = archive.bytes.clone();
11506        corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
11507        let repaired = open_archive(&one_erasure, &master_key()).unwrap();
11508        assert_eq!(
11509            repaired.extract_file("rot.bin").unwrap(),
11510            Some(payload.clone())
11511        );
11512
11513        let mut two_erasures = archive.bytes.clone();
11514        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
11515        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
11516        let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
11517        assert_eq!(
11518            unrepaired.extract_file("rot.bin").unwrap_err(),
11519            FormatError::FecTooFewAvailableShards
11520        );
11521    }
11522
11523    #[test]
11524    fn verify_rejects_missing_required_object_block_extent() {
11525        let (mut opened, missing_block) = multi_envelope_reader_fixture();
11526        assert!(opened.blocks.remove(&missing_block).is_some());
11527
11528        assert_eq!(
11529            opened.verify().unwrap_err(),
11530            FormatError::FecTooFewAvailableShards
11531        );
11532    }
11533
11534    #[test]
11535    fn parity_crc_erasure_does_not_hide_authenticated_data() {
11536        let payload = pseudo_random_bytes(12_000);
11537        let archive = write_archive(
11538            &[RegularFile::new("parity-erasure.bin", &payload)],
11539            &master_key(),
11540            parity_rich_recovery_options(),
11541        )
11542        .unwrap();
11543        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
11544        let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
11545        assert!(
11546            parity_slots.len() >= 2,
11547            "fixture must contain redundant parity shards"
11548        );
11549        let mut corrupted = archive.bytes;
11550        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
11551        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
11552
11553        let opened = open_archive(&corrupted, &master_key()).unwrap();
11554        assert_eq!(
11555            opened.extract_file("parity-erasure.bin").unwrap(),
11556            Some(payload)
11557        );
11558        opened.verify().unwrap();
11559    }
11560
11561    #[test]
11562    fn repair_patches_restore_crc_erased_payload_block() {
11563        let payload = pseudo_random_bytes(12_000);
11564        let archive = write_archive(
11565            &[RegularFile::new("rot.bin", &payload)],
11566            &master_key(),
11567            small_block_recovery_options(),
11568        )
11569        .unwrap();
11570        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
11571        let mut corrupted = archive.bytes.clone();
11572        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
11573
11574        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
11575        opened.verify().unwrap();
11576        let patches = opened.repair_patches().unwrap();
11577        assert_eq!(patches.len(), 1);
11578        apply_repair_patches(&mut corrupted, &patches);
11579
11580        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
11581        repaired.verify().unwrap();
11582        assert!(repaired.repair_patches().unwrap().is_empty());
11583    }
11584
11585    #[test]
11586    fn repair_patches_restore_crc_erased_payload_parity_block() {
11587        let payload = pseudo_random_bytes(12_000);
11588        let archive = write_archive(
11589            &[RegularFile::new("parity-erasure.bin", &payload)],
11590            &master_key(),
11591            parity_rich_recovery_options(),
11592        )
11593        .unwrap();
11594        let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
11595        let mut corrupted = archive.bytes.clone();
11596        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
11597
11598        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
11599        opened.verify().unwrap();
11600        let patches = opened.repair_patches().unwrap();
11601        assert_eq!(patches.len(), 1);
11602        apply_repair_patches(&mut corrupted, &patches);
11603
11604        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
11605        repaired.verify().unwrap();
11606        assert!(repaired.repair_patches().unwrap().is_empty());
11607    }
11608
11609    #[test]
11610    fn rejects_odd_block_size_before_fec_repair() {
11611        let archive = write_archive(
11612            &[RegularFile::new("odd-block.txt", b"payload")],
11613            &master_key(),
11614            small_block_recovery_options(),
11615        )
11616        .unwrap();
11617        let mut malformed = archive.bytes;
11618        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11619        let block_size_offset = volume_header.crypto_header_offset as usize + 24;
11620        malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
11621
11622        assert_eq!(
11623            open_archive(&malformed, &master_key()).unwrap_err(),
11624            FormatError::OddBlockSize(4097)
11625        );
11626    }
11627
11628    #[test]
11629    fn rejects_structurally_malformed_block_records_instead_of_repairing() {
11630        let archive = write_archive(
11631            &[RegularFile::new("structural-block.txt", b"payload")],
11632            &master_key(),
11633            small_block_recovery_options(),
11634        )
11635        .unwrap();
11636        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
11637
11638        let mut bad_magic = archive.bytes.clone();
11639        corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
11640        assert_eq!(
11641            open_archive(&bad_magic, &master_key()).unwrap_err(),
11642            FormatError::BadMagic {
11643                structure: "BlockRecord"
11644            }
11645        );
11646
11647        let mut bad_reserved = archive.bytes;
11648        corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
11649        assert_eq!(
11650            open_archive(&bad_reserved, &master_key()).unwrap_err(),
11651            FormatError::NonZeroReserved {
11652                structure: "BlockRecord"
11653            }
11654        );
11655    }
11656
11657    #[test]
11658    fn rejects_parity_block_with_last_data_flag() {
11659        let archive = write_archive(
11660            &[RegularFile::new("parity-flag.txt", b"payload")],
11661            &master_key(),
11662            small_block_recovery_options(),
11663        )
11664        .unwrap();
11665        let parity_slot =
11666            first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
11667        let mut malformed = archive.bytes;
11668        mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
11669            record.flags = 0x01;
11670        });
11671
11672        assert_eq!(
11673            open_archive(&malformed, &master_key()).unwrap_err(),
11674            FormatError::ParityBlockHasLastDataFlag
11675        );
11676    }
11677
11678    #[test]
11679    fn rejects_missing_and_duplicate_payload_last_data_flags() {
11680        let payload = pseudo_random_bytes(12_000);
11681        let archive = write_archive(
11682            &[RegularFile::new("flags.bin", &payload)],
11683            &master_key(),
11684            small_block_recovery_options(),
11685        )
11686        .unwrap();
11687        let payload_slots = first_payload_data_run_slots(&archive.bytes);
11688        assert!(
11689            payload_slots.len() >= 2,
11690            "fixture must contain a multi-block payload object"
11691        );
11692
11693        let mut duplicate_last = archive.bytes.clone();
11694        mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
11695            record.flags = 0x01;
11696        });
11697        let opened = open_archive(&duplicate_last, &master_key()).unwrap();
11698        assert_eq!(
11699            opened.extract_file("flags.bin").unwrap_err(),
11700            FormatError::InvalidArchive("object last-data flag is not on the final data block")
11701        );
11702
11703        let mut missing_last = archive.bytes;
11704        mutate_block_record_at_slot(
11705            &mut missing_last,
11706            *payload_slots.last().unwrap(),
11707            |record| {
11708                record.flags = 0;
11709            },
11710        );
11711        let opened = open_archive(&missing_last, &master_key()).unwrap();
11712        assert_eq!(
11713            opened.extract_file("flags.bin").unwrap_err(),
11714            FormatError::InvalidArchive("object last-data flag is not on the final data block")
11715        );
11716    }
11717
11718    #[test]
11719    fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
11720        let files = [RegularFile::new(
11721            "footer-copy.txt",
11722            b"survives one bad footer",
11723        )];
11724        let archive = write_archive(
11725            &files,
11726            &master_key(),
11727            WriterOptions {
11728                stripe_width: 2,
11729                volume_loss_tolerance: 1,
11730                ..single_stream_options()
11731            },
11732        )
11733        .unwrap();
11734        let mut volumes = archive.volumes.clone();
11735        corrupt_manifest_footer_hmac(&mut volumes[0]);
11736
11737        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11738        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
11739        assert_eq!(opened.manifest_footer.volume_index, 0);
11740        assert_eq!(opened.volume_header.volume_index, 0);
11741        assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
11742        assert_eq!(
11743            opened.extract_file("footer-copy.txt").unwrap(),
11744            Some(b"survives one bad footer".to_vec())
11745        );
11746        opened.verify().unwrap();
11747    }
11748
11749    #[test]
11750    fn manifest_footer_corruption_requires_trusted_sidecar() {
11751        let archive = write_archive(
11752            &[RegularFile::new("footer.txt", b"sidecar authority")],
11753            &master_key(),
11754            single_stream_options(),
11755        )
11756        .unwrap();
11757        let manifest_offset = terminal_material_offset(&archive.bytes);
11758        let mut corrupted = archive.bytes.clone();
11759        corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
11760        corrupt_v41_terminal_recovery(&mut corrupted);
11761
11762        assert!(open_archive(&corrupted, &master_key()).is_err());
11763
11764        let opened =
11765            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
11766                .unwrap();
11767        assert!(opened.volume_trailer.is_none());
11768        assert_eq!(
11769            opened.extract_file("footer.txt").unwrap(),
11770            Some(b"sidecar authority".to_vec())
11771        );
11772        opened.verify().unwrap();
11773    }
11774
11775    #[test]
11776    fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
11777        let archive = write_archive(
11778            &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
11779            &master_key(),
11780            single_stream_options(),
11781        )
11782        .unwrap();
11783        let strict_options = ReaderOptions {
11784            max_trailing_garbage_scan: 0,
11785            ..ReaderOptions::default()
11786        };
11787
11788        let manifest_offset = terminal_material_offset(&archive.bytes);
11789        for offset in [
11790            manifest_offset + 71,
11791            manifest_offset + MANIFEST_HMAC_COVERED_LEN,
11792        ] {
11793            let mut corrupted = archive.bytes.clone();
11794            corrupted[offset] ^= 0x01;
11795            open_archive(&corrupted, &master_key()).unwrap();
11796        }
11797
11798        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
11799        for offset in [
11800            trailer_offset + 75,
11801            trailer_offset + TRAILER_HMAC_COVERED_LEN,
11802        ] {
11803            let mut corrupted = archive.bytes.clone();
11804            corrupted[offset] ^= 0x01;
11805            OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
11806        }
11807
11808        let mut covered_sidecar = archive.bootstrap_sidecar.clone();
11809        let mut header =
11810            BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11811                .unwrap();
11812        header.manifest_footer_offset += 1;
11813        covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
11814        assert_eq!(
11815            open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
11816                .unwrap_err(),
11817            FormatError::HmacMismatch {
11818                structure: "BootstrapSidecarHeader"
11819            }
11820        );
11821
11822        let mut tag_sidecar = archive.bootstrap_sidecar.clone();
11823        let mut header =
11824            BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
11825        header.sidecar_hmac[0] ^= 1;
11826        tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
11827        assert_eq!(
11828            open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
11829                .unwrap_err(),
11830            FormatError::HmacMismatch {
11831                structure: "BootstrapSidecarHeader"
11832            }
11833        );
11834
11835        let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
11836        let header =
11837            BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11838                .unwrap();
11839        let mut header_bytes = header.to_bytes();
11840        header_bytes[124] ^= 0x01;
11841        let crc = crc32c::crc32c(&header_bytes[..124]);
11842        header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
11843        non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
11844        let opened = open_archive_with_bootstrap_sidecar(
11845            &archive.bytes,
11846            &non_covered_sidecar,
11847            &master_key(),
11848        )
11849        .unwrap();
11850        assert_eq!(
11851            opened.extract_file("hmac-boundary.txt").unwrap(),
11852            Some(b"boundary bytes".to_vec())
11853        );
11854    }
11855
11856    #[test]
11857    fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
11858        let archive = write_archive(
11859            &[RegularFile::new("volume-index.txt", b"identity")],
11860            &master_key(),
11861            single_stream_options(),
11862        )
11863        .unwrap();
11864
11865        let mut bad_trailer = archive.bytes.clone();
11866        rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
11867            trailer.volume_index = 1;
11868        });
11869        open_archive(&bad_trailer, &master_key()).unwrap();
11870
11871        let mut bad_manifest = archive.bytes;
11872        rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
11873            footer.volume_index = 1;
11874        });
11875        open_archive(&bad_manifest, &master_key()).unwrap();
11876    }
11877
11878    #[test]
11879    fn rejects_same_key_header_terminal_material_splice() {
11880        let first = write_archive(
11881            &[RegularFile::new("splice.txt", b"same shape")],
11882            &master_key(),
11883            single_stream_options(),
11884        )
11885        .unwrap();
11886        let second = write_archive(
11887            &[RegularFile::new("splice.txt", b"same shape")],
11888            &master_key(),
11889            single_stream_options(),
11890        )
11891        .unwrap();
11892        assert_ne!(first.archive_uuid, second.archive_uuid);
11893        assert_eq!(
11894            terminal_material_offset(&first.bytes),
11895            terminal_material_offset(&second.bytes)
11896        );
11897        assert_eq!(first.bytes.len(), second.bytes.len());
11898
11899        let terminal_offset = terminal_material_offset(&first.bytes);
11900        let mut spliced = first.bytes.clone();
11901        spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
11902
11903        assert_eq!(
11904            open_archive(&spliced, &master_key()).unwrap_err(),
11905            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
11906        );
11907    }
11908
11909    #[test]
11910    fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
11911        let kdf_params = crate::crypto::KdfParams::Argon2id {
11912            t_cost: 1,
11913            m_cost_kib: 8,
11914            parallelism: 1,
11915            salt: b"0123456789abcdef".to_vec(),
11916        };
11917        let archive = write_archive_with_kdf(
11918            &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
11919            &master_key(),
11920            single_stream_options(),
11921            &kdf_params,
11922        )
11923        .unwrap();
11924        let mut mutated = archive.bytes.clone();
11925        let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11926        let subkeys = Subkeys::derive(
11927            &master_key(),
11928            &volume_header.archive_uuid,
11929            &volume_header.session_id,
11930        )
11931        .unwrap();
11932
11933        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11934            let crypto_region = image
11935                .regions
11936                .iter_mut()
11937                .find(|region| region.region_type == 2)
11938                .unwrap();
11939            let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
11940            let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
11941            crypto_region.bytes[salt_start] ^= 0x01;
11942            let hmac = compute_hmac(
11943                HmacDomain::CryptoHeader,
11944                &subkeys.mac_key,
11945                &volume_header.archive_uuid,
11946                &volume_header.session_id,
11947                &crypto_region.bytes[..hmac_offset],
11948            );
11949            crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
11950        });
11951
11952        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11953        let locator = final_recovery_locator(&mutated);
11954        let crypto_start = volume_header.crypto_header_offset as usize;
11955        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11956        let parsed_crypto = CryptoHeader::parse(
11957            &mutated[crypto_start..crypto_end],
11958            volume_header.crypto_header_length,
11959        )
11960        .unwrap();
11961        assert_eq!(
11962            parse_locator_cmra_candidate(
11963                &mutated,
11964                final_offset,
11965                locator,
11966                KeyHoldingTerminalContext {
11967                    subkeys: &subkeys,
11968                    volume_header: &volume_header,
11969                    crypto_header: &parsed_crypto.fixed,
11970                    crypto_header_bytes: &mutated[crypto_start..crypto_end],
11971                },
11972            )
11973            .unwrap_err(),
11974            FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
11975        );
11976        assert!(open_archive(&mutated, &master_key()).is_err());
11977    }
11978
11979    #[test]
11980    fn rejects_same_key_crypto_header_splice_with_session_mismatch() {
11981        let base = WriterOptions {
11982            archive_uuid: Some([0x11; 16]),
11983            session_id: Some([0x22; 16]),
11984            ..single_stream_options()
11985        };
11986        let same_archive = WriterOptions {
11987            archive_uuid: Some([0x11; 16]),
11988            session_id: Some([0x33; 16]),
11989            ..single_stream_options()
11990        };
11991
11992        let first = write_archive(
11993            &[RegularFile::new("splice.txt", b"same shape")],
11994            &master_key(),
11995            base,
11996        )
11997        .unwrap();
11998        let second = write_archive(
11999            &[RegularFile::new("splice.txt", b"same shape")],
12000            &master_key(),
12001            same_archive,
12002        )
12003        .unwrap();
12004
12005        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12006        let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
12007        let crypto_start = volume_header.crypto_header_offset as usize;
12008        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12009        let second_crypto_end = second_volume_header.crypto_header_offset as usize
12010            + second_volume_header.crypto_header_length as usize;
12011        assert_eq!(crypto_end, second_crypto_end);
12012
12013        let mut spliced = first.bytes.clone();
12014        spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
12015
12016        assert_eq!(
12017            open_archive(&spliced, &master_key()).unwrap_err(),
12018            FormatError::HmacMismatch {
12019                structure: "CryptoHeader"
12020            }
12021        );
12022    }
12023
12024    #[test]
12025    fn rejects_same_key_object_splice_with_session_mismatch() {
12026        let first = write_archive(
12027            &[RegularFile::new("splice.txt", b"same shape")],
12028            &master_key(),
12029            WriterOptions {
12030                archive_uuid: Some([0x11; 16]),
12031                session_id: Some([0x22; 16]),
12032                ..single_stream_options()
12033            },
12034        )
12035        .unwrap();
12036        let second = write_archive(
12037            &[RegularFile::new("splice.txt", b"same shape")],
12038            &master_key(),
12039            WriterOptions {
12040                archive_uuid: Some([0x11; 16]),
12041                session_id: Some([0x33; 16]),
12042                ..single_stream_options()
12043            },
12044        )
12045        .unwrap();
12046
12047        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
12048        let crypto_end = volume_header.crypto_header_offset as usize
12049            + volume_header.crypto_header_length as usize;
12050        let terminal_offset = terminal_material_offset(&first.bytes);
12051        let second_terminal_offset = terminal_material_offset(&second.bytes);
12052        assert_eq!(terminal_offset, second_terminal_offset);
12053
12054        let mut spliced = first.bytes.clone();
12055        spliced[crypto_end..terminal_offset]
12056            .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
12057
12058        assert_eq!(
12059            open_archive(&spliced, &master_key()).unwrap_err(),
12060            FormatError::AeadFailure
12061        );
12062    }
12063
12064    #[test]
12065    fn rejects_authenticated_trailer_pointer_and_count_mutations() {
12066        let archive = write_archive(
12067            &[RegularFile::new(
12068                "trailer-range.txt",
12069                b"authenticated ranges",
12070            )],
12071            &master_key(),
12072            single_stream_options(),
12073        )
12074        .unwrap();
12075        let strict_options = ReaderOptions {
12076            max_trailing_garbage_scan: 0,
12077            ..ReaderOptions::default()
12078        };
12079        let bytes = archive.bytes;
12080        let manifest_offset = terminal_material_offset(&bytes);
12081        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
12082
12083        let mut wrong_footer_length = bytes.clone();
12084        rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
12085            trailer.manifest_footer_length = 42;
12086        });
12087        OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
12088            .unwrap();
12089
12090        for (label, offset) in [
12091            (
12092                "offset before trailer by 1",
12093                manifest_offset.saturating_sub(1),
12094            ),
12095            ("offset after trailer", manifest_offset + 1),
12096            ("offset at stream start", 0),
12097            ("offset at trailer", trailer_offset),
12098            ("offset beyond trailer", trailer_offset + 4),
12099        ] {
12100            let mut wrong_footer_offset = bytes.clone();
12101            rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
12102                trailer.manifest_footer_offset = offset as u64;
12103            });
12104            open_archive(&wrong_footer_offset, &master_key())
12105                .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
12106        }
12107
12108        let mut wrong_bytes_written = bytes.clone();
12109        rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
12110            trailer.bytes_written += 1;
12111        });
12112        open_archive(&wrong_bytes_written, &master_key()).unwrap();
12113
12114        let mut wrong_block_count = bytes.clone();
12115        rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
12116            trailer.block_count += 1;
12117        });
12118        open_archive(&wrong_block_count, &master_key()).unwrap();
12119
12120        let mut wrong_footer_offset = bytes.clone();
12121        rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
12122            trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
12123        });
12124        open_archive(&wrong_footer_offset, &master_key()).unwrap();
12125    }
12126
12127    #[test]
12128    fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
12129        let archive = write_archive(
12130            &[RegularFile::new(
12131                "trailer-trailing-scan.txt",
12132                b"trailer scan boundaries",
12133            )],
12134            &master_key(),
12135            single_stream_options(),
12136        )
12137        .unwrap();
12138        let options = ReaderOptions {
12139            max_trailing_garbage_scan: 8,
12140            ..ReaderOptions::default()
12141        };
12142
12143        let mut within_scan = archive.bytes.clone();
12144        within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
12145        let opened =
12146            OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
12147        assert_eq!(
12148            opened.extract_file("trailer-trailing-scan.txt").unwrap(),
12149            Some(b"trailer scan boundaries".to_vec())
12150        );
12151
12152        let mut beyond_scan = archive.bytes.clone();
12153        beyond_scan.resize(
12154            beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
12155            0xAA,
12156        );
12157        assert_eq!(
12158            OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
12159            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
12160        );
12161    }
12162
12163    #[test]
12164    fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
12165        let archive = write_archive(
12166            &[RegularFile::new("index-root-size.txt", b"extent size")],
12167            &master_key(),
12168            single_stream_options(),
12169        )
12170        .unwrap();
12171        let mut malformed = archive.bytes;
12172        let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
12173            .expect("archive should contain IndexRootData");
12174        mutate_block_record_at_slot(&mut malformed, slot, |record| {
12175            record.payload[0] ^= 0x55;
12176        });
12177
12178        assert_eq!(
12179            open_archive(&malformed, &master_key()).unwrap_err(),
12180            FormatError::AeadFailure
12181        );
12182    }
12183
12184    #[test]
12185    fn rejects_block_record_at_wrong_stripe_position() {
12186        let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
12187        let archive = write_archive(
12188            &files,
12189            &master_key(),
12190            WriterOptions {
12191                stripe_width: 2,
12192                volume_loss_tolerance: 1,
12193                ..single_stream_options()
12194            },
12195        )
12196        .unwrap();
12197        let mut volumes = archive.volumes.clone();
12198        mutate_first_block_record(&mut volumes[0], |record| {
12199            record.block_index += 2;
12200        });
12201
12202        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12203        assert_eq!(
12204            open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
12205            FormatError::InvalidArchive("BlockRecord index does not match volume position")
12206        );
12207    }
12208
12209    #[test]
12210    fn rejects_decreasing_block_record_index_in_required_region() {
12211        let archive = write_archive(
12212            &[RegularFile::new("alpha.txt", b"decreasing block index")],
12213            &master_key(),
12214            single_stream_options(),
12215        )
12216        .unwrap();
12217        assert!(block_record_slots(&archive.bytes).len() >= 2);
12218
12219        let mut malformed = archive.bytes;
12220        mutate_block_record_at_slot(&mut malformed, 1, |record| {
12221            record.block_index = 0;
12222        });
12223
12224        assert_eq!(
12225            open_archive(&malformed, &master_key()).unwrap_err(),
12226            FormatError::InvalidArchive("BlockRecord index does not match volume position")
12227        );
12228    }
12229
12230    #[test]
12231    fn rejects_duplicate_authenticated_volume_indexes() {
12232        let files = [RegularFile::new("alpha.txt", b"duplicates")];
12233        let archive = write_archive(
12234            &files,
12235            &master_key(),
12236            WriterOptions {
12237                stripe_width: 2,
12238                volume_loss_tolerance: 1,
12239                ..single_stream_options()
12240            },
12241        )
12242        .unwrap();
12243
12244        assert_eq!(
12245            open_archive_volumes(
12246                &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
12247                &master_key()
12248            )
12249            .unwrap_err(),
12250            FormatError::InvalidArchive("duplicate authenticated volume index")
12251        );
12252    }
12253
12254    #[test]
12255    fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
12256        let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
12257        let archive = write_archive(
12258            &files,
12259            &master_key(),
12260            WriterOptions {
12261                stripe_width: 2,
12262                volume_loss_tolerance: 1,
12263                ..single_stream_options()
12264            },
12265        )
12266        .unwrap();
12267        let mut conflicting = archive.volumes[0].clone();
12268        corrupt_first_block_record_payload(&mut conflicting);
12269
12270        assert_eq!(
12271            open_archive_volumes(
12272                &[archive.volumes[0].as_slice(), conflicting.as_slice()],
12273                &master_key()
12274            )
12275            .unwrap_err(),
12276            FormatError::InvalidArchive("duplicate authenticated volume index")
12277        );
12278    }
12279
12280    fn directory_hint_table_from_rows(
12281        hint_shard_index: u64,
12282        rows: &[(Vec<u8>, Vec<u32>)],
12283        shard_count: u32,
12284    ) -> DirectoryHintTable {
12285        let mut entries = Vec::new();
12286        let mut shard_row_indexes = Vec::new();
12287        let mut string_pool = Vec::new();
12288
12289        for (path, rows) in rows {
12290            let path_offset = if path.is_empty() {
12291                0
12292            } else {
12293                let offset = string_pool.len() as u64;
12294                string_pool.extend_from_slice(path);
12295                offset
12296            };
12297            let shard_list_start_index = shard_row_indexes.len() as u32;
12298            shard_row_indexes.extend_from_slice(rows);
12299            entries.push(DirectoryHintEntry {
12300                dir_hash: hash_prefix(path),
12301                path_offset,
12302                path_length: path.len() as u32,
12303                shard_list_start_index,
12304                shard_count: rows.len() as u32,
12305            });
12306        }
12307
12308        let table_bytes =
12309            directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
12310        let locating = DirectoryHintShardEntry {
12311            hint_shard_index,
12312            first_dir_hash: hash_prefix(&rows.first().unwrap().0),
12313            last_dir_hash: hash_prefix(&rows.last().unwrap().0),
12314            first_block_index: 0,
12315            data_block_count: 1,
12316            parity_block_count: 0,
12317            encrypted_size: 4096,
12318            decompressed_size: table_bytes.len() as u32,
12319            entry_count: rows.len() as u64,
12320        };
12321        DirectoryHintTable::parse(
12322            &table_bytes,
12323            &locating,
12324            shard_count,
12325            MetadataLimits::default(),
12326        )
12327        .unwrap()
12328    }
12329
12330    fn directory_hint_table_bytes(
12331        hint_shard_index: u64,
12332        entries: Vec<DirectoryHintEntry>,
12333        shard_row_indexes: Vec<u32>,
12334        string_pool: Vec<u8>,
12335    ) -> Vec<u8> {
12336        let header_len = DirectoryHintTableHeader {
12337            version: 1,
12338            hint_shard_index,
12339            entry_count: 0,
12340            entry_table_offset: 0,
12341            shard_list_offset: 0,
12342            string_pool_offset: 0,
12343            string_pool_size: 0,
12344        }
12345        .to_bytes()
12346        .len();
12347        let entry_len = entries
12348            .first()
12349            .map(|entry| entry.to_bytes().len())
12350            .unwrap_or(0);
12351        let shard_list_offset = if entries.is_empty() {
12352            0
12353        } else {
12354            header_len + entries.len() * entry_len
12355        };
12356        let string_pool_offset = if string_pool.is_empty() {
12357            0
12358        } else {
12359            shard_list_offset + shard_row_indexes.len() * 4
12360        };
12361
12362        let header = DirectoryHintTableHeader {
12363            version: 1,
12364            hint_shard_index,
12365            entry_count: entries.len() as u64,
12366            entry_table_offset: if entries.is_empty() {
12367                0
12368            } else {
12369                header_len as u64
12370            },
12371            shard_list_offset: shard_list_offset as u64,
12372            string_pool_offset: string_pool_offset as u64,
12373            string_pool_size: string_pool.len() as u64,
12374        };
12375
12376        let mut out = Vec::new();
12377        out.extend_from_slice(&header.to_bytes());
12378        for entry in entries {
12379            out.extend_from_slice(&entry.to_bytes());
12380        }
12381        for row in shard_row_indexes {
12382            out.extend_from_slice(&row.to_le_bytes());
12383        }
12384        out.extend_from_slice(&string_pool);
12385        out
12386    }
12387
12388    fn corrupt_first_block_record_payload(volume: &mut [u8]) {
12389        let (record_offset, _) = first_block_record(volume);
12390        volume[record_offset + 16] ^= 0x55;
12391    }
12392
12393    fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
12394        let (record_offset, _) = block_record_at_slot(volume, slot);
12395        volume[record_offset + 16] ^= 0x55;
12396    }
12397
12398    fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
12399        for patch in patches {
12400            let offset = patch.record_offset as usize;
12401            let end = offset + patch.record_bytes.len();
12402            volume[offset..end].copy_from_slice(&patch.record_bytes);
12403        }
12404    }
12405
12406    fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
12407        let (record_offset, _) = block_record_at_slot(volume, slot);
12408        volume[record_offset] ^= 0x55;
12409    }
12410
12411    fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
12412        let (record_offset, _) = block_record_at_slot(volume, slot);
12413        volume[record_offset + 14] = 0x01;
12414    }
12415
12416    fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
12417        let manifest_offset = terminal_material_offset(volume);
12418        volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
12419    }
12420
12421    fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
12422        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12423        CriticalRecoveryLocator::parse(
12424            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
12425        )
12426        .unwrap()
12427    }
12428
12429    fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
12430        let locator = final_recovery_locator(volume);
12431        let tuple = CmraDecoderTuple::from(locator);
12432        assert!(parity_shard_count < tuple.parity_shard_count);
12433        let cmra_offset = locator.cmra_offset as usize;
12434        let shard_size = tuple.shard_size as usize;
12435        let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
12436        let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
12437        let mut header = CriticalMetadataRecoveryHeader::parse(
12438            &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
12439        )
12440        .unwrap();
12441        header.parity_shard_count = parity_shard_count;
12442
12443        let mut cmra =
12444            Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
12445        cmra.extend_from_slice(&header.to_bytes());
12446        let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
12447        for row in 0..kept_rows {
12448            let start = rows_start + row * row_len;
12449            cmra.extend_from_slice(&volume[start..start + row_len]);
12450        }
12451
12452        let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
12453        out.extend_from_slice(&volume[..cmra_offset]);
12454        out.extend_from_slice(&cmra);
12455        let mut mirror = locator;
12456        mirror.locator_sequence = 1;
12457        mirror.cmra_length = cmra.len() as u32;
12458        mirror.cmra_parity_shard_count = parity_shard_count;
12459        out.extend_from_slice(&mirror.to_bytes());
12460        let final_locator = CriticalRecoveryLocator {
12461            locator_sequence: 0,
12462            ..mirror
12463        };
12464        out.extend_from_slice(&final_locator.to_bytes());
12465        out
12466    }
12467
12468    fn rewrite_public_cmra_image(
12469        volume: &mut [u8],
12470        mutate: impl FnOnce(&mut CriticalMetadataImage),
12471    ) {
12472        rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
12473    }
12474
12475    fn rewrite_cmra_image(
12476        volume: &mut [u8],
12477        mode: CmraRecoveryMode,
12478        mutate: impl FnOnce(&mut CriticalMetadataImage),
12479    ) {
12480        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12481        let locator = final_recovery_locator(volume);
12482        let tuple = CmraDecoderTuple::from(locator);
12483        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
12484        let mut image = recovered.image;
12485        mutate(&mut image);
12486        refresh_critical_image_region_digests(&mut image);
12487        let image_bytes = image.to_bytes().unwrap();
12488        assert_eq!(image_bytes.len(), tuple.image_length as usize);
12489
12490        let shard_size = tuple.shard_size as usize;
12491        let data_shard_count = tuple.data_shard_count as usize;
12492        let parity_shard_count = tuple.parity_shard_count as usize;
12493        assert!(image_bytes.len() <= data_shard_count * shard_size);
12494
12495        let mut data_shards = Vec::with_capacity(data_shard_count);
12496        for idx in 0..data_shard_count {
12497            let start = idx * shard_size;
12498            let end = (start + shard_size).min(image_bytes.len());
12499            let mut payload = vec![0u8; shard_size];
12500            if start < image_bytes.len() {
12501                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
12502            }
12503            data_shards.push(payload);
12504        }
12505        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
12506        let image_sha256 = sha256_bytes(&image_bytes);
12507
12508        let header = CriticalMetadataRecoveryHeader {
12509            shard_size: tuple.shard_size,
12510            data_shard_count: tuple.data_shard_count,
12511            parity_shard_count: tuple.parity_shard_count,
12512            image_length: tuple.image_length,
12513            archive_uuid_hint: locator.archive_uuid_hint,
12514            session_id_hint: locator.session_id_hint,
12515            volume_index_hint: locator.volume_index_hint,
12516            image_sha256,
12517            header_crc32c: 0,
12518        };
12519        let mut cmra = Vec::new();
12520        cmra.extend_from_slice(&header.to_bytes());
12521        for (idx, payload) in data_shards.into_iter().enumerate() {
12522            let payload_len = if idx + 1 == data_shard_count {
12523                image_bytes.len() - idx * shard_size
12524            } else {
12525                shard_size
12526            };
12527            cmra.extend_from_slice(
12528                &CriticalMetadataRecoveryShard {
12529                    shard_index: idx as u16,
12530                    shard_role: 0,
12531                    shard_payload_length: payload_len as u32,
12532                    payload,
12533                    shard_crc32c: 0,
12534                }
12535                .to_bytes(shard_size)
12536                .unwrap(),
12537            );
12538        }
12539        for (idx, payload) in parity_shards.into_iter().enumerate() {
12540            cmra.extend_from_slice(
12541                &CriticalMetadataRecoveryShard {
12542                    shard_index: (data_shard_count + idx) as u16,
12543                    shard_role: 1,
12544                    shard_payload_length: shard_size as u32,
12545                    payload,
12546                    shard_crc32c: 0,
12547                }
12548                .to_bytes(shard_size)
12549                .unwrap(),
12550            );
12551        }
12552        assert_eq!(cmra.len() as u64, recovered.cmra_length);
12553        let cmra_offset = locator.cmra_offset as usize;
12554        volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
12555
12556        rewrite_locator_image_sha(volume, final_offset, image_sha256);
12557        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
12558        rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
12559    }
12560
12561    fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
12562        image.volume_header_sha256 = sha256_bytes(
12563            &image
12564                .regions
12565                .iter()
12566                .find(|region| region.region_type == 1)
12567                .unwrap()
12568                .bytes,
12569        );
12570        image.crypto_header_sha256 = sha256_bytes(
12571            &image
12572                .regions
12573                .iter()
12574                .find(|region| region.region_type == 2)
12575                .unwrap()
12576                .bytes,
12577        );
12578        image.manifest_footer_sha256 = sha256_bytes(
12579            &image
12580                .regions
12581                .iter()
12582                .find(|region| region.region_type == 3)
12583                .unwrap()
12584                .bytes,
12585        );
12586        image.root_auth_footer_sha256 = image
12587            .regions
12588            .iter()
12589            .find(|region| region.region_type == 4)
12590            .map(|region| sha256_bytes(&region.bytes))
12591            .unwrap_or([0u8; 32]);
12592        image.volume_trailer_sha256 = sha256_bytes(
12593            &image
12594                .regions
12595                .iter()
12596                .find(|region| region.region_type == 5)
12597                .unwrap()
12598                .bytes,
12599        );
12600    }
12601
12602    fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
12603        let mut locator =
12604            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
12605                .unwrap();
12606        locator.cmra_image_sha256 = image_sha256;
12607        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
12608    }
12609
12610    fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
12611        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
12612        let final_locator = CriticalRecoveryLocator::parse(
12613            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
12614        )
12615        .unwrap();
12616        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
12617        volume[final_locator.cmra_offset as usize] ^= 0x55;
12618        volume[mirror_offset] ^= 0x55;
12619        volume[final_offset] ^= 0x55;
12620    }
12621
12622    fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
12623        let (record_offset, record_len) = first_block_record(volume);
12624        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12625        let mut record = BlockRecord::parse(
12626            &volume[record_offset..record_offset + record_len],
12627            block_size,
12628        )
12629        .unwrap();
12630        mutate(&mut record);
12631        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
12632    }
12633
12634    fn mutate_block_record_at_slot(
12635        volume: &mut [u8],
12636        slot: usize,
12637        mutate: impl FnOnce(&mut BlockRecord),
12638    ) {
12639        let (record_offset, record_len) = block_record_at_slot(volume, slot);
12640        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12641        let mut record = BlockRecord::parse(
12642            &volume[record_offset..record_offset + record_len],
12643            block_size,
12644        )
12645        .unwrap();
12646        mutate(&mut record);
12647        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
12648    }
12649
12650    fn first_block_record(volume: &[u8]) -> (usize, usize) {
12651        block_record_at_slot(volume, 0)
12652    }
12653
12654    fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
12655        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12656        let crypto_start = volume_header.crypto_header_offset as usize;
12657        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12658        let crypto_header = CryptoHeader::parse(
12659            &volume[crypto_start..crypto_end],
12660            volume_header.crypto_header_length,
12661        )
12662        .unwrap();
12663        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12664        let record_offset = crypto_end + slot * record_len;
12665        assert!(volume.len() >= record_offset + record_len);
12666        (record_offset, record_len)
12667    }
12668
12669    fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
12670        block_record_slots(volume)
12671            .into_iter()
12672            .enumerate()
12673            .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
12674    }
12675
12676    fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
12677        block_record_slots(volume)
12678            .into_iter()
12679            .enumerate()
12680            .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
12681            .collect()
12682    }
12683
12684    fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
12685        let mut slots = Vec::new();
12686        for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
12687            if record.kind == BlockKind::PayloadData {
12688                slots.push(slot);
12689            } else if !slots.is_empty() {
12690                break;
12691            }
12692        }
12693        slots
12694    }
12695
12696    fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
12697        envelope_entries_for_path(opened, path)
12698            .into_iter()
12699            .map(|entry| entry.envelope_index)
12700            .collect()
12701    }
12702
12703    fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
12704        let normalized =
12705            normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
12706        let located = opened.locate_index_file(&normalized).unwrap().unwrap();
12707        let file = &located.shard.files[located.file_index];
12708        frame_range_for_file(&located.shard, file)
12709            .unwrap()
12710            .into_iter()
12711            .map(|frame| {
12712                located
12713                    .shard
12714                    .envelopes
12715                    .iter()
12716                    .find(|entry| entry.envelope_index == frame.envelope_index)
12717                    .unwrap()
12718                    .clone()
12719            })
12720            .collect()
12721    }
12722
12723    fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
12724        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12725        let crypto_start = volume_header.crypto_header_offset as usize;
12726        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
12727        let crypto_header = CryptoHeader::parse(
12728            &volume[crypto_start..crypto_end],
12729            volume_header.crypto_header_length,
12730        )
12731        .unwrap();
12732        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12733        let manifest_offset = terminal_material_offset(volume);
12734        assert_eq!((manifest_offset - crypto_end) % record_len, 0);
12735        let record_count = (manifest_offset - crypto_end) / record_len;
12736        (0..record_count)
12737            .map(|slot| {
12738                let offset = crypto_end + slot * record_len;
12739                let record = BlockRecord::parse(
12740                    &volume[offset..offset + record_len],
12741                    record_len - BLOCK_RECORD_FRAMING_LEN,
12742                )
12743                .unwrap();
12744                (offset, record_len, record)
12745            })
12746            .collect()
12747    }
12748
12749    fn rewrite_manifest_footer(
12750        volume: &mut [u8],
12751        master_key: &MasterKey,
12752        mutate: impl FnOnce(&mut ManifestFooter),
12753    ) {
12754        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12755        let offset = terminal_material_offset(volume);
12756        let mut footer =
12757            ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
12758        mutate(&mut footer);
12759        footer.manifest_hmac = [0u8; 32];
12760        let mut footer_bytes = footer.to_bytes();
12761        let subkeys = Subkeys::derive(
12762            master_key,
12763            &volume_header.archive_uuid,
12764            &volume_header.session_id,
12765        )
12766        .unwrap();
12767        footer.manifest_hmac = compute_hmac(
12768            HmacDomain::ManifestFooter,
12769            &subkeys.mac_key,
12770            &volume_header.archive_uuid,
12771            &volume_header.session_id,
12772            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
12773        );
12774        footer_bytes = footer.to_bytes();
12775        volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
12776    }
12777
12778    fn rewrite_volume_trailer(
12779        volume: &mut [u8],
12780        master_key: &MasterKey,
12781        mutate: impl FnOnce(&mut VolumeTrailer),
12782    ) {
12783        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12784        let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
12785        let mut trailer =
12786            VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
12787        mutate(&mut trailer);
12788        trailer.trailer_hmac = [0u8; 32];
12789        let mut trailer_bytes = trailer.to_bytes();
12790        let subkeys = Subkeys::derive(
12791            master_key,
12792            &volume_header.archive_uuid,
12793            &volume_header.session_id,
12794        )
12795        .unwrap();
12796        trailer.trailer_hmac = compute_hmac(
12797            HmacDomain::VolumeTrailer,
12798            &subkeys.mac_key,
12799            &volume_header.archive_uuid,
12800            &volume_header.session_id,
12801            &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
12802        );
12803        trailer_bytes = trailer.to_bytes();
12804        volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
12805    }
12806
12807    fn rewrite_sidecar_header(
12808        sidecar: &mut [u8],
12809        master_key: &MasterKey,
12810        mutate: impl FnOnce(&mut BootstrapSidecarHeader),
12811    ) {
12812        let mut header =
12813            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12814        mutate(&mut header);
12815        write_signed_sidecar_header(sidecar, master_key, &mut header);
12816    }
12817
12818    fn write_signed_sidecar_header(
12819        sidecar: &mut [u8],
12820        master_key: &MasterKey,
12821        header: &mut BootstrapSidecarHeader,
12822    ) {
12823        header.sidecar_hmac = [0u8; 32];
12824        let mut header_bytes = header.to_bytes();
12825        let subkeys =
12826            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
12827        header.sidecar_hmac = compute_hmac(
12828            HmacDomain::BootstrapSidecar,
12829            &subkeys.mac_key,
12830            &header.archive_uuid,
12831            &header.session_id,
12832            &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
12833        );
12834        header_bytes = header.to_bytes();
12835        sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12836    }
12837
12838    fn sparse_bootstrap_sidecar(
12839        source: &[u8],
12840        master_key: &MasterKey,
12841        include_manifest: bool,
12842        include_index_root: bool,
12843        include_dictionary: bool,
12844    ) -> Vec<u8> {
12845        let source_header =
12846            BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12847        let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
12848        let mut header = BootstrapSidecarHeader {
12849            archive_uuid: source_header.archive_uuid,
12850            session_id: source_header.session_id,
12851            flags: 0,
12852            manifest_footer_offset: 0,
12853            manifest_footer_length: 0,
12854            index_root_records_offset: 0,
12855            index_root_records_length: 0,
12856            dictionary_records_offset: 0,
12857            dictionary_records_length: 0,
12858            sidecar_hmac: [0u8; 32],
12859            header_crc32c: 0,
12860        };
12861
12862        if include_manifest {
12863            assert!(source_header.has_manifest_footer());
12864            let (offset, length) = append_sidecar_section(
12865                source,
12866                &mut sidecar,
12867                source_header.manifest_footer_offset,
12868                source_header.manifest_footer_length as u64,
12869            );
12870            header.flags |= 0x01;
12871            header.manifest_footer_offset = offset;
12872            header.manifest_footer_length = length as u32;
12873        }
12874        if include_index_root {
12875            assert!(source_header.has_index_root_records());
12876            let (offset, length) = append_sidecar_section(
12877                source,
12878                &mut sidecar,
12879                source_header.index_root_records_offset,
12880                source_header.index_root_records_length,
12881            );
12882            header.flags |= 0x02;
12883            header.index_root_records_offset = offset;
12884            header.index_root_records_length = length;
12885        }
12886        if include_dictionary {
12887            assert!(source_header.has_dictionary_records());
12888            let (offset, length) = append_sidecar_section(
12889                source,
12890                &mut sidecar,
12891                source_header.dictionary_records_offset,
12892                source_header.dictionary_records_length,
12893            );
12894            header.flags |= 0x04;
12895            header.dictionary_records_offset = offset;
12896            header.dictionary_records_length = length;
12897        }
12898
12899        write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
12900        sidecar
12901    }
12902
12903    fn append_sidecar_section(
12904        source: &[u8],
12905        sidecar: &mut Vec<u8>,
12906        source_offset: u64,
12907        length: u64,
12908    ) -> (u64, u64) {
12909        let source_offset = source_offset as usize;
12910        let length = length as usize;
12911        let offset = sidecar.len() as u64;
12912        sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
12913        (offset, length as u64)
12914    }
12915
12916    fn mutate_sidecar_manifest(
12917        sidecar: &mut [u8],
12918        master_key: &MasterKey,
12919        mutate: impl FnOnce(&mut ManifestFooter),
12920    ) {
12921        let header =
12922            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12923        let offset = header.manifest_footer_offset as usize;
12924        let mut footer =
12925            ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
12926        mutate(&mut footer);
12927        footer.manifest_hmac = [0u8; 32];
12928        let mut footer_bytes = footer.to_bytes();
12929        let subkeys =
12930            Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
12931        footer.manifest_hmac = compute_hmac(
12932            HmacDomain::ManifestFooter,
12933            &subkeys.mac_key,
12934            &footer.archive_uuid,
12935            &footer.session_id,
12936            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
12937        );
12938        footer_bytes = footer.to_bytes();
12939        sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
12940    }
12941
12942    fn mutate_sidecar_index_record(
12943        sidecar: &mut [u8],
12944        record_index: usize,
12945        mutate: impl FnOnce(&mut BlockRecord),
12946    ) {
12947        let header =
12948            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12949        let record_len = sidecar_record_len(sidecar);
12950        let offset = header.index_root_records_offset as usize + record_index * record_len;
12951        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12952        let mut record =
12953            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
12954        mutate(&mut record);
12955        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
12956    }
12957
12958    fn mutate_sidecar_dictionary_record(
12959        sidecar: &mut [u8],
12960        record_index: usize,
12961        mutate: impl FnOnce(&mut BlockRecord),
12962    ) {
12963        let header =
12964            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12965        let record_len = sidecar_record_len(sidecar);
12966        let offset = header.dictionary_records_offset as usize + record_index * record_len;
12967        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
12968        let mut record =
12969            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
12970        mutate(&mut record);
12971        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
12972    }
12973
12974    fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
12975        let header =
12976            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12977        let record_len = sidecar_record_len(sidecar);
12978        let left_offset = header.index_root_records_offset as usize + left * record_len;
12979        let right_offset = header.index_root_records_offset as usize + right * record_len;
12980        for idx in 0..record_len {
12981            sidecar.swap(left_offset + idx, right_offset + idx);
12982        }
12983    }
12984
12985    fn sidecar_record_len(sidecar: &[u8]) -> usize {
12986        let header =
12987            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12988        let footer_offset = header.manifest_footer_offset as usize;
12989        let footer =
12990            ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
12991                .unwrap();
12992        let index_record_count = footer.index_root_data_block_count as usize
12993            + footer.index_root_parity_block_count as usize;
12994        header.index_root_records_length as usize / index_record_count
12995    }
12996
12997    fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
12998        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
12999        assert_eq!(volume_header.volume_index, 0);
13000        assert_eq!(volume_header.stripe_width, 1);
13001        let crypto_start = volume_header.crypto_header_offset as usize;
13002        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13003        let crypto_header = CryptoHeader::parse(
13004            &volume[crypto_start..crypto_end],
13005            volume_header.crypto_header_length,
13006        )
13007        .unwrap();
13008        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13009        let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
13010        for offset in 0..record_count {
13011            let block_index = extent.first_block_index + offset;
13012            let record_offset = crypto_end + block_index as usize * record_len;
13013            volume[record_offset + 16] ^= 0x55;
13014        }
13015    }
13016
13017    fn terminal_material_offset(volume: &[u8]) -> usize {
13018        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13019        let crypto_start = volume_header.crypto_header_offset as usize;
13020        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13021        let crypto_header = CryptoHeader::parse(
13022            &volume[crypto_start..crypto_end],
13023            volume_header.crypto_header_length,
13024        )
13025        .unwrap();
13026        let (_, offset, _) = parse_stream_block_prefix(
13027            volume,
13028            crypto_end,
13029            crypto_header.fixed.block_size as usize,
13030            &volume_header,
13031        )
13032        .unwrap();
13033        offset
13034    }
13035
13036    #[derive(Debug)]
13037    struct TestObject {
13038        extent: ObjectExtent,
13039        records: Vec<BlockRecord>,
13040    }
13041
13042    #[derive(Debug)]
13043    struct TestFileMeta {
13044        path: Vec<u8>,
13045        frame_index: u64,
13046        tar_stream_offset: u64,
13047        member_group_size: u64,
13048        file_data_size: u64,
13049    }
13050
13051    fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
13052        let volume_header = test_volume_header();
13053        let crypto_header = test_crypto_header();
13054        let subkeys = Subkeys::derive(
13055            &master_key(),
13056            &volume_header.archive_uuid,
13057            &volume_header.session_id,
13058        )
13059        .unwrap();
13060        let mut next_block_index = 0u64;
13061        let mut blocks = BTreeMap::new();
13062
13063        let healthy = test_member(b"healthy.txt", b"healthy payload\n");
13064        let broken = test_member(b"broken.txt", b"broken payload\n");
13065        let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
13066
13067        let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
13068        let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
13069
13070        let healthy_payload = encrypt_test_object(
13071            &healthy_frame,
13072            &subkeys.enc_key,
13073            &subkeys.nonce_seed,
13074            b"envelope",
13075            0,
13076            BlockKind::PayloadData,
13077            &mut next_block_index,
13078            &crypto_header,
13079            &volume_header,
13080        );
13081        let broken_payload = encrypt_test_object(
13082            &broken_frame,
13083            &subkeys.enc_key,
13084            &subkeys.nonce_seed,
13085            b"envelope",
13086            1,
13087            BlockKind::PayloadData,
13088            &mut next_block_index,
13089            &crypto_header,
13090            &volume_header,
13091        );
13092        let broken_payload_block = broken_payload.extent.first_block_index;
13093        insert_records(&mut blocks, &healthy_payload.records);
13094        insert_records(&mut blocks, &broken_payload.records);
13095
13096        let frames = vec![
13097            FrameEntry {
13098                frame_index: 0,
13099                envelope_index: 0,
13100                offset_in_envelope: 0,
13101                compressed_size: healthy_frame.len() as u32,
13102                decompressed_size: healthy.len() as u32,
13103                flags: 0x0000_0003,
13104                tar_stream_offset: 0,
13105            },
13106            FrameEntry {
13107                frame_index: 1,
13108                envelope_index: 1,
13109                offset_in_envelope: 0,
13110                compressed_size: broken_frame.len() as u32,
13111                decompressed_size: broken.len() as u32,
13112                flags: 0x0000_0003,
13113                tar_stream_offset: healthy.len() as u64,
13114            },
13115        ];
13116        let envelopes = vec![
13117            EnvelopeEntry {
13118                envelope_index: 0,
13119                first_block_index: healthy_payload.extent.first_block_index,
13120                data_block_count: healthy_payload.extent.data_block_count,
13121                parity_block_count: 0,
13122                encrypted_size: healthy_payload.extent.encrypted_size,
13123                plaintext_size: healthy_frame.len() as u32,
13124                first_frame_index: 0,
13125                frame_count: 1,
13126            },
13127            EnvelopeEntry {
13128                envelope_index: 1,
13129                first_block_index: broken_payload.extent.first_block_index,
13130                data_block_count: broken_payload.extent.data_block_count,
13131                parity_block_count: 0,
13132                encrypted_size: broken_payload.extent.encrypted_size,
13133                plaintext_size: broken_frame.len() as u32,
13134                first_frame_index: 1,
13135                frame_count: 1,
13136            },
13137        ];
13138        let files = vec![
13139            TestFileMeta {
13140                path: b"healthy.txt".to_vec(),
13141                frame_index: 0,
13142                tar_stream_offset: 0,
13143                member_group_size: healthy.len() as u64,
13144                file_data_size: b"healthy payload\n".len() as u64,
13145            },
13146            TestFileMeta {
13147                path: b"broken.txt".to_vec(),
13148                frame_index: 1,
13149                tar_stream_offset: healthy.len() as u64,
13150                member_group_size: broken.len() as u64,
13151                file_data_size: b"broken payload\n".len() as u64,
13152            },
13153        ];
13154
13155        let (index_shard_plaintext, first_path_hash, last_path_hash) =
13156            build_test_index_shard(&files, &frames, &envelopes);
13157        let index_shard = encrypt_test_object(
13158            &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
13159            &subkeys.index_shard_key,
13160            &subkeys.index_nonce_seed,
13161            b"idxshard",
13162            0,
13163            BlockKind::IndexShardData,
13164            &mut next_block_index,
13165            &crypto_header,
13166            &volume_header,
13167        );
13168        insert_records(&mut blocks, &index_shard.records);
13169
13170        let shard_entry = ShardEntry {
13171            shard_index: 0,
13172            first_block_index: index_shard.extent.first_block_index,
13173            data_block_count: index_shard.extent.data_block_count,
13174            parity_block_count: 0,
13175            encrypted_size: index_shard.extent.encrypted_size,
13176            decompressed_size: index_shard_plaintext.len() as u32,
13177            file_count: files.len() as u32,
13178            first_path_hash,
13179            last_path_hash,
13180        };
13181        let mut root_header = IndexRootHeader::empty();
13182        root_header.frame_count = frames.len() as u64;
13183        root_header.envelope_count = envelopes.len() as u64;
13184        root_header.file_count = files.len() as u64;
13185        root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
13186            + broken_payload.extent.data_block_count as u64;
13187        root_header.tar_total_size = tar_stream.len() as u64;
13188        root_header.content_sha256 = sha256_bytes(&tar_stream);
13189        let index_root = IndexRoot {
13190            header: root_header,
13191            shards: vec![shard_entry],
13192            directory_hint_shards: Vec::new(),
13193        };
13194
13195        let index_root_plaintext = index_root.to_bytes();
13196        let index_root_object = encrypt_test_object(
13197            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
13198            &subkeys.index_root_key,
13199            &subkeys.index_nonce_seed,
13200            b"idxroot",
13201            0,
13202            BlockKind::IndexRootData,
13203            &mut next_block_index,
13204            &crypto_header,
13205            &volume_header,
13206        );
13207        insert_records(&mut blocks, &index_root_object.records);
13208
13209        let archive_uuid = volume_header.archive_uuid;
13210        let session_id = volume_header.session_id;
13211        let opened = OpenedArchive {
13212            options: ReaderOptions::default(),
13213            observed_archive_bytes: 1_000_000,
13214            observed_volume_count: 1,
13215            subkeys,
13216            blocks,
13217            lazy_blocks: None,
13218            crypto_header_bytes: Vec::new(),
13219            volume_header,
13220            crypto_header,
13221            manifest_footer: ManifestFooter {
13222                archive_uuid,
13223                session_id,
13224                volume_index: 0,
13225                is_authoritative: 1,
13226                total_volumes: 1,
13227                index_root_first_block: index_root_object.extent.first_block_index,
13228                index_root_data_block_count: index_root_object.extent.data_block_count,
13229                index_root_parity_block_count: 0,
13230                index_root_encrypted_size: index_root_object.extent.encrypted_size,
13231                index_root_decompressed_size: index_root_plaintext.len() as u32,
13232                manifest_hmac: [0u8; 32],
13233            },
13234            volume_trailer: Some(VolumeTrailer {
13235                archive_uuid,
13236                session_id,
13237                volume_index: 0,
13238                block_count: next_block_index,
13239                bytes_written: 0,
13240                manifest_footer_offset: 0,
13241                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
13242                closed_at_ns: 0,
13243                root_auth_footer_offset: 0,
13244                root_auth_footer_length: 0,
13245                root_auth_flags: 0,
13246                trailer_hmac: [0u8; 32],
13247            }),
13248            root_auth_footer: None,
13249            index_root,
13250            payload_dictionary: None,
13251        };
13252        (opened, broken_payload_block)
13253    }
13254
13255    fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
13256        let locating = opened.index_root.shards[0].clone();
13257        let mut shard = opened.load_index_shard(&locating).unwrap();
13258        mutate(&mut shard);
13259        let plaintext = shard.to_bytes();
13260        let mut next_block_index = opened
13261            .blocks
13262            .keys()
13263            .last()
13264            .copied()
13265            .map(|index| index + 1)
13266            .unwrap_or(0);
13267        let replacement = encrypt_test_object(
13268            &compress_zstd_frame(&plaintext, 1).unwrap(),
13269            &opened.subkeys.index_shard_key,
13270            &opened.subkeys.index_nonce_seed,
13271            b"idxshard",
13272            locating.shard_index,
13273            BlockKind::IndexShardData,
13274            &mut next_block_index,
13275            &opened.crypto_header,
13276            &opened.volume_header,
13277        );
13278        insert_records(&mut opened.blocks, &replacement.records);
13279        opened.index_root.shards[0] = ShardEntry {
13280            shard_index: locating.shard_index,
13281            first_block_index: replacement.extent.first_block_index,
13282            data_block_count: replacement.extent.data_block_count,
13283            parity_block_count: 0,
13284            encrypted_size: replacement.extent.encrypted_size,
13285            decompressed_size: plaintext.len() as u32,
13286            file_count: shard.files.len() as u32,
13287            first_path_hash: shard.files.first().unwrap().path_hash,
13288            last_path_hash: shard.files.last().unwrap().path_hash,
13289        };
13290    }
13291
13292    fn rewrite_as_single_healthy_file(
13293        opened: &mut OpenedArchive,
13294        mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
13295    ) {
13296        let healthy_path = b"healthy.txt";
13297        let healthy_payload = b"healthy payload\n";
13298        let healthy_member = test_member(healthy_path, healthy_payload);
13299        replace_first_index_shard(opened, |shard| {
13300            let file_index = (0..shard.files.len())
13301                .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
13302                .unwrap();
13303            let mut file = shard.files[file_index].clone();
13304            let frame = shard
13305                .frames
13306                .iter()
13307                .find(|entry| entry.frame_index == 0)
13308                .unwrap()
13309                .clone();
13310            let envelope = shard
13311                .envelopes
13312                .iter()
13313                .find(|entry| entry.envelope_index == 0)
13314                .unwrap()
13315                .clone();
13316            let mut path = healthy_path.to_vec();
13317
13318            file.path_offset = 0;
13319            file.path_length = path.len() as u32;
13320            file.first_frame_index = 0;
13321            file.frame_count = 1;
13322            file.offset_in_first_frame_plaintext = 0;
13323            file.tar_member_group_size = healthy_member.len() as u64;
13324            file.file_data_size = healthy_payload.len() as u64;
13325            file.flags = 0;
13326            mutate(&mut file, &mut path);
13327            file.path_offset = 0;
13328            file.path_length = path.len() as u32;
13329            file.path_hash = hash_prefix(&path);
13330
13331            shard.files = vec![file];
13332            shard.frames = vec![frame];
13333            shard.envelopes = vec![envelope];
13334            shard.string_pool = path;
13335        });
13336
13337        opened.index_root.header.file_count = 1;
13338        opened.index_root.header.frame_count = 1;
13339        opened.index_root.header.envelope_count = 1;
13340        opened.index_root.header.payload_block_count = 1;
13341        opened.index_root.header.tar_total_size = healthy_member.len() as u64;
13342        opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
13343    }
13344
13345    fn test_volume_header() -> VolumeHeader {
13346        VolumeHeader {
13347            format_version: FORMAT_VERSION,
13348            volume_format_rev: VOLUME_FORMAT_REV,
13349            volume_index: 0,
13350            stripe_width: 1,
13351            archive_uuid: [0x31; 16],
13352            session_id: [0x42; 16],
13353            crypto_header_offset: VOLUME_HEADER_LEN as u32,
13354            crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
13355            header_crc32c: 0,
13356        }
13357    }
13358
13359    fn test_crypto_header() -> CryptoHeaderFixed {
13360        CryptoHeaderFixed {
13361            length: CRYPTO_HEADER_FIXED_LEN as u32,
13362            compression_algo: CompressionAlgo::ZstdFramed,
13363            aead_algo: AeadAlgo::AesGcmSiv256,
13364            fec_algo: FecAlgo::ReedSolomonGF16,
13365            kdf_algo: KdfAlgo::Raw,
13366            chunk_size: 4096,
13367            envelope_target_size: 8192,
13368            block_size: 4096,
13369            fec_data_shards: 4,
13370            fec_parity_shards: 0,
13371            index_fec_data_shards: 4,
13372            index_fec_parity_shards: 0,
13373            index_root_fec_data_shards: 4,
13374            index_root_fec_parity_shards: 0,
13375            stripe_width: 1,
13376            volume_loss_tolerance: 0,
13377            bit_rot_buffer_pct: 0,
13378            has_dictionary: 0,
13379            max_path_length: 4096,
13380            expected_volume_size: 0,
13381        }
13382    }
13383
13384    #[allow(clippy::too_many_arguments)]
13385    fn encrypt_test_object(
13386        plaintext: &[u8],
13387        key: &[u8; 32],
13388        nonce_seed: &[u8; 32],
13389        domain: &[u8],
13390        counter: u64,
13391        data_kind: BlockKind,
13392        next_block_index: &mut u64,
13393        crypto_header: &CryptoHeaderFixed,
13394        volume_header: &VolumeHeader,
13395    ) -> TestObject {
13396        let block_size = crypto_header.block_size as usize;
13397        let encrypted = encrypt_padded_aead_object(
13398            AeadObjectContext {
13399                algo: crypto_header.aead_algo,
13400                key,
13401                nonce_seed,
13402                domain,
13403                archive_uuid: &volume_header.archive_uuid,
13404                session_id: &volume_header.session_id,
13405                counter,
13406            },
13407            block_size,
13408            plaintext,
13409        )
13410        .unwrap();
13411        assert_eq!(encrypted.len() % block_size, 0);
13412
13413        let first_block_index = *next_block_index;
13414        let data_block_count = encrypted.len() / block_size;
13415        let records = encrypted
13416            .chunks(block_size)
13417            .enumerate()
13418            .map(|(index, payload)| BlockRecord {
13419                block_index: first_block_index + index as u64,
13420                kind: data_kind,
13421                flags: if index + 1 == data_block_count {
13422                    0x01
13423                } else {
13424                    0
13425                },
13426                payload: payload.to_vec(),
13427                record_crc32c: 0,
13428            })
13429            .collect::<Vec<_>>();
13430        *next_block_index += data_block_count as u64;
13431
13432        TestObject {
13433            extent: ObjectExtent {
13434                first_block_index,
13435                data_block_count: data_block_count as u32,
13436                parity_block_count: 0,
13437                encrypted_size: encrypted.len() as u32,
13438            },
13439            records,
13440        }
13441    }
13442
13443    fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
13444        for record in records {
13445            assert!(blocks.insert(record.block_index, record.clone()).is_none());
13446        }
13447    }
13448
13449    #[allow(clippy::too_many_arguments)]
13450    fn build_metadata_object_from_payload(
13451        payload: &[u8],
13452        _subkeys: &Subkeys,
13453        volume_header: &VolumeHeader,
13454        crypto_header: &CryptoHeaderFixed,
13455        key: &[u8; 32],
13456        nonce_seed: &[u8; 32],
13457        domain: &[u8],
13458        counter: u64,
13459        data_kind: BlockKind,
13460        next_block_index: &mut u64,
13461    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
13462        let compressed = compress_zstd_frame(payload, 1).unwrap();
13463        build_metadata_object_from_compressed(
13464            &compressed,
13465            key,
13466            nonce_seed,
13467            domain,
13468            counter,
13469            data_kind,
13470            next_block_index,
13471            crypto_header,
13472            volume_header,
13473        )
13474    }
13475
13476    #[allow(clippy::too_many_arguments)]
13477    fn build_metadata_object_from_compressed(
13478        compressed: &[u8],
13479        key: &[u8; 32],
13480        nonce_seed: &[u8; 32],
13481        domain: &[u8],
13482        counter: u64,
13483        data_kind: BlockKind,
13484        next_block_index: &mut u64,
13485        crypto_header: &CryptoHeaderFixed,
13486        volume_header: &VolumeHeader,
13487    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
13488        let object = encrypt_test_object(
13489            compressed,
13490            key,
13491            nonce_seed,
13492            domain,
13493            counter,
13494            data_kind,
13495            next_block_index,
13496            crypto_header,
13497            volume_header,
13498        );
13499
13500        let mut blocks = BTreeMap::new();
13501        for record in object.records {
13502            blocks.insert(record.block_index, record);
13503        }
13504        (object.extent, blocks)
13505    }
13506
13507    #[allow(clippy::too_many_arguments)]
13508    fn assert_metadata_object_from_compressed(
13509        compressed: &[u8],
13510        decompressed_size: usize,
13511        _subkeys: &Subkeys,
13512        volume_header: &VolumeHeader,
13513        crypto_header: &CryptoHeaderFixed,
13514        key: &[u8; 32],
13515        nonce_seed: &[u8; 32],
13516        domain: &[u8],
13517        counter: u64,
13518        data_kind: BlockKind,
13519        parity_kind: BlockKind,
13520        class_data_shards: u16,
13521        class_parity_shards: u16,
13522        next_block_index: &mut u64,
13523        expected: FormatError,
13524    ) {
13525        let (extent, blocks) = build_metadata_object_from_compressed(
13526            compressed,
13527            key,
13528            nonce_seed,
13529            domain,
13530            counter,
13531            data_kind,
13532            next_block_index,
13533            crypto_header,
13534            volume_header,
13535        );
13536        let error = load_metadata_object_from_parts(
13537            &blocks,
13538            ObjectLoadContext {
13539                volume_header,
13540                crypto_header,
13541                extent,
13542                data_kind,
13543                parity_kind,
13544                key,
13545                nonce_seed,
13546                domain,
13547                counter,
13548                class_data_shard_max: class_data_shards,
13549                class_parity_shard_max: class_parity_shards,
13550            },
13551            decompressed_size as u32,
13552        )
13553        .unwrap_err();
13554        assert_eq!(error, expected);
13555    }
13556
13557    fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
13558        let record = blocks.get_mut(&block_index).unwrap();
13559        assert_eq!(record.kind, BlockKind::PayloadData);
13560        record.payload[0] ^= 0x55;
13561    }
13562
13563    fn build_test_index_shard(
13564        files: &[TestFileMeta],
13565        frames: &[FrameEntry],
13566        envelopes: &[EnvelopeEntry],
13567    ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
13568        let mut sorted = files
13569            .iter()
13570            .map(|file| (hash_prefix(&file.path), file))
13571            .collect::<Vec<_>>();
13572        sorted.sort_by(|left, right| {
13573            (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
13574                right.0,
13575                right.1.path.as_slice(),
13576                right.1.tar_stream_offset,
13577            ))
13578        });
13579
13580        let mut string_pool = Vec::new();
13581        let mut file_entries = Vec::with_capacity(sorted.len());
13582        for (path_hash, file) in &sorted {
13583            let path_offset = string_pool.len() as u32;
13584            string_pool.extend_from_slice(&file.path);
13585            file_entries.push(FileEntry {
13586                path_hash: *path_hash,
13587                path_offset,
13588                path_length: file.path.len() as u32,
13589                first_frame_index: file.frame_index,
13590                frame_count: 1,
13591                offset_in_first_frame_plaintext: 0,
13592                tar_member_group_size: file.member_group_size,
13593                file_data_size: file.file_data_size,
13594                flags: 0,
13595            });
13596        }
13597
13598        let header = IndexShardHeader {
13599            version: 1,
13600            shard_index: 0,
13601            file_count: file_entries.len() as u32,
13602            frame_count: frames.len() as u32,
13603            envelope_count: envelopes.len() as u32,
13604            file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
13605            frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
13606                as u32,
13607            envelope_table_offset: (INDEX_SHARD_HEADER_LEN
13608                + file_entries.len() * FILE_ENTRY_LEN
13609                + frames.len() * FRAME_ENTRY_LEN) as u32,
13610            string_pool_offset: (INDEX_SHARD_HEADER_LEN
13611                + file_entries.len() * FILE_ENTRY_LEN
13612                + frames.len() * FRAME_ENTRY_LEN
13613                + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
13614            string_pool_size: string_pool.len() as u32,
13615        };
13616
13617        let mut bytes = Vec::new();
13618        bytes.extend_from_slice(&header.to_bytes());
13619        for entry in &file_entries {
13620            bytes.extend_from_slice(&entry.to_bytes());
13621        }
13622        for entry in frames {
13623            bytes.extend_from_slice(&entry.to_bytes());
13624        }
13625        for entry in envelopes {
13626            bytes.extend_from_slice(&entry.to_bytes());
13627        }
13628        bytes.extend_from_slice(&string_pool);
13629
13630        (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
13631    }
13632
13633    fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
13634        let mut out = Vec::new();
13635        out.extend_from_slice(&test_tar_header(path, data.len() as u64));
13636        out.extend_from_slice(data);
13637        out.resize(out.len() + padding_to_512(data.len()), 0);
13638        out
13639    }
13640
13641    fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
13642        let mut header = [0u8; 512];
13643        header[..path.len()].copy_from_slice(path);
13644        write_test_tar_octal(&mut header[100..108], 0o644);
13645        write_test_tar_octal(&mut header[108..116], 0);
13646        write_test_tar_octal(&mut header[116..124], 0);
13647        write_test_tar_octal(&mut header[124..136], size);
13648        write_test_tar_octal(&mut header[136..148], 0);
13649        header[148..156].fill(b' ');
13650        header[156] = b'0';
13651        header[257..263].copy_from_slice(b"ustar\0");
13652        header[263..265].copy_from_slice(b"00");
13653        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
13654        write_test_tar_checksum(&mut header[148..156], checksum);
13655        header
13656    }
13657
13658    fn write_test_tar_octal(field: &mut [u8], value: u64) {
13659        let digits = format!("{value:o}");
13660        field.fill(0);
13661        let start = field.len() - 1 - digits.len();
13662        field[..start].fill(b'0');
13663        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
13664    }
13665
13666    fn write_test_tar_checksum(field: &mut [u8], value: u64) {
13667        let digits = format!("{value:06o}");
13668        field[0..6].copy_from_slice(digits.as_bytes());
13669        field[6] = 0;
13670        field[7] = b' ';
13671    }
13672
13673    fn padding_to_512(len: usize) -> usize {
13674        let remainder = len % 512;
13675        if remainder == 0 {
13676            0
13677        } else {
13678            512 - remainder
13679        }
13680    }
13681}