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/// Receives logical regular-file bytes while the archive reader extracts data.
212///
213/// Callbacks report uncompressed member payload bytes after they are accepted by
214/// the destination writer. Each selected file is capped by its authenticated
215/// `file_data_size`.
216pub trait ArchiveExtractProgressSink {
217    /// Reports newly extracted payload bytes for one archive member.
218    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
219}
220
221impl<F> ArchiveExtractProgressSink for F
222where
223    F: FnMut(&str, u64),
224{
225    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
226        self(archive_path, bytes);
227    }
228}
229
230#[derive(Debug, Clone)]
231pub struct OpenedArchive {
232    options: ReaderOptions,
233    observed_archive_bytes: u64,
234    observed_volume_count: u32,
235    subkeys: Subkeys,
236    blocks: BTreeMap<u64, BlockRecord>,
237    lazy_blocks: Option<Arc<SeekableBlockSource>>,
238    crypto_header_bytes: Vec<u8>,
239    pub volume_header: VolumeHeader,
240    pub crypto_header: CryptoHeaderFixed,
241    pub manifest_footer: ManifestFooter,
242    pub volume_trailer: Option<VolumeTrailer>,
243    pub root_auth_footer: Option<RootAuthFooterV1>,
244    pub index_root: IndexRoot,
245    payload_dictionary: Option<Vec<u8>>,
246}
247
248#[derive(Debug)]
249pub struct ArchiveContentVerification<'a> {
250    archive: &'a OpenedArchive,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct ArchiveRepairPatch {
255    pub volume_index: u32,
256    pub block_index: u64,
257    pub record_offset: u64,
258    pub record_bytes: Vec<u8>,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub enum RootAuthDiagnostic {
263    RootAuthContentVerified,
264    RootAuthDeferredFullArchiveScanRequired,
265    AuthenticatedMetadataNotRootSigned,
266    RecoveryMarginNotRootAuthenticated,
267    ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
268    RecoveryMarginChecked,
269    RecoveryMarginFailed,
270    RecoveryMarginUnchecked,
271}
272
273impl RootAuthDiagnostic {
274    pub const fn label(self) -> &'static str {
275        match self {
276            Self::RootAuthContentVerified => "root_auth_content_verified",
277            Self::RootAuthDeferredFullArchiveScanRequired => {
278                "root_auth_deferred_full_archive_scan_required"
279            }
280            Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
281            Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
282            Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
283                "replicated_global_copy_unchecked_due_to_volume_loss"
284            }
285            Self::RecoveryMarginChecked => "recovery_margin_checked",
286            Self::RecoveryMarginFailed => "recovery_margin_failed",
287            Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
288        }
289    }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum PublicNoKeyDiagnostic {
294    PublicDataBlockCommitmentVerified,
295    PublicPhysicalCompletenessUnverified,
296    PublicRecoveryMarginUnchecked,
297}
298
299impl PublicNoKeyDiagnostic {
300    pub const fn label(self) -> &'static str {
301        match self {
302            Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
303            Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
304            Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
305        }
306    }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct RootAuthVerification {
311    pub archive_root: [u8; 32],
312    pub authenticator_id: u16,
313    pub signer_identity_type: u16,
314    pub signer_identity_bytes: Vec<u8>,
315    pub total_data_block_count: u64,
316    pub diagnostics: Vec<RootAuthDiagnostic>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct PublicNoKeyVerification {
321    pub archive_root: [u8; 32],
322    pub authenticator_id: u16,
323    pub signer_identity_type: u16,
324    pub signer_identity_bytes: Vec<u8>,
325    pub total_data_block_count: u64,
326    pub diagnostics: Vec<PublicNoKeyDiagnostic>,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330struct RootAuthMaterial {
331    critical_metadata_digest: [u8; 32],
332    index_digest: [u8; 32],
333    fec_layout_digest: [u8; 32],
334    data_block_merkle_root: [u8; 32],
335    signer_identity_digest: [u8; 32],
336    archive_root: [u8; 32],
337    total_data_block_count: u64,
338}
339
340#[derive(Debug, Clone, Copy)]
341struct ObjectExtent {
342    first_block_index: u64,
343    data_block_count: u32,
344    parity_block_count: u32,
345    encrypted_size: u32,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349enum ParityReadPolicy {
350    Always,
351    RepairOnly,
352}
353
354pub(crate) struct StreamedArchiveOpenParts {
355    pub(crate) options: ReaderOptions,
356    pub(crate) observed_archive_bytes: u64,
357    pub(crate) subkeys: Subkeys,
358    pub(crate) blocks: BTreeMap<u64, BlockRecord>,
359    pub(crate) crypto_header_bytes: Vec<u8>,
360    pub(crate) volume_header: VolumeHeader,
361    pub(crate) crypto_header: CryptoHeaderFixed,
362    pub(crate) manifest_footer: ManifestFooter,
363    pub(crate) volume_trailer: VolumeTrailer,
364    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
365}
366
367#[derive(Clone, Copy)]
368struct WinningIndexEntry {
369    start: u64,
370    file_data_size: u64,
371    shard_index: usize,
372    file_index: usize,
373}
374
375struct LocatedIndexFile {
376    shard: IndexShard,
377    file_index: usize,
378    start: u64,
379}
380
381struct ExtractProgressWriter<'a, W> {
382    inner: &'a mut W,
383    archive_path: &'a str,
384    file_data_size: u64,
385    reported_bytes: u64,
386    progress: &'a mut dyn ArchiveExtractProgressSink,
387}
388
389impl<'a, W> ExtractProgressWriter<'a, W> {
390    fn new(
391        inner: &'a mut W,
392        archive_path: &'a str,
393        file_data_size: u64,
394        progress: &'a mut dyn ArchiveExtractProgressSink,
395    ) -> Self {
396        Self {
397            inner,
398            archive_path,
399            file_data_size,
400            reported_bytes: 0,
401            progress,
402        }
403    }
404
405    fn report(&mut self, bytes: u64) {
406        if bytes == 0 || self.file_data_size == 0 {
407            return;
408        }
409        let capped_next = self
410            .reported_bytes
411            .saturating_add(bytes)
412            .min(self.file_data_size);
413        let delta = capped_next.saturating_sub(self.reported_bytes);
414        if delta == 0 {
415            return;
416        }
417        self.reported_bytes = capped_next;
418        self.progress.file_bytes_extracted(self.archive_path, delta);
419    }
420}
421
422impl<W: Write> Write for ExtractProgressWriter<'_, W> {
423    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
424        let written = self.inner.write(buf)?;
425        self.report(written as u64);
426        Ok(written)
427    }
428
429    fn flush(&mut self) -> std::io::Result<()> {
430        self.inner.flush()
431    }
432}
433
434struct DecodedTarMemberGroupReader<'a> {
435    archive: &'a OpenedArchive,
436    shard: &'a IndexShard,
437    file: &'a FileEntry,
438    decompressor: zstd::bulk::Decompressor<'static>,
439    next_frame_offset: u64,
440    cached_envelope_index: Option<u64>,
441    cached_envelope_plaintext: Vec<u8>,
442    current_frame: Vec<u8>,
443    current_frame_offset: usize,
444    remaining_group_bytes: u64,
445}
446
447struct SeekableVolumeSource {
448    reader: Arc<dyn ArchiveReadAt>,
449    volume_index: u32,
450    crypto_end: u64,
451    block_count: u64,
452    record_len: u64,
453    block_size: usize,
454}
455
456impl std::fmt::Debug for SeekableVolumeSource {
457    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458        f.debug_struct("SeekableVolumeSource")
459            .field("volume_index", &self.volume_index)
460            .field("crypto_end", &self.crypto_end)
461            .field("block_count", &self.block_count)
462            .field("record_len", &self.record_len)
463            .field("block_size", &self.block_size)
464            .finish_non_exhaustive()
465    }
466}
467
468#[derive(Debug)]
469struct SeekableBlockSource {
470    stripe_width: u32,
471    volumes: Vec<Option<SeekableVolumeSource>>,
472}
473
474trait BlockProvider {
475    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
476}
477
478struct OpenedBlockProvider<'a> {
479    memory_blocks: &'a BTreeMap<u64, BlockRecord>,
480    lazy_blocks: Option<&'a SeekableBlockSource>,
481}
482
483impl SeekableBlockSource {
484    fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
485        if self.stripe_width == 0 {
486            return Err(FormatError::ZeroStripeWidth);
487        }
488        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
489            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
490        let Some(volume) = self
491            .volumes
492            .get(volume_index as usize)
493            .and_then(Option::as_ref)
494        else {
495            return Err(FormatError::InvalidArchive(
496                "repair output requires all archive volumes",
497            ));
498        };
499        let slot = block_index / self.stripe_width as u64;
500        if slot >= volume.block_count {
501            return Err(FormatError::InvalidArchive(
502                "BlockRecord global coverage has a gap",
503            ));
504        }
505        Ok((volume_index, volume.record_offset(slot)?))
506    }
507
508    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
509        if self.stripe_width == 0 {
510            return Err(FormatError::ZeroStripeWidth);
511        }
512        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
513            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
514        let Some(volume) = self
515            .volumes
516            .get(volume_index as usize)
517            .and_then(Option::as_ref)
518        else {
519            return Ok(None);
520        };
521        let slot = block_index / self.stripe_width as u64;
522        if slot >= volume.block_count {
523            return Ok(None);
524        }
525        match volume.read_slot(slot, block_index) {
526            Ok(record) => Ok(Some(record)),
527            Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
528            Err(err) => Err(err),
529        }
530    }
531
532    fn is_complete_volume_set(&self) -> bool {
533        self.volumes.iter().all(Option::is_some)
534    }
535
536    fn total_block_count(&self) -> Result<u64, FormatError> {
537        self.volumes
538            .iter()
539            .map(|volume| {
540                volume
541                    .as_ref()
542                    .map(|volume| volume.block_count)
543                    .ok_or(FormatError::InvalidArchive(
544                        "missing volume in complete set",
545                    ))
546            })
547            .try_fold(0u64, |sum, count| {
548                checked_u64_add(sum, count?, "BlockRecord count overflow")
549            })
550    }
551}
552
553impl SeekableVolumeSource {
554    fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
555        self.crypto_end
556            .checked_add(checked_u64_mul(
557                slot,
558                self.record_len,
559                "BlockRecord offset overflow",
560            )?)
561            .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
562    }
563
564    fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
565        let record_offset = self.record_offset(slot)?;
566        let raw = read_at_vec(
567            self.reader.as_ref(),
568            record_offset,
569            usize::try_from(self.record_len)
570                .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
571            "BlockRecord",
572        )?;
573        let record = BlockRecord::parse(&raw, self.block_size)?;
574        if record.block_index != expected_block_index {
575            return Err(FormatError::InvalidArchive(
576                "BlockRecord index does not match volume position",
577            ));
578        }
579        Ok(record)
580    }
581}
582
583impl BlockProvider for BTreeMap<u64, BlockRecord> {
584    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
585        Ok(self.get(&block_index).cloned())
586    }
587}
588
589impl BlockProvider for OpenedBlockProvider<'_> {
590    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
591        if let Some(record) = self.memory_blocks.get(&block_index) {
592            return Ok(Some(record.clone()));
593        }
594        match self.lazy_blocks {
595            Some(source) => source.block(block_index),
596            None => Ok(None),
597        }
598    }
599}
600
601fn subkeys_for_open(
602    master_key: Option<&MasterKey>,
603    aead_algo: AeadAlgo,
604    archive_uuid: &[u8; 16],
605    session_id: &[u8; 16],
606) -> Result<Subkeys, FormatError> {
607    if aead_algo.is_encrypted() {
608        Subkeys::derive(
609            master_key.ok_or(FormatError::KeyMaterialMismatch)?,
610            archive_uuid,
611            session_id,
612        )
613    } else {
614        Ok(Subkeys::unencrypted_placeholder())
615    }
616}
617
618type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
619pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
620const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
621    "fast full extract requires unique archive paths";
622
623#[derive(Debug)]
624struct PayloadIndexTables {
625    shards: Vec<IndexShard>,
626    file_count: u64,
627    frames: BTreeMap<u64, FrameEntry>,
628    envelopes: BTreeMap<u64, EnvelopeEntry>,
629}
630
631pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
632    OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
633}
634
635pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
636    require_unencrypted_volume_profile(bytes)?;
637    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
638    OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
639}
640
641pub fn open_archive_volumes(
642    volumes: &[&[u8]],
643    master_key: &MasterKey,
644) -> Result<OpenedArchive, FormatError> {
645    OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
646}
647
648pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
649    for volume in volumes {
650        require_unencrypted_volume_profile(volume)?;
651    }
652    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
653    OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
654}
655
656pub fn open_archive_with_bootstrap_sidecar(
657    bytes: &[u8],
658    bootstrap_sidecar: &[u8],
659    master_key: &MasterKey,
660) -> Result<OpenedArchive, FormatError> {
661    OpenedArchive::open_with_bootstrap_sidecar_options(
662        bytes,
663        bootstrap_sidecar,
664        master_key,
665        ReaderOptions::default(),
666    )
667}
668
669fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
670    if bytes.len() < VOLUME_HEADER_LEN {
671        return Err(FormatError::InvalidLength {
672            structure: "archive",
673            expected: VOLUME_HEADER_LEN,
674            actual: bytes.len(),
675        });
676    }
677    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
678    let crypto_start = volume_header.crypto_header_offset as usize;
679    let crypto_len = volume_header.crypto_header_length as usize;
680    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
681    let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
682    if crypto_header.fixed.aead_algo == AeadAlgo::None
683        && crypto_header.fixed.kdf_algo == KdfAlgo::None
684    {
685        Ok(())
686    } else {
687        Err(FormatError::KeyMaterialMismatch)
688    }
689}
690
691pub fn open_seekable_archive<R: ArchiveReadAt>(
692    reader: R,
693    master_key: &MasterKey,
694) -> Result<OpenedArchive, FormatError> {
695    OpenedArchive::open_seekable_volumes_with_options(
696        vec![reader],
697        master_key,
698        ReaderOptions::default(),
699    )
700}
701
702pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
703    readers: Vec<R>,
704    master_key: &MasterKey,
705) -> Result<OpenedArchive, FormatError> {
706    OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
707}
708
709pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
710    reader: R,
711    bootstrap_sidecar: &[u8],
712    master_key: &MasterKey,
713) -> Result<OpenedArchive, FormatError> {
714    open_seekable_archive_with_bootstrap_sidecar_options(
715        reader,
716        bootstrap_sidecar,
717        master_key,
718        ReaderOptions::default(),
719    )
720}
721
722pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
723    reader: R,
724    bootstrap_sidecar: &[u8],
725    master_key: &MasterKey,
726    options: ReaderOptions,
727) -> Result<OpenedArchive, FormatError> {
728    OpenedArchive::open_seekable_volumes_with_options_for_mode(
729        vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
730        master_key,
731        options,
732        Some(bootstrap_sidecar),
733    )
734}
735
736pub fn open_non_seekable_archive(
737    bytes: &[u8],
738    master_key: &MasterKey,
739    bootstrap_sidecar: Option<&[u8]>,
740) -> Result<OpenedArchive, FormatError> {
741    match bootstrap_sidecar {
742        Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
743            bytes,
744            sidecar,
745            master_key,
746            ReaderOptions::default(),
747            BootstrapSidecarUse::NonSeekableRandomAccess,
748        ),
749        None => Err(FormatError::ReaderUnsupported(
750            "non-seekable random access requires a bootstrap sidecar",
751        )),
752    }
753}
754
755pub fn public_no_key_verify_archive_with<F>(
756    bytes: &[u8],
757    verifier: F,
758) -> Result<PublicNoKeyVerification, FormatError>
759where
760    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
761{
762    public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
763}
764
765pub fn public_no_key_verify_volumes_with<F>(
766    volumes: &[&[u8]],
767    verifier: F,
768) -> Result<PublicNoKeyVerification, FormatError>
769where
770    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
771{
772    public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
773}
774
775/// Decode a single-volume, dictionary-free non-seekable archive image into tar
776/// bytes after authenticating its terminal ManifestFooter and VolumeTrailer.
777///
778/// This is a whole-buffer helper, not a live provisional-output API.
779/// Callers receive no decoded bytes if terminal authentication fails.
780pub fn sequential_extract_tar_stream(
781    bytes: &[u8],
782    master_key: &MasterKey,
783) -> Result<Vec<u8>, FormatError> {
784    sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
785}
786
787impl OpenedArchive {
788    fn block_provider(&self) -> OpenedBlockProvider<'_> {
789        OpenedBlockProvider {
790            memory_blocks: &self.blocks,
791            lazy_blocks: self.lazy_blocks.as_deref(),
792        }
793    }
794
795    fn missing_volume_count(&self) -> u32 {
796        self.crypto_header
797            .stripe_width
798            .saturating_sub(self.observed_volume_count)
799    }
800
801    fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
802        let mut diagnostics = vec![
803            RootAuthDiagnostic::RootAuthContentVerified,
804            RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
805            RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
806        ];
807        if self.missing_volume_count() > 0 {
808            diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
809        }
810        diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
811        diagnostics
812    }
813
814    pub fn open_with_options(
815        bytes: &[u8],
816        master_key: &MasterKey,
817        options: ReaderOptions,
818    ) -> Result<Self, FormatError> {
819        Self::open_volumes_with_options(&[bytes], master_key, options)
820    }
821
822    pub fn open_volumes_with_options(
823        volumes: &[&[u8]],
824        master_key: &MasterKey,
825        options: ReaderOptions,
826    ) -> Result<Self, FormatError> {
827        validate_reader_options(options)?;
828        if volumes.is_empty() {
829            return Err(FormatError::InvalidArchive("no volumes supplied"));
830        }
831
832        let observed_archive_bytes =
833            observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
834        let mut first: Option<ParsedSeekableVolume> = None;
835        let mut manifest_authority: Option<ManifestFooter> = None;
836        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
837        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
838        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
839        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
840        let mut saw_root_auth_absent = false;
841        let mut first_manifest_footer_error: Option<FormatError> = None;
842        let mut seen_volume_indexes = BTreeSet::new();
843        let mut blocks = BTreeMap::new();
844        let mut erased_block_indices = BTreeSet::new();
845
846        for volume_bytes in volumes {
847            let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
848            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
849                return Err(FormatError::InvalidArchive(
850                    "duplicate authenticated volume index",
851                ));
852            }
853
854            if let Some(first) = &first {
855                validate_volume_set_member(first, &parsed)?;
856            }
857
858            if let Some(footer) = &parsed.manifest_footer {
859                if let Some(authority) = &manifest_authority {
860                    if !manifest_bootstrap_fields_match(authority, footer) {
861                        return Err(FormatError::InvalidArchive(
862                            "ManifestFooter bootstrap fields differ",
863                        ));
864                    }
865                } else {
866                    manifest_authority = Some(footer.clone());
867                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
868                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
869                }
870            } else if first_manifest_footer_error.is_none() {
871                first_manifest_footer_error = parsed.manifest_footer_error.take();
872            }
873
874            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
875                (Some(footer), Some(bytes)) => {
876                    if saw_root_auth_absent {
877                        return Err(FormatError::InvalidArchive(
878                            "root-auth footer presence differs across volumes",
879                        ));
880                    }
881                    if let Some(authority_bytes) = &root_auth_authority_bytes {
882                        if authority_bytes != bytes {
883                            return Err(FormatError::InvalidArchive(
884                                "RootAuthFooter copies differ",
885                            ));
886                        }
887                    } else {
888                        root_auth_authority = Some(footer.clone());
889                        root_auth_authority_bytes = Some(bytes.clone());
890                    }
891                }
892                (None, None) => {
893                    if root_auth_authority_bytes.is_some() {
894                        return Err(FormatError::InvalidArchive(
895                            "root-auth footer presence differs across volumes",
896                        ));
897                    }
898                    saw_root_auth_absent = true;
899                }
900                _ => {
901                    return Err(FormatError::InvalidArchive(
902                        "root-auth footer terminal state is inconsistent",
903                    ));
904                }
905            }
906
907            for (block_index, record) in &parsed.blocks {
908                if blocks.insert(*block_index, record.clone()).is_some() {
909                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
910                }
911            }
912            for block_index in &parsed.erased_block_indices {
913                erased_block_indices.insert(*block_index);
914            }
915
916            if first.is_none() {
917                first = Some(parsed);
918            }
919        }
920
921        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
922        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
923            Some(err) => err,
924            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
925        })?;
926        let authority_volume_header = manifest_authority_volume_header.ok_or(
927            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
928        )?;
929        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
930            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
931        )?;
932        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
933            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
934        let missing_volume_count = first
935            .crypto_header
936            .stripe_width
937            .checked_sub(observed_volume_count)
938            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
939        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
940            return Err(FormatError::InvalidArchive(
941                "missing volume count exceeds volume_loss_tolerance",
942            ));
943        }
944        if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
945            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
946        }
947
948        let limits = metadata_limits(&first.crypto_header);
949        let index_root_plaintext = load_metadata_object_from_parts(
950            &blocks,
951            ObjectLoadContext::index_root(
952                &first.volume_header,
953                &first.crypto_header,
954                &first.subkeys,
955                ObjectExtent {
956                    first_block_index: manifest_footer.index_root_first_block,
957                    data_block_count: manifest_footer.index_root_data_block_count,
958                    parity_block_count: manifest_footer.index_root_parity_block_count,
959                    encrypted_size: manifest_footer.index_root_encrypted_size,
960                },
961            ),
962            manifest_footer.index_root_decompressed_size,
963        )?;
964        let index_root = IndexRoot::parse(
965            &index_root_plaintext,
966            first.crypto_header.has_dictionary != 0,
967            limits,
968        )?;
969        let payload_dictionary = load_archive_dictionary(
970            &blocks,
971            &first.subkeys,
972            &first.volume_header,
973            &first.crypto_header,
974            &index_root,
975        )?;
976
977        Ok(Self {
978            options,
979            observed_archive_bytes,
980            observed_volume_count,
981            subkeys: first.subkeys,
982            blocks,
983            lazy_blocks: None,
984            crypto_header_bytes: first.crypto_header_bytes,
985            volume_header: authority_volume_header,
986            crypto_header: first.crypto_header,
987            manifest_footer,
988            volume_trailer: Some(authority_volume_trailer),
989            root_auth_footer: root_auth_authority,
990            index_root,
991            payload_dictionary,
992        })
993    }
994
995    pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
996        readers: Vec<R>,
997        master_key: &MasterKey,
998        options: ReaderOptions,
999    ) -> Result<Self, FormatError> {
1000        let readers = readers
1001            .into_iter()
1002            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1003            .collect::<Vec<_>>();
1004        Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1005    }
1006
1007    fn open_seekable_volumes_with_options_for_mode(
1008        readers: Vec<Arc<dyn ArchiveReadAt>>,
1009        master_key: &MasterKey,
1010        options: ReaderOptions,
1011        bootstrap_sidecar: Option<&[u8]>,
1012    ) -> Result<Self, FormatError> {
1013        validate_reader_options(options)?;
1014        if readers.is_empty() {
1015            return Err(FormatError::InvalidArchive("no volumes supplied"));
1016        }
1017        if bootstrap_sidecar.is_some() && readers.len() > 1 {
1018            return Err(FormatError::ReaderUnsupported(
1019                "multi-volume inputs with bootstrap sidecar are not supported",
1020            ));
1021        }
1022
1023        let observed_archive_bytes = observed_archive_size(
1024            readers
1025                .iter()
1026                .map(|reader| reader.len())
1027                .collect::<Result<Vec<_>, _>>()?
1028                .into_iter()
1029                .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1030        )?;
1031        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1032        let mut manifest_authority: Option<ManifestFooter> = None;
1033        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1034        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1035        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1036        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1037        let mut saw_root_auth_absent = false;
1038        let mut first_manifest_footer_error: Option<FormatError> = None;
1039        let mut seen_volume_indexes = BTreeSet::new();
1040        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1041
1042        for reader in readers {
1043            let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1044            if bootstrap_sidecar.is_some() {
1045                validate_bootstrap_single_volume_input(
1046                    &parsed.volume_header,
1047                    &parsed.crypto_header,
1048                )?;
1049            }
1050            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1051                return Err(FormatError::InvalidArchive(
1052                    "duplicate authenticated volume index",
1053                ));
1054            }
1055
1056            if let Some(first) = &first {
1057                validate_volume_set_member_metadata(
1058                    &first.volume_header,
1059                    &first.crypto_header,
1060                    &first.crypto_header_bytes,
1061                    &parsed.volume_header,
1062                    &parsed.crypto_header,
1063                    &parsed.crypto_header_bytes,
1064                )?;
1065            } else {
1066                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1067            }
1068
1069            if let Some(footer) = &parsed.manifest_footer {
1070                if let Some(authority) = &manifest_authority {
1071                    if !manifest_bootstrap_fields_match(authority, footer) {
1072                        return Err(FormatError::InvalidArchive(
1073                            "ManifestFooter bootstrap fields differ",
1074                        ));
1075                    }
1076                } else {
1077                    manifest_authority = Some(footer.clone());
1078                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1079                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1080                }
1081            } else if first_manifest_footer_error.is_none() {
1082                first_manifest_footer_error = parsed.manifest_footer_error.take();
1083            }
1084
1085            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1086                (Some(footer), Some(bytes)) => {
1087                    if saw_root_auth_absent {
1088                        return Err(FormatError::InvalidArchive(
1089                            "root-auth footer presence differs across volumes",
1090                        ));
1091                    }
1092                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1093                        if authority_bytes != bytes {
1094                            return Err(FormatError::InvalidArchive(
1095                                "RootAuthFooter copies differ",
1096                            ));
1097                        }
1098                    } else {
1099                        root_auth_authority = Some(footer.clone());
1100                        root_auth_authority_bytes = Some(bytes.clone());
1101                    }
1102                }
1103                (None, None) => {
1104                    if root_auth_authority_bytes.is_some() {
1105                        return Err(FormatError::InvalidArchive(
1106                            "root-auth footer presence differs across volumes",
1107                        ));
1108                    }
1109                    saw_root_auth_absent = true;
1110                }
1111                _ => {
1112                    return Err(FormatError::InvalidArchive(
1113                        "root-auth footer terminal state is inconsistent",
1114                    ));
1115                }
1116            }
1117
1118            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1119            let source = SeekableVolumeSource {
1120                reader: parsed.reader.clone(),
1121                volume_index: parsed.volume_header.volume_index,
1122                crypto_end: parsed.crypto_end,
1123                block_count: parsed.volume_trailer.block_count,
1124                record_len,
1125                block_size: parsed.crypto_header.block_size as usize,
1126            };
1127            let slot = parsed.volume_header.volume_index as usize;
1128            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1129            {
1130                return Err(FormatError::InvalidArchive(
1131                    "duplicate authenticated volume index",
1132                ));
1133            }
1134
1135            if first.is_none() {
1136                first = Some(parsed);
1137            }
1138        }
1139
1140        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1141        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1142            Some(err) => err,
1143            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1144        })?;
1145        let authority_volume_header = manifest_authority_volume_header.ok_or(
1146            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1147        )?;
1148        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1149            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1150        )?;
1151        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1152            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1153        let missing_volume_count = first
1154            .crypto_header
1155            .stripe_width
1156            .checked_sub(observed_volume_count)
1157            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1158        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1159            return Err(FormatError::InvalidArchive(
1160                "missing volume count exceeds volume_loss_tolerance",
1161            ));
1162        }
1163
1164        let mut blocks = BTreeMap::new();
1165        let sidecar = if let Some(bytes) = bootstrap_sidecar {
1166            let sidecar = parse_bootstrap_sidecar(
1167                bytes,
1168                &first.volume_header,
1169                &first.crypto_header,
1170                &first.subkeys,
1171            )?;
1172            sidecar
1173                .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1174            if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1175                if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1176                    return Err(FormatError::InvalidArchive(
1177                        "bootstrap sidecar conflicts with terminal ManifestFooter",
1178                    ));
1179                }
1180            }
1181            Some((bytes, sidecar))
1182        } else {
1183            None
1184        };
1185
1186        if let Some((sidecar_bytes, sidecar)) = &sidecar {
1187            if let Some((offset, length)) = sidecar.index_root_records_section {
1188                let index_root_records = parse_sidecar_block_records(
1189                    sidecar_bytes,
1190                    first.crypto_header.block_size as usize,
1191                    SidecarBlockRecordsSection {
1192                        offset,
1193                        length,
1194                        extent: index_root_extent_from_manifest(&manifest_footer),
1195                        data_kind: BlockKind::IndexRootData,
1196                        parity_kind: BlockKind::IndexRootParity,
1197                        structure: "IndexRoot",
1198                    },
1199                )?;
1200                insert_sidecar_records(&mut blocks, index_root_records)?;
1201            }
1202        }
1203
1204        let lazy_source = Arc::new(SeekableBlockSource {
1205            stripe_width: first.crypto_header.stripe_width,
1206            volumes: lazy_volume_slots,
1207        });
1208        let block_provider = OpenedBlockProvider {
1209            memory_blocks: &blocks,
1210            lazy_blocks: Some(lazy_source.as_ref()),
1211        };
1212        let limits = metadata_limits(&first.crypto_header);
1213        let index_root_plaintext = load_metadata_object_from_parts(
1214            &block_provider,
1215            ObjectLoadContext::index_root(
1216                &first.volume_header,
1217                &first.crypto_header,
1218                &first.subkeys,
1219                index_root_extent_from_manifest(&manifest_footer),
1220            ),
1221            manifest_footer.index_root_decompressed_size,
1222        )?;
1223        let index_root = IndexRoot::parse(
1224            &index_root_plaintext,
1225            first.crypto_header.has_dictionary != 0,
1226            limits,
1227        )?;
1228        if first.crypto_header.has_dictionary != 0 {
1229            if let Some((sidecar_bytes, sidecar)) = &sidecar {
1230                if let Some((offset, length)) = sidecar.dictionary_records_section {
1231                    let dictionary_records = parse_sidecar_block_records(
1232                        sidecar_bytes,
1233                        first.crypto_header.block_size as usize,
1234                        SidecarBlockRecordsSection {
1235                            offset,
1236                            length,
1237                            extent: dictionary_extent_from_index_root(&index_root)?,
1238                            data_kind: BlockKind::DictionaryData,
1239                            parity_kind: BlockKind::DictionaryParity,
1240                            structure: "Dictionary",
1241                        },
1242                    )?;
1243                    insert_sidecar_records(&mut blocks, dictionary_records)?;
1244                }
1245            }
1246        }
1247        let block_provider = OpenedBlockProvider {
1248            memory_blocks: &blocks,
1249            lazy_blocks: Some(lazy_source.as_ref()),
1250        };
1251        let payload_dictionary = load_archive_dictionary(
1252            &block_provider,
1253            &first.subkeys,
1254            &first.volume_header,
1255            &first.crypto_header,
1256            &index_root,
1257        )?;
1258
1259        Ok(Self {
1260            options,
1261            observed_archive_bytes,
1262            observed_volume_count,
1263            subkeys: first.subkeys,
1264            blocks,
1265            lazy_blocks: Some(lazy_source),
1266            crypto_header_bytes: first.crypto_header_bytes,
1267            volume_header: authority_volume_header,
1268            crypto_header: first.crypto_header,
1269            manifest_footer,
1270            volume_trailer: Some(authority_volume_trailer),
1271            root_auth_footer: root_auth_authority,
1272            index_root,
1273            payload_dictionary,
1274        })
1275    }
1276
1277    pub fn open_with_bootstrap_sidecar_options(
1278        bytes: &[u8],
1279        bootstrap_sidecar: &[u8],
1280        master_key: &MasterKey,
1281        options: ReaderOptions,
1282    ) -> Result<Self, FormatError> {
1283        Self::open_with_bootstrap_sidecar_options_for_mode(
1284            bytes,
1285            bootstrap_sidecar,
1286            master_key,
1287            options,
1288            BootstrapSidecarUse::SeekableAssist,
1289        )
1290    }
1291
1292    fn open_with_bootstrap_sidecar_options_for_mode(
1293        bytes: &[u8],
1294        bootstrap_sidecar: &[u8],
1295        master_key: &MasterKey,
1296        options: ReaderOptions,
1297        sidecar_use: BootstrapSidecarUse,
1298    ) -> Result<Self, FormatError> {
1299        let observed_archive_bytes =
1300            observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1301        if bytes.len() < VOLUME_HEADER_LEN {
1302            return Err(FormatError::InvalidLength {
1303                structure: "archive",
1304                expected: VOLUME_HEADER_LEN,
1305                actual: bytes.len(),
1306            });
1307        }
1308
1309        let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1310        let crypto_start = volume_header.crypto_header_offset as usize;
1311        let crypto_len = volume_header.crypto_header_length as usize;
1312        let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1313        let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1314        let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1315        let subkeys = subkeys_for_open(
1316            Some(master_key),
1317            parsed_crypto.fixed.aead_algo,
1318            &volume_header.archive_uuid,
1319            &volume_header.session_id,
1320        )?;
1321        verify_integrity_tag(
1322            HmacDomain::CryptoHeader,
1323            parsed_crypto.fixed.aead_algo,
1324            Some(&subkeys.mac_key),
1325            &volume_header.archive_uuid,
1326            &volume_header.session_id,
1327            parsed_crypto.hmac_covered_bytes,
1328            &parsed_crypto.header_hmac,
1329        )?;
1330        parsed_crypto.validate_extension_semantics()?;
1331        reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1332        validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1333        validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1334
1335        let sidecar = parse_bootstrap_sidecar(
1336            bootstrap_sidecar,
1337            &volume_header,
1338            &parsed_crypto.fixed,
1339            &subkeys,
1340        )?;
1341        sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1342
1343        let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1344            bytes,
1345            crypto_end,
1346            parsed_crypto.fixed.block_size as usize,
1347            &volume_header,
1348        )?;
1349        let terminal_material = match sidecar_use {
1350            BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1351                bytes,
1352                terminal_offset,
1353                observed_block_count,
1354                KeyHoldingTerminalContext {
1355                    subkeys: &subkeys,
1356                    volume_header: &volume_header,
1357                    crypto_header: &parsed_crypto.fixed,
1358                    crypto_header_bytes: crypto_bytes,
1359                },
1360                options,
1361            )?),
1362            BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1363                bytes,
1364                terminal_offset,
1365                observed_block_count,
1366                KeyHoldingTerminalContext {
1367                    subkeys: &subkeys,
1368                    volume_header: &volume_header,
1369                    crypto_header: &parsed_crypto.fixed,
1370                    crypto_header_bytes: crypto_bytes,
1371                },
1372                options,
1373            )
1374            .ok(),
1375        };
1376        let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1377        let manifest_authority = match sidecar_use {
1378            BootstrapSidecarUse::SeekableAssist => {
1379                let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1380                    "terminal ManifestFooter/VolumeTrailer is required",
1381                ))?;
1382                if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1383                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1384                        return Err(FormatError::InvalidArchive(
1385                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1386                        ));
1387                    }
1388                }
1389                terminal_manifest.clone()
1390            }
1391            BootstrapSidecarUse::NonSeekableRandomAccess => {
1392                let sidecar_manifest = sidecar
1393                    .manifest_footer
1394                    .as_ref()
1395                    .ok_or(FormatError::ReaderUnsupported(
1396                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1397                ))?;
1398                if let Some(terminal_manifest) = terminal_manifest {
1399                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1400                        return Err(FormatError::InvalidArchive(
1401                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1402                        ));
1403                    }
1404                }
1405                sidecar_manifest.clone()
1406            }
1407        };
1408        manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1409
1410        if let Some((offset, length)) = sidecar.index_root_records_section {
1411            let index_root_records = parse_sidecar_block_records(
1412                bootstrap_sidecar,
1413                parsed_crypto.fixed.block_size as usize,
1414                SidecarBlockRecordsSection {
1415                    offset,
1416                    length,
1417                    extent: index_root_extent_from_manifest(&manifest_authority),
1418                    data_kind: BlockKind::IndexRootData,
1419                    parity_kind: BlockKind::IndexRootParity,
1420                    structure: "IndexRoot",
1421                },
1422            )?;
1423            insert_sidecar_records(&mut blocks, index_root_records)?;
1424        }
1425
1426        let limits = metadata_limits(&parsed_crypto.fixed);
1427        let index_root_plaintext = load_metadata_object_from_parts(
1428            &blocks,
1429            ObjectLoadContext::index_root(
1430                &volume_header,
1431                &parsed_crypto.fixed,
1432                &subkeys,
1433                index_root_extent_from_manifest(&manifest_authority),
1434            ),
1435            manifest_authority.index_root_decompressed_size,
1436        )?;
1437        let index_root = IndexRoot::parse(
1438            &index_root_plaintext,
1439            parsed_crypto.fixed.has_dictionary != 0,
1440            limits,
1441        )?;
1442        if parsed_crypto.fixed.has_dictionary != 0 {
1443            if let Some((offset, length)) = sidecar.dictionary_records_section {
1444                let dictionary_records = parse_sidecar_block_records(
1445                    bootstrap_sidecar,
1446                    parsed_crypto.fixed.block_size as usize,
1447                    SidecarBlockRecordsSection {
1448                        offset,
1449                        length,
1450                        extent: dictionary_extent_from_index_root(&index_root)?,
1451                        data_kind: BlockKind::DictionaryData,
1452                        parity_kind: BlockKind::DictionaryParity,
1453                        structure: "dictionary",
1454                    },
1455                )?;
1456                insert_sidecar_records(&mut blocks, dictionary_records)?;
1457            }
1458        }
1459        let payload_dictionary = load_archive_dictionary(
1460            &blocks,
1461            &subkeys,
1462            &volume_header,
1463            &parsed_crypto.fixed,
1464            &index_root,
1465        )?;
1466
1467        Ok(Self {
1468            options,
1469            observed_archive_bytes,
1470            observed_volume_count: 1,
1471            subkeys,
1472            blocks,
1473            lazy_blocks: None,
1474            crypto_header_bytes: crypto_bytes.to_vec(),
1475            volume_header,
1476            crypto_header: parsed_crypto.fixed,
1477            manifest_footer: manifest_authority,
1478            volume_trailer: terminal_material
1479                .as_ref()
1480                .map(|(_, trailer, _)| trailer.clone()),
1481            root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
1482            index_root,
1483            payload_dictionary,
1484        })
1485    }
1486
1487    /// Return path and payload-size entries from encrypted index metadata only.
1488    ///
1489    /// Unlike [`Self::list_files`], this does not decode tar member groups, so
1490    /// it does not read or decrypt payload envelopes after the index shards are
1491    /// available.
1492    pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
1493        let shards = self.load_all_index_shards()?;
1494        final_index_entry_winners(&shards)?
1495            .into_iter()
1496            .map(|(path, winner)| {
1497                Ok(ArchiveIndexEntry {
1498                    path,
1499                    file_data_size: winner.file_data_size,
1500                })
1501            })
1502            .collect()
1503    }
1504
1505    /// Look up one archive path using encrypted index metadata only.
1506    pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
1507        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1508        self.locate_index_file(&normalized)?
1509            .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
1510            .transpose()
1511    }
1512
1513    pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
1514        let shards = self.load_all_index_shards()?;
1515        final_index_entry_winners(&shards)?
1516            .into_iter()
1517            .map(|(path, winner)| {
1518                let shard = &shards[winner.shard_index];
1519                let member =
1520                    self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
1521                Ok(ArchiveEntry {
1522                    path,
1523                    file_data_size: winner.file_data_size,
1524                    kind: member.kind,
1525                    mode: member.mode,
1526                    mtime: member.mtime,
1527                    diagnostics: member.diagnostics,
1528                })
1529            })
1530            .collect()
1531    }
1532
1533    /// Return only the regular-file payload bytes for `path`.
1534    ///
1535    /// This is a payload-only convenience for callers that do not need tar
1536    /// metadata fidelity diagnostics. Use [`Self::extract_file_with_diagnostics`]
1537    /// or [`Self::extract_member`] when unsupported local PAX/GNU metadata must
1538    /// be reported to users.
1539    pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
1540        self.extract_member(path)?
1541            .map(|member| {
1542                if member.kind != TarEntryKind::Regular {
1543                    return Err(FormatError::ReaderUnsupported(
1544                        "extract_file returns only regular file payloads",
1545                    ));
1546                }
1547                Ok(member.data)
1548            })
1549            .transpose()
1550    }
1551
1552    /// Return regular-file payload bytes together with parsed tar metadata
1553    /// diagnostics for `path`.
1554    pub fn extract_file_with_diagnostics(
1555        &self,
1556        path: &str,
1557    ) -> Result<Option<ExtractedRegularFile>, FormatError> {
1558        self.extract_member(path)?
1559            .map(|member| {
1560                if member.kind != TarEntryKind::Regular {
1561                    return Err(FormatError::ReaderUnsupported(
1562                        "extract_file_with_diagnostics returns only regular file payloads",
1563                    ));
1564                }
1565                Ok((member.data, member.diagnostics))
1566            })
1567            .transpose()
1568    }
1569
1570    /// Stream regular-file payload bytes for `path` into `writer`.
1571    ///
1572    /// This keeps extraction memory bounded by the selected payload envelope,
1573    /// one decompressed frame, and small tar metadata buffers. It returns the
1574    /// same metadata diagnostics as [`Self::extract_file_with_diagnostics`].
1575    pub fn extract_file_to_writer<W: Write>(
1576        &self,
1577        path: &str,
1578        writer: &mut W,
1579    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1580        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1581        self.locate_index_file(&normalized)?
1582            .map(|located| {
1583                self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
1584            })
1585            .transpose()
1586    }
1587
1588    /// Stream regular-file payload bytes for `path` into `writer` while
1589    /// reporting extracted logical payload bytes.
1590    pub fn extract_file_to_writer_with_progress<W: Write>(
1591        &self,
1592        path: &str,
1593        writer: &mut W,
1594        progress: &mut dyn ArchiveExtractProgressSink,
1595    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1596        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1597        self.locate_index_file(&normalized)?
1598            .map(|located| {
1599                self.stream_loaded_file_to_writer_with_progress(
1600                    &located.shard,
1601                    located.file_index,
1602                    writer,
1603                    progress,
1604                )
1605            })
1606            .transpose()
1607    }
1608
1609    pub fn extract_member(
1610        &self,
1611        path: &str,
1612    ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
1613        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1614        self.locate_index_file(&normalized)?
1615            .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
1616            .transpose()
1617    }
1618
1619    pub fn extract_file_to(
1620        &self,
1621        path: &str,
1622        root: &std::path::Path,
1623        options: SafeExtractionOptions,
1624    ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
1625        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1626        self.locate_index_file(&normalized)?
1627            .map(|located| {
1628                self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
1629            })
1630            .transpose()
1631    }
1632
1633    pub fn extract_indexed_files_to(
1634        &self,
1635        root: &std::path::Path,
1636        options: SafeExtractionOptions,
1637        jobs: usize,
1638    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1639        if jobs == 0 {
1640            return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
1641        }
1642
1643        let shards = self.load_all_index_shards()?;
1644        let entries = final_index_entry_winners(&shards)?.into_iter().collect();
1645        self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
1646    }
1647
1648    pub fn verify(&self) -> Result<(), FormatError> {
1649        self.verify_content().map(|_| ())
1650    }
1651
1652    pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
1653        let tables = self.load_payload_index_tables()?;
1654        let streamed = self.scan_seekable_payload(
1655            &tables,
1656            u64::MAX,
1657            NoopTarStreamObserver,
1658            true,
1659            ParityReadPolicy::Always,
1660        )?;
1661        self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
1662        Ok(ArchiveContentVerification { archive: self })
1663    }
1664
1665    pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
1666        let lazy_source = self
1667            .lazy_blocks
1668            .as_ref()
1669            .ok_or(FormatError::ReaderUnsupported(
1670                "repair output requires seekable archive input",
1671            ))?;
1672        if !lazy_source.is_complete_volume_set() {
1673            return Err(FormatError::ReaderUnsupported(
1674                "repair output requires all archive volumes",
1675            ));
1676        }
1677
1678        let shards = self.load_all_index_shards()?;
1679        let rows = self.root_auth_fec_layout_rows(&shards)?;
1680        let block_provider = self.block_provider();
1681        let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
1682        for row in rows.into_iter().filter(|row| row.present) {
1683            self.collect_repair_patches_for_object(
1684                &block_provider,
1685                lazy_source,
1686                row,
1687                &mut patches,
1688            )?;
1689        }
1690        Ok(patches.into_values().collect())
1691    }
1692
1693    pub fn extract_all_to(
1694        &self,
1695        root: &std::path::Path,
1696        options: SafeExtractionOptions,
1697    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1698        let tables = self.load_payload_index_tables()?;
1699        if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
1700            return Err(FormatError::ReaderUnsupported(
1701                FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
1702            ));
1703        }
1704
1705        let observer = TarStreamFilesystemRestoreObserver::new(root, options);
1706        let streamed = self.scan_seekable_payload(
1707            &tables,
1708            total_extraction_size_cap(self.options, self.observed_archive_bytes),
1709            observer,
1710            false,
1711            ParityReadPolicy::RepairOnly,
1712        )?;
1713        self.validate_streamed_payload_summary(&tables, &streamed, true, false)?;
1714        streamed
1715            .tar
1716            .members
1717            .into_iter()
1718            .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
1719            .collect()
1720    }
1721
1722    fn collect_repair_patches_for_object(
1723        &self,
1724        blocks: &impl BlockProvider,
1725        source: &SeekableBlockSource,
1726        row: FecLayoutObjectRow,
1727        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1728    ) -> Result<(), FormatError> {
1729        let (data_kind, parity_kind, data_max, parity_max) =
1730            self.fec_object_class_shape(row.object_class)?;
1731        let extent = ObjectExtent {
1732            first_block_index: row.first_block_index,
1733            data_block_count: row.data_block_count,
1734            parity_block_count: row.parity_block_count,
1735            encrypted_size: row.encrypted_size,
1736        };
1737        validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
1738
1739        let block_size = self.crypto_header.block_size as usize;
1740        let data_count = extent.data_block_count as usize;
1741        let parity_count = extent.parity_block_count as usize;
1742        let mut data_shards = Vec::with_capacity(data_count);
1743        let mut parity_shards = Vec::with_capacity(parity_count);
1744
1745        for offset in 0..data_count {
1746            let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1747            match blocks.block(block_index)? {
1748                Some(record) => {
1749                    if record.kind != data_kind {
1750                        return Err(FormatError::InvalidArchive(
1751                            "object data block has unexpected kind",
1752                        ));
1753                    }
1754                    let should_be_last = offset + 1 == data_count;
1755                    if record.is_last_data() != should_be_last {
1756                        return Err(FormatError::InvalidArchive(
1757                            "object last-data flag is not on the final data block",
1758                        ));
1759                    }
1760                    data_shards.push(Some(record.payload.clone()));
1761                }
1762                None => data_shards.push(None),
1763            }
1764        }
1765
1766        for offset in 0..parity_count {
1767            let block_index = checked_u64_add(
1768                extent.first_block_index,
1769                data_count as u64 + offset as u64,
1770                "object",
1771            )?;
1772            match blocks.block(block_index)? {
1773                Some(record) => {
1774                    if record.kind != parity_kind {
1775                        return Err(FormatError::InvalidArchive(
1776                            "object parity block has unexpected kind",
1777                        ));
1778                    }
1779                    if record.is_last_data() {
1780                        return Err(FormatError::InvalidArchive(
1781                            "object parity block has last-data flag",
1782                        ));
1783                    }
1784                    parity_shards.push(Some(record.payload.clone()));
1785                }
1786                None => parity_shards.push(None),
1787            }
1788        }
1789
1790        let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
1791        for (offset, payload) in repaired_data.iter().enumerate() {
1792            if data_shards[offset].is_none() {
1793                let block_index =
1794                    checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1795                let flags = if offset + 1 == data_count { 0x01 } else { 0 };
1796                self.insert_repair_patch(
1797                    patches,
1798                    source,
1799                    block_index,
1800                    data_kind,
1801                    flags,
1802                    payload.clone(),
1803                )?;
1804            }
1805        }
1806
1807        if parity_count > 0 {
1808            let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
1809            for (offset, payload) in repaired_parity.into_iter().enumerate() {
1810                if parity_shards[offset].as_ref() != Some(&payload) {
1811                    let block_index = checked_u64_add(
1812                        extent.first_block_index,
1813                        data_count as u64 + offset as u64,
1814                        "object",
1815                    )?;
1816                    self.insert_repair_patch(
1817                        patches,
1818                        source,
1819                        block_index,
1820                        parity_kind,
1821                        0,
1822                        payload,
1823                    )?;
1824                }
1825            }
1826        }
1827
1828        Ok(())
1829    }
1830
1831    fn insert_repair_patch(
1832        &self,
1833        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1834        source: &SeekableBlockSource,
1835        block_index: u64,
1836        kind: BlockKind,
1837        flags: u8,
1838        payload: Vec<u8>,
1839    ) -> Result<(), FormatError> {
1840        let (volume_index, record_offset) = source.record_location(block_index)?;
1841        let record = BlockRecord {
1842            block_index,
1843            kind,
1844            flags,
1845            payload,
1846            record_crc32c: 0,
1847        };
1848        let patch = ArchiveRepairPatch {
1849            volume_index,
1850            block_index,
1851            record_offset,
1852            record_bytes: record.to_bytes(),
1853        };
1854        if let Some(existing) = patches.insert(block_index, patch.clone()) {
1855            if existing != patch {
1856                return Err(FormatError::InvalidArchive(
1857                    "conflicting repair patch for BlockRecord",
1858                ));
1859            }
1860        }
1861        Ok(())
1862    }
1863
1864    fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
1865        if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
1866            && self.index_root.directory_hint_shards.is_empty()
1867        {
1868            return Err(FormatError::InvalidArchive(
1869                "IndexRoot file_count requires directory hints",
1870            ));
1871        }
1872
1873        let shards = self.load_all_index_shards()?;
1874        let mut file_count = 0u64;
1875        let mut frames = BTreeMap::<u64, FrameEntry>::new();
1876        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
1877
1878        for shard in &shards {
1879            file_count = file_count
1880                .checked_add(shard.files.len() as u64)
1881                .ok_or(FormatError::InvalidArchive("file count overflow"))?;
1882            for frame in &shard.frames {
1883                if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
1884                    if existing != *frame {
1885                        return Err(FormatError::InvalidArchive(
1886                            "duplicate FrameEntry rows do not match",
1887                        ));
1888                    }
1889                }
1890            }
1891            for envelope in &shard.envelopes {
1892                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
1893                {
1894                    if existing != *envelope {
1895                        return Err(FormatError::InvalidArchive(
1896                            "duplicate EnvelopeEntry rows do not match",
1897                        ));
1898                    }
1899                }
1900            }
1901        }
1902        validate_global_file_table_order(&shards)?;
1903
1904        if file_count != self.index_root.header.file_count {
1905            return Err(FormatError::InvalidArchive(
1906                "IndexRoot file_count does not match decoded shards",
1907            ));
1908        }
1909        verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
1910        verify_dense_keys(
1911            &envelopes,
1912            self.index_root.header.envelope_count,
1913            "EnvelopeEntry",
1914        )?;
1915        validate_envelope_frame_coverage(&frames, &envelopes)?;
1916        self.validate_encrypted_object_block_ranges(&envelopes)?;
1917
1918        let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
1919            sum.checked_add(envelope.data_block_count as u64)
1920                .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1921        })?;
1922        if payload_block_count != self.index_root.header.payload_block_count {
1923            return Err(FormatError::InvalidArchive(
1924                "IndexRoot payload_block_count does not match envelopes",
1925            ));
1926        }
1927
1928        Ok(PayloadIndexTables {
1929            shards,
1930            file_count,
1931            frames,
1932            envelopes,
1933        })
1934    }
1935
1936    fn scan_seekable_payload<O: TarStreamObserver>(
1937        &self,
1938        tables: &PayloadIndexTables,
1939        extraction_cap: u64,
1940        observer: O,
1941        hash_content: bool,
1942        parity_policy: ParityReadPolicy,
1943    ) -> Result<StreamedPayloadSummary, FormatError> {
1944        let mut tar = TarStreamSummaryValidator::with_observer(
1945            self.crypto_header.max_path_length,
1946            extraction_cap,
1947            usize::MAX,
1948            self.index_root.header.file_count,
1949            observer,
1950        );
1951        let mut content_hasher = hash_content.then(Sha256::new);
1952        let mut streamed_frames = Vec::with_capacity(tables.frames.len());
1953        let streamed_envelopes = tables
1954            .envelopes
1955            .values()
1956            .map(|envelope| StreamedEnvelopeSummary {
1957                envelope_index: envelope.envelope_index,
1958                first_block_index: envelope.first_block_index,
1959                data_block_count: envelope.data_block_count,
1960                parity_block_count: envelope.parity_block_count,
1961                encrypted_size: envelope.encrypted_size,
1962                plaintext_size: envelope.plaintext_size,
1963                first_frame_index: envelope.first_frame_index,
1964                frame_count: envelope.frame_count,
1965            })
1966            .collect::<Vec<_>>();
1967        let mut cached_envelope_index = None;
1968        let mut cached_envelope_plaintext = Vec::new();
1969        let mut decompressor = self.new_payload_decompressor()?;
1970
1971        for frame in tables.frames.values() {
1972            let envelope =
1973                tables
1974                    .envelopes
1975                    .get(&frame.envelope_index)
1976                    .ok_or(FormatError::InvalidArchive(
1977                        "FrameEntry references missing EnvelopeEntry",
1978                    ))?;
1979            if cached_envelope_index != Some(envelope.envelope_index) {
1980                cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
1981                cached_envelope_index = Some(envelope.envelope_index);
1982            }
1983            let compressed = slice(
1984                &cached_envelope_plaintext,
1985                frame.offset_in_envelope as usize,
1986                frame.compressed_size as usize,
1987                "FrameEntry",
1988            )?;
1989            let tar_stream_offset = tar.tar_total_size();
1990            let decoded = self.decompress_payload_frame_with(
1991                &mut decompressor,
1992                compressed,
1993                frame.decompressed_size,
1994            )?;
1995            if decoded.is_empty() {
1996                return Err(FormatError::InvalidArchive(
1997                    "zstd payload frame decompressed to zero bytes",
1998                ));
1999            }
2000            if let Some(hasher) = &mut content_hasher {
2001                hasher.update(&decoded);
2002            }
2003            tar.observe(&decoded)?;
2004            streamed_frames.push(StreamedFrameSummary {
2005                frame_index: frame.frame_index,
2006                envelope_index: frame.envelope_index,
2007                offset_in_envelope: frame.offset_in_envelope,
2008                compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2009                    FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2010                })?,
2011                decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2012                    FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2013                })?,
2014                tar_stream_offset,
2015            });
2016        }
2017
2018        let mut content_sha256 = [0u8; 32];
2019        if let Some(hasher) = content_hasher {
2020            let digest = hasher.finalize();
2021            content_sha256.copy_from_slice(&digest);
2022        }
2023        Ok(StreamedPayloadSummary {
2024            tar: tar.finish()?,
2025            content_sha256,
2026            envelopes: streamed_envelopes,
2027            frames: streamed_frames,
2028        })
2029    }
2030
2031    fn validate_streamed_payload_summary(
2032        &self,
2033        tables: &PayloadIndexTables,
2034        streamed: &StreamedPayloadSummary,
2035        enforce_total_extraction_cap: bool,
2036        enforce_content_sha256: bool,
2037    ) -> Result<(), FormatError> {
2038        if enforce_total_extraction_cap
2039            && streamed.tar.total_extraction_size
2040                > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2041        {
2042            return Err(FormatError::ReaderUnsupported(
2043                "total extraction size exceeds configured cap",
2044            ));
2045        }
2046
2047        let streamed_payload_block_count =
2048            streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2049                sum.checked_add(envelope.data_block_count as u64)
2050                    .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2051            })?;
2052        if streamed_payload_block_count != self.index_root.header.payload_block_count {
2053            return Err(FormatError::InvalidArchive(
2054                "streamed payload block count does not match IndexRoot",
2055            ));
2056        }
2057
2058        if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2059            return Err(FormatError::InvalidArchive(
2060                "IndexRoot tar_total_size does not match streamed tar stream",
2061            ));
2062        }
2063        if enforce_content_sha256
2064            && streamed.content_sha256 != self.index_root.header.content_sha256
2065        {
2066            return Err(FormatError::InvalidArchive(
2067                "IndexRoot content_sha256 does not match decoded tar stream",
2068            ));
2069        }
2070
2071        let streamed_envelopes = streamed.envelope_map()?;
2072        for envelope in tables.envelopes.values() {
2073            let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2074                FormatError::InvalidArchive(
2075                    "metadata references missing streamed payload envelope",
2076                ),
2077            )?;
2078            if actual.first_block_index != envelope.first_block_index
2079                || actual.data_block_count != envelope.data_block_count
2080                || actual.parity_block_count != envelope.parity_block_count
2081                || actual.encrypted_size != envelope.encrypted_size
2082                || actual.plaintext_size != envelope.plaintext_size
2083                || actual.first_frame_index != envelope.first_frame_index
2084                || actual.frame_count != envelope.frame_count
2085            {
2086                return Err(FormatError::InvalidArchive(
2087                    "EnvelopeEntry does not match streamed payload envelope",
2088                ));
2089            }
2090        }
2091
2092        let streamed_frames = streamed.frame_map()?;
2093        for frame in tables.frames.values() {
2094            let actual =
2095                streamed_frames
2096                    .get(&frame.frame_index)
2097                    .ok_or(FormatError::InvalidArchive(
2098                        "metadata references missing streamed payload frame",
2099                    ))?;
2100            if actual.envelope_index != frame.envelope_index
2101                || actual.offset_in_envelope != frame.offset_in_envelope
2102                || actual.compressed_size != frame.compressed_size
2103                || actual.decompressed_size != frame.decompressed_size
2104                || actual.tar_stream_offset != frame.tar_stream_offset
2105                || streamed.frame_flags(actual)? != frame.flags
2106            {
2107                return Err(FormatError::InvalidArchive(
2108                    "FrameEntry does not match streamed payload frame",
2109                ));
2110            }
2111        }
2112
2113        let streamed_members = streamed.member_start_map()?;
2114        if streamed.tar.members.len() as u64 != tables.file_count {
2115            return Err(FormatError::InvalidArchive(
2116                "streamed tar member count does not match decoded shards",
2117            ));
2118        }
2119        let mut file_extents = Vec::new();
2120        let mut directory_hint_map = DirectoryHintMap::new();
2121        for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2122            let shard_row_index = u32::try_from(shard_row_index)
2123                .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2124            for idx in 0..shard.files.len() {
2125                let file = &shard.files[idx];
2126                let start =
2127                    shard
2128                        .tar_member_group_start(idx)
2129                        .ok_or(FormatError::InvalidArchive(
2130                            "FileEntry tar member start is missing",
2131                        ))?;
2132                file_extents.push((start, file.tar_member_group_size));
2133                let path = shard
2134                    .file_path(idx)
2135                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2136                let member = streamed_members
2137                    .get(&start)
2138                    .ok_or(FormatError::InvalidArchive(
2139                        "FileEntry tar member start is missing from streamed tar",
2140                    ))?;
2141                if member.path != path {
2142                    return Err(FormatError::InvalidArchive(
2143                        "tar member path does not match FileEntry path",
2144                    ));
2145                }
2146                if member.logical_size != file.file_data_size {
2147                    return Err(FormatError::InvalidArchive(
2148                        "tar member size does not match FileEntry file_data_size",
2149                    ));
2150                }
2151                if member.group_size != file.tar_member_group_size {
2152                    return Err(FormatError::InvalidArchive(
2153                        "FileEntry does not match streamed tar member",
2154                    ));
2155                }
2156                add_expected_directory_hint_rows(
2157                    &mut directory_hint_map,
2158                    shard_row_index,
2159                    path,
2160                    member.kind,
2161                );
2162            }
2163        }
2164        validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2165        if !self.index_root.directory_hint_shards.is_empty() {
2166            let hint_tables = self.load_all_directory_hint_tables()?;
2167            validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2168        }
2169
2170        Ok(())
2171    }
2172
2173    pub(crate) fn from_streamed_parts(
2174        parts: StreamedArchiveOpenParts,
2175    ) -> Result<Self, FormatError> {
2176        let limits = metadata_limits(&parts.crypto_header);
2177        let index_root_plaintext = load_metadata_object_from_parts(
2178            &parts.blocks,
2179            ObjectLoadContext::index_root(
2180                &parts.volume_header,
2181                &parts.crypto_header,
2182                &parts.subkeys,
2183                ObjectExtent {
2184                    first_block_index: parts.manifest_footer.index_root_first_block,
2185                    data_block_count: parts.manifest_footer.index_root_data_block_count,
2186                    parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2187                    encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2188                },
2189            ),
2190            parts.manifest_footer.index_root_decompressed_size,
2191        )?;
2192        let index_root = IndexRoot::parse(
2193            &index_root_plaintext,
2194            parts.crypto_header.has_dictionary != 0,
2195            limits,
2196        )?;
2197        let payload_dictionary = load_archive_dictionary(
2198            &parts.blocks,
2199            &parts.subkeys,
2200            &parts.volume_header,
2201            &parts.crypto_header,
2202            &index_root,
2203        )?;
2204
2205        Ok(Self {
2206            options: parts.options,
2207            observed_archive_bytes: parts.observed_archive_bytes,
2208            observed_volume_count: 1,
2209            subkeys: parts.subkeys,
2210            blocks: parts.blocks,
2211            lazy_blocks: None,
2212            crypto_header_bytes: parts.crypto_header_bytes,
2213            volume_header: parts.volume_header,
2214            crypto_header: parts.crypto_header,
2215            manifest_footer: parts.manifest_footer,
2216            volume_trailer: Some(parts.volume_trailer),
2217            root_auth_footer: parts.root_auth_footer,
2218            index_root,
2219            payload_dictionary,
2220        })
2221    }
2222
2223    pub(crate) fn verify_streamed_payload_summary(
2224        &self,
2225        streamed: &StreamedPayloadSummary,
2226    ) -> Result<(), FormatError> {
2227        let tables = self.load_payload_index_tables()?;
2228        self.validate_streamed_payload_summary(&tables, streamed, true, true)
2229    }
2230
2231    pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2232    where
2233        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2234    {
2235        let content_verification = self.verify_content()?;
2236        self.verify_root_auth_with_verified_content(&content_verification, verifier)
2237    }
2238
2239    pub fn verify_root_auth_with_verified_content<F>(
2240        &self,
2241        content_verification: &ArchiveContentVerification<'_>,
2242        mut verifier: F,
2243    ) -> Result<RootAuthVerification, FormatError>
2244    where
2245        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2246    {
2247        if !std::ptr::eq(content_verification.archive, self) {
2248            return Err(FormatError::InvalidArchive(
2249                "content verification does not match archive",
2250            ));
2251        }
2252        let footer = self
2253            .root_auth_footer
2254            .as_ref()
2255            .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2256        let material = self.recompute_root_auth_material(footer)?;
2257        if material.critical_metadata_digest != footer.critical_metadata_digest
2258            || material.index_digest != footer.index_digest
2259            || material.fec_layout_digest != footer.fec_layout_digest
2260            || material.data_block_merkle_root != footer.data_block_merkle_root
2261            || material.signer_identity_digest != footer.signer_identity_digest
2262            || material.archive_root != footer.archive_root
2263            || material.total_data_block_count != footer.total_data_block_count
2264        {
2265            return Err(FormatError::InvalidArchive(
2266                "RootAuthFooter commitments do not match recomputed archive root",
2267            ));
2268        }
2269        if !verifier(footer, &material.archive_root)? {
2270            return Err(FormatError::InvalidArchive(
2271                "root-auth authenticator verification failed",
2272            ));
2273        }
2274        Ok(RootAuthVerification {
2275            archive_root: material.archive_root,
2276            authenticator_id: footer.authenticator_id,
2277            signer_identity_type: footer.signer_identity_type,
2278            signer_identity_bytes: footer.signer_identity_bytes.clone(),
2279            total_data_block_count: footer.total_data_block_count,
2280            diagnostics: self.root_auth_success_diagnostics(),
2281        })
2282    }
2283
2284    fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2285        parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2286            self.load_index_shard(entry)
2287        })
2288    }
2289
2290    fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2291        let block_provider = self.block_provider();
2292        let plaintext = load_metadata_object_from_parts(
2293            &block_provider,
2294            ObjectLoadContext::index_shard(
2295                &self.volume_header,
2296                &self.crypto_header,
2297                &self.subkeys,
2298                entry,
2299            ),
2300            entry.decompressed_size,
2301        )?;
2302        IndexShard::parse(&plaintext, entry, self.metadata_limits())
2303    }
2304
2305    fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2306        parallel_map_ref(
2307            &self.index_root.directory_hint_shards,
2308            self.options.jobs,
2309            |entry| self.load_directory_hint_table(entry),
2310        )
2311    }
2312
2313    fn load_directory_hint_table(
2314        &self,
2315        entry: &DirectoryHintShardEntry,
2316    ) -> Result<DirectoryHintTable, FormatError> {
2317        let block_provider = self.block_provider();
2318        let plaintext = load_metadata_object_from_parts(
2319            &block_provider,
2320            ObjectLoadContext::directory_hint(
2321                &self.volume_header,
2322                &self.crypto_header,
2323                &self.subkeys,
2324                entry,
2325            ),
2326            entry.decompressed_size,
2327        )?;
2328        DirectoryHintTable::parse(
2329            &plaintext,
2330            entry,
2331            self.index_root.header.shard_count,
2332            self.metadata_limits(),
2333        )
2334    }
2335
2336    fn load_payload_envelope(
2337        &self,
2338        envelope: &EnvelopeEntry,
2339        parity_policy: ParityReadPolicy,
2340    ) -> Result<Vec<u8>, FormatError> {
2341        let block_provider = self.block_provider();
2342        let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2343            &block_provider,
2344            ObjectLoadContext::payload(
2345                &self.volume_header,
2346                &self.crypto_header,
2347                &self.subkeys,
2348                envelope,
2349            ),
2350            parity_policy,
2351        )?;
2352        if plaintext.len() != envelope.plaintext_size as usize {
2353            return Err(FormatError::InvalidArchive(
2354                "payload envelope plaintext_size mismatch",
2355            ));
2356        }
2357        Ok(plaintext)
2358    }
2359
2360    fn locate_index_file(
2361        &self,
2362        normalized: &[u8],
2363    ) -> Result<Option<LocatedIndexFile>, FormatError> {
2364        let candidate_indexes = self
2365            .index_root
2366            .candidate_shards_for_path(normalized, self.metadata_limits())?;
2367        let mut winner: Option<LocatedIndexFile> = None;
2368
2369        for row_index in candidate_indexes {
2370            let locating =
2371                self.index_root
2372                    .shards
2373                    .get(row_index)
2374                    .ok_or(FormatError::InvalidArchive(
2375                        "candidate shard row is out of bounds",
2376                    ))?;
2377            let shard = self.load_index_shard(locating)?;
2378            if let Some(file_index) = shard.lookup_file_index(normalized) {
2379                let start =
2380                    shard
2381                        .tar_member_group_start(file_index)
2382                        .ok_or(FormatError::InvalidArchive(
2383                            "FileEntry tar member start is missing",
2384                        ))?;
2385                if winner
2386                    .as_ref()
2387                    .map(|existing| start > existing.start)
2388                    .unwrap_or(true)
2389                {
2390                    winner = Some(LocatedIndexFile {
2391                        shard,
2392                        file_index,
2393                        start,
2394                    });
2395                }
2396            }
2397        }
2398
2399        Ok(winner)
2400    }
2401
2402    fn extract_loaded_member(
2403        &self,
2404        shard: &IndexShard,
2405        file_index: usize,
2406    ) -> Result<ExtractedArchiveMember, FormatError> {
2407        let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
2408        Ok(ExtractedArchiveMember {
2409            path: utf8_path(&member.path)?,
2410            kind: member.kind,
2411            data: member.data,
2412            link_target: member
2413                .link_target
2414                .map(|target| utf8_path(&target))
2415                .transpose()?,
2416            diagnostics: member.diagnostics,
2417        })
2418    }
2419
2420    fn extract_loaded_owned_tar_member(
2421        &self,
2422        shard: &IndexShard,
2423        file_index: usize,
2424    ) -> Result<OwnedTarMember, FormatError> {
2425        self.decode_loaded_owned_tar_member(shard, file_index, true)
2426    }
2427
2428    fn stream_loaded_file_to_writer<W: Write>(
2429        &self,
2430        shard: &IndexShard,
2431        file_index: usize,
2432        writer: &mut W,
2433    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
2434        let file = shard
2435            .files
2436            .get(file_index)
2437            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2438        self.validate_total_extraction_size(file.file_data_size)?;
2439        let expected_path = shard
2440            .file_path(file_index)
2441            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2442        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2443        stream_regular_tar_member_group_to_writer(
2444            &mut reader,
2445            expected_path,
2446            file.file_data_size,
2447            file.tar_member_group_size,
2448            self.crypto_header.max_path_length,
2449            writer,
2450        )
2451    }
2452
2453    fn stream_loaded_file_to_writer_with_progress<W: Write>(
2454        &self,
2455        shard: &IndexShard,
2456        file_index: usize,
2457        writer: &mut W,
2458        progress: &mut dyn ArchiveExtractProgressSink,
2459    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
2460        let file = shard
2461            .files
2462            .get(file_index)
2463            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2464        self.validate_total_extraction_size(file.file_data_size)?;
2465        let expected_path = shard
2466            .file_path(file_index)
2467            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2468        let archive_path = utf8_path(expected_path)?;
2469        let mut progress_writer =
2470            ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
2471        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2472        stream_regular_tar_member_group_to_writer(
2473            &mut reader,
2474            expected_path,
2475            file.file_data_size,
2476            file.tar_member_group_size,
2477            self.crypto_header.max_path_length,
2478            &mut progress_writer,
2479        )
2480    }
2481
2482    fn stream_loaded_file_to_path(
2483        &self,
2484        shard: &IndexShard,
2485        file_index: usize,
2486        root: &std::path::Path,
2487        options: SafeExtractionOptions,
2488    ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
2489        let file = shard
2490            .files
2491            .get(file_index)
2492            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2493        self.validate_total_extraction_size(file.file_data_size)?;
2494        let expected_path = shard
2495            .file_path(file_index)
2496            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2497        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2498        restore_streaming_tar_member_group(
2499            root,
2500            expected_path,
2501            file.file_data_size,
2502            file.tar_member_group_size,
2503            self.crypto_header.max_path_length,
2504            options,
2505            &mut reader,
2506        )
2507        .map_err(format_error_from_extract_error)
2508    }
2509
2510    fn extract_winning_index_entries_to(
2511        &self,
2512        shards: &[IndexShard],
2513        entries: Vec<(String, WinningIndexEntry)>,
2514        root: &std::path::Path,
2515        options: SafeExtractionOptions,
2516        jobs: usize,
2517    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2518        if entries.is_empty() {
2519            return Ok(Vec::new());
2520        }
2521        if jobs <= 1 || entries.len() <= 1 {
2522            return entries
2523                .into_iter()
2524                .map(|(path, entry)| {
2525                    let shard =
2526                        shards
2527                            .get(entry.shard_index)
2528                            .ok_or(FormatError::InvalidArchive(
2529                                "winning FileEntry shard is out of bounds",
2530                            ))?;
2531                    let diagnostics =
2532                        self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
2533                    Ok((path, diagnostics))
2534                })
2535                .collect();
2536        }
2537
2538        let worker_count = jobs.min(entries.len());
2539        let chunk_size = entries.len().div_ceil(worker_count);
2540        std::thread::scope(|scope| {
2541            let handles = entries
2542                .chunks(chunk_size)
2543                .map(|chunk| {
2544                    scope.spawn(move || {
2545                        let mut out = Vec::with_capacity(chunk.len());
2546                        for (path, entry) in chunk {
2547                            let shard = shards.get(entry.shard_index).ok_or(
2548                                FormatError::InvalidArchive(
2549                                    "winning FileEntry shard is out of bounds",
2550                                ),
2551                            )?;
2552                            let diagnostics = self.stream_loaded_file_to_path(
2553                                shard,
2554                                entry.file_index,
2555                                root,
2556                                options,
2557                            )?;
2558                            out.push((path.clone(), diagnostics));
2559                        }
2560                        Ok(out)
2561                    })
2562                })
2563                .collect::<Vec<_>>();
2564            let mut out = Vec::new();
2565            for handle in handles {
2566                let mut chunk = handle
2567                    .join()
2568                    .map_err(|_| FormatError::ReaderUnsupported("extract worker panicked"))??;
2569                out.append(&mut chunk);
2570            }
2571            Ok(out)
2572        })
2573    }
2574
2575    fn decode_loaded_owned_tar_member(
2576        &self,
2577        shard: &IndexShard,
2578        file_index: usize,
2579        enforce_extraction_cap: bool,
2580    ) -> Result<OwnedTarMember, FormatError> {
2581        let file = shard
2582            .files
2583            .get(file_index)
2584            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2585        if enforce_extraction_cap {
2586            self.validate_total_extraction_size(file.file_data_size)?;
2587        }
2588        let expected_path = shard
2589            .file_path(file_index)
2590            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2591        let frames = frame_range_for_file(shard, file)?;
2592        let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
2593        let mut decoded = Vec::new();
2594
2595        for frame in frames {
2596            let envelope = shard
2597                .envelopes
2598                .iter()
2599                .find(|entry| entry.envelope_index == frame.envelope_index)
2600                .ok_or(FormatError::InvalidArchive(
2601                    "FrameEntry references missing EnvelopeEntry",
2602                ))?;
2603            if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
2604                entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
2605            }
2606            let envelope_plaintext = envelope_cache
2607                .get(&envelope.envelope_index)
2608                .expect("inserted above");
2609            let compressed = slice(
2610                envelope_plaintext,
2611                frame.offset_in_envelope as usize,
2612                frame.compressed_size as usize,
2613                "FrameEntry",
2614            )?;
2615            decoded.extend_from_slice(
2616                &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
2617            );
2618        }
2619
2620        let offset = file.offset_in_first_frame_plaintext as usize;
2621        let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
2622        let group = slice(&decoded, offset, group_len, "FileEntry")?;
2623        let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
2624        if member.path != expected_path {
2625            return Err(FormatError::InvalidArchive(
2626                "tar member path does not match FileEntry path",
2627            ));
2628        }
2629        if member.logical_size != file.file_data_size {
2630            return Err(FormatError::InvalidArchive(
2631                "tar member size does not match FileEntry file_data_size",
2632            ));
2633        }
2634        Ok(member.to_owned_member())
2635    }
2636
2637    fn metadata_limits(&self) -> MetadataLimits {
2638        metadata_limits(&self.crypto_header)
2639    }
2640
2641    fn recompute_root_auth_material(
2642        &self,
2643        footer: &RootAuthFooterV1,
2644    ) -> Result<RootAuthMaterial, FormatError> {
2645        let footer_length = footer.footer_length()?;
2646        let root_auth_descriptor_digest = root_auth_descriptor_digest(
2647            footer.authenticator_id,
2648            footer.signer_identity_type,
2649            &footer.signer_identity_bytes,
2650            u32::try_from(footer.authenticator_value.len()).map_err(|_| {
2651                FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
2652            })?,
2653            footer_length,
2654        )?;
2655        let signer_identity_digest =
2656            signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
2657        let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
2658        let crypto_pre_hmac_len = self
2659            .crypto_header_bytes
2660            .len()
2661            .checked_sub(CRYPTO_HEADER_HMAC_LEN)
2662            .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
2663        let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
2664            archive_uuid: self.volume_header.archive_uuid,
2665            session_id: self.volume_header.session_id,
2666            stripe_width: self.crypto_header.stripe_width,
2667            total_volumes: self.manifest_footer.total_volumes,
2668            compression_algo: self.crypto_header.compression_algo,
2669            aead_algo: self.crypto_header.aead_algo,
2670            fec_algo: self.crypto_header.fec_algo,
2671            kdf_algo: self.crypto_header.kdf_algo,
2672            crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
2673            chunk_size: self.crypto_header.chunk_size,
2674            envelope_target_size: self.crypto_header.envelope_target_size,
2675            block_size: self.crypto_header.block_size,
2676            fec_data_shards: self.crypto_header.fec_data_shards,
2677            fec_parity_shards: self.crypto_header.fec_parity_shards,
2678            index_fec_data_shards: self.crypto_header.index_fec_data_shards,
2679            index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
2680            index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
2681            index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
2682            volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
2683            bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
2684            has_dictionary: self.crypto_header.has_dictionary,
2685            manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
2686            index_root_first_block: self.manifest_footer.index_root_first_block,
2687            index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
2688            index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
2689            index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
2690            index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
2691            root_auth_descriptor_digest,
2692        })?;
2693        let index_root_plaintext = self.index_root.to_bytes();
2694        let index_digest = index_digest(&index_root_plaintext);
2695        let shards = self.load_all_index_shards()?;
2696        let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
2697        let fec_layout_digest = fec_layout_digest(&fec_layout_rows)?;
2698        let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
2699        let total_data_block_count = u64::try_from(data_leaves.len())
2700            .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
2701        let data_block_merkle_root = data_block_merkle_root(&data_leaves);
2702        let archive_root = archive_root(ArchiveRootInputs {
2703            archive_uuid: self.volume_header.archive_uuid,
2704            session_id: self.volume_header.session_id,
2705            format_version: FORMAT_VERSION,
2706            volume_format_rev: VOLUME_FORMAT_REV,
2707            compression_algo: self.crypto_header.compression_algo,
2708            aead_algo: self.crypto_header.aead_algo,
2709            fec_algo: self.crypto_header.fec_algo,
2710            kdf_algo: self.crypto_header.kdf_algo,
2711            critical_metadata_digest,
2712            index_digest,
2713            fec_layout_digest,
2714            total_data_block_count,
2715            data_block_merkle_root,
2716            root_auth_descriptor_digest,
2717            signer_identity_digest,
2718        });
2719        Ok(RootAuthMaterial {
2720            critical_metadata_digest,
2721            index_digest,
2722            fec_layout_digest,
2723            data_block_merkle_root,
2724            signer_identity_digest,
2725            archive_root,
2726            total_data_block_count,
2727        })
2728    }
2729
2730    fn root_auth_fec_layout_rows(
2731        &self,
2732        shards: &[IndexShard],
2733    ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
2734        let mut rows = Vec::new();
2735        rows.push(FecLayoutObjectRow {
2736            object_class: 1,
2737            present: true,
2738            object_id: 0,
2739            first_block_index: self.manifest_footer.index_root_first_block,
2740            data_block_count: self.manifest_footer.index_root_data_block_count,
2741            parity_block_count: self.manifest_footer.index_root_parity_block_count,
2742            encrypted_size: self.manifest_footer.index_root_encrypted_size,
2743            plain_size: self.manifest_footer.index_root_decompressed_size,
2744        });
2745        if self.crypto_header.has_dictionary != 0 {
2746            rows.push(FecLayoutObjectRow {
2747                object_class: 2,
2748                present: true,
2749                object_id: 0,
2750                first_block_index: self.index_root.header.dictionary_first_block,
2751                data_block_count: self.index_root.header.dictionary_data_block_count,
2752                parity_block_count: self.index_root.header.dictionary_parity_block_count,
2753                encrypted_size: self.index_root.header.dictionary_encrypted_size,
2754                plain_size: self.index_root.header.dictionary_decompressed_size,
2755            });
2756        } else {
2757            rows.push(FecLayoutObjectRow {
2758                object_class: 2,
2759                present: false,
2760                object_id: 0,
2761                first_block_index: 0,
2762                data_block_count: 0,
2763                parity_block_count: 0,
2764                encrypted_size: 0,
2765                plain_size: 0,
2766            });
2767        }
2768        for entry in &self.index_root.shards {
2769            rows.push(FecLayoutObjectRow {
2770                object_class: 3,
2771                present: true,
2772                object_id: entry.shard_index,
2773                first_block_index: entry.first_block_index,
2774                data_block_count: entry.data_block_count,
2775                parity_block_count: entry.parity_block_count,
2776                encrypted_size: entry.encrypted_size,
2777                plain_size: entry.decompressed_size,
2778            });
2779        }
2780        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2781        for shard in shards {
2782            for envelope in &shard.envelopes {
2783                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2784                {
2785                    if existing != *envelope {
2786                        return Err(FormatError::InvalidArchive(
2787                            "duplicate EnvelopeEntry rows do not match",
2788                        ));
2789                    }
2790                }
2791            }
2792        }
2793        for envelope in envelopes.values() {
2794            rows.push(FecLayoutObjectRow {
2795                object_class: 4,
2796                present: true,
2797                object_id: envelope.envelope_index,
2798                first_block_index: envelope.first_block_index,
2799                data_block_count: envelope.data_block_count,
2800                parity_block_count: envelope.parity_block_count,
2801                encrypted_size: envelope.encrypted_size,
2802                plain_size: envelope.plaintext_size,
2803            });
2804        }
2805        for entry in &self.index_root.directory_hint_shards {
2806            rows.push(FecLayoutObjectRow {
2807                object_class: 5,
2808                present: true,
2809                object_id: entry.hint_shard_index,
2810                first_block_index: entry.first_block_index,
2811                data_block_count: entry.data_block_count,
2812                parity_block_count: entry.parity_block_count,
2813                encrypted_size: entry.encrypted_size,
2814                plain_size: entry.decompressed_size,
2815            });
2816        }
2817        Ok(rows)
2818    }
2819
2820    fn fec_object_class_shape(
2821        &self,
2822        object_class: u8,
2823    ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
2824        match object_class {
2825            1 => Ok((
2826                BlockKind::IndexRootData,
2827                BlockKind::IndexRootParity,
2828                self.crypto_header.index_root_fec_data_shards,
2829                self.crypto_header.index_root_fec_parity_shards,
2830            )),
2831            2 => Ok((
2832                BlockKind::DictionaryData,
2833                BlockKind::DictionaryParity,
2834                self.crypto_header.index_root_fec_data_shards,
2835                self.crypto_header.index_root_fec_parity_shards,
2836            )),
2837            3 => Ok((
2838                BlockKind::IndexShardData,
2839                BlockKind::IndexShardParity,
2840                self.crypto_header.index_fec_data_shards,
2841                self.crypto_header.index_fec_parity_shards,
2842            )),
2843            4 => Ok((
2844                BlockKind::PayloadData,
2845                BlockKind::PayloadParity,
2846                self.crypto_header.fec_data_shards,
2847                self.crypto_header.fec_parity_shards,
2848            )),
2849            5 => Ok((
2850                BlockKind::DirectoryHintData,
2851                BlockKind::DirectoryHintParity,
2852                self.crypto_header.index_fec_data_shards,
2853                self.crypto_header.index_fec_parity_shards,
2854            )),
2855            _ => Err(FormatError::InvalidArchive(
2856                "unknown root-auth FEC row class",
2857            )),
2858        }
2859    }
2860
2861    fn root_auth_data_block_leaves(
2862        &self,
2863        rows: &[FecLayoutObjectRow],
2864    ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
2865        let block_provider = self.block_provider();
2866        let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
2867        let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
2868            let row = **row;
2869            let (data_kind, parity_kind, data_max, parity_max) =
2870                self.fec_object_class_shape(row.object_class)?;
2871            let extent = ObjectExtent {
2872                first_block_index: row.first_block_index,
2873                data_block_count: row.data_block_count,
2874                parity_block_count: row.parity_block_count,
2875                encrypted_size: row.encrypted_size,
2876            };
2877            let repaired = load_repaired_object_data_shards_from_parts(
2878                &block_provider,
2879                &self.crypto_header,
2880                extent,
2881                data_kind,
2882                parity_kind,
2883                data_max,
2884                parity_max,
2885            )?;
2886            let mut leaves = Vec::new();
2887            for (offset, payload) in repaired.into_iter().enumerate() {
2888                leaves.push(DataBlockMerkleLeaf {
2889                    block_index: checked_u64_add(
2890                        row.first_block_index,
2891                        offset as u64,
2892                        "root-auth data block",
2893                    )?,
2894                    kind: data_kind,
2895                    flags: if offset + 1 == row.data_block_count as usize {
2896                        0x01
2897                    } else {
2898                        0
2899                    },
2900                    payload,
2901                });
2902            }
2903            Ok(leaves)
2904        })?;
2905        let mut leaves = Vec::new();
2906        for mut chunk in chunks {
2907            leaves.append(&mut chunk);
2908        }
2909        leaves.sort_by_key(|leaf| leaf.block_index);
2910        Ok(leaves)
2911    }
2912
2913    fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
2914        let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
2915        if logical_size > cap {
2916            return Err(FormatError::ReaderUnsupported(
2917                "total extraction size exceeds configured cap",
2918            ));
2919        }
2920        Ok(())
2921    }
2922
2923    fn decompress_payload_frame(
2924        &self,
2925        compressed: &[u8],
2926        decompressed_size: u32,
2927    ) -> Result<Vec<u8>, FormatError> {
2928        let mut decompressor = self.new_payload_decompressor()?;
2929        self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
2930    }
2931
2932    fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
2933        match &self.payload_dictionary {
2934            Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
2935            None => zstd::bulk::Decompressor::new(),
2936        }
2937        .map_err(|_| FormatError::ZstdDecompressionFailure)
2938    }
2939
2940    fn decompress_payload_frame_with(
2941        &self,
2942        decompressor: &mut zstd::bulk::Decompressor<'static>,
2943        compressed: &[u8],
2944        decompressed_size: u32,
2945    ) -> Result<Vec<u8>, FormatError> {
2946        validate_exact_zstd_frame(compressed)?;
2947        let expected = decompressed_size as usize;
2948        let decoded = decompressor
2949            .decompress(compressed, expected)
2950            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
2951        if decoded.len() != expected {
2952            return Err(FormatError::ZstdDecompressedSizeMismatch {
2953                expected,
2954                actual: decoded.len(),
2955            });
2956        }
2957        Ok(decoded)
2958    }
2959
2960    fn validate_encrypted_object_block_ranges(
2961        &self,
2962        envelopes: &BTreeMap<u64, EnvelopeEntry>,
2963    ) -> Result<(), FormatError> {
2964        let mut ranges = Vec::new();
2965        ranges.push(object_block_range(
2966            self.manifest_footer.index_root_first_block,
2967            self.manifest_footer.index_root_data_block_count,
2968            self.manifest_footer.index_root_parity_block_count,
2969            "IndexRoot",
2970        )?);
2971        for shard in &self.index_root.shards {
2972            ranges.push(object_block_range(
2973                shard.first_block_index,
2974                shard.data_block_count,
2975                shard.parity_block_count,
2976                "IndexShard",
2977            )?);
2978        }
2979        for hint in &self.index_root.directory_hint_shards {
2980            ranges.push(object_block_range(
2981                hint.first_block_index,
2982                hint.data_block_count,
2983                hint.parity_block_count,
2984                "DirectoryHintShardEntry",
2985            )?);
2986        }
2987        if self.crypto_header.has_dictionary != 0 {
2988            ranges.push(object_block_range(
2989                self.index_root.header.dictionary_first_block,
2990                self.index_root.header.dictionary_data_block_count,
2991                self.index_root.header.dictionary_parity_block_count,
2992                "dictionary",
2993            )?);
2994        }
2995        for envelope in envelopes.values() {
2996            ranges.push(object_block_range(
2997                envelope.first_block_index,
2998                envelope.data_block_count,
2999                envelope.parity_block_count,
3000                "EnvelopeEntry",
3001            )?);
3002        }
3003        validate_non_overlapping_object_ranges(&mut ranges)?;
3004        if let Some(source) = &self.lazy_blocks {
3005            if source.is_complete_volume_set() {
3006                validate_exact_coverage_ranges_u64(
3007                    &mut ranges,
3008                    source.total_block_count()?,
3009                    "encrypted object block ranges do not cover complete archive exactly",
3010                )?;
3011            }
3012        }
3013        Ok(())
3014    }
3015}
3016
3017impl<'a> DecodedTarMemberGroupReader<'a> {
3018    fn new(
3019        archive: &'a OpenedArchive,
3020        shard: &'a IndexShard,
3021        file: &'a FileEntry,
3022    ) -> Result<Self, FormatError> {
3023        Ok(Self {
3024            archive,
3025            shard,
3026            file,
3027            decompressor: archive.new_payload_decompressor()?,
3028            next_frame_offset: 0,
3029            cached_envelope_index: None,
3030            cached_envelope_plaintext: Vec::new(),
3031            current_frame: Vec::new(),
3032            current_frame_offset: 0,
3033            remaining_group_bytes: file.tar_member_group_size,
3034        })
3035    }
3036
3037    fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3038        while self.current_frame_offset >= self.current_frame.len() {
3039            if self.next_frame_offset >= self.file.frame_count as u64 {
3040                return Err(
3041                    FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3042                );
3043            }
3044            let frame_index = self
3045                .file
3046                .first_frame_index
3047                .checked_add(self.next_frame_offset)
3048                .ok_or(FormatError::InvalidArchive(
3049                    "FileEntry frame range overflow",
3050                ))?;
3051            let frame = frame_by_index(self.shard, frame_index)?;
3052            let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3053            if self.cached_envelope_index != Some(envelope.envelope_index) {
3054                self.cached_envelope_plaintext = self
3055                    .archive
3056                    .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3057                self.cached_envelope_index = Some(envelope.envelope_index);
3058            }
3059            let compressed = slice(
3060                &self.cached_envelope_plaintext,
3061                frame.offset_in_envelope as usize,
3062                frame.compressed_size as usize,
3063                "FrameEntry",
3064            )?;
3065            let decoded = self.archive.decompress_payload_frame_with(
3066                &mut self.decompressor,
3067                compressed,
3068                frame.decompressed_size,
3069            )?;
3070            let offset = if self.next_frame_offset == 0 {
3071                self.file.offset_in_first_frame_plaintext as usize
3072            } else {
3073                0
3074            };
3075            if offset > decoded.len() {
3076                return Err(FormatError::InvalidArchive(
3077                    "offset in first frame is outside the first referenced frame",
3078                )
3079                .into());
3080            }
3081            self.next_frame_offset += 1;
3082            self.current_frame = decoded;
3083            self.current_frame_offset = offset;
3084        }
3085        Ok(())
3086    }
3087}
3088
3089impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3090    fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3091        if buf.is_empty() {
3092            return Ok(0);
3093        }
3094        if self.remaining_group_bytes == 0 {
3095            return Ok(0);
3096        }
3097        self.ensure_frame_available()?;
3098        let available = self.current_frame.len() - self.current_frame_offset;
3099        let len = available
3100            .min(buf.len())
3101            .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3102        if len == 0 {
3103            return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3104        }
3105        buf[..len].copy_from_slice(
3106            &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3107        );
3108        self.current_frame_offset += len;
3109        self.remaining_group_bytes -= len as u64;
3110        Ok(len)
3111    }
3112}
3113
3114fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3115    shard
3116        .frames
3117        .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3118        .map(|idx| &shard.frames[idx])
3119        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3120}
3121
3122fn envelope_by_index(
3123    shard: &IndexShard,
3124    envelope_index: u64,
3125) -> Result<&EnvelopeEntry, FormatError> {
3126    shard
3127        .envelopes
3128        .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3129        .map(|idx| &shard.envelopes[idx])
3130        .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3131}
3132
3133fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3134    match err {
3135        ExtractError::Format(err) => err,
3136        ExtractError::Output(_) => {
3137            FormatError::FilesystemExtractionFailed("failed to write regular file")
3138        }
3139    }
3140}
3141
3142fn final_index_entry_winners(
3143    shards: &[IndexShard],
3144) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3145    let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3146    for (shard_index, shard) in shards.iter().enumerate() {
3147        for (idx, file) in shard.files.iter().enumerate() {
3148            let path = utf8_path(
3149                shard
3150                    .file_path(idx)
3151                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3152            )?;
3153            let start = shard
3154                .tar_member_group_start(idx)
3155                .ok_or(FormatError::InvalidArchive(
3156                    "FileEntry tar member start is missing",
3157                ))?;
3158            if let Some(winner) = final_entries.get_mut(&path) {
3159                if start >= winner.start {
3160                    winner.start = start;
3161                    winner.file_data_size = file.file_data_size;
3162                    winner.shard_index = shard_index;
3163                    winner.file_index = idx;
3164                }
3165            } else {
3166                final_entries.insert(
3167                    path,
3168                    WinningIndexEntry {
3169                        start,
3170                        file_data_size: file.file_data_size,
3171                        shard_index,
3172                        file_index: idx,
3173                    },
3174                );
3175            }
3176        }
3177    }
3178    Ok(final_entries)
3179}
3180
3181fn archive_index_entry_from_loaded_file(
3182    shard: &IndexShard,
3183    file_index: usize,
3184) -> Result<ArchiveIndexEntry, FormatError> {
3185    let file = shard
3186        .files
3187        .get(file_index)
3188        .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3189    let path = utf8_path(
3190        shard
3191            .file_path(file_index)
3192            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3193    )?;
3194    Ok(ArchiveIndexEntry {
3195        path,
3196        file_data_size: file.file_data_size,
3197    })
3198}
3199
3200#[derive(Debug)]
3201struct ParsedSeekableVolume {
3202    volume_header: VolumeHeader,
3203    crypto_header: CryptoHeaderFixed,
3204    crypto_header_bytes: Vec<u8>,
3205    subkeys: Subkeys,
3206    manifest_footer: Option<ManifestFooter>,
3207    manifest_footer_error: Option<FormatError>,
3208    root_auth_footer: Option<RootAuthFooterV1>,
3209    root_auth_footer_bytes: Option<Vec<u8>>,
3210    volume_trailer: VolumeTrailer,
3211    blocks: BTreeMap<u64, BlockRecord>,
3212    erased_block_indices: BTreeSet<u64>,
3213}
3214
3215struct ParsedSeekableReadAtVolume {
3216    reader: Arc<dyn ArchiveReadAt>,
3217    volume_header: VolumeHeader,
3218    crypto_header: CryptoHeaderFixed,
3219    crypto_header_bytes: Vec<u8>,
3220    subkeys: Subkeys,
3221    manifest_footer: Option<ManifestFooter>,
3222    manifest_footer_error: Option<FormatError>,
3223    root_auth_footer: Option<RootAuthFooterV1>,
3224    root_auth_footer_bytes: Option<Vec<u8>>,
3225    volume_trailer: VolumeTrailer,
3226    crypto_end: u64,
3227}
3228
3229struct ParsedOpenPrefix {
3230    volume_header: VolumeHeader,
3231    crypto_header: CryptoHeaderFixed,
3232    crypto_header_bytes: Vec<u8>,
3233    crypto_end: usize,
3234    subkeys: Subkeys,
3235}
3236
3237struct ParsedReadAtOpenPrefix {
3238    volume_header: VolumeHeader,
3239    crypto_header: CryptoHeaderFixed,
3240    crypto_header_bytes: Vec<u8>,
3241    crypto_end: u64,
3242    subkeys: Subkeys,
3243}
3244
3245fn parse_seekable_volume(
3246    bytes: &[u8],
3247    master_key: &MasterKey,
3248    options: ReaderOptions,
3249) -> Result<ParsedSeekableVolume, FormatError> {
3250    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3251        return Err(FormatError::InvalidLength {
3252            structure: "archive",
3253            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3254            actual: bytes.len(),
3255        });
3256    }
3257
3258    let prefix = match parse_open_prefix(bytes, master_key) {
3259        Ok(prefix) => prefix,
3260        Err(prefix_err) => {
3261            return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
3262                .or(Err(prefix_err));
3263        }
3264    };
3265    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3266    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
3267        Ok(parsed) => Ok(parsed),
3268        Err(prefix_err) => {
3269            match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
3270                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3271                    Ok(recovered)
3272                }
3273                Ok(_) | Err(_) => Err(prefix_err),
3274            }
3275        }
3276    }
3277}
3278
3279fn parse_seekable_volume_with_prefix(
3280    bytes: &[u8],
3281    prefix: ParsedOpenPrefix,
3282    options: ReaderOptions,
3283) -> Result<ParsedSeekableVolume, FormatError> {
3284    let ParsedOpenPrefix {
3285        volume_header,
3286        crypto_header,
3287        crypto_header_bytes,
3288        crypto_end,
3289        subkeys,
3290    } = prefix;
3291    let crypto_bytes = crypto_header_bytes.as_slice();
3292
3293    let terminal = locate_v41_terminal(
3294        bytes,
3295        KeyHoldingTerminalContext {
3296            subkeys: &subkeys,
3297            volume_header: &volume_header,
3298            crypto_header: &crypto_header,
3299            crypto_header_bytes: crypto_bytes,
3300        },
3301        options,
3302    )?;
3303    finish_parse_seekable_volume(
3304        bytes,
3305        volume_header,
3306        crypto_header,
3307        crypto_header_bytes,
3308        crypto_end,
3309        subkeys,
3310        terminal,
3311    )
3312}
3313
3314fn parse_seekable_volume_from_recovered_terminal(
3315    bytes: &[u8],
3316    master_key: &MasterKey,
3317    options: ReaderOptions,
3318) -> Result<ParsedSeekableVolume, FormatError> {
3319    let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
3320    let crypto_end = checked_add(
3321        authority.volume_header.crypto_header_offset as usize,
3322        authority.volume_header.crypto_header_length as usize,
3323        "CryptoHeader",
3324    )?;
3325    finish_parse_seekable_volume(
3326        bytes,
3327        authority.volume_header,
3328        authority.crypto_header,
3329        authority.crypto_header_bytes,
3330        crypto_end,
3331        authority.subkeys,
3332        authority.terminal,
3333    )
3334}
3335
3336fn parse_open_prefix(
3337    bytes: &[u8],
3338    master_key: &MasterKey,
3339) -> Result<ParsedOpenPrefix, FormatError> {
3340    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3341    let crypto_start = volume_header.crypto_header_offset as usize;
3342    let crypto_len = volume_header.crypto_header_length as usize;
3343    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3344    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3345    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3346    let subkeys = subkeys_for_open(
3347        Some(master_key),
3348        parsed_crypto.fixed.aead_algo,
3349        &volume_header.archive_uuid,
3350        &volume_header.session_id,
3351    )?;
3352    verify_integrity_tag(
3353        HmacDomain::CryptoHeader,
3354        parsed_crypto.fixed.aead_algo,
3355        Some(&subkeys.mac_key),
3356        &volume_header.archive_uuid,
3357        &volume_header.session_id,
3358        parsed_crypto.hmac_covered_bytes,
3359        &parsed_crypto.header_hmac,
3360    )?;
3361    parsed_crypto.validate_extension_semantics()?;
3362    validate_seekable_supported_volume(
3363        &volume_header,
3364        &parsed_crypto.fixed,
3365        &parsed_crypto.extensions,
3366    )?;
3367    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3368    let crypto_header = parsed_crypto.fixed.clone();
3369    Ok(ParsedOpenPrefix {
3370        volume_header,
3371        crypto_header,
3372        crypto_header_bytes: crypto_bytes.to_vec(),
3373        crypto_end,
3374        subkeys,
3375    })
3376}
3377
3378fn finish_parse_seekable_volume(
3379    bytes: &[u8],
3380    volume_header: VolumeHeader,
3381    crypto_header: CryptoHeaderFixed,
3382    crypto_header_bytes: Vec<u8>,
3383    crypto_end: usize,
3384    subkeys: Subkeys,
3385    terminal: V41Terminal,
3386) -> Result<ParsedSeekableVolume, FormatError> {
3387    let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
3388    let volume_trailer = terminal.volume_trailer.clone();
3389    validate_trailer_identity(&volume_header, &volume_trailer)?;
3390
3391    let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
3392    let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
3393    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3394        if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
3395            || volume_trailer
3396                .root_auth_footer_offset
3397                .checked_add(volume_trailer.root_auth_footer_length as u64)
3398                .ok_or(FormatError::InvalidArchive(
3399                    "RootAuthFooter terminal boundary overflow",
3400                ))?
3401                != trailer_offset as u64
3402        {
3403            return Err(FormatError::InvalidArchive(
3404                "RootAuthFooter does not sit before selected trailer",
3405            ));
3406        }
3407    } else if manifest_end != trailer_offset {
3408        return Err(FormatError::InvalidArchive(
3409            "ManifestFooter does not end at selected trailer",
3410        ));
3411    }
3412    let manifest_bytes = &terminal.manifest_footer_bytes;
3413    let (manifest_footer, manifest_footer_error) =
3414        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3415        {
3416            Ok(footer) => (Some(footer), None),
3417            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3418            Err(err) => return Err(err),
3419        };
3420
3421    let block_region = parse_block_region(
3422        bytes,
3423        crypto_end,
3424        manifest_offset,
3425        crypto_header.block_size as usize,
3426        &volume_header,
3427        &volume_trailer,
3428    )?;
3429
3430    Ok(ParsedSeekableVolume {
3431        volume_header,
3432        crypto_header,
3433        crypto_header_bytes,
3434        subkeys,
3435        manifest_footer,
3436        manifest_footer_error,
3437        root_auth_footer: terminal.root_auth_footer,
3438        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3439        volume_trailer,
3440        blocks: block_region.blocks,
3441        erased_block_indices: block_region.erased_block_indices,
3442    })
3443}
3444
3445fn parse_seekable_read_at_volume(
3446    reader: Arc<dyn ArchiveReadAt>,
3447    master_key: &MasterKey,
3448    options: ReaderOptions,
3449) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3450    let observed_len = reader.len()?;
3451    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
3452        return Err(FormatError::InvalidLength {
3453            structure: "archive",
3454            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3455            actual: to_usize(observed_len, "archive")?,
3456        });
3457    }
3458
3459    let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
3460        Ok(prefix) => prefix,
3461        Err(prefix_err) => {
3462            return parse_seekable_read_at_volume_from_recovered_terminal(
3463                reader,
3464                observed_len,
3465                master_key,
3466                options,
3467            )
3468            .or(Err(prefix_err));
3469        }
3470    };
3471    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3472    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
3473        Ok(parsed) => Ok(parsed),
3474        Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
3475            reader,
3476            observed_len,
3477            master_key,
3478            options,
3479        ) {
3480            Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3481                Ok(recovered)
3482            }
3483            Ok(_) | Err(_) => Err(prefix_err),
3484        },
3485    }
3486}
3487
3488fn parse_seekable_read_at_volume_with_prefix(
3489    reader: Arc<dyn ArchiveReadAt>,
3490    observed_len: u64,
3491    prefix: ParsedReadAtOpenPrefix,
3492    options: ReaderOptions,
3493) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3494    let ParsedReadAtOpenPrefix {
3495        volume_header,
3496        crypto_header,
3497        crypto_header_bytes,
3498        crypto_end,
3499        subkeys,
3500    } = prefix;
3501
3502    let terminal = locate_v41_terminal_read_at(
3503        reader.as_ref(),
3504        observed_len,
3505        KeyHoldingTerminalContext {
3506            subkeys: &subkeys,
3507            volume_header: &volume_header,
3508            crypto_header: &crypto_header,
3509            crypto_header_bytes: &crypto_header_bytes,
3510        },
3511        options,
3512    )?;
3513    finish_parse_seekable_read_at_volume(
3514        reader,
3515        volume_header,
3516        crypto_header,
3517        crypto_header_bytes,
3518        crypto_end,
3519        subkeys,
3520        terminal,
3521    )
3522}
3523
3524fn parse_seekable_read_at_volume_from_recovered_terminal(
3525    reader: Arc<dyn ArchiveReadAt>,
3526    observed_len: u64,
3527    master_key: &MasterKey,
3528    options: ReaderOptions,
3529) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3530    let authority =
3531        locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
3532    let crypto_end = checked_u64_add(
3533        authority.volume_header.crypto_header_offset as u64,
3534        authority.volume_header.crypto_header_length as u64,
3535        "CryptoHeader",
3536    )?;
3537    finish_parse_seekable_read_at_volume(
3538        reader,
3539        authority.volume_header,
3540        authority.crypto_header,
3541        authority.crypto_header_bytes,
3542        crypto_end,
3543        authority.subkeys,
3544        authority.terminal,
3545    )
3546}
3547
3548fn parse_read_at_open_prefix(
3549    reader: &dyn ArchiveReadAt,
3550    master_key: &MasterKey,
3551) -> Result<ParsedReadAtOpenPrefix, FormatError> {
3552    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
3553    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
3554    let crypto_start = volume_header.crypto_header_offset as u64;
3555    let crypto_len = volume_header.crypto_header_length as u64;
3556    let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
3557    let crypto_bytes = read_at_vec(
3558        reader,
3559        crypto_start,
3560        to_usize(crypto_len, "CryptoHeader")?,
3561        "CryptoHeader",
3562    )?;
3563    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
3564    let subkeys = subkeys_for_open(
3565        Some(master_key),
3566        parsed_crypto.fixed.aead_algo,
3567        &volume_header.archive_uuid,
3568        &volume_header.session_id,
3569    )?;
3570    verify_integrity_tag(
3571        HmacDomain::CryptoHeader,
3572        parsed_crypto.fixed.aead_algo,
3573        Some(&subkeys.mac_key),
3574        &volume_header.archive_uuid,
3575        &volume_header.session_id,
3576        parsed_crypto.hmac_covered_bytes,
3577        &parsed_crypto.header_hmac,
3578    )?;
3579    parsed_crypto.validate_extension_semantics()?;
3580    validate_seekable_supported_volume(
3581        &volume_header,
3582        &parsed_crypto.fixed,
3583        &parsed_crypto.extensions,
3584    )?;
3585    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3586    let crypto_header = parsed_crypto.fixed.clone();
3587    drop(parsed_crypto);
3588    Ok(ParsedReadAtOpenPrefix {
3589        volume_header,
3590        crypto_header,
3591        crypto_header_bytes: crypto_bytes,
3592        crypto_end,
3593        subkeys,
3594    })
3595}
3596
3597fn finish_parse_seekable_read_at_volume(
3598    reader: Arc<dyn ArchiveReadAt>,
3599    volume_header: VolumeHeader,
3600    crypto_header: CryptoHeaderFixed,
3601    crypto_header_bytes: Vec<u8>,
3602    crypto_end: u64,
3603    subkeys: Subkeys,
3604    terminal: V41Terminal,
3605) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3606    let volume_trailer = terminal.volume_trailer.clone();
3607    validate_trailer_identity(&volume_header, &volume_trailer)?;
3608
3609    let manifest_offset = volume_trailer.manifest_footer_offset;
3610    let manifest_end = checked_u64_add(
3611        manifest_offset,
3612        MANIFEST_FOOTER_LEN as u64,
3613        "ManifestFooter",
3614    )?;
3615    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3616        if volume_trailer.root_auth_footer_offset != manifest_end
3617            || volume_trailer
3618                .root_auth_footer_offset
3619                .checked_add(volume_trailer.root_auth_footer_length as u64)
3620                .ok_or(FormatError::InvalidArchive(
3621                    "RootAuthFooter terminal boundary overflow",
3622                ))?
3623                != terminal.image.volume_trailer_offset
3624        {
3625            return Err(FormatError::InvalidArchive(
3626                "RootAuthFooter does not sit before selected trailer",
3627            ));
3628        }
3629    } else if manifest_end != terminal.image.volume_trailer_offset {
3630        return Err(FormatError::InvalidArchive(
3631            "ManifestFooter does not end at selected trailer",
3632        ));
3633    }
3634    validate_seekable_block_region_layout(
3635        crypto_end,
3636        manifest_offset,
3637        crypto_header.block_size as usize,
3638        &volume_trailer,
3639    )?;
3640
3641    let manifest_bytes = &terminal.manifest_footer_bytes;
3642    let (manifest_footer, manifest_footer_error) =
3643        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3644        {
3645            Ok(footer) => (Some(footer), None),
3646            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3647            Err(err) => return Err(err),
3648        };
3649
3650    Ok(ParsedSeekableReadAtVolume {
3651        reader,
3652        volume_header,
3653        crypto_header,
3654        crypto_header_bytes,
3655        subkeys,
3656        manifest_footer,
3657        manifest_footer_error,
3658        root_auth_footer: terminal.root_auth_footer,
3659        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3660        volume_trailer,
3661        crypto_end,
3662    })
3663}
3664
3665#[derive(Debug)]
3666struct ParsedPublicNoKeyVolume {
3667    volume_header: VolumeHeader,
3668    crypto_header: CryptoHeaderFixed,
3669    root_auth_footer: RootAuthFooterV1,
3670    root_auth_footer_bytes: Vec<u8>,
3671    blocks: BTreeMap<u64, BlockRecord>,
3672}
3673
3674pub fn public_no_key_verify_volumes_with_options<F>(
3675    volumes: &[&[u8]],
3676    mut verifier: F,
3677    options: ReaderOptions,
3678) -> Result<PublicNoKeyVerification, FormatError>
3679where
3680    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3681{
3682    validate_reader_options(options)?;
3683    if volumes.is_empty() {
3684        return Err(FormatError::InvalidArchive("no volumes supplied"));
3685    }
3686    let mut parsed = Vec::with_capacity(volumes.len());
3687    for volume in volumes {
3688        parsed.push(parse_public_no_key_volume(volume, options)?);
3689    }
3690    let first = parsed
3691        .first()
3692        .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
3693    if parsed.len() != first.crypto_header.stripe_width as usize {
3694        return Err(FormatError::ReaderUnsupported(
3695            "public no-key verification requires a complete volume set",
3696        ));
3697    }
3698
3699    let mut seen_volume_indexes = BTreeSet::new();
3700    let mut blocks = BTreeMap::new();
3701    for volume in &parsed {
3702        if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
3703            || volume.volume_header.session_id != first.volume_header.session_id
3704            || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
3705        {
3706            return Err(FormatError::InvalidArchive(
3707                "public no-key volume global metadata differs",
3708            ));
3709        }
3710        if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
3711            return Err(FormatError::InvalidArchive(
3712                "public no-key RootAuthFooter copies differ",
3713            ));
3714        }
3715        if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
3716            return Err(FormatError::InvalidArchive(
3717                "duplicate public no-key volume index",
3718            ));
3719        }
3720        for (block_index, record) in &volume.blocks {
3721            if blocks.insert(*block_index, record.clone()).is_some() {
3722                return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
3723            }
3724        }
3725    }
3726    validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
3727
3728    let footer = &first.root_auth_footer;
3729    let mut data_leaves = blocks
3730        .values()
3731        .filter(|record| record.kind.is_data())
3732        .map(|record| DataBlockMerkleLeaf {
3733            block_index: record.block_index,
3734            kind: record.kind,
3735            flags: record.flags,
3736            payload: record.payload.clone(),
3737        })
3738        .collect::<Vec<_>>();
3739    data_leaves.sort_by_key(|leaf| leaf.block_index);
3740    let total_data_block_count = u64::try_from(data_leaves.len())
3741        .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
3742    let observed_data_root = data_block_merkle_root(&data_leaves);
3743    if total_data_block_count != footer.total_data_block_count
3744        || observed_data_root != footer.data_block_merkle_root
3745    {
3746        return Err(FormatError::InvalidArchive(
3747            "public no-key data-block commitment mismatch",
3748        ));
3749    }
3750    let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
3751    if archive_root != footer.archive_root {
3752        return Err(FormatError::InvalidArchive(
3753            "public no-key archive_root mismatch",
3754        ));
3755    }
3756    if !verifier(footer, &archive_root)? {
3757        return Err(FormatError::InvalidArchive(
3758            "public no-key authenticator verification failed",
3759        ));
3760    }
3761    Ok(PublicNoKeyVerification {
3762        archive_root,
3763        authenticator_id: footer.authenticator_id,
3764        signer_identity_type: footer.signer_identity_type,
3765        signer_identity_bytes: footer.signer_identity_bytes.clone(),
3766        total_data_block_count,
3767        diagnostics: vec![
3768            PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
3769            PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
3770            PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
3771        ],
3772    })
3773}
3774
3775fn parse_public_no_key_volume(
3776    bytes: &[u8],
3777    options: ReaderOptions,
3778) -> Result<ParsedPublicNoKeyVolume, FormatError> {
3779    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3780        return Err(FormatError::InvalidLength {
3781            structure: "archive",
3782            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3783            actual: bytes.len(),
3784        });
3785    }
3786    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3787    let crypto_start = volume_header.crypto_header_offset as usize;
3788    let crypto_len = volume_header.crypto_header_length as usize;
3789    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3790    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3791    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3792    parsed_crypto.validate_extension_semantics()?;
3793    validate_seekable_supported_volume(
3794        &volume_header,
3795        &parsed_crypto.fixed,
3796        &parsed_crypto.extensions,
3797    )?;
3798    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3799
3800    let terminal =
3801        locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
3802    let block_region = parse_public_block_observation(
3803        bytes,
3804        crypto_end,
3805        &terminal.image,
3806        parsed_crypto.fixed.block_size as usize,
3807        &volume_header,
3808    )?;
3809    Ok(ParsedPublicNoKeyVolume {
3810        volume_header,
3811        crypto_header: parsed_crypto.fixed,
3812        root_auth_footer: terminal.root_auth_footer,
3813        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3814        blocks: block_region,
3815    })
3816}
3817
3818fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3819    left.length == right.length
3820        && left.stripe_width == right.stripe_width
3821        && left.block_size == right.block_size
3822        && left.compression_algo == right.compression_algo
3823        && left.aead_algo == right.aead_algo
3824        && left.fec_algo == right.fec_algo
3825        && left.kdf_algo == right.kdf_algo
3826}
3827
3828fn recompute_public_archive_root(
3829    footer: &RootAuthFooterV1,
3830    crypto_header: &CryptoHeaderFixed,
3831) -> Result<[u8; 32], FormatError> {
3832    let descriptor_digest = root_auth_descriptor_digest(
3833        footer.authenticator_id,
3834        footer.signer_identity_type,
3835        &footer.signer_identity_bytes,
3836        u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3837            FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3838        })?,
3839        footer.footer_length()?,
3840    )?;
3841    let signer_digest =
3842        signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3843    if signer_digest != footer.signer_identity_digest {
3844        return Err(FormatError::InvalidArchive(
3845            "public no-key signer identity digest mismatch",
3846        ));
3847    }
3848    Ok(archive_root(ArchiveRootInputs {
3849        archive_uuid: footer.archive_uuid,
3850        session_id: footer.session_id,
3851        format_version: FORMAT_VERSION,
3852        volume_format_rev: VOLUME_FORMAT_REV,
3853        compression_algo: crypto_header.compression_algo,
3854        aead_algo: crypto_header.aead_algo,
3855        fec_algo: crypto_header.fec_algo,
3856        kdf_algo: crypto_header.kdf_algo,
3857        critical_metadata_digest: footer.critical_metadata_digest,
3858        index_digest: footer.index_digest,
3859        fec_layout_digest: footer.fec_layout_digest,
3860        total_data_block_count: footer.total_data_block_count,
3861        data_block_merkle_root: footer.data_block_merkle_root,
3862        root_auth_descriptor_digest: descriptor_digest,
3863        signer_identity_digest: signer_digest,
3864    }))
3865}
3866
3867fn parse_valid_manifest_footer(
3868    volume_header: &VolumeHeader,
3869    crypto_header: &CryptoHeaderFixed,
3870    subkeys: &Subkeys,
3871    manifest_bytes: &[u8],
3872) -> Result<ManifestFooter, FormatError> {
3873    let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3874    validate_manifest_footer(
3875        volume_header,
3876        crypto_header,
3877        &manifest_footer,
3878        subkeys,
3879        manifest_bytes,
3880    )?;
3881    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
3882    Ok(manifest_footer)
3883}
3884
3885fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3886    matches!(
3887        error,
3888        FormatError::BadMagic {
3889            structure: "ManifestFooter",
3890        } | FormatError::NonZeroReserved {
3891            structure: "ManifestFooter",
3892        } | FormatError::InvalidAuthoritativeFlag(_)
3893            | FormatError::HmacMismatch {
3894                structure: "ManifestFooter",
3895            }
3896            | FormatError::IntegrityDigestMismatch {
3897                structure: "ManifestFooter",
3898            }
3899    )
3900}
3901
3902fn validate_seekable_supported_volume(
3903    volume_header: &VolumeHeader,
3904    crypto_header: &CryptoHeaderFixed,
3905    extensions: &[ExtensionTlv<'_>],
3906) -> Result<(), FormatError> {
3907    reject_unsupported_raw_stream_profile(extensions)?;
3908    if crypto_header.stripe_width != volume_header.stripe_width {
3909        return Err(FormatError::InvalidArchive(
3910            "VolumeHeader and CryptoHeader stripe_width differ",
3911        ));
3912    }
3913    Ok(())
3914}
3915
3916pub(crate) fn validate_crypto_class_parity_exactness(
3917    crypto_header: &CryptoHeaderFixed,
3918) -> Result<(), FormatError> {
3919    let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3920    if crypto_header.fec_parity_shards as u32 != fec {
3921        return Err(FormatError::InvalidArchive(
3922            "fec_parity_shards does not match v41 compute_parity",
3923        ));
3924    }
3925    let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3926    if crypto_header.index_fec_parity_shards as u32 != index {
3927        return Err(FormatError::InvalidArchive(
3928            "index_fec_parity_shards does not match v41 compute_parity",
3929        ));
3930    }
3931    let index_root = required_object_parity(
3932        crypto_header.index_root_fec_data_shards as u64,
3933        crypto_header,
3934    )?;
3935    if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3936        return Err(FormatError::InvalidArchive(
3937            "index_root_fec_parity_shards does not match v41 compute_parity",
3938        ));
3939    }
3940    Ok(())
3941}
3942
3943fn validate_volume_set_member(
3944    first: &ParsedSeekableVolume,
3945    candidate: &ParsedSeekableVolume,
3946) -> Result<(), FormatError> {
3947    validate_volume_set_member_metadata(
3948        &first.volume_header,
3949        &first.crypto_header,
3950        &first.crypto_header_bytes,
3951        &candidate.volume_header,
3952        &candidate.crypto_header,
3953        &candidate.crypto_header_bytes,
3954    )
3955}
3956
3957fn validate_volume_set_member_metadata(
3958    first_volume_header: &VolumeHeader,
3959    first_crypto_header: &CryptoHeaderFixed,
3960    first_crypto_header_bytes: &[u8],
3961    candidate_volume_header: &VolumeHeader,
3962    candidate_crypto_header: &CryptoHeaderFixed,
3963    candidate_crypto_header_bytes: &[u8],
3964) -> Result<(), FormatError> {
3965    if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3966        || candidate_volume_header.session_id != first_volume_header.session_id
3967    {
3968        return Err(FormatError::InvalidArchive(
3969            "mixed archive or session IDs in volume set",
3970        ));
3971    }
3972    if candidate_crypto_header_bytes != first_crypto_header_bytes
3973        || candidate_crypto_header != first_crypto_header
3974    {
3975        return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3976    }
3977    Ok(())
3978}
3979
3980pub(crate) fn manifest_bootstrap_fields_match(
3981    left: &ManifestFooter,
3982    right: &ManifestFooter,
3983) -> bool {
3984    left.archive_uuid == right.archive_uuid
3985        && left.session_id == right.session_id
3986        && left.is_authoritative == right.is_authoritative
3987        && left.total_volumes == right.total_volumes
3988        && left.index_root_first_block == right.index_root_first_block
3989        && left.index_root_data_block_count == right.index_root_data_block_count
3990        && left.index_root_parity_block_count == right.index_root_parity_block_count
3991        && left.index_root_encrypted_size == right.index_root_encrypted_size
3992        && left.index_root_decompressed_size == right.index_root_decompressed_size
3993}
3994
3995fn validate_complete_global_block_coverage(
3996    blocks: &BTreeMap<u64, BlockRecord>,
3997    erased_block_indices: &BTreeSet<u64>,
3998) -> Result<(), FormatError> {
3999    let mut expected = 0u64;
4000    let mut block_iter = blocks.keys().copied().peekable();
4001    let mut erasure_iter = erased_block_indices.iter().copied().peekable();
4002
4003    loop {
4004        let next_block = block_iter.peek().copied();
4005        let next_erasure = erasure_iter.peek().copied();
4006        let next = match (next_block, next_erasure) {
4007            (Some(block), Some(erasure)) if block == erasure => {
4008                return Err(FormatError::InvalidArchive(
4009                    "BlockRecord index is both present and erased",
4010                ));
4011            }
4012            (Some(block), Some(erasure)) => block.min(erasure),
4013            (Some(block), None) => block,
4014            (None, Some(erasure)) => erasure,
4015            (None, None) => return Ok(()),
4016        };
4017
4018        if next != expected {
4019            return Err(FormatError::InvalidArchive(
4020                "complete volume set has missing global blocks",
4021            ));
4022        }
4023        if next_block == Some(next) {
4024            block_iter.next();
4025        }
4026        if next_erasure == Some(next) {
4027            erasure_iter.next();
4028        }
4029        expected = expected
4030            .checked_add(1)
4031            .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
4032    }
4033}
4034
4035#[derive(Debug)]
4036struct V41Terminal {
4037    image: CriticalMetadataImage,
4038    manifest_footer_bytes: Vec<u8>,
4039    root_auth_footer_bytes: Option<Vec<u8>>,
4040    root_auth_footer: Option<RootAuthFooterV1>,
4041    volume_trailer: VolumeTrailer,
4042}
4043
4044pub(crate) struct SequentialTerminalMaterial {
4045    pub(crate) manifest_footer: ManifestFooter,
4046    pub(crate) volume_trailer: VolumeTrailer,
4047    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
4048}
4049
4050#[derive(Debug)]
4051struct V41PublicTerminal {
4052    image: CriticalMetadataImage,
4053    root_auth_footer_bytes: Vec<u8>,
4054    root_auth_footer: RootAuthFooterV1,
4055}
4056
4057#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4058struct CmraDecoderTuple {
4059    shard_size: u32,
4060    data_shard_count: u16,
4061    parity_shard_count: u16,
4062    image_length: u32,
4063    image_sha256: [u8; 32],
4064}
4065
4066#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4067struct CmraIdentityHints {
4068    archive_uuid: [u8; 16],
4069    session_id: [u8; 16],
4070    volume_index: u32,
4071}
4072
4073impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
4074    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
4075        Self {
4076            shard_size: header.shard_size,
4077            data_shard_count: header.data_shard_count,
4078            parity_shard_count: header.parity_shard_count,
4079            image_length: header.image_length,
4080            image_sha256: header.image_sha256,
4081        }
4082    }
4083}
4084
4085impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
4086    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
4087        Self {
4088            archive_uuid: header.archive_uuid_hint,
4089            session_id: header.session_id_hint,
4090            volume_index: header.volume_index_hint,
4091        }
4092    }
4093}
4094
4095impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
4096    fn from(locator: CriticalRecoveryLocator) -> Self {
4097        Self {
4098            shard_size: locator.cmra_shard_size,
4099            data_shard_count: locator.cmra_data_shard_count,
4100            parity_shard_count: locator.cmra_parity_shard_count,
4101            image_length: locator.cmra_image_length,
4102            image_sha256: locator.cmra_image_sha256,
4103        }
4104    }
4105}
4106
4107impl From<CriticalRecoveryLocator> for CmraIdentityHints {
4108    fn from(locator: CriticalRecoveryLocator) -> Self {
4109        Self {
4110            archive_uuid: locator.archive_uuid_hint,
4111            session_id: locator.session_id_hint,
4112            volume_index: locator.volume_index_hint,
4113        }
4114    }
4115}
4116
4117#[derive(Debug)]
4118struct RecoveredCmra {
4119    image: CriticalMetadataImage,
4120    tuple: CmraDecoderTuple,
4121    header_hints: Option<CmraIdentityHints>,
4122    cmra_length: u64,
4123}
4124
4125#[derive(Debug)]
4126struct TerminalCandidate {
4127    terminal: V41Terminal,
4128    anchor: usize,
4129    locator_sequence: Option<u32>,
4130    cmra_offset: u64,
4131    cmra_length: u64,
4132}
4133
4134#[derive(Debug)]
4135struct PublicTerminalCandidate {
4136    terminal: V41PublicTerminal,
4137    anchor: usize,
4138    cmra_offset: u64,
4139    cmra_length: u64,
4140}
4141
4142#[derive(Debug)]
4143struct RecoveredTerminalAuthority {
4144    terminal: V41Terminal,
4145    volume_header: VolumeHeader,
4146    crypto_header: CryptoHeaderFixed,
4147    crypto_header_bytes: Vec<u8>,
4148    subkeys: Subkeys,
4149}
4150
4151#[derive(Debug)]
4152struct TerminalAuthorityCandidate {
4153    authority: RecoveredTerminalAuthority,
4154    anchor: usize,
4155    cmra_offset: u64,
4156    cmra_length: u64,
4157}
4158
4159#[derive(Debug, Clone, Copy)]
4160enum CmraRecoveryMode {
4161    KeyHolding,
4162    PublicNoKey,
4163}
4164
4165#[derive(Clone, Copy)]
4166pub(crate) struct KeyHoldingTerminalContext<'a> {
4167    pub(crate) subkeys: &'a Subkeys,
4168    pub(crate) volume_header: &'a VolumeHeader,
4169    pub(crate) crypto_header: &'a CryptoHeaderFixed,
4170    pub(crate) crypto_header_bytes: &'a [u8],
4171}
4172
4173fn locate_v41_terminal(
4174    bytes: &[u8],
4175    context: KeyHoldingTerminalContext<'_>,
4176    options: ReaderOptions,
4177) -> Result<V41Terminal, FormatError> {
4178    locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
4179}
4180
4181fn locate_v41_terminal_read_at(
4182    reader: &dyn ArchiveReadAt,
4183    len: u64,
4184    context: KeyHoldingTerminalContext<'_>,
4185    options: ReaderOptions,
4186) -> Result<V41Terminal, FormatError> {
4187    let mut candidates = Vec::new();
4188    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4189        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4190        collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
4191    }
4192    if len >= LOCATOR_PAIR_LEN as u64 {
4193        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4194        collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
4195    }
4196
4197    if candidates.is_empty() {
4198        let scan = max_critical_recovery_scan(options)? as u64;
4199        let scan_start = len.saturating_sub(scan);
4200        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4201        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4202        let mut offset = tail.len().saturating_sub(4);
4203        while offset < tail.len() {
4204            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4205            if tail.get(offset..offset + 4) == Some(b"TZCL") {
4206                collect_v41_locator_candidate_read_at(
4207                    reader,
4208                    absolute_offset,
4209                    2,
4210                    context,
4211                    &mut candidates,
4212                );
4213            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4214                if let Ok(candidate) =
4215                    parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
4216                {
4217                    candidates.push(candidate);
4218                }
4219            }
4220            if offset == 0 {
4221                break;
4222            }
4223            offset -= 1;
4224        }
4225    }
4226
4227    choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4228}
4229
4230fn locate_v41_terminal_authority(
4231    bytes: &[u8],
4232    master_key: &MasterKey,
4233    options: ReaderOptions,
4234) -> Result<RecoveredTerminalAuthority, FormatError> {
4235    let mut candidates = Vec::new();
4236    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4237        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4238        collect_v41_locator_authority_candidate(
4239            bytes,
4240            final_offset,
4241            0,
4242            master_key,
4243            &mut candidates,
4244        );
4245    }
4246    if bytes.len() >= LOCATOR_PAIR_LEN {
4247        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4248        collect_v41_locator_authority_candidate(
4249            bytes,
4250            mirror_offset,
4251            1,
4252            master_key,
4253            &mut candidates,
4254        );
4255    }
4256
4257    if candidates.is_empty() {
4258        let scan = max_critical_recovery_scan(options)?;
4259        let scan_start = bytes.len().saturating_sub(scan);
4260        let mut offset = bytes.len().saturating_sub(4);
4261        while offset >= scan_start {
4262            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4263                collect_v41_locator_authority_candidate(
4264                    bytes,
4265                    offset,
4266                    2,
4267                    master_key,
4268                    &mut candidates,
4269                );
4270            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4271                if let Ok(candidate) =
4272                    parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
4273                {
4274                    candidates.push(candidate);
4275                }
4276            }
4277            if offset == 0 {
4278                break;
4279            }
4280            offset -= 1;
4281        }
4282    }
4283
4284    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4285}
4286
4287fn locate_v41_terminal_authority_read_at(
4288    reader: &dyn ArchiveReadAt,
4289    len: u64,
4290    master_key: &MasterKey,
4291    options: ReaderOptions,
4292) -> Result<RecoveredTerminalAuthority, FormatError> {
4293    let mut candidates = Vec::new();
4294    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4295        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4296        collect_v41_locator_authority_candidate_read_at(
4297            reader,
4298            final_offset,
4299            0,
4300            master_key,
4301            &mut candidates,
4302        );
4303    }
4304    if len >= LOCATOR_PAIR_LEN as u64 {
4305        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4306        collect_v41_locator_authority_candidate_read_at(
4307            reader,
4308            mirror_offset,
4309            1,
4310            master_key,
4311            &mut candidates,
4312        );
4313    }
4314
4315    if candidates.is_empty() {
4316        let scan = max_critical_recovery_scan(options)? as u64;
4317        let scan_start = len.saturating_sub(scan);
4318        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4319        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4320        let mut offset = tail.len().saturating_sub(4);
4321        while offset < tail.len() {
4322            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4323            if tail.get(offset..offset + 4) == Some(b"TZCL") {
4324                collect_v41_locator_authority_candidate_read_at(
4325                    reader,
4326                    absolute_offset,
4327                    2,
4328                    master_key,
4329                    &mut candidates,
4330                );
4331            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4332                if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
4333                    reader,
4334                    absolute_offset,
4335                    master_key,
4336                ) {
4337                    candidates.push(candidate);
4338                }
4339            }
4340            if offset == 0 {
4341                break;
4342            }
4343            offset -= 1;
4344        }
4345    }
4346
4347    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4348}
4349
4350fn locate_v41_terminal_candidate(
4351    bytes: &[u8],
4352    context: KeyHoldingTerminalContext<'_>,
4353    options: ReaderOptions,
4354) -> Result<TerminalCandidate, FormatError> {
4355    let mut candidates = Vec::new();
4356    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4357        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4358        collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
4359    }
4360    if bytes.len() >= LOCATOR_PAIR_LEN {
4361        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4362        collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
4363    }
4364
4365    if candidates.is_empty() {
4366        let scan = max_critical_recovery_scan(options)?;
4367        let scan_start = bytes.len().saturating_sub(scan);
4368        let mut offset = bytes.len().saturating_sub(4);
4369        while offset >= scan_start {
4370            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4371                collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
4372            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4373                if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
4374                    candidates.push(candidate);
4375                }
4376            }
4377            if offset == 0 {
4378                break;
4379            }
4380            offset -= 1;
4381        }
4382    }
4383
4384    choose_v41_terminal_candidate(candidates)
4385}
4386
4387fn locate_v41_public_terminal(
4388    bytes: &[u8],
4389    volume_header: &VolumeHeader,
4390    crypto_header: &CryptoHeaderFixed,
4391    options: ReaderOptions,
4392) -> Result<V41PublicTerminal, FormatError> {
4393    let mut candidates = Vec::new();
4394    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4395        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4396        collect_v41_public_locator_candidate(
4397            bytes,
4398            final_offset,
4399            0,
4400            volume_header,
4401            crypto_header,
4402            &mut candidates,
4403        );
4404    }
4405    if bytes.len() >= LOCATOR_PAIR_LEN {
4406        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4407        collect_v41_public_locator_candidate(
4408            bytes,
4409            mirror_offset,
4410            1,
4411            volume_header,
4412            crypto_header,
4413            &mut candidates,
4414        );
4415    }
4416
4417    if candidates.is_empty() {
4418        let scan = max_critical_recovery_scan(options)?;
4419        let scan_start = bytes.len().saturating_sub(scan);
4420        let mut offset = bytes.len().saturating_sub(4);
4421        while offset >= scan_start {
4422            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4423                collect_v41_public_locator_candidate(
4424                    bytes,
4425                    offset,
4426                    2,
4427                    volume_header,
4428                    crypto_header,
4429                    &mut candidates,
4430                );
4431            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4432                if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
4433                    bytes,
4434                    offset,
4435                    volume_header,
4436                    crypto_header,
4437                ) {
4438                    candidates.push(candidate);
4439                }
4440            }
4441            if offset == 0 {
4442                break;
4443            }
4444            offset -= 1;
4445        }
4446    }
4447
4448    choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4449}
4450
4451fn collect_v41_locator_candidate(
4452    bytes: &[u8],
4453    offset: usize,
4454    expected_sequence: u32,
4455    context: KeyHoldingTerminalContext<'_>,
4456    candidates: &mut Vec<TerminalCandidate>,
4457) {
4458    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4459        return;
4460    };
4461    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4462        return;
4463    };
4464    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4465        return;
4466    }
4467    if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
4468        candidates.push(candidate);
4469    }
4470}
4471
4472fn collect_v41_locator_candidate_read_at(
4473    reader: &dyn ArchiveReadAt,
4474    offset: u64,
4475    expected_sequence: u32,
4476    context: KeyHoldingTerminalContext<'_>,
4477    candidates: &mut Vec<TerminalCandidate>,
4478) {
4479    let Ok(raw) = read_at_vec(
4480        reader,
4481        offset,
4482        CRITICAL_RECOVERY_LOCATOR_LEN,
4483        "CriticalRecoveryLocator",
4484    ) else {
4485        return;
4486    };
4487    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4488        return;
4489    };
4490    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4491        return;
4492    }
4493    if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
4494        candidates.push(candidate);
4495    }
4496}
4497
4498fn collect_v41_locator_authority_candidate(
4499    bytes: &[u8],
4500    offset: usize,
4501    expected_sequence: u32,
4502    master_key: &MasterKey,
4503    candidates: &mut Vec<TerminalAuthorityCandidate>,
4504) {
4505    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4506        return;
4507    };
4508    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4509        return;
4510    };
4511    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4512        return;
4513    }
4514    if let Ok(candidate) =
4515        parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
4516    {
4517        candidates.push(candidate);
4518    }
4519}
4520
4521fn collect_v41_locator_authority_candidate_read_at(
4522    reader: &dyn ArchiveReadAt,
4523    offset: u64,
4524    expected_sequence: u32,
4525    master_key: &MasterKey,
4526    candidates: &mut Vec<TerminalAuthorityCandidate>,
4527) {
4528    let Ok(raw) = read_at_vec(
4529        reader,
4530        offset,
4531        CRITICAL_RECOVERY_LOCATOR_LEN,
4532        "CriticalRecoveryLocator",
4533    ) else {
4534        return;
4535    };
4536    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4537        return;
4538    };
4539    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4540        return;
4541    }
4542    if let Ok(candidate) =
4543        parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
4544    {
4545        candidates.push(candidate);
4546    }
4547}
4548
4549fn collect_v41_public_locator_candidate(
4550    bytes: &[u8],
4551    offset: usize,
4552    expected_sequence: u32,
4553    volume_header: &VolumeHeader,
4554    crypto_header: &CryptoHeaderFixed,
4555    candidates: &mut Vec<PublicTerminalCandidate>,
4556) {
4557    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4558        return;
4559    };
4560    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4561        return;
4562    };
4563    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4564        return;
4565    }
4566    if let Ok(candidate) =
4567        parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
4568    {
4569        candidates.push(candidate);
4570    }
4571}
4572
4573fn choose_v41_terminal_candidate(
4574    mut candidates: Vec<TerminalCandidate>,
4575) -> Result<TerminalCandidate, FormatError> {
4576    candidates.sort_by_key(|candidate| candidate.anchor);
4577    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4578        "no valid v41 CMRA candidate found",
4579    ))?;
4580    if let Some(previous) = candidates.last() {
4581        if previous.anchor == winner.anchor
4582            && (previous.cmra_offset != winner.cmra_offset
4583                || previous.cmra_length != winner.cmra_length)
4584        {
4585            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4586        }
4587    }
4588    Ok(winner)
4589}
4590
4591fn choose_v41_terminal_authority_candidate(
4592    mut candidates: Vec<TerminalAuthorityCandidate>,
4593) -> Result<TerminalAuthorityCandidate, FormatError> {
4594    candidates.sort_by_key(|candidate| candidate.anchor);
4595    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4596        "no valid v41 CMRA candidate found",
4597    ))?;
4598    if let Some(previous) = candidates.last() {
4599        if previous.anchor == winner.anchor
4600            && (previous.cmra_offset != winner.cmra_offset
4601                || previous.cmra_length != winner.cmra_length)
4602        {
4603            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4604        }
4605    }
4606    Ok(winner)
4607}
4608
4609fn choose_v41_public_terminal_candidate(
4610    mut candidates: Vec<PublicTerminalCandidate>,
4611) -> Result<PublicTerminalCandidate, FormatError> {
4612    candidates.sort_by_key(|candidate| candidate.anchor);
4613    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4614        "no valid v41 public CMRA candidate found",
4615    ))?;
4616    if let Some(previous) = candidates.last() {
4617        if previous.anchor == winner.anchor
4618            && (previous.cmra_offset != winner.cmra_offset
4619                || previous.cmra_length != winner.cmra_length)
4620        {
4621            return Err(FormatError::InvalidArchive(
4622                "ambiguous v41 public CMRA candidates",
4623            ));
4624        }
4625    }
4626    Ok(winner)
4627}
4628
4629fn parse_locator_cmra_candidate(
4630    bytes: &[u8],
4631    locator_offset: usize,
4632    locator: CriticalRecoveryLocator,
4633    context: KeyHoldingTerminalContext<'_>,
4634) -> Result<TerminalCandidate, FormatError> {
4635    let tuple = CmraDecoderTuple::from(locator);
4636    validate_cmra_decoder_tuple(tuple)?;
4637    let expected_cmra_length = cmra_serialized_length(tuple)?;
4638    if locator.cmra_length as u64 != expected_cmra_length {
4639        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4640    }
4641    validate_locator_position(locator_offset, locator)?;
4642    let recovered = recover_cmra(
4643        bytes,
4644        locator.cmra_offset,
4645        Some(tuple),
4646        CmraRecoveryMode::KeyHolding,
4647    )?;
4648    if recovered.tuple != tuple {
4649        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4650    }
4651    if expected_cmra_length != recovered.cmra_length {
4652        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4653    }
4654    validate_locator_image_boundary(locator, &recovered.image)?;
4655    validate_cmra_identity_hints(
4656        recovered.header_hints,
4657        Some(CmraIdentityHints::from(locator)),
4658        &recovered.image,
4659    )?;
4660    let terminal =
4661        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
4662    Ok(TerminalCandidate {
4663        terminal,
4664        anchor: locator_offset
4665            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4666            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4667        locator_sequence: Some(locator.locator_sequence),
4668        cmra_offset: locator.cmra_offset,
4669        cmra_length: recovered.cmra_length,
4670    })
4671}
4672
4673fn parse_locator_cmra_candidate_read_at(
4674    reader: &dyn ArchiveReadAt,
4675    locator_offset: u64,
4676    locator: CriticalRecoveryLocator,
4677    context: KeyHoldingTerminalContext<'_>,
4678) -> Result<TerminalCandidate, FormatError> {
4679    let tuple = CmraDecoderTuple::from(locator);
4680    validate_cmra_decoder_tuple(tuple)?;
4681    let expected_cmra_length = cmra_serialized_length(tuple)?;
4682    if locator.cmra_length as u64 != expected_cmra_length {
4683        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4684    }
4685    validate_locator_position(
4686        to_usize(locator_offset, "CriticalRecoveryLocator")?,
4687        locator,
4688    )?;
4689    let recovered = recover_cmra_read_at(
4690        reader,
4691        locator.cmra_offset,
4692        Some(tuple),
4693        CmraRecoveryMode::KeyHolding,
4694    )?;
4695    if recovered.tuple != tuple {
4696        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4697    }
4698    if expected_cmra_length != recovered.cmra_length {
4699        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4700    }
4701    validate_locator_image_boundary(locator, &recovered.image)?;
4702    validate_cmra_identity_hints(
4703        recovered.header_hints,
4704        Some(CmraIdentityHints::from(locator)),
4705        &recovered.image,
4706    )?;
4707    let terminal = validate_recovered_terminal_read_at(
4708        recovered.image,
4709        recovered.tuple,
4710        reader,
4711        context,
4712        false,
4713    )?;
4714    Ok(TerminalCandidate {
4715        terminal,
4716        anchor: to_usize(
4717            checked_u64_add(
4718                locator_offset,
4719                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4720                "locator anchor overflow",
4721            )?,
4722            "locator anchor overflow",
4723        )?,
4724        locator_sequence: Some(locator.locator_sequence),
4725        cmra_offset: locator.cmra_offset,
4726        cmra_length: recovered.cmra_length,
4727    })
4728}
4729
4730fn parse_locator_cmra_authority_candidate(
4731    bytes: &[u8],
4732    locator_offset: usize,
4733    locator: CriticalRecoveryLocator,
4734    master_key: &MasterKey,
4735) -> Result<TerminalAuthorityCandidate, FormatError> {
4736    let tuple = CmraDecoderTuple::from(locator);
4737    validate_cmra_decoder_tuple(tuple)?;
4738    let expected_cmra_length = cmra_serialized_length(tuple)?;
4739    if locator.cmra_length as u64 != expected_cmra_length {
4740        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4741    }
4742    validate_locator_position(locator_offset, locator)?;
4743    let recovered = recover_cmra(
4744        bytes,
4745        locator.cmra_offset,
4746        Some(tuple),
4747        CmraRecoveryMode::KeyHolding,
4748    )?;
4749    if recovered.tuple != tuple {
4750        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4751    }
4752    if expected_cmra_length != recovered.cmra_length {
4753        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4754    }
4755    validate_locator_image_boundary(locator, &recovered.image)?;
4756    validate_cmra_identity_hints(
4757        recovered.header_hints,
4758        Some(CmraIdentityHints::from(locator)),
4759        &recovered.image,
4760    )?;
4761    let cmra_length = recovered.cmra_length;
4762    let authority =
4763        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4764    Ok(TerminalAuthorityCandidate {
4765        authority,
4766        anchor: locator_offset
4767            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4768            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4769        cmra_offset: locator.cmra_offset,
4770        cmra_length,
4771    })
4772}
4773
4774fn parse_locator_cmra_authority_candidate_read_at(
4775    reader: &dyn ArchiveReadAt,
4776    locator_offset: u64,
4777    locator: CriticalRecoveryLocator,
4778    master_key: &MasterKey,
4779) -> Result<TerminalAuthorityCandidate, FormatError> {
4780    let tuple = CmraDecoderTuple::from(locator);
4781    validate_cmra_decoder_tuple(tuple)?;
4782    let expected_cmra_length = cmra_serialized_length(tuple)?;
4783    if locator.cmra_length as u64 != expected_cmra_length {
4784        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4785    }
4786    validate_locator_position(
4787        to_usize(locator_offset, "CriticalRecoveryLocator")?,
4788        locator,
4789    )?;
4790    let recovered = recover_cmra_read_at(
4791        reader,
4792        locator.cmra_offset,
4793        Some(tuple),
4794        CmraRecoveryMode::KeyHolding,
4795    )?;
4796    if recovered.tuple != tuple {
4797        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4798    }
4799    if expected_cmra_length != recovered.cmra_length {
4800        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4801    }
4802    validate_locator_image_boundary(locator, &recovered.image)?;
4803    validate_cmra_identity_hints(
4804        recovered.header_hints,
4805        Some(CmraIdentityHints::from(locator)),
4806        &recovered.image,
4807    )?;
4808    let cmra_length = recovered.cmra_length;
4809    let authority =
4810        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4811    Ok(TerminalAuthorityCandidate {
4812        authority,
4813        anchor: to_usize(
4814            checked_u64_add(
4815                locator_offset,
4816                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4817                "locator anchor overflow",
4818            )?,
4819            "locator anchor overflow",
4820        )?,
4821        cmra_offset: locator.cmra_offset,
4822        cmra_length,
4823    })
4824}
4825
4826fn parse_public_locator_cmra_candidate(
4827    bytes: &[u8],
4828    locator_offset: usize,
4829    locator: CriticalRecoveryLocator,
4830    volume_header: &VolumeHeader,
4831    crypto_header: &CryptoHeaderFixed,
4832) -> Result<PublicTerminalCandidate, FormatError> {
4833    let tuple = CmraDecoderTuple::from(locator);
4834    validate_cmra_decoder_tuple(tuple)?;
4835    let expected_cmra_length = cmra_serialized_length(tuple)?;
4836    if locator.cmra_length as u64 != expected_cmra_length {
4837        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4838    }
4839    validate_locator_position(locator_offset, locator)?;
4840    let recovered = recover_cmra(
4841        bytes,
4842        locator.cmra_offset,
4843        Some(tuple),
4844        CmraRecoveryMode::PublicNoKey,
4845    )?;
4846    if recovered.tuple != tuple {
4847        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4848    }
4849    if expected_cmra_length != recovered.cmra_length {
4850        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4851    }
4852    validate_locator_image_boundary(locator, &recovered.image)?;
4853    validate_cmra_identity_hints(
4854        recovered.header_hints,
4855        Some(CmraIdentityHints::from(locator)),
4856        &recovered.image,
4857    )?;
4858    let terminal = validate_recovered_public_terminal(
4859        recovered.image,
4860        bytes,
4861        volume_header,
4862        crypto_header,
4863        false,
4864    )?;
4865    Ok(PublicTerminalCandidate {
4866        terminal,
4867        anchor: locator_offset
4868            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4869            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4870        cmra_offset: locator.cmra_offset,
4871        cmra_length: recovered.cmra_length,
4872    })
4873}
4874
4875fn parse_locatorless_cmra_candidate(
4876    bytes: &[u8],
4877    cmra_offset: usize,
4878    context: KeyHoldingTerminalContext<'_>,
4879) -> Result<TerminalCandidate, FormatError> {
4880    let recovered = recover_cmra(
4881        bytes,
4882        cmra_offset as u64,
4883        None,
4884        CmraRecoveryMode::KeyHolding,
4885    )?;
4886    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4887        return Err(FormatError::InvalidArchive(
4888            "locatorless CMRA boundary mismatch",
4889        ));
4890    }
4891    if recovered
4892        .image
4893        .volume_trailer_offset
4894        .checked_add(VOLUME_TRAILER_LEN as u64)
4895        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4896        != cmra_offset as u64
4897    {
4898        return Err(FormatError::InvalidArchive(
4899            "locatorless trailer boundary mismatch",
4900        ));
4901    }
4902    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4903    let terminal =
4904        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
4905    Ok(TerminalCandidate {
4906        terminal,
4907        anchor: cmra_offset
4908            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4909            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4910        locator_sequence: None,
4911        cmra_offset: cmra_offset as u64,
4912        cmra_length: recovered.cmra_length,
4913    })
4914}
4915
4916fn parse_locatorless_cmra_candidate_read_at(
4917    reader: &dyn ArchiveReadAt,
4918    cmra_offset: u64,
4919    context: KeyHoldingTerminalContext<'_>,
4920) -> Result<TerminalCandidate, FormatError> {
4921    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4922    if recovered.image.body_bytes_before_cmra != cmra_offset {
4923        return Err(FormatError::InvalidArchive(
4924            "locatorless CMRA boundary mismatch",
4925        ));
4926    }
4927    if recovered
4928        .image
4929        .volume_trailer_offset
4930        .checked_add(VOLUME_TRAILER_LEN as u64)
4931        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4932        != cmra_offset
4933    {
4934        return Err(FormatError::InvalidArchive(
4935            "locatorless trailer boundary mismatch",
4936        ));
4937    }
4938    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4939    let terminal = validate_recovered_terminal_read_at(
4940        recovered.image,
4941        recovered.tuple,
4942        reader,
4943        context,
4944        true,
4945    )?;
4946    Ok(TerminalCandidate {
4947        terminal,
4948        anchor: to_usize(
4949            checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
4950            "CMRA anchor overflow",
4951        )?,
4952        locator_sequence: None,
4953        cmra_offset,
4954        cmra_length: recovered.cmra_length,
4955    })
4956}
4957
4958fn parse_locatorless_cmra_authority_candidate(
4959    bytes: &[u8],
4960    cmra_offset: usize,
4961    master_key: &MasterKey,
4962) -> Result<TerminalAuthorityCandidate, FormatError> {
4963    let recovered = recover_cmra(
4964        bytes,
4965        cmra_offset as u64,
4966        None,
4967        CmraRecoveryMode::KeyHolding,
4968    )?;
4969    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4970        return Err(FormatError::InvalidArchive(
4971            "locatorless CMRA boundary mismatch",
4972        ));
4973    }
4974    if recovered
4975        .image
4976        .volume_trailer_offset
4977        .checked_add(VOLUME_TRAILER_LEN as u64)
4978        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4979        != cmra_offset as u64
4980    {
4981        return Err(FormatError::InvalidArchive(
4982            "locatorless trailer boundary mismatch",
4983        ));
4984    }
4985    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4986    let cmra_length = recovered.cmra_length;
4987    let authority =
4988        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
4989    Ok(TerminalAuthorityCandidate {
4990        authority,
4991        anchor: cmra_offset
4992            .checked_add(to_usize(cmra_length, "CMRA")?)
4993            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4994        cmra_offset: cmra_offset as u64,
4995        cmra_length,
4996    })
4997}
4998
4999fn parse_locatorless_cmra_authority_candidate_read_at(
5000    reader: &dyn ArchiveReadAt,
5001    cmra_offset: u64,
5002    master_key: &MasterKey,
5003) -> Result<TerminalAuthorityCandidate, FormatError> {
5004    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
5005    if recovered.image.body_bytes_before_cmra != cmra_offset {
5006        return Err(FormatError::InvalidArchive(
5007            "locatorless CMRA boundary mismatch",
5008        ));
5009    }
5010    if recovered
5011        .image
5012        .volume_trailer_offset
5013        .checked_add(VOLUME_TRAILER_LEN as u64)
5014        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
5015        != cmra_offset
5016    {
5017        return Err(FormatError::InvalidArchive(
5018            "locatorless trailer boundary mismatch",
5019        ));
5020    }
5021    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
5022    let cmra_length = recovered.cmra_length;
5023    let authority =
5024        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
5025    Ok(TerminalAuthorityCandidate {
5026        authority,
5027        anchor: to_usize(
5028            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
5029            "CMRA anchor overflow",
5030        )?,
5031        cmra_offset,
5032        cmra_length,
5033    })
5034}
5035
5036fn parse_public_locatorless_cmra_candidate(
5037    bytes: &[u8],
5038    cmra_offset: usize,
5039    volume_header: &VolumeHeader,
5040    crypto_header: &CryptoHeaderFixed,
5041) -> Result<PublicTerminalCandidate, FormatError> {
5042    let recovered = recover_cmra(
5043        bytes,
5044        cmra_offset as u64,
5045        None,
5046        CmraRecoveryMode::PublicNoKey,
5047    )?;
5048    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
5049        return Err(FormatError::InvalidArchive(
5050            "locatorless CMRA boundary mismatch",
5051        ));
5052    }
5053    if recovered
5054        .image
5055        .volume_trailer_offset
5056        .checked_add(VOLUME_TRAILER_LEN as u64)
5057        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
5058        != cmra_offset as u64
5059    {
5060        return Err(FormatError::InvalidArchive(
5061            "locatorless trailer boundary mismatch",
5062        ));
5063    }
5064    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
5065    let terminal = validate_recovered_public_terminal(
5066        recovered.image,
5067        bytes,
5068        volume_header,
5069        crypto_header,
5070        true,
5071    )?;
5072    Ok(PublicTerminalCandidate {
5073        terminal,
5074        anchor: cmra_offset
5075            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
5076            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
5077        cmra_offset: cmra_offset as u64,
5078        cmra_length: recovered.cmra_length,
5079    })
5080}
5081
5082fn validate_locator_position(
5083    locator_offset: usize,
5084    locator: CriticalRecoveryLocator,
5085) -> Result<(), FormatError> {
5086    if locator.cmra_offset != locator.body_bytes_before_cmra {
5087        return Err(FormatError::InvalidArchive(
5088            "locator CMRA boundary mismatch",
5089        ));
5090    }
5091    if locator
5092        .volume_trailer_offset
5093        .checked_add(VOLUME_TRAILER_LEN as u64)
5094        .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
5095        != locator.cmra_offset
5096    {
5097        return Err(FormatError::InvalidArchive(
5098            "locator trailer boundary mismatch",
5099        ));
5100    }
5101    let expected_offset = match locator.locator_sequence {
5102        1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
5103        0 => locator
5104            .cmra_offset
5105            .checked_add(locator.cmra_length as u64)
5106            .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
5107        _ => None,
5108    }
5109    .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
5110    if expected_offset != locator_offset as u64 {
5111        return Err(FormatError::InvalidArchive(
5112            "locator position does not match sequence",
5113        ));
5114    }
5115    Ok(())
5116}
5117
5118fn validate_locator_image_boundary(
5119    locator: CriticalRecoveryLocator,
5120    image: &CriticalMetadataImage,
5121) -> Result<(), FormatError> {
5122    if locator.volume_trailer_offset != image.volume_trailer_offset
5123        || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
5124        || image
5125            .volume_trailer_offset
5126            .checked_add(VOLUME_TRAILER_LEN as u64)
5127            .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5128            != locator.cmra_offset
5129    {
5130        return Err(FormatError::InvalidArchive(
5131            "locator and CMRA image boundaries differ",
5132        ));
5133    }
5134    Ok(())
5135}
5136
5137fn validate_cmra_identity_hints(
5138    header_hints: Option<CmraIdentityHints>,
5139    locator_hints: Option<CmraIdentityHints>,
5140    image: &CriticalMetadataImage,
5141) -> Result<(), FormatError> {
5142    if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
5143        if header != locator {
5144            return Err(FormatError::InvalidArchive(
5145                "CMRA header and locator identity hints differ",
5146            ));
5147        }
5148    }
5149    for hints in [header_hints, locator_hints].into_iter().flatten() {
5150        if hints.archive_uuid != image.archive_uuid
5151            || hints.session_id != image.session_id
5152            || hints.volume_index != image.volume_index
5153        {
5154            return Err(FormatError::InvalidArchive(
5155                "CMRA identity hints do not match recovered image",
5156            ));
5157        }
5158    }
5159    Ok(())
5160}
5161
5162fn recover_cmra(
5163    bytes: &[u8],
5164    cmra_offset: u64,
5165    locator_tuple: Option<CmraDecoderTuple>,
5166    mode: CmraRecoveryMode,
5167) -> Result<RecoveredCmra, FormatError> {
5168    let offset = to_usize(cmra_offset, "CMRA")?;
5169    let header_bytes = slice(
5170        bytes,
5171        offset,
5172        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5173        "CriticalMetadataRecoveryHeader",
5174    )?;
5175    let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
5176    validate_cmra_decoder_tuple(tuple)?;
5177    let cmra_length = cmra_serialized_length(tuple)?;
5178    let cmra_len = to_usize(cmra_length, "CMRA")?;
5179    let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
5180    recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
5181}
5182
5183fn recover_cmra_read_at(
5184    reader: &dyn ArchiveReadAt,
5185    cmra_offset: u64,
5186    locator_tuple: Option<CmraDecoderTuple>,
5187    mode: CmraRecoveryMode,
5188) -> Result<RecoveredCmra, FormatError> {
5189    let header_bytes = read_at_vec(
5190        reader,
5191        cmra_offset,
5192        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5193        "CriticalMetadataRecoveryHeader",
5194    )?;
5195    let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
5196    validate_cmra_decoder_tuple(tuple)?;
5197    let cmra_length = cmra_serialized_length(tuple)?;
5198    let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
5199    recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
5200}
5201
5202fn recover_cmra_header_tuple(
5203    header_bytes: &[u8],
5204    locator_tuple: Option<CmraDecoderTuple>,
5205) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
5206    let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
5207    Ok(match (parsed_header, locator_tuple) {
5208        (Ok(header), Some(locator_tuple)) => {
5209            let header_tuple = CmraDecoderTuple::from(header);
5210            if header_tuple != locator_tuple {
5211                return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
5212            }
5213            (locator_tuple, Some(CmraIdentityHints::from(header)))
5214        }
5215        (Ok(header), None) => (
5216            CmraDecoderTuple::from(header),
5217            Some(CmraIdentityHints::from(header)),
5218        ),
5219        (Err(_), Some(tuple)) => (tuple, None),
5220        (Err(err), _) => return Err(err),
5221    })
5222}
5223
5224fn recover_cmra_from_bytes(
5225    cmra_bytes: &[u8],
5226    tuple: CmraDecoderTuple,
5227    header_hints: Option<CmraIdentityHints>,
5228    cmra_length: u64,
5229    mode: CmraRecoveryMode,
5230) -> Result<RecoveredCmra, FormatError> {
5231    let shard_size = tuple.shard_size as usize;
5232    let mut data_shards = vec![None; tuple.data_shard_count as usize];
5233    let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
5234    let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
5235    for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
5236        let raw = slice(
5237            cmra_bytes,
5238            cursor,
5239            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5240            "CriticalMetadataRecoveryShard",
5241        )?;
5242        let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
5243        if let Some(shard) = shard {
5244            validate_cmra_shard(&shard, idx, tuple)?;
5245            if shard.shard_role == 0 {
5246                let data_slot = data_shards
5247                    .get_mut(idx)
5248                    .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
5249                *data_slot = Some(shard.payload);
5250            } else {
5251                let parity_idx = idx - tuple.data_shard_count as usize;
5252                let parity_slot =
5253                    parity_shards
5254                        .get_mut(parity_idx)
5255                        .ok_or(FormatError::InvalidArchive(
5256                            "CMRA parity shard out of range",
5257                        ))?;
5258                *parity_slot = Some(shard.payload);
5259            }
5260        }
5261        cursor = checked_add(
5262            cursor,
5263            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5264            "CriticalMetadataRecoveryShard",
5265        )?;
5266    }
5267    let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
5268    let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
5269    for shard in repaired {
5270        image_bytes.extend_from_slice(&shard);
5271    }
5272    image_bytes.truncate(tuple.image_length as usize);
5273    if sha256_bytes(&image_bytes) != tuple.image_sha256 {
5274        return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
5275    }
5276    let image = CriticalMetadataImage::parse(&image_bytes)?;
5277    validate_critical_metadata_image(&image, mode)?;
5278    Ok(RecoveredCmra {
5279        image,
5280        tuple,
5281        header_hints,
5282        cmra_length,
5283    })
5284}
5285
5286fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
5287    let shard_size = tuple.shard_size as u64;
5288    if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
5289        return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
5290    }
5291    let image_length = tuple.image_length as u64;
5292    let min = critical_image_min();
5293    let cap = critical_image_cap()?;
5294    if image_length < min || image_length > cap {
5295        return Err(FormatError::InvalidArchive(
5296            "CMRA image_length is outside bounds",
5297        ));
5298    }
5299    let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
5300    if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
5301        return Err(FormatError::InvalidArchive(
5302            "CMRA data_shard_count does not match image length",
5303        ));
5304    }
5305    let max_parity = 2u64.max(ceil_div_u64(
5306        checked_u64_mul(
5307            expected_data_shards,
5308            READER_MAX_CMRA_PARITY_PCT as u64,
5309            "CMRA parity overflow",
5310        )?,
5311        100,
5312    )?);
5313    if tuple.parity_shard_count as u64 > max_parity {
5314        return Err(FormatError::ReaderResourceLimitExceeded {
5315            field: "CMRA parity shard count",
5316            cap: max_parity,
5317            actual: tuple.parity_shard_count as u64,
5318        });
5319    }
5320    let total = expected_data_shards
5321        .checked_add(tuple.parity_shard_count as u64)
5322        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5323    if total > 65_535 {
5324        return Err(FormatError::FecTooManyShards(total as usize));
5325    }
5326    Ok(())
5327}
5328
5329fn validate_cmra_writer_parity_lower_bound(
5330    tuple: CmraDecoderTuple,
5331    bit_rot_buffer_pct: u8,
5332) -> Result<(), FormatError> {
5333    let min_parity = 2u64.max(ceil_div_u64(
5334        checked_u64_mul(
5335            tuple.data_shard_count as u64,
5336            bit_rot_buffer_pct as u64,
5337            "CMRA parity lower-bound overflow",
5338        )?,
5339        100,
5340    )?);
5341    if (tuple.parity_shard_count as u64) < min_parity {
5342        return Err(FormatError::InvalidArchive(
5343            "CMRA parity shard count is below authenticated bit-rot lower bound",
5344        ));
5345    }
5346    Ok(())
5347}
5348
5349fn validate_cmra_shard(
5350    shard: &CriticalMetadataRecoveryShard,
5351    serialized_idx: usize,
5352    tuple: CmraDecoderTuple,
5353) -> Result<(), FormatError> {
5354    if shard.shard_index as usize != serialized_idx {
5355        return Err(FormatError::InvalidArchive(
5356            "CMRA shards are not in canonical order",
5357        ));
5358    }
5359    let data_count = tuple.data_shard_count as usize;
5360    let shard_size = tuple.shard_size as usize;
5361    if serialized_idx < data_count {
5362        if shard.shard_role != 0 {
5363            return Err(FormatError::InvalidArchive(
5364                "CMRA data shard has wrong role",
5365            ));
5366        }
5367        let expected_len = if serialized_idx + 1 == data_count {
5368            let used = tuple.image_length as usize - serialized_idx * shard_size;
5369            if used == 0 {
5370                shard_size
5371            } else {
5372                used
5373            }
5374        } else {
5375            shard_size
5376        };
5377        if shard.shard_payload_length as usize != expected_len {
5378            return Err(FormatError::InvalidArchive(
5379                "CMRA data shard payload length is non-canonical",
5380            ));
5381        }
5382        if serialized_idx + 1 == data_count
5383            && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
5384        {
5385            return Err(FormatError::InvalidArchive(
5386                "CMRA final data shard padding is non-zero",
5387            ));
5388        }
5389    } else {
5390        if shard.shard_role != 1 {
5391            return Err(FormatError::InvalidArchive(
5392                "CMRA parity shard has wrong role",
5393            ));
5394        }
5395        if shard.shard_payload_length as usize != shard_size {
5396            return Err(FormatError::InvalidArchive(
5397                "CMRA parity shard payload length is non-canonical",
5398            ));
5399        }
5400    }
5401    Ok(())
5402}
5403
5404fn validate_critical_metadata_image(
5405    image: &CriticalMetadataImage,
5406    mode: CmraRecoveryMode,
5407) -> Result<(), FormatError> {
5408    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5409    if image.volume_header_offset != 0
5410        || image.volume_header_length != VOLUME_HEADER_LEN as u32
5411        || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
5412        || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5413        || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
5414        || image.body_bytes_before_cmra
5415            != image
5416                .volume_trailer_offset
5417                .checked_add(VOLUME_TRAILER_LEN as u64)
5418                .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5419    {
5420        return Err(FormatError::InvalidArchive(
5421            "CriticalMetadataImage fixed layout is invalid",
5422        ));
5423    }
5424    if root_auth_present {
5425        if image.root_auth_footer_offset == 0
5426            || image.root_auth_footer_length == 0
5427            || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5428        {
5429            return Err(FormatError::InvalidArchive(
5430                "CriticalMetadataImage root-auth range is invalid",
5431            ));
5432        }
5433    } else if image.root_auth_footer_offset != 0
5434        || image.root_auth_footer_length != 0
5435        || image.root_auth_footer_sha256 != [0u8; 32]
5436    {
5437        return Err(FormatError::InvalidArchive(
5438            "CriticalMetadataImage root-auth fields must be zero when absent",
5439        ));
5440    }
5441    let block_record_len = image_block_record_len_from_region(image)?;
5442    let block_record_len_u64 = u64::try_from(block_record_len)
5443        .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
5444    match mode {
5445        CmraRecoveryMode::KeyHolding => {
5446            let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
5447                FormatError::InvalidArchive("BlockRecord region length overflow"),
5448            )?;
5449            if image.block_records_length != expected_len {
5450                return Err(FormatError::InvalidArchive(
5451                    "CriticalMetadataImage terminal equations are invalid",
5452                ));
5453            }
5454        }
5455        CmraRecoveryMode::PublicNoKey => {
5456            if image.block_records_length % block_record_len_u64 != 0 {
5457                return Err(FormatError::InvalidArchive(
5458                    "CriticalMetadataImage BlockRecord region is not aligned",
5459                ));
5460            }
5461        }
5462    }
5463    if image.block_records_offset
5464        != image
5465            .crypto_header_offset
5466            .checked_add(image.crypto_header_length as u64)
5467            .ok_or(FormatError::InvalidArchive(
5468                "CryptoHeader boundary overflow",
5469            ))?
5470        || image.manifest_footer_offset
5471            != image
5472                .block_records_offset
5473                .checked_add(image.block_records_length)
5474                .ok_or(FormatError::InvalidArchive(
5475                    "ManifestFooter boundary overflow",
5476                ))?
5477    {
5478        return Err(FormatError::InvalidArchive(
5479            "CriticalMetadataImage terminal equations are invalid",
5480        ));
5481    }
5482    let manifest_end = image
5483        .manifest_footer_offset
5484        .checked_add(MANIFEST_FOOTER_LEN as u64)
5485        .ok_or(FormatError::InvalidArchive(
5486            "RootAuthFooter boundary overflow",
5487        ))?;
5488    if root_auth_present {
5489        if image.root_auth_footer_offset != manifest_end
5490            || image
5491                .root_auth_footer_offset
5492                .checked_add(image.root_auth_footer_length as u64)
5493                .ok_or(FormatError::InvalidArchive(
5494                    "VolumeTrailer boundary overflow",
5495                ))?
5496                != image.volume_trailer_offset
5497        {
5498            return Err(FormatError::InvalidArchive(
5499                "CriticalMetadataImage root-auth terminal equations are invalid",
5500            ));
5501        }
5502    } else if image.volume_trailer_offset != manifest_end {
5503        return Err(FormatError::InvalidArchive(
5504            "CriticalMetadataImage unsigned terminal equations are invalid",
5505        ));
5506    }
5507    let expected_types: &[u16] = if root_auth_present {
5508        &[1, 2, 3, 4, 5]
5509    } else {
5510        &[1, 2, 3, 5]
5511    };
5512    if image.regions.len() != expected_types.len()
5513        || image
5514            .regions
5515            .iter()
5516            .map(|region| region.region_type)
5517            .ne(expected_types.iter().copied())
5518    {
5519        return Err(FormatError::InvalidArchive(
5520            "CriticalMetadataImage regions are not canonical",
5521        ));
5522    }
5523    validate_image_region(
5524        image,
5525        1,
5526        image.volume_header_offset,
5527        image.volume_header_length,
5528    )?;
5529    validate_image_region(
5530        image,
5531        2,
5532        image.crypto_header_offset,
5533        image.crypto_header_length,
5534    )?;
5535    validate_image_region(
5536        image,
5537        3,
5538        image.manifest_footer_offset,
5539        image.manifest_footer_length,
5540    )?;
5541    if root_auth_present {
5542        validate_image_region(
5543            image,
5544            4,
5545            image.root_auth_footer_offset,
5546            image.root_auth_footer_length,
5547        )?;
5548    }
5549    validate_image_region(
5550        image,
5551        5,
5552        image.volume_trailer_offset,
5553        image.volume_trailer_length,
5554    )?;
5555    if sha256_region(image, 1)? != image.volume_header_sha256
5556        || sha256_region(image, 2)? != image.crypto_header_sha256
5557        || sha256_region(image, 3)? != image.manifest_footer_sha256
5558        || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
5559        || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
5560        || sha256_region(image, 5)? != image.volume_trailer_sha256
5561    {
5562        return Err(FormatError::InvalidArchive(
5563            "CriticalMetadataImage region digest mismatch",
5564        ));
5565    }
5566    Ok(())
5567}
5568
5569fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
5570    let crypto_region = image
5571        .region(2)
5572        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5573    let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5574    crypto.fixed.validate_supported_profile()?;
5575    Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
5576}
5577
5578fn validate_image_region(
5579    image: &CriticalMetadataImage,
5580    region_type: u16,
5581    offset: u64,
5582    length: u32,
5583) -> Result<(), FormatError> {
5584    let region = image
5585        .region(region_type)
5586        .ok_or(FormatError::InvalidArchive(
5587            "missing CriticalMetadataImage region",
5588        ))?;
5589    if region.offset != offset || region.bytes.len() != length as usize {
5590        return Err(FormatError::InvalidArchive(
5591            "CriticalMetadataImage region range mismatch",
5592        ));
5593    }
5594    Ok(())
5595}
5596
5597fn validate_image_identity(
5598    image: &CriticalMetadataImage,
5599    volume_header: &VolumeHeader,
5600    crypto_header: &CryptoHeaderFixed,
5601) -> Result<(), FormatError> {
5602    if image.archive_uuid != volume_header.archive_uuid
5603        || image.session_id != volume_header.session_id
5604        || image.volume_index != volume_header.volume_index
5605        || image.stripe_width != volume_header.stripe_width
5606        || image.stripe_width != crypto_header.stripe_width
5607    {
5608        return Err(FormatError::InvalidArchive(
5609            "CriticalMetadataImage identity does not match selected volume",
5610        ));
5611    }
5612    Ok(())
5613}
5614
5615fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
5616    Ok(sha256_bytes(
5617        &image
5618            .region(region_type)
5619            .ok_or(FormatError::InvalidArchive(
5620                "missing CriticalMetadataImage region",
5621            ))?
5622            .bytes,
5623    ))
5624}
5625
5626fn validate_recovered_terminal_authority(
5627    image: CriticalMetadataImage,
5628    tuple: CmraDecoderTuple,
5629    master_key: &MasterKey,
5630    require_cmra_boundary_magic: bool,
5631) -> Result<RecoveredTerminalAuthority, FormatError> {
5632    let volume_header_region = image
5633        .region(1)
5634        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5635    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5636    let crypto_region = image
5637        .region(2)
5638        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5639    let crypto_header_bytes = crypto_region.bytes.clone();
5640    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
5641    let subkeys = subkeys_for_open(
5642        Some(master_key),
5643        parsed_crypto.fixed.aead_algo,
5644        &volume_header.archive_uuid,
5645        &volume_header.session_id,
5646    )?;
5647    verify_integrity_tag(
5648        HmacDomain::CryptoHeader,
5649        parsed_crypto.fixed.aead_algo,
5650        Some(&subkeys.mac_key),
5651        &volume_header.archive_uuid,
5652        &volume_header.session_id,
5653        parsed_crypto.hmac_covered_bytes,
5654        &parsed_crypto.header_hmac,
5655    )?;
5656    parsed_crypto.validate_extension_semantics()?;
5657    validate_seekable_supported_volume(
5658        &volume_header,
5659        &parsed_crypto.fixed,
5660        &parsed_crypto.extensions,
5661    )?;
5662    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5663    let crypto_header = parsed_crypto.fixed.clone();
5664    if crypto_header.bit_rot_buffer_pct == 0 {
5665        return Err(FormatError::InvalidArchive(
5666            "CMRA startup recovery requires a nonzero bit-rot budget",
5667        ));
5668    }
5669    drop(parsed_crypto);
5670
5671    let terminal = validate_recovered_terminal_inner(
5672        image,
5673        tuple,
5674        require_cmra_boundary_magic,
5675        true,
5676        KeyHoldingTerminalContext {
5677            subkeys: &subkeys,
5678            volume_header: &volume_header,
5679            crypto_header: &crypto_header,
5680            crypto_header_bytes: &crypto_header_bytes,
5681        },
5682    )?;
5683    Ok(RecoveredTerminalAuthority {
5684        terminal,
5685        volume_header,
5686        crypto_header,
5687        crypto_header_bytes,
5688        subkeys,
5689    })
5690}
5691
5692fn validate_recovered_terminal(
5693    image: CriticalMetadataImage,
5694    tuple: CmraDecoderTuple,
5695    bytes: &[u8],
5696    context: KeyHoldingTerminalContext<'_>,
5697    require_cmra_boundary_magic: bool,
5698) -> Result<V41Terminal, FormatError> {
5699    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5700    let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
5701    validate_recovered_terminal_inner(
5702        image,
5703        tuple,
5704        require_cmra_boundary_magic,
5705        cmra_boundary_magic_ok,
5706        context,
5707    )
5708}
5709
5710fn validate_recovered_terminal_read_at(
5711    image: CriticalMetadataImage,
5712    tuple: CmraDecoderTuple,
5713    reader: &dyn ArchiveReadAt,
5714    context: KeyHoldingTerminalContext<'_>,
5715    require_cmra_boundary_magic: bool,
5716) -> Result<V41Terminal, FormatError> {
5717    let mut magic = [0u8; 4];
5718    reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
5719    validate_recovered_terminal_inner(
5720        image,
5721        tuple,
5722        require_cmra_boundary_magic,
5723        magic == *b"TZCR",
5724        context,
5725    )
5726}
5727
5728fn validate_recovered_terminal_inner(
5729    image: CriticalMetadataImage,
5730    tuple: CmraDecoderTuple,
5731    require_cmra_boundary_magic: bool,
5732    cmra_boundary_magic_ok: bool,
5733    context: KeyHoldingTerminalContext<'_>,
5734) -> Result<V41Terminal, FormatError> {
5735    let subkeys = context.subkeys;
5736    let volume_header = context.volume_header;
5737    let crypto_header = context.crypto_header;
5738    let volume_header_region = image
5739        .region(1)
5740        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5741    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5742    if &recovered_volume_header != volume_header {
5743        return Err(FormatError::InvalidArchive(
5744            "CMRA VolumeHeader differs from parsed VolumeHeader",
5745        ));
5746    }
5747    validate_image_identity(&image, volume_header, crypto_header)?;
5748    let crypto_region = image
5749        .region(2)
5750        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5751    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5752    if recovered_crypto.fixed != *crypto_header {
5753        return Err(FormatError::InvalidArchive(
5754            "CMRA CryptoHeader differs from parsed CryptoHeader",
5755        ));
5756    }
5757    let recovered_pre_hmac_len = crypto_region
5758        .bytes
5759        .len()
5760        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5761        .ok_or(FormatError::InvalidArchive(
5762            "CMRA CryptoHeader is too short",
5763        ))?;
5764    let parsed_pre_hmac_len = context
5765        .crypto_header_bytes
5766        .len()
5767        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5768        .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
5769    if recovered_pre_hmac_len != parsed_pre_hmac_len
5770        || crypto_region.bytes[..recovered_pre_hmac_len]
5771            != context.crypto_header_bytes[..parsed_pre_hmac_len]
5772    {
5773        return Err(FormatError::InvalidArchive(
5774            "CMRA CryptoHeader differs from parsed CryptoHeader",
5775        ));
5776    }
5777    verify_integrity_tag(
5778        HmacDomain::CryptoHeader,
5779        recovered_crypto.fixed.aead_algo,
5780        Some(&subkeys.mac_key),
5781        &volume_header.archive_uuid,
5782        &volume_header.session_id,
5783        recovered_crypto.hmac_covered_bytes,
5784        &recovered_crypto.header_hmac,
5785    )?;
5786    validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
5787    recovered_crypto.validate_extension_semantics()?;
5788
5789    let manifest_region = image
5790        .region(3)
5791        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5792    let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
5793    validate_manifest_footer(
5794        volume_header,
5795        crypto_header,
5796        &manifest_footer,
5797        subkeys,
5798        &manifest_region.bytes,
5799    )?;
5800    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5801
5802    let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
5803        let root_auth_region = image
5804            .region(4)
5805            .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5806        let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5807        if footer.archive_uuid != volume_header.archive_uuid
5808            || footer.session_id != volume_header.session_id
5809            || footer.footer_length()? != image.root_auth_footer_length
5810        {
5811            return Err(FormatError::InvalidArchive(
5812                "RootAuthFooter identity or length does not match terminal image",
5813            ));
5814        }
5815        Some(footer)
5816    } else {
5817        None
5818    };
5819
5820    let trailer_region = image
5821        .region(5)
5822        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5823    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5824    verify_integrity_tag(
5825        HmacDomain::VolumeTrailer,
5826        crypto_header.aead_algo,
5827        Some(&subkeys.mac_key),
5828        &volume_header.archive_uuid,
5829        &volume_header.session_id,
5830        &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
5831        &trailer.trailer_hmac,
5832    )?;
5833    validate_trailer_identity(volume_header, &trailer)?;
5834    validate_v41_trailer_equations(&image, &trailer)?;
5835
5836    if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
5837        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5838    }
5839
5840    let manifest_footer_bytes = manifest_region.bytes.clone();
5841    let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
5842    Ok(V41Terminal {
5843        image,
5844        manifest_footer_bytes,
5845        root_auth_footer_bytes,
5846        root_auth_footer,
5847        volume_trailer: trailer,
5848    })
5849}
5850
5851fn validate_recovered_public_terminal(
5852    image: CriticalMetadataImage,
5853    bytes: &[u8],
5854    volume_header: &VolumeHeader,
5855    crypto_header: &CryptoHeaderFixed,
5856    require_cmra_boundary_magic: bool,
5857) -> Result<V41PublicTerminal, FormatError> {
5858    if image.layout_flags & 0x0000_0001 == 0 {
5859        return Err(FormatError::ReaderUnsupported(
5860            "public no-key verification requires root-auth",
5861        ));
5862    }
5863    let volume_header_region = image
5864        .region(1)
5865        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5866    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5867    if &recovered_volume_header != volume_header {
5868        return Err(FormatError::InvalidArchive(
5869            "CMRA VolumeHeader differs from parsed VolumeHeader",
5870        ));
5871    }
5872    validate_image_identity(&image, volume_header, crypto_header)?;
5873    let crypto_region = image
5874        .region(2)
5875        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5876    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5877    if recovered_crypto.fixed != *crypto_header {
5878        return Err(FormatError::InvalidArchive(
5879            "CMRA CryptoHeader differs from parsed CryptoHeader",
5880        ));
5881    }
5882    recovered_crypto.validate_extension_semantics()?;
5883
5884    image
5885        .region(3)
5886        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5887
5888    let root_auth_region = image
5889        .region(4)
5890        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5891    let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5892    if root_auth_footer.archive_uuid != volume_header.archive_uuid
5893        || root_auth_footer.session_id != volume_header.session_id
5894        || root_auth_footer.footer_length()? != image.root_auth_footer_length
5895    {
5896        return Err(FormatError::InvalidArchive(
5897            "public RootAuthFooter identity or length does not match terminal image",
5898        ));
5899    }
5900
5901    let trailer_region = image
5902        .region(5)
5903        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5904    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5905    validate_trailer_identity(volume_header, &trailer)?;
5906    validate_v41_public_trailer_profile(&image, &trailer)?;
5907
5908    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5909    if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
5910        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5911    }
5912
5913    let root_auth_footer_bytes = root_auth_region.bytes.clone();
5914    Ok(V41PublicTerminal {
5915        image,
5916        root_auth_footer_bytes,
5917        root_auth_footer,
5918    })
5919}
5920
5921fn validate_v41_trailer_equations(
5922    image: &CriticalMetadataImage,
5923    trailer: &VolumeTrailer,
5924) -> Result<(), FormatError> {
5925    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5926    if trailer.bytes_written != image.volume_trailer_offset
5927        || trailer.manifest_footer_offset != image.manifest_footer_offset
5928        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5929        || trailer.block_count != image.block_count
5930    {
5931        return Err(FormatError::InvalidArchive(
5932            "VolumeTrailer does not match v41 terminal layout",
5933        ));
5934    }
5935    if root_auth_present {
5936        if trailer.root_auth_flags != 0x0000_0001
5937            || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5938            || trailer.root_auth_footer_length != image.root_auth_footer_length
5939            || image.root_auth_footer_offset
5940                != image
5941                    .manifest_footer_offset
5942                    .checked_add(MANIFEST_FOOTER_LEN as u64)
5943                    .ok_or(FormatError::InvalidArchive(
5944                        "RootAuthFooter trailer boundary overflow",
5945                    ))?
5946            || image
5947                .root_auth_footer_offset
5948                .checked_add(image.root_auth_footer_length as u64)
5949                .ok_or(FormatError::InvalidArchive(
5950                    "RootAuthFooter trailer boundary overflow",
5951                ))?
5952                != image.volume_trailer_offset
5953        {
5954            return Err(FormatError::InvalidArchive(
5955                "VolumeTrailer root-auth fields do not match v41 terminal layout",
5956            ));
5957        }
5958    } else if trailer.root_auth_footer_offset != 0
5959        || trailer.root_auth_footer_length != 0
5960        || trailer.root_auth_flags != 0
5961    {
5962        return Err(FormatError::InvalidArchive(
5963            "VolumeTrailer root-auth fields must be zero when absent",
5964        ));
5965    }
5966    Ok(())
5967}
5968
5969fn validate_v41_public_trailer_profile(
5970    image: &CriticalMetadataImage,
5971    trailer: &VolumeTrailer,
5972) -> Result<(), FormatError> {
5973    if trailer.bytes_written != image.volume_trailer_offset
5974        || trailer.manifest_footer_offset != image.manifest_footer_offset
5975        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5976    {
5977        return Err(FormatError::InvalidArchive(
5978            "VolumeTrailer does not match v41 public terminal layout",
5979        ));
5980    }
5981    if trailer.root_auth_flags != 0x0000_0001
5982        || trailer.root_auth_footer_offset == 0
5983        || trailer.root_auth_footer_length == 0
5984        || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5985        || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5986        || trailer.root_auth_footer_length != image.root_auth_footer_length
5987        || image.root_auth_footer_offset
5988            != image
5989                .manifest_footer_offset
5990                .checked_add(MANIFEST_FOOTER_LEN as u64)
5991                .ok_or(FormatError::InvalidArchive(
5992                    "RootAuthFooter trailer boundary overflow",
5993                ))?
5994        || image
5995            .root_auth_footer_offset
5996            .checked_add(image.root_auth_footer_length as u64)
5997            .ok_or(FormatError::InvalidArchive(
5998                "RootAuthFooter trailer boundary overflow",
5999            ))?
6000            != image.volume_trailer_offset
6001    {
6002        return Err(FormatError::InvalidArchive(
6003            "VolumeTrailer root-auth fields do not match v41 public terminal layout",
6004        ));
6005    }
6006    Ok(())
6007}
6008
6009fn critical_image_min() -> u64 {
6010    const MIN_CRYPTO_HEADER_LEN: u64 = 116;
6011    CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
6012        + 4 * SERIALIZED_REGION_HEADER_LEN as u64
6013        + VOLUME_HEADER_LEN as u64
6014        + MIN_CRYPTO_HEADER_LEN
6015        + MANIFEST_FOOTER_LEN as u64
6016        + VOLUME_TRAILER_LEN as u64
6017        + IMAGE_CRC_LEN as u64
6018}
6019
6020fn critical_image_cap() -> Result<u64, FormatError> {
6021    [
6022        CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
6023        5 * SERIALIZED_REGION_HEADER_LEN as u64,
6024        VOLUME_HEADER_LEN as u64,
6025        READER_MAX_CRYPTO_HEADER_LEN as u64,
6026        MANIFEST_FOOTER_LEN as u64,
6027        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
6028        VOLUME_TRAILER_LEN as u64,
6029        IMAGE_CRC_LEN as u64,
6030    ]
6031    .into_iter()
6032    .try_fold(0u64, |total, value| {
6033        total
6034            .checked_add(value)
6035            .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
6036    })
6037}
6038
6039fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
6040    let shard_total = (tuple.data_shard_count as u64)
6041        .checked_add(tuple.parity_shard_count as u64)
6042        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
6043    let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
6044        .checked_add(tuple.shard_size as u64)
6045        .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
6046    checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
6047        .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
6048        .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
6049}
6050
6051fn cmra_worst_case_cap() -> Result<u64, FormatError> {
6052    let cap = critical_image_cap()?;
6053    let mut worst = 0u64;
6054    let mut shard_size = 512u64;
6055    while shard_size <= 4096 {
6056        let data = ceil_div_u64(cap, shard_size)?;
6057        let parity = 2u64.max(ceil_div_u64(
6058            checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
6059            100,
6060        )?);
6061        let tuple = CmraDecoderTuple {
6062            shard_size: shard_size as u32,
6063            data_shard_count: u16::try_from(data)
6064                .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
6065            parity_shard_count: u16::try_from(parity)
6066                .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
6067            image_length: u32::try_from(cap)
6068                .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
6069            image_sha256: [0u8; 32],
6070        };
6071        worst = worst.max(cmra_serialized_length(tuple)?);
6072        shard_size += 2;
6073    }
6074    Ok(worst)
6075}
6076
6077pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
6078    let total = [
6079        MANIFEST_FOOTER_LEN as u64,
6080        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
6081        VOLUME_TRAILER_LEN as u64,
6082        cmra_worst_case_cap()?,
6083        LOCATOR_PAIR_LEN as u64,
6084    ]
6085    .into_iter()
6086    .try_fold(0u64, |sum, value| {
6087        sum.checked_add(value)
6088            .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
6089    })?;
6090    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
6091}
6092
6093fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
6094    let worst = cmra_worst_case_cap()?;
6095    let total = options
6096        .max_trailing_garbage_scan
6097        .try_into()
6098        .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
6099        .and_then(|scan: u64| {
6100            scan.checked_add(worst)
6101                .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
6102                .ok_or(FormatError::InvalidArchive("scan cap overflow"))
6103        })?;
6104    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
6105}
6106
6107fn validate_bootstrap_single_volume_input(
6108    volume_header: &VolumeHeader,
6109    crypto_header: &CryptoHeaderFixed,
6110) -> Result<(), FormatError> {
6111    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
6112        return Err(FormatError::ReaderUnsupported(
6113            "bootstrap sidecar reader supports only single-volume archive input",
6114        ));
6115    }
6116    if crypto_header.stripe_width != volume_header.stripe_width {
6117        return Err(FormatError::InvalidArchive(
6118            "VolumeHeader and CryptoHeader stripe_width differ",
6119        ));
6120    }
6121    Ok(())
6122}
6123
6124#[derive(Debug)]
6125struct ParsedBootstrapSidecar {
6126    manifest_footer: Option<ManifestFooter>,
6127    index_root_records_section: Option<(u64, u64)>,
6128    dictionary_records_section: Option<(u64, u64)>,
6129}
6130
6131pub(crate) struct NonSeekableBootstrapMaterial {
6132    pub(crate) manifest_footer: ManifestFooter,
6133    pub(crate) payload_dictionary: Option<Vec<u8>>,
6134}
6135
6136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6137enum BootstrapSidecarUse {
6138    SeekableAssist,
6139    NonSeekableRandomAccess,
6140}
6141
6142impl ParsedBootstrapSidecar {
6143    fn require_sections_for(
6144        &self,
6145        sidecar_use: BootstrapSidecarUse,
6146        crypto_header: &CryptoHeaderFixed,
6147    ) -> Result<(), FormatError> {
6148        if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
6149            if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
6150                return Err(FormatError::ReaderUnsupported(
6151                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6152                ));
6153            }
6154            if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
6155                return Err(FormatError::ReaderUnsupported(
6156                    "dictionary bootstrap required",
6157                ));
6158            }
6159        }
6160        Ok(())
6161    }
6162}
6163
6164pub(crate) fn parse_non_seekable_bootstrap_material(
6165    bootstrap_sidecar: &[u8],
6166    volume_header: &VolumeHeader,
6167    crypto_header: &CryptoHeaderFixed,
6168    subkeys: &Subkeys,
6169) -> Result<NonSeekableBootstrapMaterial, FormatError> {
6170    validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
6171    let sidecar =
6172        parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
6173    sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
6174    let manifest_footer = sidecar
6175        .manifest_footer
6176        .clone()
6177        .ok_or(FormatError::ReaderUnsupported(
6178            "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6179        ))?;
6180
6181    let mut blocks = BTreeMap::new();
6182    let (offset, length) =
6183        sidecar
6184            .index_root_records_section
6185            .ok_or(FormatError::ReaderUnsupported(
6186                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6187            ))?;
6188    let index_root_records = parse_sidecar_block_records(
6189        bootstrap_sidecar,
6190        crypto_header.block_size as usize,
6191        SidecarBlockRecordsSection {
6192            offset,
6193            length,
6194            extent: index_root_extent_from_manifest(&manifest_footer),
6195            data_kind: BlockKind::IndexRootData,
6196            parity_kind: BlockKind::IndexRootParity,
6197            structure: "IndexRoot",
6198        },
6199    )?;
6200    insert_sidecar_records(&mut blocks, index_root_records)?;
6201
6202    let limits = metadata_limits(crypto_header);
6203    let index_root_plaintext = load_metadata_object_from_parts(
6204        &blocks,
6205        ObjectLoadContext::index_root(
6206            volume_header,
6207            crypto_header,
6208            subkeys,
6209            index_root_extent_from_manifest(&manifest_footer),
6210        ),
6211        manifest_footer.index_root_decompressed_size,
6212    )?;
6213    let index_root = IndexRoot::parse(
6214        &index_root_plaintext,
6215        crypto_header.has_dictionary != 0,
6216        limits,
6217    )?;
6218
6219    if crypto_header.has_dictionary != 0 {
6220        let (offset, length) =
6221            sidecar
6222                .dictionary_records_section
6223                .ok_or(FormatError::ReaderUnsupported(
6224                    "dictionary bootstrap required",
6225                ))?;
6226        let dictionary_records = parse_sidecar_block_records(
6227            bootstrap_sidecar,
6228            crypto_header.block_size as usize,
6229            SidecarBlockRecordsSection {
6230                offset,
6231                length,
6232                extent: dictionary_extent_from_index_root(&index_root)?,
6233                data_kind: BlockKind::DictionaryData,
6234                parity_kind: BlockKind::DictionaryParity,
6235                structure: "dictionary",
6236            },
6237        )?;
6238        insert_sidecar_records(&mut blocks, dictionary_records)?;
6239    }
6240    let payload_dictionary =
6241        load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
6242
6243    Ok(NonSeekableBootstrapMaterial {
6244        manifest_footer,
6245        payload_dictionary,
6246    })
6247}
6248
6249fn parse_bootstrap_sidecar(
6250    bytes: &[u8],
6251    volume_header: &VolumeHeader,
6252    crypto_header: &CryptoHeaderFixed,
6253    subkeys: &Subkeys,
6254) -> Result<ParsedBootstrapSidecar, FormatError> {
6255    let header_bytes = slice(
6256        bytes,
6257        0,
6258        BOOTSTRAP_SIDECAR_HEADER_LEN,
6259        "BootstrapSidecarHeader",
6260    )?;
6261    let header = BootstrapSidecarHeader::parse(header_bytes)?;
6262    if header.archive_uuid != volume_header.archive_uuid
6263        || header.session_id != volume_header.session_id
6264    {
6265        return Err(FormatError::InvalidArchive(
6266            "bootstrap sidecar identity does not match VolumeHeader",
6267        ));
6268    }
6269    verify_integrity_tag(
6270        HmacDomain::BootstrapSidecar,
6271        crypto_header.aead_algo,
6272        Some(&subkeys.mac_key),
6273        &volume_header.archive_uuid,
6274        &volume_header.session_id,
6275        &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
6276        &header.sidecar_hmac,
6277    )?;
6278    header.validate_packed_layout(bytes.len() as u64)?;
6279    validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
6280
6281    if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
6282        return Err(FormatError::InvalidArchive(
6283            "bootstrap sidecar has dictionary records while has_dictionary is false",
6284        ));
6285    }
6286
6287    let manifest_footer = if header.has_manifest_footer() {
6288        let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
6289        let manifest_bytes = slice(
6290            bytes,
6291            manifest_offset,
6292            MANIFEST_FOOTER_LEN,
6293            "ManifestFooter",
6294        )?;
6295        let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
6296        validate_sidecar_manifest_footer(
6297            volume_header,
6298            crypto_header,
6299            &manifest_footer,
6300            subkeys,
6301            manifest_bytes,
6302        )?;
6303        manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
6304        Some(manifest_footer)
6305    } else {
6306        None
6307    };
6308
6309    Ok(ParsedBootstrapSidecar {
6310        manifest_footer,
6311        index_root_records_section: header.has_index_root_records().then_some((
6312            header.index_root_records_offset,
6313            header.index_root_records_length,
6314        )),
6315        dictionary_records_section: header.has_dictionary_records().then_some((
6316            header.dictionary_records_offset,
6317            header.dictionary_records_length,
6318        )),
6319    })
6320}
6321
6322fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
6323    ObjectExtent {
6324        first_block_index: manifest_footer.index_root_first_block,
6325        data_block_count: manifest_footer.index_root_data_block_count,
6326        parity_block_count: manifest_footer.index_root_parity_block_count,
6327        encrypted_size: manifest_footer.index_root_encrypted_size,
6328    }
6329}
6330
6331fn insert_sidecar_records(
6332    blocks: &mut BTreeMap<u64, BlockRecord>,
6333    records: Vec<BlockRecord>,
6334) -> Result<(), FormatError> {
6335    for record in records {
6336        if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
6337            if existing != record {
6338                return Err(FormatError::InvalidArchive(
6339                    "bootstrap sidecar conflicts with volume BlockRecord",
6340                ));
6341            }
6342        }
6343    }
6344    Ok(())
6345}
6346
6347fn validate_sidecar_manifest_footer(
6348    volume_header: &VolumeHeader,
6349    crypto_header: &CryptoHeaderFixed,
6350    footer: &ManifestFooter,
6351    subkeys: &Subkeys,
6352    raw: &[u8],
6353) -> Result<(), FormatError> {
6354    if footer.archive_uuid != volume_header.archive_uuid
6355        || footer.session_id != volume_header.session_id
6356    {
6357        return Err(FormatError::InvalidArchive(
6358            "sidecar ManifestFooter identity does not match VolumeHeader",
6359        ));
6360    }
6361    if footer.volume_index != 0 {
6362        return Err(FormatError::InvalidArchive(
6363            "sidecar ManifestFooter volume_index must be zero",
6364        ));
6365    }
6366    if footer.total_volumes != crypto_header.stripe_width {
6367        return Err(FormatError::InvalidArchive(
6368            "sidecar ManifestFooter total_volumes does not match stripe_width",
6369        ));
6370    }
6371    if footer.is_authoritative != 1 {
6372        return Err(FormatError::InvalidArchive(
6373            "sidecar ManifestFooter is not authoritative",
6374        ));
6375    }
6376    verify_integrity_tag(
6377        HmacDomain::ManifestFooter,
6378        crypto_header.aead_algo,
6379        Some(&subkeys.mac_key),
6380        &volume_header.archive_uuid,
6381        &volume_header.session_id,
6382        &raw[..MANIFEST_HMAC_COVERED_LEN],
6383        &footer.manifest_hmac,
6384    )
6385}
6386
6387fn validate_sidecar_size_cap(
6388    header: &BootstrapSidecarHeader,
6389    crypto_header: &CryptoHeaderFixed,
6390    file_size: u64,
6391) -> Result<(), FormatError> {
6392    let record_len = checked_u64_add(
6393        crypto_header.block_size as u64,
6394        BLOCK_RECORD_FRAMING_LEN as u64,
6395        "bootstrap sidecar cap overflow",
6396    )?;
6397    let max_index_records = crypto_header.index_root_fec_data_shards as u64
6398        + crypto_header.index_root_fec_parity_shards as u64;
6399    let max_record_section_bytes = checked_u64_mul(
6400        max_index_records,
6401        record_len,
6402        "bootstrap sidecar cap overflow",
6403    )?;
6404    if header.index_root_records_length % record_len != 0 {
6405        return Err(FormatError::InvalidArchive(
6406            "bootstrap sidecar IndexRoot records length is not aligned",
6407        ));
6408    }
6409    if header.index_root_records_length / record_len > max_index_records {
6410        return Err(FormatError::InvalidArchive(
6411            "bootstrap sidecar IndexRoot records exceed resource cap",
6412        ));
6413    }
6414    if header.dictionary_records_length % record_len != 0 {
6415        return Err(FormatError::InvalidArchive(
6416            "bootstrap sidecar dictionary records length is not aligned",
6417        ));
6418    }
6419    if header.dictionary_records_length / record_len > max_index_records {
6420        return Err(FormatError::InvalidArchive(
6421            "bootstrap sidecar dictionary records exceed resource cap",
6422        ));
6423    }
6424
6425    let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
6426    if header.has_manifest_footer() {
6427        cap = cap
6428            .checked_add(MANIFEST_FOOTER_LEN as u64)
6429            .ok_or(FormatError::InvalidArchive(
6430                "bootstrap sidecar cap overflow",
6431            ))?;
6432    }
6433    if header.has_index_root_records() {
6434        cap = checked_u64_add(
6435            cap,
6436            max_record_section_bytes,
6437            "bootstrap sidecar cap overflow",
6438        )?;
6439    }
6440    if header.has_dictionary_records() {
6441        cap = checked_u64_add(
6442            cap,
6443            max_record_section_bytes,
6444            "bootstrap sidecar cap overflow",
6445        )?;
6446    }
6447    if file_size > cap {
6448        return Err(FormatError::InvalidArchive(
6449            "bootstrap sidecar exceeds resource cap",
6450        ));
6451    }
6452    Ok(())
6453}
6454
6455#[derive(Debug, Clone, Copy)]
6456struct SidecarBlockRecordsSection {
6457    offset: u64,
6458    length: u64,
6459    extent: ObjectExtent,
6460    data_kind: BlockKind,
6461    parity_kind: BlockKind,
6462    structure: &'static str,
6463}
6464
6465fn parse_sidecar_block_records(
6466    sidecar_bytes: &[u8],
6467    block_size: usize,
6468    section: SidecarBlockRecordsSection,
6469) -> Result<Vec<BlockRecord>, FormatError> {
6470    let record_len = block_size
6471        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6472        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6473    if section.length % record_len as u64 != 0 {
6474        return Err(FormatError::InvalidArchive(
6475            "sidecar BlockRecord section is not aligned",
6476        ));
6477    }
6478    let expected_count =
6479        section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
6480    let actual_count = usize::try_from(section.length / record_len as u64)
6481        .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
6482    if actual_count != expected_count {
6483        return Err(FormatError::InvalidArchive(
6484            "sidecar BlockRecord section does not match declared extent",
6485        ));
6486    }
6487    let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
6488    let raw = slice(
6489        sidecar_bytes,
6490        start,
6491        to_usize(section.length, "BootstrapSidecarHeader")?,
6492        "BootstrapSidecarHeader",
6493    )?;
6494    let mut records = Vec::with_capacity(expected_count);
6495
6496    for idx in 0..expected_count {
6497        let record = BlockRecord::parse(
6498            slice(raw, idx * record_len, record_len, "BlockRecord")?,
6499            block_size,
6500        )?;
6501        let expected_block_index = checked_u64_add(
6502            section.extent.first_block_index,
6503            idx as u64,
6504            section.structure,
6505        )?;
6506        if record.block_index != expected_block_index {
6507            return Err(FormatError::InvalidArchive(
6508                "sidecar BlockRecord section has missing or duplicate blocks",
6509            ));
6510        }
6511        let expected_kind = if idx < section.extent.data_block_count as usize {
6512            section.data_kind
6513        } else {
6514            section.parity_kind
6515        };
6516        if record.kind != expected_kind {
6517            return Err(FormatError::InvalidArchive(
6518                "sidecar BlockRecord section has wrong kind",
6519            ));
6520        }
6521        let should_be_last = idx + 1 == section.extent.data_block_count as usize;
6522        if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
6523        {
6524            return Err(FormatError::InvalidArchive(
6525                "sidecar BlockRecord section has wrong last-data flag",
6526            ));
6527        }
6528        records.push(record);
6529    }
6530
6531    Ok(records)
6532}
6533
6534fn validate_trailer_identity(
6535    volume_header: &VolumeHeader,
6536    trailer: &VolumeTrailer,
6537) -> Result<(), FormatError> {
6538    if trailer.archive_uuid != volume_header.archive_uuid
6539        || trailer.session_id != volume_header.session_id
6540        || trailer.volume_index != volume_header.volume_index
6541    {
6542        return Err(FormatError::InvalidArchive(
6543            "VolumeTrailer identity does not match VolumeHeader",
6544        ));
6545    }
6546    Ok(())
6547}
6548
6549fn validate_manifest_footer(
6550    volume_header: &VolumeHeader,
6551    crypto_header: &CryptoHeaderFixed,
6552    footer: &ManifestFooter,
6553    subkeys: &Subkeys,
6554    raw: &[u8],
6555) -> Result<(), FormatError> {
6556    if footer.archive_uuid != volume_header.archive_uuid
6557        || footer.session_id != volume_header.session_id
6558        || footer.volume_index != volume_header.volume_index
6559    {
6560        return Err(FormatError::InvalidArchive(
6561            "ManifestFooter identity does not match VolumeHeader",
6562        ));
6563    }
6564    if footer.total_volumes != volume_header.stripe_width {
6565        return Err(FormatError::InvalidArchive(
6566            "ManifestFooter total_volumes does not match stripe_width",
6567        ));
6568    }
6569    if footer.is_authoritative != 1 {
6570        return Err(FormatError::InvalidArchive(
6571            "ManifestFooter is not authoritative",
6572        ));
6573    }
6574    verify_integrity_tag(
6575        HmacDomain::ManifestFooter,
6576        crypto_header.aead_algo,
6577        Some(&subkeys.mac_key),
6578        &volume_header.archive_uuid,
6579        &volume_header.session_id,
6580        &raw[..MANIFEST_HMAC_COVERED_LEN],
6581        &footer.manifest_hmac,
6582    )
6583}
6584
6585#[derive(Debug)]
6586struct ParsedBlockRegion {
6587    blocks: BTreeMap<u64, BlockRecord>,
6588    erased_block_indices: BTreeSet<u64>,
6589}
6590
6591fn parse_block_region(
6592    bytes: &[u8],
6593    start: usize,
6594    end: usize,
6595    block_size: usize,
6596    volume_header: &VolumeHeader,
6597    trailer: &VolumeTrailer,
6598) -> Result<ParsedBlockRegion, FormatError> {
6599    if end < start {
6600        return Err(FormatError::InvalidArchive(
6601            "ManifestFooter starts before BlockRecord region",
6602        ));
6603    }
6604    let record_len = block_size
6605        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6606        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6607    let region_len = end - start;
6608    if region_len % record_len != 0 {
6609        return Err(FormatError::InvalidArchive(
6610            "BlockRecord region length is not aligned",
6611        ));
6612    }
6613    let observed_count = region_len / record_len;
6614    if observed_count as u64 != trailer.block_count {
6615        return Err(FormatError::InvalidArchive(
6616            "VolumeTrailer block_count does not match BlockRecord region",
6617        ));
6618    }
6619
6620    let mut blocks = BTreeMap::new();
6621    let mut erased_block_indices = BTreeSet::new();
6622    for idx in 0..observed_count {
6623        let offset = start + idx * record_len;
6624        let expected_block_index = checked_u64_add(
6625            volume_header.volume_index as u64,
6626            checked_u64_mul(
6627                idx as u64,
6628                volume_header.stripe_width as u64,
6629                "BlockRecord index overflow",
6630            )?,
6631            "BlockRecord index overflow",
6632        )?;
6633        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6634        match BlockRecord::parse(raw, block_size) {
6635            Ok(record) => {
6636                if record.block_index != expected_block_index {
6637                    return Err(FormatError::InvalidArchive(
6638                        "BlockRecord index does not match volume position",
6639                    ));
6640                }
6641                if blocks.insert(record.block_index, record).is_some() {
6642                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6643                }
6644            }
6645            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
6646                if !erased_block_indices.insert(expected_block_index) {
6647                    return Err(FormatError::InvalidArchive(
6648                        "duplicate erased BlockRecord index",
6649                    ));
6650                }
6651            }
6652            Err(err) => return Err(err),
6653        }
6654    }
6655
6656    Ok(ParsedBlockRegion {
6657        blocks,
6658        erased_block_indices,
6659    })
6660}
6661
6662fn validate_seekable_block_region_layout(
6663    start: u64,
6664    end: u64,
6665    block_size: usize,
6666    trailer: &VolumeTrailer,
6667) -> Result<(), FormatError> {
6668    if end < start {
6669        return Err(FormatError::InvalidArchive(
6670            "ManifestFooter starts before BlockRecord region",
6671        ));
6672    }
6673    let record_len = block_record_len(block_size)?;
6674    let region_len = end - start;
6675    if region_len % record_len != 0 {
6676        return Err(FormatError::InvalidArchive(
6677            "BlockRecord region length is not aligned",
6678        ));
6679    }
6680    let observed_count = region_len / record_len;
6681    if observed_count != trailer.block_count {
6682        return Err(FormatError::InvalidArchive(
6683            "VolumeTrailer block_count does not match BlockRecord region",
6684        ));
6685    }
6686    Ok(())
6687}
6688
6689fn parse_public_block_observation(
6690    bytes: &[u8],
6691    start: usize,
6692    image: &CriticalMetadataImage,
6693    block_size: usize,
6694    volume_header: &VolumeHeader,
6695) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
6696    let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
6697    if start != image_start {
6698        return Err(FormatError::InvalidArchive(
6699            "public BlockRecord observation start mismatch",
6700        ));
6701    }
6702    let scan_limit_u64 = image
6703        .block_records_offset
6704        .checked_add(image.block_records_length)
6705        .ok_or(FormatError::InvalidArchive(
6706            "public BlockRecord observation limit overflow",
6707        ))?;
6708    if scan_limit_u64 != image.manifest_footer_offset {
6709        return Err(FormatError::InvalidArchive(
6710            "public BlockRecord observation limit mismatch",
6711        ));
6712    }
6713    let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
6714    if scan_limit < start {
6715        return Err(FormatError::InvalidArchive(
6716            "public BlockRecord observation limit before start",
6717        ));
6718    }
6719    let record_len = block_size
6720        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6721        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6722    let region_len = scan_limit - start;
6723    if region_len % record_len != 0 {
6724        return Err(FormatError::InvalidArchive(
6725            "public BlockRecord observation window is not aligned",
6726        ));
6727    }
6728
6729    let mut blocks = BTreeMap::new();
6730    let mut offset = start;
6731    let mut observed_slot = 0u64;
6732    while offset < scan_limit {
6733        let magic_end = checked_add(offset, 4, "BlockRecord")?;
6734        if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
6735            break;
6736        }
6737        let record_end = checked_add(offset, record_len, "BlockRecord")?;
6738        if record_end > scan_limit {
6739            return Err(FormatError::InvalidArchive(
6740                "public BlockRecord observation slot is incomplete",
6741            ));
6742        }
6743        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6744        let record = BlockRecord::parse(raw, block_size)?;
6745        let expected_block_index = checked_u64_add(
6746            volume_header.volume_index as u64,
6747            checked_u64_mul(
6748                observed_slot,
6749                volume_header.stripe_width as u64,
6750                "BlockRecord index overflow",
6751            )?,
6752            "BlockRecord index overflow",
6753        )?;
6754        if record.block_index != expected_block_index {
6755            return Err(FormatError::InvalidArchive(
6756                "public BlockRecord index does not match volume position",
6757            ));
6758        }
6759        if blocks.insert(record.block_index, record).is_some() {
6760            return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6761        }
6762        offset = record_end;
6763        observed_slot = observed_slot
6764            .checked_add(1)
6765            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6766    }
6767
6768    let mut scan = if offset < scan_limit {
6769        checked_add(offset, record_len, "BlockRecord")?
6770    } else {
6771        scan_limit
6772    };
6773    while scan < scan_limit {
6774        let magic_end = checked_add(scan, 4, "BlockRecord")?;
6775        let record_end = checked_add(scan, record_len, "BlockRecord")?;
6776        if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
6777            let raw = slice(bytes, scan, record_len, "BlockRecord")?;
6778            if BlockRecord::parse(raw, block_size).is_ok() {
6779                return Err(FormatError::InvalidArchive(
6780                    "public observation has ambiguous extra BlockRecord",
6781                ));
6782            }
6783        }
6784        scan = record_end;
6785    }
6786
6787    Ok(blocks)
6788}
6789
6790pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
6791    matches!(
6792        error,
6793        FormatError::BadCrc {
6794            structure: "BlockRecord",
6795        } | FormatError::BadMagic {
6796            structure: "BlockRecord",
6797        } | FormatError::NonZeroReserved {
6798            structure: "BlockRecord",
6799        }
6800    )
6801}
6802
6803fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
6804    let len = block_size
6805        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6806        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6807    u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
6808}
6809
6810fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
6811    lhs.checked_mul(rhs)
6812        .ok_or(FormatError::InvalidArchive(reason))
6813}
6814
6815fn parse_stream_block_prefix(
6816    bytes: &[u8],
6817    start: usize,
6818    block_size: usize,
6819    volume_header: &VolumeHeader,
6820) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
6821    let record_len = block_size
6822        .checked_add(BLOCK_RECORD_FRAMING_LEN)
6823        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6824    let mut blocks = BTreeMap::new();
6825    let mut offset = start;
6826    let mut observed_block_count = 0u64;
6827
6828    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6829        let expected_block_index =
6830            expected_stream_block_index(volume_header, observed_block_count)?;
6831        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6832        match BlockRecord::parse(raw, block_size) {
6833            Ok(record) => {
6834                if record.block_index != expected_block_index {
6835                    return Err(FormatError::InvalidArchive(
6836                        "BlockRecord index does not match stream position",
6837                    ));
6838                }
6839                if blocks.insert(record.block_index, record).is_some() {
6840                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6841                }
6842            }
6843            Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
6844            Err(err) => return Err(err),
6845        }
6846        offset = checked_add(offset, record_len, "BlockRecord")?;
6847        observed_block_count = observed_block_count
6848            .checked_add(1)
6849            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6850    }
6851
6852    Ok((blocks, offset, observed_block_count))
6853}
6854
6855pub(crate) fn expected_stream_block_index(
6856    volume_header: &VolumeHeader,
6857    observed_block_count: u64,
6858) -> Result<u64, FormatError> {
6859    checked_u64_add(
6860        volume_header.volume_index as u64,
6861        checked_u64_mul(
6862            observed_block_count,
6863            volume_header.stripe_width as u64,
6864            "BlockRecord index overflow",
6865        )?,
6866        "BlockRecord index overflow",
6867    )
6868}
6869
6870fn parse_sequential_block_or_erasure(
6871    bytes: &[u8],
6872    offset: usize,
6873    record_len: usize,
6874    block_size: usize,
6875    volume_header: &VolumeHeader,
6876    observed_block_count: u64,
6877) -> Result<Option<BlockRecord>, FormatError> {
6878    let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
6879    let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6880    match BlockRecord::parse(raw, block_size) {
6881        Ok(record) => {
6882            if record.block_index != expected_block_index {
6883                return Err(FormatError::InvalidArchive(
6884                    "BlockRecord index does not match stream position",
6885                ));
6886            }
6887            Ok(Some(record))
6888        }
6889        Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
6890        Err(err) => Err(err),
6891    }
6892}
6893
6894fn parse_terminal_material(
6895    bytes: &[u8],
6896    manifest_offset: usize,
6897    observed_block_count: u64,
6898    context: KeyHoldingTerminalContext<'_>,
6899    options: ReaderOptions,
6900) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
6901    let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
6902    if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
6903        return Err(FormatError::InvalidArchive(
6904            "sequential terminal does not end at EOF",
6905        ));
6906    }
6907    let terminal = candidate.terminal;
6908    if terminal.image.manifest_footer_offset != manifest_offset as u64 {
6909        return Err(FormatError::InvalidArchive(
6910            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6911        ));
6912    }
6913    if terminal.volume_trailer.block_count != observed_block_count {
6914        return Err(FormatError::InvalidArchive(
6915            "VolumeTrailer block_count does not match observed stream",
6916        ));
6917    }
6918    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6919    Ok((
6920        manifest_footer,
6921        terminal.volume_trailer,
6922        terminal.root_auth_footer,
6923    ))
6924}
6925
6926pub(crate) fn parse_terminal_material_read_at(
6927    reader: &dyn ArchiveReadAt,
6928    input_len: u64,
6929    manifest_offset: u64,
6930    observed_block_count: u64,
6931    context: KeyHoldingTerminalContext<'_>,
6932) -> Result<SequentialTerminalMaterial, FormatError> {
6933    let mut candidates = Vec::new();
6934    if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
6935        collect_v41_locator_candidate_read_at(
6936            reader,
6937            input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6938            0,
6939            context,
6940            &mut candidates,
6941        );
6942    }
6943    if input_len >= LOCATOR_PAIR_LEN as u64 {
6944        collect_v41_locator_candidate_read_at(
6945            reader,
6946            input_len - LOCATOR_PAIR_LEN as u64,
6947            1,
6948            context,
6949            &mut candidates,
6950        );
6951    }
6952
6953    let candidate = choose_v41_terminal_candidate(candidates)?;
6954    if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
6955        return Err(FormatError::InvalidArchive(
6956            "sequential terminal does not end at EOF",
6957        ));
6958    }
6959    let terminal = candidate.terminal;
6960    if terminal.image.manifest_footer_offset != manifest_offset {
6961        return Err(FormatError::InvalidArchive(
6962            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6963        ));
6964    }
6965    if terminal.volume_trailer.block_count != observed_block_count {
6966        return Err(FormatError::InvalidArchive(
6967            "VolumeTrailer block_count does not match observed stream",
6968        ));
6969    }
6970    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6971    Ok(SequentialTerminalMaterial {
6972        manifest_footer,
6973        volume_trailer: terminal.volume_trailer,
6974        root_auth_footer: terminal.root_auth_footer,
6975    })
6976}
6977
6978fn terminal_candidate_reaches_eof(
6979    candidate: &TerminalCandidate,
6980    input_len: usize,
6981) -> Result<bool, FormatError> {
6982    let expected_end =
6983        match candidate.locator_sequence {
6984            Some(0) => candidate.anchor,
6985            Some(1) => candidate
6986                .anchor
6987                .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6988                .ok_or(FormatError::InvalidArchive(
6989                    "terminal EOF boundary overflow",
6990                ))?,
6991            None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
6992                FormatError::InvalidArchive("terminal EOF boundary overflow"),
6993            )?,
6994            Some(_) => {
6995                return Err(FormatError::InvalidArchive(
6996                    "invalid terminal locator sequence",
6997                ))
6998            }
6999        };
7000    Ok(expected_end == input_len)
7001}
7002
7003#[derive(Debug, Default)]
7004struct PendingSequentialEnvelope {
7005    data_shards: Vec<Option<Vec<u8>>>,
7006    parity_shards: Vec<Option<Vec<u8>>>,
7007    saw_last_data: bool,
7008    awaiting_tentative_parity: bool,
7009}
7010
7011impl PendingSequentialEnvelope {
7012    fn is_empty(&self) -> bool {
7013        self.data_shards.is_empty() && self.parity_shards.is_empty()
7014    }
7015}
7016
7017fn handle_sequential_payload_erasure(
7018    pending: &mut PendingSequentialEnvelope,
7019    crypto_header: &CryptoHeaderFixed,
7020    metadata_seen: bool,
7021) -> Result<(), FormatError> {
7022    if metadata_seen || pending.saw_last_data {
7023        return Err(FormatError::BadCrc {
7024            structure: "BlockRecord",
7025        });
7026    }
7027    if !sequential_payload_parity_is_guaranteed(crypto_header) {
7028        return Err(FormatError::BadCrc {
7029            structure: "BlockRecord",
7030        });
7031    }
7032    pending.data_shards.push(None);
7033    pending.awaiting_tentative_parity = true;
7034    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
7035        return Err(FormatError::InvalidArchive(
7036            "sequential payload envelope exceeds data-shard cap",
7037        ));
7038    }
7039    Ok(())
7040}
7041
7042fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
7043    crypto_header.fec_parity_shards > 0
7044        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
7045}
7046
7047fn sequential_extract_tar_stream_with_options(
7048    bytes: &[u8],
7049    master_key: &MasterKey,
7050    options: ReaderOptions,
7051) -> Result<Vec<u8>, FormatError> {
7052    validate_reader_options(options)?;
7053    if bytes.len() < VOLUME_HEADER_LEN {
7054        return Err(FormatError::InvalidLength {
7055            structure: "archive",
7056            expected: VOLUME_HEADER_LEN,
7057            actual: bytes.len(),
7058        });
7059    }
7060
7061    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
7062    let crypto_start = volume_header.crypto_header_offset as usize;
7063    let crypto_len = volume_header.crypto_header_length as usize;
7064    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
7065    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
7066    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
7067    let subkeys = subkeys_for_open(
7068        Some(master_key),
7069        parsed_crypto.fixed.aead_algo,
7070        &volume_header.archive_uuid,
7071        &volume_header.session_id,
7072    )?;
7073    verify_integrity_tag(
7074        HmacDomain::CryptoHeader,
7075        parsed_crypto.fixed.aead_algo,
7076        Some(&subkeys.mac_key),
7077        &volume_header.archive_uuid,
7078        &volume_header.session_id,
7079        parsed_crypto.hmac_covered_bytes,
7080        &parsed_crypto.header_hmac,
7081    )?;
7082    parsed_crypto.validate_extension_semantics()?;
7083    validate_sequential_supported_volume(
7084        &volume_header,
7085        &parsed_crypto.fixed,
7086        &parsed_crypto.extensions,
7087    )?;
7088    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7089
7090    let block_size = parsed_crypto.fixed.block_size as usize;
7091    let record_len = block_size
7092        .checked_add(BLOCK_RECORD_FRAMING_LEN)
7093        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
7094    let mut offset = crypto_end;
7095    let mut observed_block_count = 0u64;
7096    let mut metadata_seen = false;
7097    let mut pending = PendingSequentialEnvelope::default();
7098    let mut next_envelope_index = 0u64;
7099    let mut tar_stream = Vec::new();
7100    let max_tar_stream_size = options.max_verify_tar_size;
7101    let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
7102    let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
7103    let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
7104        parsed_crypto.fixed.max_path_length,
7105        total_extraction_cap,
7106    );
7107
7108    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
7109        let record = parse_sequential_block_or_erasure(
7110            bytes,
7111            offset,
7112            record_len,
7113            block_size,
7114            &volume_header,
7115            observed_block_count,
7116        )?;
7117        observed_block_count = observed_block_count
7118            .checked_add(1)
7119            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
7120        let Some(record) = record else {
7121            handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
7122            offset = checked_add(offset, record_len, "BlockRecord")?;
7123            continue;
7124        };
7125
7126        match record.kind {
7127            BlockKind::PayloadData => {
7128                if metadata_seen {
7129                    return Err(FormatError::InvalidArchive(
7130                        "payload BlockRecord appears after metadata",
7131                    ));
7132                }
7133                if pending.awaiting_tentative_parity {
7134                    return Err(FormatError::InvalidArchive(
7135                        "sequential payload envelope boundary is ambiguous after CRC erasure",
7136                    ));
7137                }
7138                if pending.saw_last_data {
7139                    finalize_sequential_envelope(
7140                        &mut pending,
7141                        SequentialEnvelopeDecodeContext {
7142                            crypto_header: &parsed_crypto.fixed,
7143                            subkeys: &subkeys,
7144                            volume_header: &volume_header,
7145                            next_envelope_index: &mut next_envelope_index,
7146                            tar_stream: &mut tar_stream,
7147                            max_tar_stream_size,
7148                            tar_stream_total_validator: &mut tar_stream_total_validator,
7149                        },
7150                    )?;
7151                }
7152                let is_last_data = record.is_last_data();
7153                pending.data_shards.push(Some(record.payload));
7154                if is_last_data {
7155                    pending.saw_last_data = true;
7156                }
7157                if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
7158                    return Err(FormatError::InvalidArchive(
7159                        "sequential payload envelope exceeds data-shard cap",
7160                    ));
7161                }
7162            }
7163            BlockKind::PayloadParity => {
7164                if metadata_seen {
7165                    return Err(FormatError::InvalidArchive(
7166                        "payload parity BlockRecord appears after metadata",
7167                    ));
7168                }
7169                if pending.awaiting_tentative_parity {
7170                    pending.awaiting_tentative_parity = false;
7171                    pending.saw_last_data = true;
7172                } else if pending.data_shards.is_empty() || !pending.saw_last_data {
7173                    return Err(FormatError::InvalidArchive(
7174                        "payload parity appears before envelope data is complete",
7175                    ));
7176                }
7177                pending.parity_shards.push(Some(record.payload));
7178                if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
7179                    return Err(FormatError::InvalidArchive(
7180                        "sequential payload envelope exceeds parity-shard cap",
7181                    ));
7182                }
7183            }
7184            _ => {
7185                if !pending.is_empty() {
7186                    finalize_sequential_envelope(
7187                        &mut pending,
7188                        SequentialEnvelopeDecodeContext {
7189                            crypto_header: &parsed_crypto.fixed,
7190                            subkeys: &subkeys,
7191                            volume_header: &volume_header,
7192                            next_envelope_index: &mut next_envelope_index,
7193                            tar_stream: &mut tar_stream,
7194                            max_tar_stream_size,
7195                            tar_stream_total_validator: &mut tar_stream_total_validator,
7196                        },
7197                    )?;
7198                }
7199                metadata_seen = true;
7200            }
7201        }
7202
7203        offset = checked_add(offset, record_len, "BlockRecord")?;
7204    }
7205
7206    if !pending.is_empty() {
7207        finalize_sequential_envelope(
7208            &mut pending,
7209            SequentialEnvelopeDecodeContext {
7210                crypto_header: &parsed_crypto.fixed,
7211                subkeys: &subkeys,
7212                volume_header: &volume_header,
7213                next_envelope_index: &mut next_envelope_index,
7214                tar_stream: &mut tar_stream,
7215                max_tar_stream_size,
7216                tar_stream_total_validator: &mut tar_stream_total_validator,
7217            },
7218        )?;
7219    }
7220
7221    parse_terminal_material(
7222        bytes,
7223        offset,
7224        observed_block_count,
7225        KeyHoldingTerminalContext {
7226            subkeys: &subkeys,
7227            volume_header: &volume_header,
7228            crypto_header: &parsed_crypto.fixed,
7229            crypto_header_bytes: crypto_bytes,
7230        },
7231        options,
7232    )?;
7233    // This public helper is intentionally whole-buffer: decoded payload bytes
7234    // stay internal until terminal ManifestFooter and VolumeTrailer HMACs pass.
7235    validate_tar_stream_total_extraction_size(
7236        &tar_stream,
7237        parsed_crypto.fixed.max_path_length,
7238        total_extraction_cap,
7239    )?;
7240    Ok(tar_stream)
7241}
7242
7243fn validate_sequential_supported_volume(
7244    volume_header: &VolumeHeader,
7245    crypto_header: &CryptoHeaderFixed,
7246    extensions: &[ExtensionTlv<'_>],
7247) -> Result<(), FormatError> {
7248    reject_unsupported_raw_stream_profile(extensions)?;
7249    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
7250        return Err(FormatError::ReaderUnsupported(
7251            "sequential reader supports only single-volume archive input",
7252        ));
7253    }
7254    if crypto_header.stripe_width != volume_header.stripe_width {
7255        return Err(FormatError::InvalidArchive(
7256            "VolumeHeader and CryptoHeader stripe_width differ",
7257        ));
7258    }
7259    if crypto_header.has_dictionary != 0 {
7260        return Err(FormatError::ReaderUnsupported(
7261            "dictionary bootstrap required for non-seekable sequential extraction",
7262        ));
7263    }
7264    Ok(())
7265}
7266
7267struct SequentialEnvelopeDecodeContext<'a> {
7268    crypto_header: &'a CryptoHeaderFixed,
7269    subkeys: &'a Subkeys,
7270    volume_header: &'a VolumeHeader,
7271    next_envelope_index: &'a mut u64,
7272    tar_stream: &'a mut Vec<u8>,
7273    max_tar_stream_size: usize,
7274    tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
7275}
7276
7277fn finalize_sequential_envelope(
7278    pending: &mut PendingSequentialEnvelope,
7279    context: SequentialEnvelopeDecodeContext<'_>,
7280) -> Result<(), FormatError> {
7281    if !pending.saw_last_data {
7282        return Err(FormatError::InvalidArchive(
7283            "sequential payload envelope is missing last-data flag",
7284        ));
7285    }
7286    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
7287        return Err(FormatError::InvalidArchive(
7288            "sequential payload envelope exceeds data-shard cap",
7289        ));
7290    }
7291    if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
7292        return Err(FormatError::InvalidArchive(
7293            "sequential payload envelope exceeds parity-shard cap",
7294        ));
7295    }
7296    let required_parity =
7297        required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
7298    if pending.parity_shards.len() < required_parity as usize {
7299        return Err(FormatError::InvalidArchive(
7300            "sequential payload envelope has insufficient parity for recovery settings",
7301        ));
7302    }
7303
7304    let repaired = repair_data_gf16(
7305        &pending.data_shards,
7306        &pending.parity_shards,
7307        context.crypto_header.block_size as usize,
7308    )?;
7309    let mut encrypted =
7310        Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
7311    for shard in repaired {
7312        encrypted.extend_from_slice(&shard);
7313    }
7314    let plaintext = decrypt_padded_aead_object(
7315        AeadObjectContext {
7316            algo: context.crypto_header.aead_algo,
7317            key: &context.subkeys.enc_key,
7318            nonce_seed: &context.subkeys.nonce_seed,
7319            domain: b"envelope",
7320            archive_uuid: &context.volume_header.archive_uuid,
7321            session_id: &context.volume_header.session_id,
7322            counter: *context.next_envelope_index,
7323        },
7324        &encrypted,
7325    )?;
7326    decode_concatenated_zstd_frames_with_cap(
7327        &plaintext,
7328        None,
7329        context.tar_stream,
7330        context.max_tar_stream_size,
7331        Some(context.tar_stream_total_validator),
7332    )?;
7333    *context.next_envelope_index = (*context.next_envelope_index)
7334        .checked_add(1)
7335        .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
7336    *pending = PendingSequentialEnvelope::default();
7337    Ok(())
7338}
7339
7340fn decode_concatenated_zstd_frames_with_cap(
7341    plaintext: &[u8],
7342    dictionary: Option<&[u8]>,
7343    output: &mut Vec<u8>,
7344    max_output_len: usize,
7345    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7346) -> Result<(), FormatError> {
7347    let mut cursor = 0usize;
7348    while cursor < plaintext.len() {
7349        let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
7350            .map_err(|_| FormatError::InvalidZstdFrame)?;
7351        if frame_len == 0 {
7352            return Err(FormatError::InvalidZstdFrame);
7353        }
7354        let end = checked_add(cursor, frame_len, "zstd frame")?;
7355        validate_exact_zstd_frame(&plaintext[cursor..end])?;
7356        if let Some(dictionary) = dictionary {
7357            let mut decoder =
7358                zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
7359                    .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7360            read_zstd_frame_to_capped_output(
7361                &mut decoder,
7362                output,
7363                max_output_len,
7364                tar_stream_total_validator.as_deref_mut(),
7365            )?;
7366        } else {
7367            let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
7368                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7369            read_zstd_frame_to_capped_output(
7370                &mut decoder,
7371                output,
7372                max_output_len,
7373                tar_stream_total_validator.as_deref_mut(),
7374            )?;
7375        }
7376        cursor = end;
7377    }
7378    Ok(())
7379}
7380
7381fn read_zstd_frame_to_capped_output<R: Read>(
7382    decoder: &mut R,
7383    output: &mut Vec<u8>,
7384    max_output_len: usize,
7385    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7386) -> Result<(), FormatError> {
7387    let mut buf = [0u8; 64 * 1024];
7388    loop {
7389        let read = decoder
7390            .read(&mut buf)
7391            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7392        if read == 0 {
7393            return Ok(());
7394        }
7395        let next_len = output
7396            .len()
7397            .checked_add(read)
7398            .ok_or(FormatError::ReaderUnsupported(
7399                "sequential tar stream exceeds configured verification cap",
7400            ))?;
7401        if next_len > max_output_len {
7402            return Err(FormatError::ReaderUnsupported(
7403                "sequential tar stream exceeds configured verification cap",
7404            ));
7405        }
7406        output.extend_from_slice(&buf[..read]);
7407        if let Some(validator) = tar_stream_total_validator.as_mut() {
7408            validator.observe(output)?;
7409        }
7410    }
7411}
7412
7413fn load_archive_dictionary(
7414    blocks: &impl BlockProvider,
7415    subkeys: &Subkeys,
7416    volume_header: &VolumeHeader,
7417    crypto_header: &CryptoHeaderFixed,
7418    index_root: &IndexRoot,
7419) -> Result<Option<Vec<u8>>, FormatError> {
7420    if crypto_header.has_dictionary == 0 {
7421        return Ok(None);
7422    }
7423    let plaintext = load_metadata_object_from_parts(
7424        blocks,
7425        ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
7426        index_root.header.dictionary_decompressed_size,
7427    )?;
7428    Ok(Some(plaintext))
7429}
7430
7431#[derive(Clone, Copy)]
7432struct ObjectLoadContext<'a> {
7433    volume_header: &'a VolumeHeader,
7434    crypto_header: &'a CryptoHeaderFixed,
7435    extent: ObjectExtent,
7436    data_kind: BlockKind,
7437    parity_kind: BlockKind,
7438    key: &'a [u8; 32],
7439    nonce_seed: &'a [u8; 32],
7440    domain: &'a [u8],
7441    counter: u64,
7442    class_data_shard_max: u16,
7443    class_parity_shard_max: u16,
7444}
7445
7446impl<'a> ObjectLoadContext<'a> {
7447    fn index_root(
7448        volume_header: &'a VolumeHeader,
7449        crypto_header: &'a CryptoHeaderFixed,
7450        subkeys: &'a Subkeys,
7451        extent: ObjectExtent,
7452    ) -> Self {
7453        Self {
7454            volume_header,
7455            crypto_header,
7456            extent,
7457            data_kind: BlockKind::IndexRootData,
7458            parity_kind: BlockKind::IndexRootParity,
7459            key: &subkeys.index_root_key,
7460            nonce_seed: &subkeys.index_nonce_seed,
7461            domain: b"idxroot",
7462            counter: 0,
7463            class_data_shard_max: crypto_header.index_root_fec_data_shards,
7464            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7465        }
7466    }
7467
7468    fn index_shard(
7469        volume_header: &'a VolumeHeader,
7470        crypto_header: &'a CryptoHeaderFixed,
7471        subkeys: &'a Subkeys,
7472        entry: &ShardEntry,
7473    ) -> Self {
7474        Self {
7475            volume_header,
7476            crypto_header,
7477            extent: ObjectExtent {
7478                first_block_index: entry.first_block_index,
7479                data_block_count: entry.data_block_count,
7480                parity_block_count: entry.parity_block_count,
7481                encrypted_size: entry.encrypted_size,
7482            },
7483            data_kind: BlockKind::IndexShardData,
7484            parity_kind: BlockKind::IndexShardParity,
7485            key: &subkeys.index_shard_key,
7486            nonce_seed: &subkeys.index_nonce_seed,
7487            domain: b"idxshard",
7488            counter: entry.shard_index,
7489            class_data_shard_max: crypto_header.index_fec_data_shards,
7490            class_parity_shard_max: crypto_header.index_fec_parity_shards,
7491        }
7492    }
7493
7494    fn directory_hint(
7495        volume_header: &'a VolumeHeader,
7496        crypto_header: &'a CryptoHeaderFixed,
7497        subkeys: &'a Subkeys,
7498        entry: &DirectoryHintShardEntry,
7499    ) -> Self {
7500        Self {
7501            volume_header,
7502            crypto_header,
7503            extent: ObjectExtent {
7504                first_block_index: entry.first_block_index,
7505                data_block_count: entry.data_block_count,
7506                parity_block_count: entry.parity_block_count,
7507                encrypted_size: entry.encrypted_size,
7508            },
7509            data_kind: BlockKind::DirectoryHintData,
7510            parity_kind: BlockKind::DirectoryHintParity,
7511            key: &subkeys.dir_hint_key,
7512            nonce_seed: &subkeys.index_nonce_seed,
7513            domain: b"dirhint",
7514            counter: entry.hint_shard_index,
7515            class_data_shard_max: crypto_header.index_fec_data_shards,
7516            class_parity_shard_max: crypto_header.index_fec_parity_shards,
7517        }
7518    }
7519
7520    fn dictionary(
7521        volume_header: &'a VolumeHeader,
7522        crypto_header: &'a CryptoHeaderFixed,
7523        subkeys: &'a Subkeys,
7524        index_root: &IndexRoot,
7525    ) -> Result<Self, FormatError> {
7526        Ok(Self {
7527            volume_header,
7528            crypto_header,
7529            extent: dictionary_extent_from_index_root(index_root)?,
7530            data_kind: BlockKind::DictionaryData,
7531            parity_kind: BlockKind::DictionaryParity,
7532            key: &subkeys.dictionary_key,
7533            nonce_seed: &subkeys.index_nonce_seed,
7534            domain: b"dict",
7535            counter: 0,
7536            class_data_shard_max: crypto_header.index_root_fec_data_shards,
7537            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7538        })
7539    }
7540
7541    fn payload(
7542        volume_header: &'a VolumeHeader,
7543        crypto_header: &'a CryptoHeaderFixed,
7544        subkeys: &'a Subkeys,
7545        envelope: &EnvelopeEntry,
7546    ) -> Self {
7547        Self {
7548            volume_header,
7549            crypto_header,
7550            extent: ObjectExtent {
7551                first_block_index: envelope.first_block_index,
7552                data_block_count: envelope.data_block_count,
7553                parity_block_count: envelope.parity_block_count,
7554                encrypted_size: envelope.encrypted_size,
7555            },
7556            data_kind: BlockKind::PayloadData,
7557            parity_kind: BlockKind::PayloadParity,
7558            key: &subkeys.enc_key,
7559            nonce_seed: &subkeys.nonce_seed,
7560            domain: b"envelope",
7561            counter: envelope.envelope_index,
7562            class_data_shard_max: crypto_header.fec_data_shards,
7563            class_parity_shard_max: crypto_header.fec_parity_shards,
7564        }
7565    }
7566}
7567
7568fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
7569    if index_root.header.dictionary_data_block_count == 0
7570        || index_root.header.dictionary_encrypted_size == 0
7571        || index_root.header.dictionary_decompressed_size == 0
7572    {
7573        return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
7574    }
7575    Ok(ObjectExtent {
7576        first_block_index: index_root.header.dictionary_first_block,
7577        data_block_count: index_root.header.dictionary_data_block_count,
7578        parity_block_count: index_root.header.dictionary_parity_block_count,
7579        encrypted_size: index_root.header.dictionary_encrypted_size,
7580    })
7581}
7582
7583fn load_metadata_object_from_parts(
7584    blocks: &impl BlockProvider,
7585    context: ObjectLoadContext<'_>,
7586    decompressed_size: u32,
7587) -> Result<Vec<u8>, FormatError> {
7588    let compressed = load_decrypted_object_from_parts(blocks, context)?;
7589    decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
7590}
7591
7592fn load_decrypted_object_from_parts(
7593    blocks: &impl BlockProvider,
7594    context: ObjectLoadContext<'_>,
7595) -> Result<Vec<u8>, FormatError> {
7596    load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
7597}
7598
7599fn load_decrypted_object_from_parts_with_parity_policy(
7600    blocks: &impl BlockProvider,
7601    context: ObjectLoadContext<'_>,
7602    parity_policy: ParityReadPolicy,
7603) -> Result<Vec<u8>, FormatError> {
7604    let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
7605        blocks,
7606        context.crypto_header,
7607        context.extent,
7608        context.data_kind,
7609        context.parity_kind,
7610        context.class_data_shard_max,
7611        context.class_parity_shard_max,
7612        parity_policy,
7613    )?;
7614    let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
7615    for shard in repaired {
7616        encrypted.extend_from_slice(&shard);
7617    }
7618    if encrypted.len() != context.extent.encrypted_size as usize {
7619        return Err(FormatError::InvalidArchive(
7620            "object encrypted size does not match repaired shards",
7621        ));
7622    }
7623
7624    decrypt_padded_aead_object(
7625        AeadObjectContext {
7626            algo: context.crypto_header.aead_algo,
7627            key: context.key,
7628            nonce_seed: context.nonce_seed,
7629            domain: context.domain,
7630            archive_uuid: &context.volume_header.archive_uuid,
7631            session_id: &context.volume_header.session_id,
7632            counter: context.counter,
7633        },
7634        &encrypted,
7635    )
7636}
7637
7638fn load_repaired_object_data_shards_from_parts(
7639    blocks: &impl BlockProvider,
7640    crypto_header: &CryptoHeaderFixed,
7641    extent: ObjectExtent,
7642    data_kind: BlockKind,
7643    parity_kind: BlockKind,
7644    class_data_shard_max: u16,
7645    class_parity_shard_max: u16,
7646) -> Result<Vec<Vec<u8>>, FormatError> {
7647    load_repaired_object_data_shards_from_parts_with_parity_policy(
7648        blocks,
7649        crypto_header,
7650        extent,
7651        data_kind,
7652        parity_kind,
7653        class_data_shard_max,
7654        class_parity_shard_max,
7655        ParityReadPolicy::Always,
7656    )
7657}
7658
7659#[allow(clippy::too_many_arguments)]
7660fn load_repaired_object_data_shards_from_parts_with_parity_policy(
7661    blocks: &impl BlockProvider,
7662    crypto_header: &CryptoHeaderFixed,
7663    extent: ObjectExtent,
7664    data_kind: BlockKind,
7665    parity_kind: BlockKind,
7666    class_data_shard_max: u16,
7667    class_parity_shard_max: u16,
7668    parity_policy: ParityReadPolicy,
7669) -> Result<Vec<Vec<u8>>, FormatError> {
7670    validate_object_extent(
7671        extent,
7672        crypto_header,
7673        class_data_shard_max,
7674        class_parity_shard_max,
7675    )?;
7676    let block_size = crypto_header.block_size as usize;
7677    let data_count = extent.data_block_count as usize;
7678    let parity_count = extent.parity_block_count as usize;
7679    let mut data_shards = Vec::with_capacity(data_count);
7680    let mut parity_shards = Vec::with_capacity(parity_count);
7681
7682    for offset in 0..data_count {
7683        let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
7684        if let Some(record) = blocks.block(block_index)? {
7685            if record.kind != data_kind {
7686                return Err(FormatError::InvalidArchive(
7687                    "object data block has unexpected kind",
7688                ));
7689            }
7690            let should_be_last = offset + 1 == data_count;
7691            if record.is_last_data() != should_be_last {
7692                return Err(FormatError::InvalidArchive(
7693                    "object last-data flag is not on the final data block",
7694                ));
7695            }
7696            data_shards.push(Some(record.payload.clone()));
7697        } else {
7698            data_shards.push(None);
7699        }
7700    }
7701
7702    if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
7703        return repair_data_gf16(&data_shards, &[], block_size);
7704    }
7705
7706    for offset in 0..parity_count {
7707        let block_index = checked_u64_add(
7708            extent.first_block_index,
7709            data_count as u64 + offset as u64,
7710            "object",
7711        )?;
7712        if let Some(record) = blocks.block(block_index)? {
7713            if record.kind != parity_kind {
7714                return Err(FormatError::InvalidArchive(
7715                    "object parity block has unexpected kind",
7716                ));
7717            }
7718            if record.is_last_data() {
7719                return Err(FormatError::InvalidArchive(
7720                    "object parity block has last-data flag",
7721                ));
7722            }
7723            parity_shards.push(Some(record.payload.clone()));
7724        } else {
7725            parity_shards.push(None);
7726        }
7727    }
7728
7729    repair_data_gf16(&data_shards, &parity_shards, block_size)
7730}
7731
7732fn validate_object_extent(
7733    extent: ObjectExtent,
7734    crypto_header: &CryptoHeaderFixed,
7735    class_data_shard_max: u16,
7736    class_parity_shard_max: u16,
7737) -> Result<(), FormatError> {
7738    if extent.data_block_count == 0 || extent.encrypted_size == 0 {
7739        return Err(FormatError::InvalidArchive(
7740            "encrypted object has zero data blocks or size",
7741        ));
7742    }
7743    if extent.data_block_count > class_data_shard_max as u32 {
7744        return Err(FormatError::InvalidArchive(
7745            "encrypted object exceeds its class data-shard maximum",
7746        ));
7747    }
7748    if extent.parity_block_count > class_parity_shard_max as u32 {
7749        return Err(FormatError::InvalidArchive(
7750            "encrypted object exceeds its class parity-shard maximum",
7751        ));
7752    }
7753    let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
7754    if extent.parity_block_count != required_parity {
7755        return Err(FormatError::InvalidArchive(
7756            "encrypted object parity does not match v41 compute_parity",
7757        ));
7758    }
7759    let total = checked_u64_add(
7760        extent.data_block_count as u64,
7761        extent.parity_block_count as u64,
7762        "encrypted object shard count overflow",
7763    )?;
7764    if total > 65_535 {
7765        return Err(FormatError::FecTooManyShards(total as usize));
7766    }
7767    let expected = checked_u64_mul(
7768        extent.data_block_count as u64,
7769        crypto_header.block_size as u64,
7770        "encrypted object size overflow",
7771    )?;
7772    if expected != extent.encrypted_size as u64 {
7773        return Err(FormatError::InvalidArchive(
7774            "encrypted object size is not data_block_count * block_size",
7775        ));
7776    }
7777    if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
7778        return Err(FormatError::InvalidArchive(
7779            "encrypted object is too small for AEAD tag",
7780        ));
7781    }
7782    Ok(())
7783}
7784
7785pub(crate) fn required_object_parity(
7786    data_block_count: u64,
7787    crypto_header: &CryptoHeaderFixed,
7788) -> Result<u32, FormatError> {
7789    let min_parity =
7790        if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
7791            1
7792        } else {
7793            0
7794        };
7795    let mut parity = 0u64;
7796    for _ in 0..100 {
7797        let total = data_block_count
7798            .checked_add(parity)
7799            .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
7800        let by_volume = checked_u64_mul(
7801            crypto_header.volume_loss_tolerance as u64,
7802            ceil_div_u64(total, crypto_header.stripe_width as u64)?,
7803            "volume-loss parity overflow",
7804        )?;
7805        let by_bitrot = ceil_div_u64(
7806            checked_u64_mul(
7807                total,
7808                crypto_header.bit_rot_buffer_pct as u64,
7809                "bit-rot parity overflow",
7810            )?,
7811            100,
7812        )?;
7813        let next = by_volume
7814            .checked_add(by_bitrot)
7815            .ok_or(FormatError::InvalidArchive("parity overflow"))?
7816            .max(min_parity);
7817        if next == parity {
7818            return u32::try_from(next)
7819                .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
7820        }
7821        parity = next;
7822    }
7823    Err(FormatError::InvalidArchive(
7824        "parity calculation did not converge",
7825    ))
7826}
7827
7828fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
7829    if denominator == 0 {
7830        return Err(FormatError::InvalidArchive("division by zero"));
7831    }
7832    numerator
7833        .checked_add(denominator - 1)
7834        .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
7835        .map(|value| value / denominator)
7836}
7837
7838fn frame_range_for_file<'b>(
7839    shard: &'b IndexShard,
7840    file: &FileEntry,
7841) -> Result<Vec<&'b FrameEntry>, FormatError> {
7842    let mut frames = Vec::with_capacity(file.frame_count as usize);
7843    for offset in 0..file.frame_count as u64 {
7844        let frame_index =
7845            file.first_frame_index
7846                .checked_add(offset)
7847                .ok_or(FormatError::InvalidArchive(
7848                    "FileEntry frame range overflow",
7849                ))?;
7850        let frame = shard
7851            .frames
7852            .iter()
7853            .find(|entry| entry.frame_index == frame_index)
7854            .ok_or(FormatError::InvalidArchive(
7855                "FileEntry references missing FrameEntry",
7856            ))?;
7857        frames.push(frame);
7858    }
7859    Ok(frames)
7860}
7861
7862fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
7863    MetadataLimits {
7864        block_size: crypto_header.block_size,
7865        max_path_length: crypto_header.max_path_length,
7866        max_payload_data_shards: crypto_header.fec_data_shards,
7867        max_payload_parity_shards: crypto_header.fec_parity_shards,
7868        max_index_data_shards: crypto_header.index_fec_data_shards,
7869        max_index_parity_shards: crypto_header.index_fec_parity_shards,
7870        max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
7871        max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
7872        ..MetadataLimits::default()
7873    }
7874}
7875
7876fn verify_dense_keys<T>(
7877    entries: &BTreeMap<u64, T>,
7878    expected_count: u64,
7879    structure: &'static str,
7880) -> Result<(), FormatError> {
7881    if entries.len() as u64 != expected_count {
7882        return Err(FormatError::InvalidArchive(
7883            "decoded table count does not match IndexRoot",
7884        ));
7885    }
7886    for expected in 0..expected_count {
7887        if !entries.contains_key(&expected) {
7888            return Err(FormatError::InvalidMetadata {
7889                structure,
7890                reason: "global index coverage has a gap",
7891            });
7892        }
7893    }
7894    Ok(())
7895}
7896
7897fn validate_envelope_frame_coverage(
7898    frames: &BTreeMap<u64, FrameEntry>,
7899    envelopes: &BTreeMap<u64, EnvelopeEntry>,
7900) -> Result<(), FormatError> {
7901    let mut accounted_frames = BTreeSet::new();
7902    for envelope in envelopes.values() {
7903        let first = envelope.first_frame_index;
7904        let end =
7905            first
7906                .checked_add(envelope.frame_count as u64)
7907                .ok_or(FormatError::InvalidArchive(
7908                    "EnvelopeEntry frame range overflow",
7909                ))?;
7910        let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
7911        for frame_index in first..end {
7912            let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
7913                "EnvelopeEntry references missing FrameEntry",
7914            ))?;
7915            if frame.envelope_index != envelope.envelope_index {
7916                return Err(FormatError::InvalidArchive(
7917                    "FrameEntry envelope_index does not match containing EnvelopeEntry",
7918                ));
7919            }
7920            if !accounted_frames.insert(frame_index) {
7921                return Err(FormatError::InvalidArchive(
7922                    "FrameEntry is covered by multiple EnvelopeEntries",
7923                ));
7924            }
7925            let start = frame.offset_in_envelope as usize;
7926            let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
7927            if end > envelope.plaintext_size as usize {
7928                return Err(FormatError::InvalidArchive(
7929                    "FrameEntry exceeds EnvelopeEntry plaintext_size",
7930                ));
7931            }
7932            ranges.push((start, end));
7933        }
7934        validate_exact_coverage_ranges(
7935            &mut ranges,
7936            envelope.plaintext_size as usize,
7937            "EnvelopeEntry frame coverage has a gap or overlap",
7938        )?;
7939    }
7940
7941    for frame_index in frames.keys() {
7942        if !accounted_frames.contains(frame_index) {
7943            return Err(FormatError::InvalidArchive(
7944                "FrameEntry is not covered by any EnvelopeEntry",
7945            ));
7946        }
7947    }
7948    Ok(())
7949}
7950
7951fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
7952    let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
7953    for shard in shards {
7954        for (idx, file) in shard.files.iter().enumerate() {
7955            let path = shard
7956                .file_path(idx)
7957                .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
7958                .to_vec();
7959            let start = shard
7960                .tar_member_group_start(idx)
7961                .ok_or(FormatError::InvalidArchive(
7962                    "FileEntry tar member start is missing",
7963                ))?;
7964            let key = (file.path_hash, path, start);
7965            validate_global_file_table_key_step(previous.as_ref(), &key)?;
7966            previous = Some(key);
7967        }
7968    }
7969    Ok(())
7970}
7971
7972fn validate_global_file_table_key_step(
7973    previous: Option<&([u8; 8], Vec<u8>, u64)>,
7974    current: &([u8; 8], Vec<u8>, u64),
7975) -> Result<(), FormatError> {
7976    if let Some(previous) = previous {
7977        if previous >= current {
7978            return Err(FormatError::InvalidArchive(
7979                "global FileEntry rows are not sorted and unique",
7980            ));
7981        }
7982    }
7983    Ok(())
7984}
7985
7986fn validate_file_extent_coverage_ranges(
7987    extents: &[(u64, u64)],
7988    tar_len: u64,
7989) -> Result<(), FormatError> {
7990    let mut ranges = Vec::with_capacity(extents.len());
7991    for (start, len) in extents {
7992        let end = checked_u64_add(*start, *len, "FileEntry")?;
7993        if end > tar_len {
7994            return Err(FormatError::InvalidArchive(
7995                "FileEntry extent exceeds IndexRoot tar_total_size",
7996            ));
7997        }
7998        ranges.push((*start, end));
7999    }
8000    validate_exact_coverage_ranges_u64(
8001        &mut ranges,
8002        tar_len,
8003        "FileEntry extents do not cover tar stream exactly",
8004    )
8005}
8006
8007fn add_expected_directory_hint_rows(
8008    map: &mut DirectoryHintMap,
8009    shard_row_index: u32,
8010    path: &[u8],
8011    kind: TarEntryKind,
8012) {
8013    map.entry(Vec::new()).or_default().insert(shard_row_index);
8014    for (idx, byte) in path.iter().enumerate() {
8015        if *byte == b'/' {
8016            map.entry(path[..idx].to_vec())
8017                .or_default()
8018                .insert(shard_row_index);
8019        }
8020    }
8021    if kind == TarEntryKind::Directory {
8022        map.entry(path.to_vec())
8023            .or_default()
8024            .insert(shard_row_index);
8025    }
8026}
8027
8028fn validate_directory_hint_tables_against_expected(
8029    tables: &[DirectoryHintTable],
8030    expected: &DirectoryHintMap,
8031) -> Result<(), FormatError> {
8032    let mut actual = Vec::new();
8033    let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
8034
8035    for table in tables {
8036        for entry_index in 0..table.entries.len() {
8037            let path = table
8038                .entry_path(entry_index)
8039                .ok_or(FormatError::InvalidArchive(
8040                    "DirectoryHintEntry path is missing",
8041                ))?;
8042            let key = (hash_prefix(path), path.to_vec());
8043            if let Some(previous) = &previous_key {
8044                if previous >= &key {
8045                    return Err(FormatError::InvalidArchive(
8046                        "DirectoryHintEntry rows are not globally sorted",
8047                    ));
8048                }
8049            }
8050            previous_key = Some(key);
8051
8052            let rows =
8053                table
8054                    .shard_rows_for_entry(entry_index)
8055                    .ok_or(FormatError::InvalidArchive(
8056                        "DirectoryHintEntry shard rows are missing",
8057                    ))?;
8058            actual.push((path.to_vec(), rows.to_vec()));
8059        }
8060    }
8061
8062    if actual != sorted_directory_hint_rows(expected) {
8063        return Err(FormatError::InvalidArchive(
8064            "directory hint map does not match decoded files",
8065        ));
8066    }
8067    Ok(())
8068}
8069
8070fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
8071    let mut rows = map
8072        .iter()
8073        .map(|(path, shard_rows)| {
8074            (
8075                path.clone(),
8076                shard_rows.iter().copied().collect::<Vec<u32>>(),
8077            )
8078        })
8079        .collect::<Vec<_>>();
8080    rows.sort_by(|(left_path, _), (right_path, _)| {
8081        hash_prefix(left_path)
8082            .cmp(&hash_prefix(right_path))
8083            .then_with(|| left_path.cmp(right_path))
8084    });
8085    rows
8086}
8087
8088fn validate_exact_coverage_ranges(
8089    ranges: &mut [(usize, usize)],
8090    expected_end: usize,
8091    reason: &'static str,
8092) -> Result<(), FormatError> {
8093    ranges.sort_unstable();
8094    let mut cursor = 0usize;
8095    for (start, end) in ranges.iter().copied() {
8096        if start != cursor || end < start {
8097            return Err(FormatError::InvalidArchive(reason));
8098        }
8099        cursor = end;
8100    }
8101    if cursor != expected_end {
8102        return Err(FormatError::InvalidArchive(reason));
8103    }
8104    Ok(())
8105}
8106
8107fn validate_exact_coverage_ranges_u64(
8108    ranges: &mut [(u64, u64)],
8109    expected_end: u64,
8110    reason: &'static str,
8111) -> Result<(), FormatError> {
8112    ranges.sort_unstable();
8113    let mut cursor = 0u64;
8114    for (start, end) in ranges.iter().copied() {
8115        if start != cursor || end < start {
8116            return Err(FormatError::InvalidArchive(reason));
8117        }
8118        cursor = end;
8119    }
8120    if cursor != expected_end {
8121        return Err(FormatError::InvalidArchive(reason));
8122    }
8123    Ok(())
8124}
8125
8126fn object_block_range(
8127    first_block_index: u64,
8128    data_block_count: u32,
8129    parity_block_count: u32,
8130    structure: &'static str,
8131) -> Result<(u64, u64), FormatError> {
8132    let total = data_block_count as u64 + parity_block_count as u64;
8133    if total == 0 {
8134        return Err(FormatError::InvalidArchive(structure));
8135    }
8136    let end = checked_u64_add(first_block_index, total, structure)?;
8137    Ok((first_block_index, end))
8138}
8139
8140fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
8141    ranges.sort_unstable();
8142    for pair in ranges.windows(2) {
8143        if pair[0].1 > pair[1].0 {
8144            return Err(FormatError::InvalidArchive(
8145                "encrypted object block ranges overlap",
8146            ));
8147        }
8148    }
8149    Ok(())
8150}
8151
8152pub(crate) fn observed_archive_size(
8153    sizes: impl IntoIterator<Item = u64>,
8154) -> Result<u64, FormatError> {
8155    sizes.into_iter().try_fold(0u64, |sum, size| {
8156        sum.checked_add(size).ok_or(FormatError::InvalidArchive(
8157            "observed archive size overflow",
8158        ))
8159    })
8160}
8161
8162pub(crate) fn total_extraction_size_cap(
8163    options: ReaderOptions,
8164    observed_archive_bytes: u64,
8165) -> u64 {
8166    options
8167        .max_total_extraction_size
8168        .min(observed_archive_bytes.saturating_mul(10))
8169}
8170
8171fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
8172    std::str::from_utf8(bytes)
8173        .map(|path| path.to_owned())
8174        .map_err(|_| FormatError::UnsafeArchivePath)
8175}
8176
8177fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
8178    let mut bytes = [0u8; 104];
8179    bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
8180    bytes[36..40].fill(0);
8181    bytes
8182}
8183
8184fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
8185    let digest = Sha256::digest(bytes);
8186    let mut out = [0u8; 32];
8187    out.copy_from_slice(&digest);
8188    out
8189}
8190
8191fn slice<'b>(
8192    bytes: &'b [u8],
8193    offset: usize,
8194    len: usize,
8195    structure: &'static str,
8196) -> Result<&'b [u8], FormatError> {
8197    let end = checked_add(offset, len, structure)?;
8198    bytes.get(offset..end).ok_or(FormatError::InvalidLength {
8199        structure,
8200        expected: end,
8201        actual: bytes.len(),
8202    })
8203}
8204
8205fn read_at_vec(
8206    reader: &dyn ArchiveReadAt,
8207    offset: u64,
8208    len: usize,
8209    structure: &'static str,
8210) -> Result<Vec<u8>, FormatError> {
8211    let expected_end = offset
8212        .checked_add(len as u64)
8213        .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
8214    let observed_len = reader.len()?;
8215    if expected_end > observed_len {
8216        return Err(FormatError::InvalidLength {
8217            structure,
8218            expected: to_usize(expected_end, structure)?,
8219            actual: to_usize(observed_len, structure)?,
8220        });
8221    }
8222    let mut out = vec![0u8; len];
8223    reader.read_exact_at(offset, &mut out)?;
8224    Ok(out)
8225}
8226
8227fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
8228where
8229    T: Sync,
8230    U: Send,
8231    F: Fn(&T) -> Result<U, FormatError> + Sync,
8232{
8233    if jobs <= 1 || items.len() <= 1 {
8234        return items.iter().map(f).collect();
8235    }
8236    let worker_count = jobs.min(items.len());
8237    let chunk_size = items.len().div_ceil(worker_count);
8238    let mut out = Vec::with_capacity(items.len());
8239    thread::scope(|scope| {
8240        let handles = items
8241            .chunks(chunk_size)
8242            .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
8243            .collect::<Vec<_>>();
8244        for handle in handles {
8245            let mut chunk = handle
8246                .join()
8247                .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
8248            out.append(&mut chunk);
8249        }
8250        Ok(out)
8251    })
8252}
8253
8254fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
8255    lhs.checked_add(rhs)
8256        .ok_or(FormatError::InvalidArchive(structure))
8257}
8258
8259fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
8260    lhs.checked_add(rhs)
8261        .ok_or(FormatError::InvalidArchive(structure))
8262}
8263
8264fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
8265    usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
8266}
8267
8268#[cfg(test)]
8269mod tests {
8270    use std::fs;
8271
8272    use super::*;
8273    use crate::compression::compress_zstd_frame;
8274    use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
8275    use crate::fec::encode_parity_gf16;
8276    use crate::format::{
8277        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
8278        VOLUME_FORMAT_REV,
8279    };
8280    use crate::metadata::{
8281        DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
8282        ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
8283    };
8284    use crate::non_seekable_reader::{
8285        extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
8286        verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
8287        NonSeekableReaderOptions, SequentialRootAuthStatus,
8288    };
8289    use crate::writer::{
8290        write_archive, write_archive_with_dictionary, write_archive_with_kdf,
8291        write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
8292        WriterOptions,
8293    };
8294
8295    fn master_key() -> MasterKey {
8296        MasterKey::from_raw_key(&[0x42; 32]).unwrap()
8297    }
8298
8299    #[test]
8300    fn reader_defaults_use_available_parallelism_jobs() {
8301        let options = ReaderOptions::default();
8302
8303        assert_eq!(options.jobs, default_jobs());
8304        assert!(options.jobs >= 1);
8305    }
8306
8307    #[test]
8308    fn reader_options_reject_zero_jobs() {
8309        let err = OpenedArchive::open_with_options(
8310            &[],
8311            &master_key(),
8312            ReaderOptions {
8313                jobs: 0,
8314                ..ReaderOptions::default()
8315            },
8316        )
8317        .unwrap_err();
8318
8319        assert_eq!(
8320            err,
8321            FormatError::ReaderUnsupported("jobs must be at least 1")
8322        );
8323    }
8324
8325    const TEST_ROOT_AUTH_ID: u16 = 0xe001;
8326    const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
8327
8328    fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
8329        RootAuthWriterConfig {
8330            authenticator_id: TEST_ROOT_AUTH_ID,
8331            signer_identity_type: 0,
8332            signer_identity: &[],
8333            authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
8334        }
8335    }
8336
8337    fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
8338        request.archive_root.to_vec()
8339    }
8340
8341    fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
8342        footer.authenticator_id == TEST_ROOT_AUTH_ID
8343            && footer.signer_identity_type == 0
8344            && footer.signer_identity_bytes.is_empty()
8345            && footer.authenticator_value.as_slice() == archive_root
8346    }
8347
8348    fn dictionary() -> &'static [u8] {
8349        b"dir/dict.txt common words common words common words dictionary payload"
8350    }
8351
8352    #[derive(Clone)]
8353    struct CountingReadAt {
8354        bytes: std::sync::Arc<Vec<u8>>,
8355        reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
8356        denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
8357    }
8358
8359    impl CountingReadAt {
8360        fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
8361            Self {
8362                bytes: std::sync::Arc::new(bytes),
8363                reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
8364                denied_ranges: std::sync::Arc::new(denied_ranges),
8365            }
8366        }
8367
8368        fn reads(&self) -> Vec<(u64, u64)> {
8369            self.reads.lock().unwrap().clone()
8370        }
8371    }
8372
8373    impl ArchiveReadAt for CountingReadAt {
8374        fn len(&self) -> Result<u64, FormatError> {
8375            u64::try_from(self.bytes.as_ref().len())
8376                .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
8377        }
8378
8379        fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
8380            let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
8381            self.reads.lock().unwrap().push((offset, end));
8382            if self
8383                .denied_ranges
8384                .iter()
8385                .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
8386            {
8387                return Err(FormatError::InvalidArchive("denied test read"));
8388            }
8389            let start = to_usize(offset, "archive")?;
8390            let end_usize = checked_add(start, buf.len(), "archive")?;
8391            let source = self
8392                .bytes
8393                .get(start..end_usize)
8394                .ok_or(FormatError::InvalidLength {
8395                    structure: "archive",
8396                    expected: end_usize,
8397                    actual: self.bytes.as_ref().len(),
8398                })?;
8399            buf.copy_from_slice(source);
8400            Ok(())
8401        }
8402    }
8403
8404    fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
8405        left_start < right_end && right_start < left_end
8406    }
8407
8408    fn single_stream_options() -> WriterOptions {
8409        WriterOptions {
8410            stripe_width: 1,
8411            volume_loss_tolerance: 0,
8412            ..WriterOptions::default()
8413        }
8414    }
8415
8416    struct ChunkedReader {
8417        bytes: Vec<u8>,
8418        cursor: usize,
8419        max_chunk: usize,
8420    }
8421
8422    impl ChunkedReader {
8423        fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
8424            Self {
8425                bytes,
8426                cursor: 0,
8427                max_chunk,
8428            }
8429        }
8430    }
8431
8432    impl std::io::Read for ChunkedReader {
8433        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
8434            if self.cursor >= self.bytes.len() {
8435                return Ok(0);
8436            }
8437            let available = self.bytes.len() - self.cursor;
8438            let len = available.min(buf.len()).min(self.max_chunk);
8439            buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
8440            self.cursor += len;
8441            Ok(len)
8442        }
8443    }
8444
8445    #[test]
8446    fn global_file_table_key_step_rejects_distinct_path_regression() {
8447        let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
8448        let current = ([1u8; 8], b"a.txt".to_vec(), 0);
8449
8450        assert_eq!(
8451            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
8452            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8453        );
8454    }
8455
8456    #[test]
8457    fn global_file_table_key_step_rejects_duplicate_full_key() {
8458        let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
8459        let current = ([1u8; 8], b"a.txt".to_vec(), 7);
8460
8461        assert_eq!(
8462            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
8463            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8464        );
8465    }
8466
8467    fn small_block_recovery_options() -> WriterOptions {
8468        WriterOptions {
8469            block_size: 4096,
8470            chunk_size: 32 * 1024,
8471            envelope_target_size: 32 * 1024,
8472            stripe_width: 1,
8473            volume_loss_tolerance: 0,
8474            bit_rot_buffer_pct: 1,
8475            fec_data_shards: 16,
8476            fec_parity_shards: 1,
8477            index_fec_data_shards: 4,
8478            index_fec_parity_shards: 1,
8479            index_root_fec_data_shards: 16,
8480            index_root_fec_parity_shards: 1,
8481            ..WriterOptions::default()
8482        }
8483    }
8484
8485    fn parity_rich_recovery_options() -> WriterOptions {
8486        WriterOptions {
8487            block_size: 4096,
8488            chunk_size: 32 * 1024,
8489            envelope_target_size: 32 * 1024,
8490            stripe_width: 1,
8491            volume_loss_tolerance: 0,
8492            bit_rot_buffer_pct: 40,
8493            fec_data_shards: 16,
8494            fec_parity_shards: 16,
8495            index_fec_data_shards: 4,
8496            index_fec_parity_shards: 4,
8497            index_root_fec_data_shards: 16,
8498            index_root_fec_parity_shards: 16,
8499            ..WriterOptions::default()
8500        }
8501    }
8502
8503    fn pseudo_random_bytes(len: usize) -> Vec<u8> {
8504        let mut state = 0x1234_5678u32;
8505        (0..len)
8506            .map(|_| {
8507                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
8508                (state >> 24) as u8
8509            })
8510            .collect()
8511    }
8512
8513    #[test]
8514    fn opens_lists_verifies_and_extracts_one_file_archive() {
8515        let archive = write_archive(
8516            &[RegularFile::new("dir/hello.txt", b"hello m7")],
8517            &master_key(),
8518            single_stream_options(),
8519        )
8520        .unwrap();
8521        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8522
8523        assert_eq!(
8524            opened.list_files().unwrap(),
8525            vec![ArchiveEntry {
8526                path: "dir/hello.txt".to_string(),
8527                file_data_size: 8,
8528                kind: TarEntryKind::Regular,
8529                mode: 0o644,
8530                mtime: 0,
8531                diagnostics: Vec::new(),
8532            }]
8533        );
8534        opened.verify().unwrap();
8535        assert_eq!(
8536            opened.extract_file("dir/hello.txt").unwrap(),
8537            Some(b"hello m7".to_vec())
8538        );
8539        assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
8540    }
8541
8542    #[test]
8543    fn root_auth_archive_round_trips_and_verifies_with_callback() {
8544        let archive = write_archive_with_root_auth(
8545            &[RegularFile::new("signed.txt", b"root-auth payload")],
8546            &master_key(),
8547            single_stream_options(),
8548            RootAuthWriterConfig {
8549                authenticator_id: 0x7777,
8550                signer_identity_type: 1,
8551                signer_identity: b"test signer",
8552                authenticator_value_length: 32,
8553            },
8554            |request| Ok(request.archive_root.to_vec()),
8555        )
8556        .unwrap();
8557
8558        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8559        opened.verify().unwrap();
8560        let verified = opened
8561            .verify_root_auth_with(|footer, archive_root| {
8562                Ok(footer.authenticator_value == archive_root.as_slice())
8563            })
8564            .unwrap();
8565
8566        assert_eq!(verified.authenticator_id, 0x7777);
8567        assert_eq!(verified.signer_identity_type, 1);
8568        assert_eq!(verified.signer_identity_bytes, b"test signer");
8569        assert_eq!(
8570            verified.archive_root,
8571            opened.root_auth_footer.as_ref().unwrap().archive_root
8572        );
8573        assert_eq!(
8574            verified.diagnostics,
8575            vec![
8576                RootAuthDiagnostic::RootAuthContentVerified,
8577                RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
8578                RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
8579                RootAuthDiagnostic::RecoveryMarginUnchecked,
8580            ]
8581        );
8582    }
8583
8584    #[test]
8585    fn root_auth_verification_requires_authenticator_success() {
8586        let archive = write_archive_with_root_auth(
8587            &[RegularFile::new("signed.txt", b"root-auth payload")],
8588            &master_key(),
8589            single_stream_options(),
8590            RootAuthWriterConfig {
8591                authenticator_id: 9,
8592                signer_identity_type: 1,
8593                signer_identity: b"test signer",
8594                authenticator_value_length: 32,
8595            },
8596            |request| Ok(request.archive_root.to_vec()),
8597        )
8598        .unwrap();
8599        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8600
8601        assert_eq!(
8602            opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
8603            FormatError::InvalidArchive("root-auth authenticator verification failed")
8604        );
8605    }
8606
8607    #[test]
8608    fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
8609        let archive = write_archive_with_root_auth(
8610            &[RegularFile::new("public.txt", b"public commitment")],
8611            &master_key(),
8612            single_stream_options(),
8613            RootAuthWriterConfig {
8614                authenticator_id: 0x2222,
8615                signer_identity_type: 1,
8616                signer_identity: b"public verifier",
8617                authenticator_value_length: 32,
8618            },
8619            |request| Ok(request.archive_root.to_vec()),
8620        )
8621        .unwrap();
8622
8623        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8624            Ok(footer.authenticator_value == archive_root.as_slice())
8625        })
8626        .unwrap();
8627
8628        assert_eq!(verified.authenticator_id, 0x2222);
8629        assert_eq!(verified.signer_identity_bytes, b"public verifier");
8630        assert!(verified.total_data_block_count > 0);
8631    }
8632
8633    #[test]
8634    fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
8635        let archive = write_archive_with_root_auth(
8636            &[RegularFile::new(
8637                "public-fields.txt",
8638                b"public source authority",
8639            )],
8640            &master_key(),
8641            single_stream_options(),
8642            RootAuthWriterConfig {
8643                authenticator_id: 0x2222,
8644                signer_identity_type: 1,
8645                signer_identity: b"public verifier",
8646                authenticator_value_length: 32,
8647            },
8648            |request| Ok(request.archive_root.to_vec()),
8649        )
8650        .unwrap();
8651        let mut bytes = archive.bytes.clone();
8652
8653        rewrite_public_cmra_image(&mut bytes, |image| {
8654            let manifest_region = image
8655                .regions
8656                .iter_mut()
8657                .find(|region| region.region_type == 3)
8658                .unwrap();
8659            manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
8660
8661            let trailer_region = image
8662                .regions
8663                .iter_mut()
8664                .find(|region| region.region_type == 5)
8665                .unwrap();
8666            let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
8667            trailer.block_count += 7;
8668            trailer_region.bytes = trailer.to_bytes().to_vec();
8669        });
8670
8671        public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
8672            Ok(footer.authenticator_value == archive_root.as_slice())
8673        })
8674        .unwrap();
8675    }
8676
8677    #[test]
8678    fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
8679        let archive = write_archive_with_root_auth(
8680            &[RegularFile::new(
8681                "public-crypto.txt",
8682                b"cross-volume public profile",
8683            )],
8684            &master_key(),
8685            WriterOptions {
8686                stripe_width: 2,
8687                volume_loss_tolerance: 0,
8688                ..WriterOptions::default()
8689            },
8690            RootAuthWriterConfig {
8691                authenticator_id: 0x3333,
8692                signer_identity_type: 1,
8693                signer_identity: b"public verifier",
8694                authenticator_value_length: 32,
8695            },
8696            |request| Ok(request.archive_root.to_vec()),
8697        )
8698        .unwrap();
8699        let mut volumes = archive.volumes.clone();
8700        let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
8701        let crypto_offset = volume_header.crypto_header_offset as usize;
8702        let expected_volume_size = 123_456_789u64;
8703        volumes[1][crypto_offset + 52..crypto_offset + 60]
8704            .copy_from_slice(&expected_volume_size.to_le_bytes());
8705        rewrite_public_cmra_image(&mut volumes[1], |image| {
8706            let crypto_region = image
8707                .regions
8708                .iter_mut()
8709                .find(|region| region.region_type == 2)
8710                .unwrap();
8711            crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
8712        });
8713
8714        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
8715        public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
8716            Ok(footer.authenticator_value == archive_root.as_slice())
8717        })
8718        .unwrap();
8719    }
8720
8721    #[test]
8722    fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
8723        let archive = write_archive_with_root_auth(
8724            &[RegularFile::new("cmra-header.txt", b"header fallback")],
8725            &master_key(),
8726            single_stream_options(),
8727            RootAuthWriterConfig {
8728                authenticator_id: 0x4444,
8729                signer_identity_type: 1,
8730                signer_identity: b"public verifier",
8731                authenticator_value_length: 32,
8732            },
8733            |request| Ok(request.archive_root.to_vec()),
8734        )
8735        .unwrap();
8736        let final_locator = final_recovery_locator(&archive.bytes);
8737
8738        let mut bad_crc = archive.bytes.clone();
8739        let crc_offset =
8740            final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
8741        bad_crc[crc_offset] ^= 0x55;
8742        public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
8743            Ok(footer.authenticator_value == archive_root.as_slice())
8744        })
8745        .unwrap();
8746
8747        let mut bad_magic = archive.bytes.clone();
8748        bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
8749        public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
8750            Ok(footer.authenticator_value == archive_root.as_slice())
8751        })
8752        .unwrap();
8753
8754        let mut bad_hint = archive.bytes.clone();
8755        bad_hint[crc_offset] ^= 0xAA;
8756        for offset in [
8757            bad_hint.len() - LOCATOR_PAIR_LEN,
8758            bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
8759        ] {
8760            let mut locator = CriticalRecoveryLocator::parse(
8761                &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8762            )
8763            .unwrap();
8764            locator.volume_index_hint += 1;
8765            bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
8766                .copy_from_slice(&locator.to_bytes());
8767        }
8768        assert_eq!(
8769            public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
8770            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
8771        );
8772    }
8773
8774    #[test]
8775    fn recovers_physical_volume_header_magic_from_cmra_authority() {
8776        let payload = b"front header authority".to_vec();
8777        let archive = write_archive(
8778            &[RegularFile::new("volume-header.txt", &payload)],
8779            &master_key(),
8780            small_block_recovery_options(),
8781        )
8782        .unwrap();
8783
8784        let mut corrupted = archive.bytes;
8785        corrupted[0] ^= 0x55;
8786
8787        let opened = open_archive(&corrupted, &master_key()).unwrap();
8788        assert_eq!(
8789            opened.extract_file("volume-header.txt").unwrap(),
8790            Some(payload)
8791        );
8792        opened.verify().unwrap();
8793    }
8794
8795    #[test]
8796    fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8797        let payload = b"crc-valid wrong volume index".to_vec();
8798        let mut options = small_block_recovery_options();
8799        options.stripe_width = 2;
8800        options.volume_loss_tolerance = 0;
8801        let archive = write_archive(
8802            &[RegularFile::new("volume-index.txt", &payload)],
8803            &master_key(),
8804            options,
8805        )
8806        .unwrap();
8807
8808        let mut corrupted = archive.volumes[0].clone();
8809        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8810        assert_eq!(header.volume_index, 0);
8811        header.volume_index = 1;
8812        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8813        assert_eq!(
8814            VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
8815                .unwrap()
8816                .volume_index,
8817            1
8818        );
8819
8820        let opened = open_archive_volumes(
8821            &[corrupted.as_slice(), archive.volumes[1].as_slice()],
8822            &master_key(),
8823        )
8824        .unwrap();
8825        assert_eq!(opened.volume_header.volume_index, 0);
8826        assert_eq!(
8827            opened.extract_file("volume-index.txt").unwrap(),
8828            Some(payload)
8829        );
8830        opened.verify().unwrap();
8831    }
8832
8833    #[test]
8834    fn recovers_physical_crypto_header_magic_from_cmra_authority() {
8835        let payload = b"crypto header authority".to_vec();
8836        let archive = write_archive(
8837            &[RegularFile::new("crypto-header.txt", &payload)],
8838            &master_key(),
8839            small_block_recovery_options(),
8840        )
8841        .unwrap();
8842        let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
8843            .unwrap()
8844            .crypto_header_offset;
8845
8846        let mut corrupted = archive.bytes;
8847        corrupted[crypto_offset as usize] ^= 0x55;
8848
8849        let opened = open_archive(&corrupted, &master_key()).unwrap();
8850        assert_eq!(
8851            opened.extract_file("crypto-header.txt").unwrap(),
8852            Some(payload)
8853        );
8854        opened.verify().unwrap();
8855    }
8856
8857    #[test]
8858    fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
8859        let payload = b"read-at header authority".to_vec();
8860        let archive = write_archive(
8861            &[RegularFile::new("read-at-header.txt", &payload)],
8862            &master_key(),
8863            small_block_recovery_options(),
8864        )
8865        .unwrap();
8866
8867        let mut corrupted = archive.bytes;
8868        corrupted[0] ^= 0x55;
8869
8870        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8871        assert_eq!(
8872            opened.extract_file("read-at-header.txt").unwrap(),
8873            Some(payload)
8874        );
8875        opened.verify().unwrap();
8876    }
8877
8878    #[test]
8879    fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8880        let payload = b"read-at crc-valid wrong volume index".to_vec();
8881        let mut options = small_block_recovery_options();
8882        options.stripe_width = 2;
8883        options.volume_loss_tolerance = 0;
8884        let archive = write_archive(
8885            &[RegularFile::new("read-at-volume-index.txt", &payload)],
8886            &master_key(),
8887            options,
8888        )
8889        .unwrap();
8890
8891        let mut corrupted = archive.volumes[0].clone();
8892        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8893        assert_eq!(header.volume_index, 0);
8894        header.volume_index = 1;
8895        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8896
8897        let opened = open_seekable_archive_volumes(
8898            vec![corrupted, archive.volumes[1].clone()],
8899            &master_key(),
8900        )
8901        .unwrap();
8902        assert_eq!(opened.volume_header.volume_index, 0);
8903        assert_eq!(
8904            opened.extract_file("read-at-volume-index.txt").unwrap(),
8905            Some(payload)
8906        );
8907        opened.verify().unwrap();
8908    }
8909
8910    #[test]
8911    fn recovers_cmra_header_magic_from_locator_tuple() {
8912        let payload = b"cmra header authority".to_vec();
8913        let archive = write_archive(
8914            &[RegularFile::new("cmra-header-magic.txt", &payload)],
8915            &master_key(),
8916            small_block_recovery_options(),
8917        )
8918        .unwrap();
8919        let locator = final_recovery_locator(&archive.bytes);
8920
8921        let mut corrupted = archive.bytes;
8922        corrupted[locator.cmra_offset as usize] ^= 0x55;
8923
8924        let opened = open_archive(&corrupted, &master_key()).unwrap();
8925        assert_eq!(
8926            opened.extract_file("cmra-header-magic.txt").unwrap(),
8927            Some(payload)
8928        );
8929        opened.verify().unwrap();
8930    }
8931
8932    #[test]
8933    fn recovers_cmra_shard_magic_as_erasure() {
8934        let payload = b"cmra shard authority".to_vec();
8935        let archive = write_archive(
8936            &[RegularFile::new("cmra-shard-magic.txt", &payload)],
8937            &master_key(),
8938            small_block_recovery_options(),
8939        )
8940        .unwrap();
8941        let locator = final_recovery_locator(&archive.bytes);
8942        let first_shard_offset =
8943            locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
8944
8945        let mut corrupted = archive.bytes;
8946        corrupted[first_shard_offset] ^= 0x55;
8947
8948        let opened = open_archive(&corrupted, &master_key()).unwrap();
8949        assert_eq!(
8950            opened.extract_file("cmra-shard-magic.txt").unwrap(),
8951            Some(payload)
8952        );
8953        opened.verify().unwrap();
8954    }
8955
8956    #[test]
8957    fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
8958        let archive = write_archive(
8959            &[RegularFile::new(
8960                "cmra-floor.txt",
8961                b"authenticated CMRA floor",
8962            )],
8963            &master_key(),
8964            single_stream_options(),
8965        )
8966        .unwrap();
8967        let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
8968        let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8969        let locator = CriticalRecoveryLocator::parse(
8970            &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8971        )
8972        .unwrap();
8973        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
8974        let crypto_start = volume_header.crypto_header_offset as usize;
8975        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8976        let crypto_header = CryptoHeader::parse(
8977            &malformed[crypto_start..crypto_end],
8978            volume_header.crypto_header_length,
8979        )
8980        .unwrap();
8981        let subkeys = Subkeys::derive(
8982            &master_key(),
8983            &volume_header.archive_uuid,
8984            &volume_header.session_id,
8985        )
8986        .unwrap();
8987
8988        assert_eq!(
8989            parse_locator_cmra_candidate(
8990                &malformed,
8991                final_offset,
8992                locator,
8993                KeyHoldingTerminalContext {
8994                    subkeys: &subkeys,
8995                    volume_header: &volume_header,
8996                    crypto_header: &crypto_header.fixed,
8997                    crypto_header_bytes: &malformed[crypto_start..crypto_end],
8998                },
8999            )
9000            .unwrap_err(),
9001            FormatError::InvalidArchive(
9002                "CMRA parity shard count is below authenticated bit-rot lower bound"
9003            )
9004        );
9005        assert!(open_archive(&malformed, &master_key()).is_err());
9006    }
9007
9008    #[test]
9009    fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
9010        let archive = write_archive(
9011            &[RegularFile::new(
9012                "locator-order.txt",
9013                b"locator tuple first",
9014            )],
9015            &master_key(),
9016            single_stream_options(),
9017        )
9018        .unwrap();
9019        let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
9020        let mut locator = final_recovery_locator(&archive.bytes);
9021        locator.cmra_shard_size = 513;
9022        locator.body_bytes_before_cmra = locator.cmra_offset + 1;
9023        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9024        let crypto_start = volume_header.crypto_header_offset as usize;
9025        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9026        let crypto_header = CryptoHeader::parse(
9027            &archive.bytes[crypto_start..crypto_end],
9028            volume_header.crypto_header_length,
9029        )
9030        .unwrap();
9031        let subkeys = Subkeys::derive(
9032            &master_key(),
9033            &volume_header.archive_uuid,
9034            &volume_header.session_id,
9035        )
9036        .unwrap();
9037
9038        assert_eq!(
9039            parse_locator_cmra_candidate(
9040                &archive.bytes,
9041                final_offset,
9042                locator,
9043                KeyHoldingTerminalContext {
9044                    subkeys: &subkeys,
9045                    volume_header: &volume_header,
9046                    crypto_header: &crypto_header.fixed,
9047                    crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
9048                },
9049            )
9050            .unwrap_err(),
9051            FormatError::InvalidArchive("CMRA shard_size is invalid")
9052        );
9053    }
9054
9055    #[test]
9056    fn sequential_extract_rejects_bytes_after_terminal_locator() {
9057        let archive = write_archive(
9058            &[RegularFile::new("seq.txt", b"sequential EOF")],
9059            &master_key(),
9060            single_stream_options(),
9061        )
9062        .unwrap();
9063        let mut appended = archive.bytes.clone();
9064        appended.extend_from_slice(&[0xAA; 32]);
9065
9066        assert_eq!(
9067            sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
9068            FormatError::InvalidArchive("sequential terminal does not end at EOF")
9069        );
9070    }
9071
9072    #[test]
9073    fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
9074        let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
9075        let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
9076
9077        assert_eq!(
9078            validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
9079            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
9080        );
9081    }
9082
9083    #[test]
9084    fn root_auth_verifies_key_holding_and_public_no_key_modes() {
9085        let archive = write_archive_with_root_auth(
9086            &[RegularFile::new("signed.txt", b"ed25519 payload")],
9087            &master_key(),
9088            single_stream_options(),
9089            test_root_auth_config(),
9090            |request| Ok(test_root_auth_value(request)),
9091        )
9092        .unwrap();
9093
9094        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9095        let root_auth = opened
9096            .verify_root_auth_with(|footer, archive_root| {
9097                Ok(test_root_auth_verifies(footer, archive_root))
9098            })
9099            .unwrap();
9100        assert_eq!(
9101            root_auth.archive_root,
9102            opened.root_auth_footer.as_ref().unwrap().archive_root
9103        );
9104
9105        let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
9106            Ok(test_root_auth_verifies(footer, archive_root))
9107        })
9108        .unwrap();
9109        assert_eq!(public.archive_root, root_auth.archive_root);
9110        assert_eq!(
9111            public.diagnostics,
9112            vec![
9113                PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
9114                PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
9115                PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
9116            ]
9117        );
9118    }
9119
9120    #[test]
9121    fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
9122        let options = WriterOptions {
9123            block_size: 4096,
9124            chunk_size: 16 * 1024,
9125            envelope_target_size: 16 * 1024,
9126            stripe_width: 2,
9127            volume_loss_tolerance: 1,
9128            bit_rot_buffer_pct: 0,
9129            fec_data_shards: 16,
9130            fec_parity_shards: 1,
9131            index_fec_data_shards: 4,
9132            index_fec_parity_shards: 1,
9133            index_root_fec_data_shards: 16,
9134            index_root_fec_parity_shards: 1,
9135            ..WriterOptions::default()
9136        };
9137        let archive = write_archive_with_root_auth(
9138            &[RegularFile::new("missing-volume.txt", b"recover me")],
9139            &master_key(),
9140            options,
9141            test_root_auth_config(),
9142            |request| Ok(test_root_auth_value(request)),
9143        )
9144        .unwrap();
9145
9146        let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
9147        let root_auth = opened
9148            .verify_root_auth_with(|footer, archive_root| {
9149                Ok(test_root_auth_verifies(footer, archive_root))
9150            })
9151            .unwrap();
9152        assert!(root_auth
9153            .diagnostics
9154            .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
9155    }
9156
9157    #[test]
9158    fn public_no_key_rejects_unsigned_archives() {
9159        let archive = write_archive(
9160            &[RegularFile::new("plain.txt", b"unsigned")],
9161            &master_key(),
9162            single_stream_options(),
9163        )
9164        .unwrap();
9165
9166        assert_eq!(
9167            public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
9168            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
9169        );
9170    }
9171
9172    #[test]
9173    fn unsigned_archive_reports_root_auth_absent() {
9174        let archive = write_archive(
9175            &[RegularFile::new("plain.txt", b"unsigned")],
9176            &master_key(),
9177            single_stream_options(),
9178        )
9179        .unwrap();
9180        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9181
9182        assert_eq!(
9183            opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
9184            FormatError::ReaderUnsupported("root-auth footer is absent")
9185        );
9186    }
9187
9188    #[test]
9189    fn safe_extract_writes_regular_file_under_root() {
9190        let archive = write_archive(
9191            &[RegularFile::new("dir/hello.txt", b"safe m8")],
9192            &master_key(),
9193            single_stream_options(),
9194        )
9195        .unwrap();
9196        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9197        let tmp = tempfile::tempdir().unwrap();
9198
9199        opened
9200            .extract_file_to(
9201                "dir/hello.txt",
9202                tmp.path(),
9203                SafeExtractionOptions::default(),
9204            )
9205            .unwrap()
9206            .unwrap();
9207
9208        assert_eq!(
9209            std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
9210            b"safe m8"
9211        );
9212    }
9213
9214    #[test]
9215    fn seekable_extract_all_to_streams_unique_archive() {
9216        let archive = write_archive(
9217            &[
9218                RegularFile::new("alpha.txt", b"alpha"),
9219                RegularFile::new("dir/beta.txt", b"beta"),
9220            ],
9221            &master_key(),
9222            single_stream_options(),
9223        )
9224        .unwrap();
9225        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9226        let tmp = tempfile::tempdir().unwrap();
9227
9228        let diagnostics = opened
9229            .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9230            .unwrap();
9231
9232        assert_eq!(diagnostics.len(), 2);
9233        assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
9234        assert_eq!(
9235            fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
9236            b"beta"
9237        );
9238    }
9239
9240    #[test]
9241    fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
9242        let archive = write_archive(
9243            &[
9244                RegularFile::new("same.txt", b"old"),
9245                RegularFile::new("same.txt", b"new"),
9246            ],
9247            &master_key(),
9248            single_stream_options(),
9249        )
9250        .unwrap();
9251        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9252        let tmp = tempfile::tempdir().unwrap();
9253
9254        assert_eq!(
9255            opened
9256                .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9257                .unwrap_err(),
9258            FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
9259        );
9260    }
9261
9262    #[test]
9263    fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
9264        let archive = write_archive(
9265            &[
9266                RegularFile::new("same.txt", b"old"),
9267                RegularFile::new("same.txt", b"new"),
9268            ],
9269            &master_key(),
9270            single_stream_options(),
9271        )
9272        .unwrap();
9273        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9274        let tmp = tempfile::tempdir().unwrap();
9275
9276        let diagnostics = opened
9277            .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
9278            .unwrap();
9279
9280        assert_eq!(diagnostics.len(), 1);
9281        assert_eq!(diagnostics[0].0, "same.txt");
9282        assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
9283    }
9284
9285    #[test]
9286    fn safe_extract_rejects_overwriting_existing_file_by_default() {
9287        let archive = write_archive(
9288            &[RegularFile::new("hello.txt", b"new")],
9289            &master_key(),
9290            single_stream_options(),
9291        )
9292        .unwrap();
9293        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9294        let tmp = tempfile::tempdir().unwrap();
9295        std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
9296
9297        assert_eq!(
9298            opened
9299                .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
9300                .unwrap_err(),
9301            FormatError::UnsafeOverwrite
9302        );
9303        assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
9304    }
9305
9306    #[test]
9307    fn opens_and_verifies_empty_archive() {
9308        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9309        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9310
9311        assert!(opened.list_files().unwrap().is_empty());
9312        opened.verify().unwrap();
9313    }
9314
9315    #[test]
9316    fn default_reader_options_allow_v36_trailing_garbage_scan() {
9317        let archive = write_archive(
9318            &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
9319            &master_key(),
9320            single_stream_options(),
9321        )
9322        .unwrap();
9323        let mut with_trailing_garbage = archive.bytes.clone();
9324        with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
9325
9326        let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
9327        assert_eq!(
9328            opened.extract_file("garbage-tolerant.txt").unwrap(),
9329            Some(b"still intact".to_vec())
9330        );
9331    }
9332
9333    #[test]
9334    fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
9335        assert_eq!(
9336            open_archive(
9337                &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
9338                &master_key()
9339            )
9340            .unwrap_err(),
9341            FormatError::InvalidLength {
9342                structure: "archive",
9343                expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9344                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
9345            }
9346        );
9347
9348        let mut header = test_volume_header();
9349        header.crypto_header_length = 512;
9350        let mut unavailable_crypto = header.to_bytes().to_vec();
9351        unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
9352
9353        assert_eq!(
9354            open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
9355            FormatError::InvalidLength {
9356                structure: "CryptoHeader",
9357                expected: VOLUME_HEADER_LEN + 512,
9358                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9359            }
9360        );
9361    }
9362
9363    #[test]
9364    fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
9365        let archive = write_archive(
9366            &[RegularFile::new("offset.txt", b"offset")],
9367            &master_key(),
9368            small_block_recovery_options(),
9369        )
9370        .unwrap();
9371        let mut mutated = archive.bytes;
9372        let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
9373        header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
9374        mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
9375
9376        let opened = open_archive(&mutated, &master_key()).unwrap();
9377        assert_eq!(
9378            opened.volume_header.crypto_header_offset,
9379            VOLUME_HEADER_LEN as u32
9380        );
9381        assert_eq!(
9382            opened.extract_file("offset.txt").unwrap(),
9383            Some(b"offset".to_vec())
9384        );
9385        opened.verify().unwrap();
9386    }
9387
9388    #[test]
9389    fn rejects_wrong_key_before_metadata_release() {
9390        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9391        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
9392
9393        assert_eq!(
9394            open_archive(&archive.bytes, &wrong).unwrap_err(),
9395            FormatError::HmacMismatch {
9396                structure: "CryptoHeader"
9397            }
9398        );
9399    }
9400
9401    #[test]
9402    fn rejects_payload_tamper_even_with_recomputed_block_crc() {
9403        let mut archive = write_archive(
9404            &[RegularFile::new("file.txt", b"authenticated")],
9405            &master_key(),
9406            single_stream_options(),
9407        )
9408        .unwrap()
9409        .bytes;
9410        let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
9411        let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
9412        let crypto = CryptoHeader::parse(
9413            &archive[VOLUME_HEADER_LEN..crypto_end],
9414            volume.crypto_header_length,
9415        )
9416        .unwrap();
9417        let block_size = crypto.fixed.block_size as usize;
9418        archive[crypto_end + 16] ^= 1;
9419        let crc_offset = crypto_end + 16 + block_size;
9420        let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
9421        archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9422
9423        let opened = open_archive(&archive, &master_key()).unwrap();
9424        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9425    }
9426
9427    #[test]
9428    fn list_and_extract_use_final_view_for_duplicate_paths() {
9429        let archive = write_archive(
9430            &[
9431                RegularFile::new("same.txt", b"old"),
9432                RegularFile::new("same.txt", b"newer"),
9433            ],
9434            &master_key(),
9435            single_stream_options(),
9436        )
9437        .unwrap();
9438        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9439
9440        assert_eq!(
9441            opened.list_index_entries().unwrap(),
9442            vec![ArchiveIndexEntry {
9443                path: "same.txt".to_string(),
9444                file_data_size: 5,
9445            }]
9446        );
9447        assert_eq!(
9448            opened.lookup_index_entry("same.txt").unwrap(),
9449            Some(ArchiveIndexEntry {
9450                path: "same.txt".to_string(),
9451                file_data_size: 5,
9452            })
9453        );
9454        assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
9455        assert_eq!(
9456            opened.list_files().unwrap(),
9457            vec![ArchiveEntry {
9458                path: "same.txt".to_string(),
9459                file_data_size: 5,
9460                kind: TarEntryKind::Regular,
9461                mode: 0o644,
9462                mtime: 0,
9463                diagnostics: Vec::new(),
9464            }]
9465        );
9466        assert_eq!(
9467            opened.extract_file("same.txt").unwrap(),
9468            Some(b"newer".to_vec())
9469        );
9470        opened.verify().unwrap();
9471    }
9472
9473    #[test]
9474    fn index_entries_do_not_decrypt_payload_envelopes() {
9475        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9476        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9477
9478        assert_eq!(
9479            opened.list_index_entries().unwrap(),
9480            vec![
9481                ArchiveIndexEntry {
9482                    path: "broken.txt".to_string(),
9483                    file_data_size: b"broken payload\n".len() as u64,
9484                },
9485                ArchiveIndexEntry {
9486                    path: "healthy.txt".to_string(),
9487                    file_data_size: b"healthy payload\n".len() as u64,
9488                },
9489            ]
9490        );
9491        assert_eq!(
9492            opened.lookup_index_entry("broken.txt").unwrap(),
9493            Some(ArchiveIndexEntry {
9494                path: "broken.txt".to_string(),
9495                file_data_size: b"broken payload\n".len() as u64,
9496            })
9497        );
9498        assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
9499    }
9500
9501    #[test]
9502    fn extract_file_does_not_decrypt_unselected_payload_envelope() {
9503        // This fixture corrupts only the unselected envelope, proving selected
9504        // extraction does not decrypt unrelated payload envelopes.
9505        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9506        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9507
9508        assert_eq!(
9509            opened.extract_file("healthy.txt").unwrap(),
9510            Some(b"healthy payload\n".to_vec())
9511        );
9512        assert_eq!(
9513            opened.extract_file("broken.txt").unwrap_err(),
9514            FormatError::AeadFailure
9515        );
9516        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9517    }
9518
9519    #[test]
9520    fn seekable_extract_does_not_read_unselected_payload_envelope() {
9521        let healthy = pseudo_random_bytes(64 * 1024);
9522        let broken = pseudo_random_bytes(64 * 1024);
9523        let options = WriterOptions {
9524            block_size: 4096,
9525            chunk_size: 4096,
9526            envelope_target_size: 8192,
9527            stripe_width: 1,
9528            volume_loss_tolerance: 0,
9529            bit_rot_buffer_pct: 0,
9530            fec_data_shards: 4,
9531            fec_parity_shards: 0,
9532            index_fec_data_shards: 4,
9533            index_fec_parity_shards: 0,
9534            index_root_fec_data_shards: 4,
9535            index_root_fec_parity_shards: 0,
9536            ..WriterOptions::default()
9537        };
9538        let archive = write_archive(
9539            &[
9540                RegularFile::new("healthy.bin", &healthy),
9541                RegularFile::new("broken.bin", &broken),
9542            ],
9543            &master_key(),
9544            options,
9545        )
9546        .unwrap();
9547        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9548        let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
9549        let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
9550        let denied_block_indices = broken_envelopes
9551            .iter()
9552            .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
9553            .flat_map(|envelope| {
9554                let block_count =
9555                    envelope.data_block_count as u64 + envelope.parity_block_count as u64;
9556                envelope.first_block_index..envelope.first_block_index + block_count
9557            })
9558            .collect::<BTreeSet<_>>();
9559        assert!(
9560            !denied_block_indices.is_empty(),
9561            "fixture must place broken.bin in at least one unshared envelope"
9562        );
9563        let denied_ranges = block_record_slots(&archive.bytes)
9564            .into_iter()
9565            .filter_map(|(offset, len, record)| {
9566                denied_block_indices
9567                    .contains(&record.block_index)
9568                    .then_some((offset as u64, (offset + len) as u64))
9569            })
9570            .collect::<Vec<_>>();
9571        assert!(!denied_ranges.is_empty());
9572
9573        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9574        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9575
9576        assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
9577        for (read_start, read_end) in reader.reads() {
9578            assert!(
9579                denied_ranges
9580                    .iter()
9581                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9582                "targeted extract read an unrelated payload BlockRecord range"
9583            );
9584        }
9585        assert_eq!(
9586            opened.extract_file("broken.bin").unwrap_err(),
9587            FormatError::InvalidArchive("denied test read")
9588        );
9589    }
9590
9591    #[test]
9592    fn extract_file_to_writer_streams_before_reading_later_envelopes() {
9593        struct FailOnFirstWrite;
9594
9595        impl std::io::Write for FailOnFirstWrite {
9596            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
9597                Err(std::io::Error::other("sink stopped"))
9598            }
9599
9600            fn flush(&mut self) -> std::io::Result<()> {
9601                Ok(())
9602            }
9603        }
9604
9605        let payload = pseudo_random_bytes(128 * 1024);
9606        let options = WriterOptions {
9607            block_size: 4096,
9608            chunk_size: 4096,
9609            envelope_target_size: 8192,
9610            stripe_width: 1,
9611            volume_loss_tolerance: 0,
9612            bit_rot_buffer_pct: 0,
9613            fec_data_shards: 4,
9614            fec_parity_shards: 0,
9615            index_fec_data_shards: 4,
9616            index_fec_parity_shards: 0,
9617            index_root_fec_data_shards: 4,
9618            index_root_fec_parity_shards: 0,
9619            ..WriterOptions::default()
9620        };
9621        let archive = write_archive(
9622            &[RegularFile::new("large.bin", &payload)],
9623            &master_key(),
9624            options,
9625        )
9626        .unwrap();
9627        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9628        let envelopes = envelope_entries_for_path(&eager, "large.bin");
9629        let first_envelope = envelopes
9630            .first()
9631            .expect("large fixture should have at least one envelope")
9632            .envelope_index;
9633        let later_envelope_blocks = envelopes
9634            .iter()
9635            .filter(|entry| entry.envelope_index != first_envelope)
9636            .flat_map(|entry| {
9637                let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
9638                entry.first_block_index..entry.first_block_index + block_count
9639            })
9640            .collect::<BTreeSet<_>>();
9641        assert!(
9642            !later_envelope_blocks.is_empty(),
9643            "fixture must span more than one payload envelope"
9644        );
9645        let denied_ranges = block_record_slots(&archive.bytes)
9646            .into_iter()
9647            .filter_map(|(offset, len, record)| {
9648                later_envelope_blocks
9649                    .contains(&record.block_index)
9650                    .then_some((offset as u64, (offset + len) as u64))
9651            })
9652            .collect::<Vec<_>>();
9653        assert!(!denied_ranges.is_empty());
9654
9655        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9656        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9657        let mut writer = FailOnFirstWrite;
9658
9659        let err = opened
9660            .extract_file_to_writer("large.bin", &mut writer)
9661            .unwrap_err();
9662        assert_eq!(err.to_string(), "extraction output write failed");
9663        for (read_start, read_end) in reader.reads() {
9664            assert!(
9665                denied_ranges
9666                    .iter()
9667                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9668                "streaming writer read a later payload envelope before surfacing writer failure"
9669            );
9670        }
9671    }
9672
9673    #[test]
9674    fn extract_file_to_writer_writes_bounded_chunks() {
9675        struct ChunkRecorder {
9676            total: usize,
9677            max_write: usize,
9678            writes: usize,
9679        }
9680
9681        impl std::io::Write for ChunkRecorder {
9682            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
9683                self.total += buf.len();
9684                self.max_write = self.max_write.max(buf.len());
9685                self.writes += 1;
9686                Ok(buf.len())
9687            }
9688
9689            fn flush(&mut self) -> std::io::Result<()> {
9690                Ok(())
9691            }
9692        }
9693
9694        let payload = pseudo_random_bytes(128 * 1024);
9695        let options = WriterOptions {
9696            block_size: 4096,
9697            chunk_size: 4096,
9698            envelope_target_size: 8192,
9699            stripe_width: 1,
9700            volume_loss_tolerance: 0,
9701            bit_rot_buffer_pct: 0,
9702            fec_data_shards: 4,
9703            fec_parity_shards: 0,
9704            index_fec_data_shards: 4,
9705            index_fec_parity_shards: 0,
9706            index_root_fec_data_shards: 4,
9707            index_root_fec_parity_shards: 0,
9708            ..WriterOptions::default()
9709        };
9710        let archive = write_archive(
9711            &[RegularFile::new("large.bin", &payload)],
9712            &master_key(),
9713            options,
9714        )
9715        .unwrap();
9716        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9717        let mut writer = ChunkRecorder {
9718            total: 0,
9719            max_write: 0,
9720            writes: 0,
9721        };
9722
9723        opened
9724            .extract_file_to_writer("large.bin", &mut writer)
9725            .unwrap()
9726            .unwrap();
9727
9728        assert_eq!(writer.total, payload.len());
9729        assert!(writer.writes > 1);
9730        assert!(
9731            writer.max_write <= options.chunk_size as usize,
9732            "writer saw a {} byte chunk, larger than the {} byte frame target",
9733            writer.max_write,
9734            options.chunk_size
9735        );
9736    }
9737
9738    #[test]
9739    fn extract_file_to_writer_with_progress_reports_payload_bytes() {
9740        struct ChunkRecorder {
9741            total: usize,
9742        }
9743
9744        impl std::io::Write for ChunkRecorder {
9745            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
9746                self.total += buf.len();
9747                Ok(buf.len())
9748            }
9749
9750            fn flush(&mut self) -> std::io::Result<()> {
9751                Ok(())
9752            }
9753        }
9754
9755        let payload = pseudo_random_bytes(128 * 1024);
9756        let options = WriterOptions {
9757            block_size: 4096,
9758            chunk_size: 4096,
9759            envelope_target_size: 8192,
9760            stripe_width: 1,
9761            volume_loss_tolerance: 0,
9762            bit_rot_buffer_pct: 0,
9763            fec_data_shards: 4,
9764            fec_parity_shards: 0,
9765            index_fec_data_shards: 4,
9766            index_fec_parity_shards: 0,
9767            index_root_fec_data_shards: 4,
9768            index_root_fec_parity_shards: 0,
9769            ..WriterOptions::default()
9770        };
9771        let archive = write_archive(
9772            &[RegularFile::new("large.bin", &payload)],
9773            &master_key(),
9774            options,
9775        )
9776        .unwrap();
9777        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9778        let mut writer = ChunkRecorder { total: 0 };
9779        let mut progress_events = Vec::new();
9780        let mut progress = |archive_path: &str, bytes: u64| {
9781            progress_events.push((archive_path.to_owned(), bytes));
9782        };
9783
9784        opened
9785            .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
9786            .unwrap()
9787            .unwrap();
9788
9789        let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
9790        assert_eq!(writer.total, payload.len());
9791        assert_eq!(reported_bytes, payload.len() as u64);
9792        assert!(progress_events.len() > 1);
9793        assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
9794    }
9795
9796    #[test]
9797    fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
9798        let payload = pseudo_random_bytes(128 * 1024);
9799        let options = WriterOptions {
9800            block_size: 4096,
9801            chunk_size: 4096,
9802            envelope_target_size: 8192,
9803            stripe_width: 1,
9804            volume_loss_tolerance: 0,
9805            bit_rot_buffer_pct: 0,
9806            fec_data_shards: 4,
9807            fec_parity_shards: 0,
9808            index_fec_data_shards: 4,
9809            index_fec_parity_shards: 0,
9810            index_root_fec_data_shards: 4,
9811            index_root_fec_parity_shards: 0,
9812            ..WriterOptions::default()
9813        };
9814        let archive = write_archive(
9815            &[RegularFile::new("large.bin", &payload)],
9816            &master_key(),
9817            options,
9818        )
9819        .unwrap();
9820        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9821        let envelopes = envelope_entries_for_path(&eager, "large.bin");
9822        let last_envelope = envelopes
9823            .last()
9824            .expect("large fixture should have at least one envelope");
9825        assert_ne!(
9826            envelopes.first().unwrap().envelope_index,
9827            last_envelope.envelope_index,
9828            "fixture must span more than one payload envelope"
9829        );
9830        let corrupt_slot = block_record_slots(&archive.bytes)
9831            .into_iter()
9832            .enumerate()
9833            .find_map(|(slot, (_, _, record))| {
9834                (record.block_index == last_envelope.first_block_index).then_some(slot)
9835            })
9836            .unwrap();
9837        let mut corrupted = archive.bytes;
9838        corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
9839        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
9840        let tmp = tempfile::tempdir().unwrap();
9841
9842        assert!(matches!(
9843            opened
9844                .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
9845                .unwrap_err(),
9846            FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
9847        ));
9848        assert!(!tmp.path().join("large.bin").exists());
9849        assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
9850    }
9851
9852    #[test]
9853    fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
9854        let archive = write_archive(
9855            &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
9856            &master_key(),
9857            single_stream_options(),
9858        )
9859        .unwrap();
9860        let opened = open_archive_with_bootstrap_sidecar(
9861            &archive.bytes,
9862            &archive.bootstrap_sidecar,
9863            &master_key(),
9864        )
9865        .unwrap();
9866
9867        assert_eq!(
9868            opened.list_files().unwrap(),
9869            vec![ArchiveEntry {
9870                path: "dir/sidecar.txt".to_string(),
9871                file_data_size: 13,
9872                kind: TarEntryKind::Regular,
9873                mode: 0o644,
9874                mtime: 0,
9875                diagnostics: Vec::new(),
9876            }]
9877        );
9878        assert_eq!(
9879            opened.extract_file("dir/sidecar.txt").unwrap(),
9880            Some(b"hello sidecar".to_vec())
9881        );
9882        opened.verify().unwrap();
9883    }
9884
9885    #[test]
9886    fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
9887        let archive = write_archive_with_dictionary(
9888            &[RegularFile::new(
9889                "dir/dict.txt",
9890                b"common words common words dictionary payload",
9891            )],
9892            &master_key(),
9893            single_stream_options(),
9894            dictionary(),
9895        )
9896        .unwrap();
9897        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9898
9899        assert_eq!(opened.crypto_header.has_dictionary, 1);
9900        assert!(opened.index_root.header.dictionary_data_block_count > 0);
9901        assert_eq!(
9902            opened.list_files().unwrap(),
9903            vec![ArchiveEntry {
9904                path: "dir/dict.txt".to_string(),
9905                file_data_size: 44,
9906                kind: TarEntryKind::Regular,
9907                mode: 0o644,
9908                mtime: 0,
9909                diagnostics: Vec::new(),
9910            }]
9911        );
9912        assert_eq!(
9913            opened.extract_file("dir/dict.txt").unwrap(),
9914            Some(b"common words common words dictionary payload".to_vec())
9915        );
9916        opened.verify().unwrap();
9917    }
9918
9919    #[test]
9920    fn dictionary_object_tamper_fails_before_payload_decompression() {
9921        let archive = write_archive_with_dictionary(
9922            &[RegularFile::new(
9923                "dir/dict.txt",
9924                b"common words common words dictionary payload",
9925            )],
9926            &master_key(),
9927            single_stream_options(),
9928            dictionary(),
9929        )
9930        .unwrap();
9931        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9932        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9933        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
9934        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
9935        let dictionary_offset =
9936            crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
9937
9938        let mut tampered = archive.bytes.clone();
9939        tampered[dictionary_offset + 16] ^= 0x01;
9940        let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
9941        let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
9942        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9943
9944        assert_eq!(
9945            open_archive(&tampered, &master_key()).unwrap_err(),
9946            FormatError::AeadFailure
9947        );
9948    }
9949
9950    #[test]
9951    fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
9952        let archive = write_archive_with_dictionary(
9953            &[RegularFile::new(
9954                "dict-sidecar.txt",
9955                b"common words common words sidecar payload",
9956            )],
9957            &master_key(),
9958            single_stream_options(),
9959            dictionary(),
9960        )
9961        .unwrap();
9962        let opened = open_non_seekable_archive(
9963            &archive.bytes,
9964            &master_key(),
9965            Some(&archive.bootstrap_sidecar),
9966        )
9967        .unwrap();
9968
9969        assert_eq!(
9970            opened.extract_file("dict-sidecar.txt").unwrap(),
9971            Some(b"common words common words sidecar payload".to_vec())
9972        );
9973        opened.verify().unwrap();
9974    }
9975
9976    #[test]
9977    fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
9978        let archive = write_archive(
9979            &[RegularFile::new(
9980                "sidecar-terminal.txt",
9981                b"sidecar authority",
9982            )],
9983            &master_key(),
9984            single_stream_options(),
9985        )
9986        .unwrap();
9987        let mut corrupted = archive.bytes.clone();
9988        corrupt_v41_terminal_recovery(&mut corrupted);
9989        assert!(open_archive(&corrupted, &master_key()).is_err());
9990
9991        let opened =
9992            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
9993                .unwrap();
9994
9995        assert!(opened.volume_trailer.is_none());
9996        assert_eq!(
9997            opened.extract_file("sidecar-terminal.txt").unwrap(),
9998            Some(b"sidecar authority".to_vec())
9999        );
10000        opened.verify().unwrap();
10001    }
10002
10003    #[test]
10004    fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
10005        let archive = write_archive_with_dictionary(
10006            &[RegularFile::new(
10007                "dict-no-terminal.txt",
10008                b"common words common words without terminal",
10009            )],
10010            &master_key(),
10011            single_stream_options(),
10012            dictionary(),
10013        )
10014        .unwrap();
10015        let terminal_offset = terminal_material_offset(&archive.bytes);
10016        let truncated = archive.bytes[..terminal_offset].to_vec();
10017        assert!(open_archive(&truncated, &master_key()).is_err());
10018
10019        let opened =
10020            open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
10021                .unwrap();
10022
10023        assert!(opened.volume_trailer.is_none());
10024        assert_eq!(
10025            opened.extract_file("dict-no-terminal.txt").unwrap(),
10026            Some(b"common words common words without terminal".to_vec())
10027        );
10028        opened.verify().unwrap();
10029    }
10030
10031    #[test]
10032    fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
10033        let archive = write_archive(
10034            &[RegularFile::new(
10035                "sidecar-erasure.txt",
10036                b"repair through sidecar",
10037            )],
10038            &master_key(),
10039            single_stream_options(),
10040        )
10041        .unwrap();
10042        let mut corrupted = archive.bytes.clone();
10043        corrupt_first_block_record_payload(&mut corrupted);
10044
10045        let opened = open_archive_with_bootstrap_sidecar(
10046            &corrupted,
10047            &archive.bootstrap_sidecar,
10048            &master_key(),
10049        )
10050        .unwrap();
10051        assert_eq!(
10052            opened.extract_file("sidecar-erasure.txt").unwrap(),
10053            Some(b"repair through sidecar".to_vec())
10054        );
10055    }
10056
10057    #[test]
10058    fn extraction_rejects_logical_payload_above_total_size_cap() {
10059        let archive = write_archive(
10060            &[RegularFile::new("cap.txt", b"payload")],
10061            &master_key(),
10062            single_stream_options(),
10063        )
10064        .unwrap();
10065        let options = ReaderOptions {
10066            max_total_extraction_size: 3,
10067            ..ReaderOptions::default()
10068        };
10069        let opened =
10070            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
10071
10072        assert_eq!(
10073            opened.extract_file("cap.txt").unwrap_err(),
10074            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10075        );
10076    }
10077
10078    #[test]
10079    fn verify_does_not_apply_extraction_payload_cap() {
10080        let archive = write_archive(
10081            &[RegularFile::new("verify-cap.txt", b"payload")],
10082            &master_key(),
10083            single_stream_options(),
10084        )
10085        .unwrap();
10086        let options = ReaderOptions {
10087            max_total_extraction_size: 3,
10088            ..ReaderOptions::default()
10089        };
10090        let opened =
10091            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
10092
10093        opened.verify().unwrap();
10094        assert_eq!(
10095            opened.extract_file("verify-cap.txt").unwrap_err(),
10096            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10097        );
10098    }
10099
10100    #[test]
10101    fn verify_streams_past_legacy_in_memory_tar_cap() {
10102        let data = vec![0x5a; 4096];
10103        let archive = write_archive(
10104            &[RegularFile::new("verify-large.txt", &data)],
10105            &master_key(),
10106            single_stream_options(),
10107        )
10108        .unwrap();
10109        let options = ReaderOptions {
10110            max_verify_tar_size: 1,
10111            ..ReaderOptions::default()
10112        };
10113        let opened =
10114            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
10115
10116        opened.verify().unwrap();
10117    }
10118
10119    #[test]
10120    fn dictionary_sidecar_requires_dictionary_record_section() {
10121        let archive = write_archive_with_dictionary(
10122            &[RegularFile::new("dict-missing.txt", b"common words")],
10123            &master_key(),
10124            single_stream_options(),
10125            dictionary(),
10126        )
10127        .unwrap();
10128        let header = BootstrapSidecarHeader::parse(
10129            &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
10130        )
10131        .unwrap();
10132        let mut missing_dictionary =
10133            archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
10134        rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
10135            header.flags &= !0x04;
10136            header.dictionary_records_offset = 0;
10137            header.dictionary_records_length = 0;
10138        });
10139
10140        assert_eq!(
10141            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
10142                .unwrap_err(),
10143            FormatError::ReaderUnsupported("dictionary bootstrap required")
10144        );
10145    }
10146
10147    #[test]
10148    fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
10149        let archive = write_archive_with_dictionary(
10150            &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
10151            &master_key(),
10152            single_stream_options(),
10153            dictionary(),
10154        )
10155        .unwrap();
10156
10157        let mut wrong_kind = archive.bootstrap_sidecar.clone();
10158        mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
10159            record.kind = BlockKind::IndexRootData;
10160        });
10161        assert_eq!(
10162            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
10163                .unwrap_err(),
10164            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
10165        );
10166
10167        let mut wrong_last = archive.bootstrap_sidecar.clone();
10168        mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
10169            record.flags = 0;
10170        });
10171        assert_eq!(
10172            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
10173                .unwrap_err(),
10174            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
10175        );
10176    }
10177
10178    #[test]
10179    fn non_seekable_random_access_requires_sidecar() {
10180        let archive = write_archive(
10181            &[RegularFile::new("file.txt", b"payload")],
10182            &master_key(),
10183            single_stream_options(),
10184        )
10185        .unwrap();
10186
10187        assert_eq!(
10188            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10189            FormatError::ReaderUnsupported(
10190                "non-seekable random access requires a bootstrap sidecar"
10191            )
10192        );
10193        assert!(open_non_seekable_archive(
10194            &archive.bytes,
10195            &master_key(),
10196            Some(&archive.bootstrap_sidecar)
10197        )
10198        .is_ok());
10199    }
10200
10201    #[test]
10202    fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
10203        let archive = write_archive(
10204            &[RegularFile::new("sparse.txt", b"sparse sidecar")],
10205            &master_key(),
10206            single_stream_options(),
10207        )
10208        .unwrap();
10209        let index_root_only = sparse_bootstrap_sidecar(
10210            &archive.bootstrap_sidecar,
10211            &master_key(),
10212            false,
10213            true,
10214            false,
10215        );
10216
10217        assert_eq!(
10218            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
10219                .unwrap_err(),
10220            FormatError::ReaderUnsupported(
10221                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10222            )
10223        );
10224    }
10225
10226    #[test]
10227    fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
10228        let archive = write_archive(
10229            &[RegularFile::new("sparse-index.txt", b"recover index root")],
10230            &master_key(),
10231            single_stream_options(),
10232        )
10233        .unwrap();
10234        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10235        let mut corrupted = archive.bytes.clone();
10236        corrupt_object_extent_records(
10237            &mut corrupted,
10238            index_root_extent_from_manifest(&opened.manifest_footer),
10239        );
10240        assert!(open_archive(&corrupted, &master_key()).is_err());
10241
10242        let index_root_only = sparse_bootstrap_sidecar(
10243            &archive.bootstrap_sidecar,
10244            &master_key(),
10245            false,
10246            true,
10247            false,
10248        );
10249        let recovered =
10250            open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
10251                .unwrap();
10252
10253        assert_eq!(
10254            recovered.extract_file("sparse-index.txt").unwrap(),
10255            Some(b"recover index root".to_vec())
10256        );
10257        recovered.verify().unwrap();
10258    }
10259
10260    #[test]
10261    fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
10262        let archive = write_archive_with_dictionary(
10263            &[RegularFile::new(
10264                "sparse-dict.txt",
10265                b"common words common words sparse dictionary",
10266            )],
10267            &master_key(),
10268            single_stream_options(),
10269            dictionary(),
10270        )
10271        .unwrap();
10272        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10273        let mut corrupted = archive.bytes.clone();
10274        corrupt_object_extent_records(
10275            &mut corrupted,
10276            dictionary_extent_from_index_root(&opened.index_root).unwrap(),
10277        );
10278        assert!(open_archive(&corrupted, &master_key()).is_err());
10279
10280        let dictionary_only = sparse_bootstrap_sidecar(
10281            &archive.bootstrap_sidecar,
10282            &master_key(),
10283            false,
10284            false,
10285            true,
10286        );
10287        assert_eq!(
10288            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
10289                .unwrap_err(),
10290            FormatError::ReaderUnsupported(
10291                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10292            )
10293        );
10294
10295        let recovered =
10296            open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
10297                .unwrap();
10298        assert_eq!(
10299            recovered.extract_file("sparse-dict.txt").unwrap(),
10300            Some(b"common words common words sparse dictionary".to_vec())
10301        );
10302        recovered.verify().unwrap();
10303    }
10304
10305    #[test]
10306    fn sequential_extracts_dictionary_free_tar_stream() {
10307        let archive = write_archive(
10308            &[RegularFile::new("seq.txt", b"streaming")],
10309            &master_key(),
10310            single_stream_options(),
10311        )
10312        .unwrap();
10313
10314        let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
10315        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10316        assert_eq!(member.path, b"seq.txt");
10317        assert_eq!(member.data, b"streaming");
10318    }
10319
10320    #[test]
10321    fn sequential_rejects_logical_payload_above_total_size_cap() {
10322        let archive = write_archive(
10323            &[RegularFile::new("seq-cap.txt", b"payload")],
10324            &master_key(),
10325            single_stream_options(),
10326        )
10327        .unwrap();
10328        let options = ReaderOptions {
10329            max_total_extraction_size: 3,
10330            ..ReaderOptions::default()
10331        };
10332
10333        assert_eq!(
10334            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10335                .unwrap_err(),
10336            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10337        );
10338    }
10339
10340    #[test]
10341    fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
10342        let archive = write_archive(
10343            &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
10344            &master_key(),
10345            single_stream_options(),
10346        )
10347        .unwrap();
10348        let options = ReaderOptions {
10349            max_verify_tar_size: 512,
10350            ..ReaderOptions::default()
10351        };
10352
10353        assert_eq!(
10354            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10355                .unwrap_err(),
10356            FormatError::ReaderUnsupported(
10357                "sequential tar stream exceeds configured verification cap"
10358            )
10359        );
10360    }
10361
10362    #[test]
10363    fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
10364        let archive = write_archive(
10365            &[RegularFile::new("seq-erasure.txt", b"stream repair")],
10366            &master_key(),
10367            single_stream_options(),
10368        )
10369        .unwrap();
10370        let mut corrupted = archive.bytes;
10371        corrupt_first_block_record_payload(&mut corrupted);
10372
10373        let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
10374        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10375        assert_eq!(member.path, b"seq-erasure.txt");
10376        assert_eq!(member.data, b"stream repair");
10377    }
10378
10379    #[test]
10380    fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
10381        let archive = write_archive(
10382            &[RegularFile::new("seq-no-parity.txt", b"no repair")],
10383            &master_key(),
10384            WriterOptions {
10385                bit_rot_buffer_pct: 0,
10386                fec_parity_shards: 0,
10387                index_fec_parity_shards: 0,
10388                index_root_fec_parity_shards: 0,
10389                ..single_stream_options()
10390            },
10391        )
10392        .unwrap();
10393        let mut corrupted = archive.bytes;
10394        corrupt_first_block_record_payload(&mut corrupted);
10395
10396        assert_eq!(
10397            sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
10398            FormatError::BadCrc {
10399                structure: "BlockRecord"
10400            }
10401        );
10402    }
10403
10404    #[test]
10405    fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
10406        let archive = write_archive(
10407            &[RegularFile::new(
10408                "seq.txt",
10409                b"payload must not be returned after terminal auth failure",
10410            )],
10411            &master_key(),
10412            single_stream_options(),
10413        )
10414        .unwrap();
10415        let mut corrupted = archive.bytes;
10416        corrupt_v41_terminal_recovery(&mut corrupted);
10417
10418        match sequential_extract_tar_stream(&corrupted, &master_key()) {
10419            Ok(bytes) => panic!(
10420                "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
10421                bytes.len()
10422            ),
10423            Err(err) => assert_eq!(
10424                err,
10425                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10426            ),
10427        }
10428    }
10429
10430    #[test]
10431    fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
10432        let archive = write_archive_with_dictionary(
10433            &[RegularFile::new(
10434                "seq-dict.txt",
10435                b"common words common words dictionary payload",
10436            )],
10437            &master_key(),
10438            single_stream_options(),
10439            b"common words dictionary",
10440        )
10441        .unwrap();
10442
10443        match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
10444            Ok(bytes) => panic!(
10445                "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
10446                bytes.len()
10447            ),
10448            Err(err) => assert_eq!(
10449                err,
10450                FormatError::ReaderUnsupported(
10451                    "dictionary bootstrap required for non-seekable sequential extraction"
10452                )
10453            ),
10454        }
10455    }
10456
10457    #[test]
10458    fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
10459        let archive = write_archive_with_dictionary(
10460            &[RegularFile::new(
10461                "seq-dict-open.txt",
10462                b"common words common words bootstrap required",
10463            )],
10464            &master_key(),
10465            single_stream_options(),
10466            b"common words bootstrap",
10467        )
10468        .unwrap();
10469
10470        assert_eq!(
10471            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10472            FormatError::ReaderUnsupported(
10473                "non-seekable random access requires a bootstrap sidecar"
10474            )
10475        );
10476    }
10477
10478    #[test]
10479    fn sequential_zstd_stream_rejects_skippable_frame_segments() {
10480        let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
10481        let mut output = Vec::new();
10482
10483        assert_eq!(
10484            decode_concatenated_zstd_frames_with_cap(
10485                &skippable,
10486                None,
10487                &mut output,
10488                usize::MAX,
10489                None,
10490            )
10491            .unwrap_err(),
10492            FormatError::NotStandardZstdFrame
10493        );
10494        assert!(output.is_empty());
10495    }
10496
10497    #[test]
10498    fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
10499        let archive = write_archive(
10500            &[RegularFile::new("live.txt", b"stream verify")],
10501            &master_key(),
10502            single_stream_options(),
10503        )
10504        .unwrap();
10505
10506        let report =
10507            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10508
10509        assert_eq!(report.file_count, 1);
10510        assert_eq!(report.total_volumes, 1);
10511        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
10512        assert!(report.payload_block_count > 0);
10513    }
10514
10515    #[test]
10516    fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
10517        let archive = write_archive(
10518            &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
10519            &master_key(),
10520            single_stream_options(),
10521        )
10522        .unwrap();
10523
10524        let report =
10525            verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
10526                .unwrap();
10527
10528        assert_eq!(report.file_count, 1);
10529        assert_eq!(report.tar_total_size % 512, 0);
10530    }
10531
10532    #[test]
10533    fn live_non_seekable_verify_stream_accepts_empty_archive() {
10534        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10535
10536        let report =
10537            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10538
10539        assert_eq!(report.file_count, 0);
10540        assert_eq!(report.payload_block_count, 0);
10541        assert_eq!(report.tar_total_size, 0);
10542    }
10543
10544    #[test]
10545    fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
10546        let archive = write_archive_with_dictionary(
10547            &[RegularFile::new(
10548                "live-dict.txt",
10549                b"common words common words dictionary payload",
10550            )],
10551            &master_key(),
10552            single_stream_options(),
10553            b"common words dictionary",
10554        )
10555        .unwrap();
10556
10557        assert_eq!(
10558            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
10559                .unwrap_err(),
10560            FormatError::ReaderUnsupported(
10561                "dictionary bootstrap required for non-seekable sequential verification"
10562            )
10563        );
10564    }
10565
10566    #[test]
10567    fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
10568        let archive = write_archive_with_dictionary(
10569            &[RegularFile::new(
10570                "live-dict-sidecar.txt",
10571                b"common words common words dictionary payload",
10572            )],
10573            &master_key(),
10574            single_stream_options(),
10575            b"common words dictionary",
10576        )
10577        .unwrap();
10578
10579        let report = verify_non_seekable_stream_with_bootstrap_sidecar(
10580            std::io::Cursor::new(archive.bytes),
10581            &archive.bootstrap_sidecar,
10582            &master_key(),
10583            NonSeekableReaderOptions::default(),
10584        )
10585        .unwrap();
10586
10587        assert_eq!(report.file_count, 1);
10588        assert_eq!(report.total_volumes, 1);
10589    }
10590
10591    #[test]
10592    fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
10593        let archive = write_archive(
10594            &[RegularFile::new("tail-cap.txt", b"payload")],
10595            &master_key(),
10596            single_stream_options(),
10597        )
10598        .unwrap();
10599        let options = NonSeekableReaderOptions {
10600            max_terminal_tail_size: 8,
10601            ..NonSeekableReaderOptions::default()
10602        };
10603
10604        assert_eq!(
10605            verify_non_seekable_stream_with_options(
10606                std::io::Cursor::new(archive.bytes),
10607                &master_key(),
10608                options
10609            )
10610            .unwrap_err(),
10611            FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
10612        );
10613    }
10614
10615    #[test]
10616    fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
10617        let archive = write_archive(
10618            &[RegularFile::new("metadata-cap.txt", b"payload")],
10619            &master_key(),
10620            single_stream_options(),
10621        )
10622        .unwrap();
10623        let options = NonSeekableReaderOptions {
10624            max_retained_metadata_bytes: 1,
10625            ..NonSeekableReaderOptions::default()
10626        };
10627
10628        assert_eq!(
10629            verify_non_seekable_stream_with_options(
10630                std::io::Cursor::new(archive.bytes),
10631                &master_key(),
10632                options
10633            )
10634            .unwrap_err(),
10635            FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
10636        );
10637    }
10638
10639    #[test]
10640    fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
10641        let archive = write_archive(
10642            &[RegularFile::new("metadata-erasure.txt", b"payload")],
10643            &master_key(),
10644            single_stream_options(),
10645        )
10646        .unwrap();
10647        let mut corrupted = archive.bytes;
10648        let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
10649        corrupt_block_record_payload_at_slot(&mut corrupted, slot);
10650
10651        let report =
10652            verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
10653
10654        assert_eq!(report.file_count, 1);
10655    }
10656
10657    #[test]
10658    fn live_non_seekable_verify_rejects_member_count_above_cap() {
10659        let archive = write_archive(
10660            &[RegularFile::new("member-cap.txt", b"payload")],
10661            &master_key(),
10662            single_stream_options(),
10663        )
10664        .unwrap();
10665        let options = NonSeekableReaderOptions {
10666            max_streamed_member_count: 0,
10667            ..NonSeekableReaderOptions::default()
10668        };
10669
10670        assert_eq!(
10671            verify_non_seekable_stream_with_options(
10672                std::io::Cursor::new(archive.bytes),
10673                &master_key(),
10674                options
10675            )
10676            .unwrap_err(),
10677            FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
10678        );
10679    }
10680
10681    #[test]
10682    fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
10683        let archive = write_archive(
10684            &[RegularFile::new("live-total-cap.txt", b"payload")],
10685            &master_key(),
10686            single_stream_options(),
10687        )
10688        .unwrap();
10689        let mut options = NonSeekableReaderOptions::default();
10690        options.reader.max_total_extraction_size = 3;
10691
10692        assert_eq!(
10693            verify_non_seekable_stream_with_options(
10694                std::io::Cursor::new(archive.bytes),
10695                &master_key(),
10696                options
10697            )
10698            .unwrap_err(),
10699            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10700        );
10701    }
10702
10703    #[test]
10704    fn live_non_seekable_verify_reports_root_auth_wire_only() {
10705        let archive = write_archive_with_root_auth(
10706            &[RegularFile::new("signed-live.txt", b"root-auth stream")],
10707            &master_key(),
10708            single_stream_options(),
10709            test_root_auth_config(),
10710            |request| Ok(test_root_auth_value(request)),
10711        )
10712        .unwrap();
10713
10714        let report =
10715            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10716
10717        assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
10718    }
10719
10720    #[test]
10721    fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
10722        let archive = write_archive(
10723            &[
10724                RegularFile::new("alpha.txt", b"alpha"),
10725                RegularFile::new("nested/beta.txt", b"beta"),
10726            ],
10727            &master_key(),
10728            single_stream_options(),
10729        )
10730        .unwrap();
10731        let tmp = tempfile::tempdir().unwrap();
10732        let out = tmp.path().join("out");
10733
10734        let report = extract_non_seekable_stream_to_dir(
10735            std::io::Cursor::new(archive.bytes),
10736            &master_key(),
10737            &out,
10738            NonSeekableReaderOptions::default(),
10739            SafeExtractionOptions::default(),
10740        )
10741        .unwrap();
10742
10743        assert_eq!(report.verification.file_count, 2);
10744        assert_eq!(report.extracted_member_count, 2);
10745        assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
10746        assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
10747    }
10748
10749    #[test]
10750    fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
10751        let archive = write_archive(
10752            &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
10753            &master_key(),
10754            single_stream_options(),
10755        )
10756        .unwrap();
10757        let tmp = tempfile::tempdir().unwrap();
10758        let out = tmp.path().join("out");
10759
10760        extract_non_seekable_stream_to_dir(
10761            ChunkedReader::new(archive.bytes, 1),
10762            &master_key(),
10763            &out,
10764            NonSeekableReaderOptions::default(),
10765            SafeExtractionOptions::default(),
10766        )
10767        .unwrap();
10768
10769        assert_eq!(
10770            fs::read(out.join("tiny-extract.txt")).unwrap(),
10771            b"chunked extraction"
10772        );
10773    }
10774
10775    #[test]
10776    fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
10777        let archive = write_archive(
10778            &[RegularFile::new("late-fail.txt", b"must remain staged")],
10779            &master_key(),
10780            single_stream_options(),
10781        )
10782        .unwrap();
10783        let mut corrupted = archive.bytes;
10784        corrupt_v41_terminal_recovery(&mut corrupted);
10785        let tmp = tempfile::tempdir().unwrap();
10786        let out = tmp.path().join("out");
10787
10788        match extract_non_seekable_stream_to_dir(
10789            std::io::Cursor::new(corrupted),
10790            &master_key(),
10791            &out,
10792            NonSeekableReaderOptions::default(),
10793            SafeExtractionOptions::default(),
10794        )
10795        .unwrap_err()
10796        {
10797            ExtractError::Format(err) => assert_eq!(
10798                err,
10799                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10800            ),
10801            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10802        }
10803        assert!(!out.exists());
10804    }
10805
10806    #[test]
10807    fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
10808        let archive = write_archive(
10809            &[RegularFile::new("same.txt", b"new")],
10810            &master_key(),
10811            single_stream_options(),
10812        )
10813        .unwrap();
10814        let tmp = tempfile::tempdir().unwrap();
10815        let out = tmp.path().join("out");
10816        fs::create_dir(&out).unwrap();
10817        fs::write(out.join("same.txt"), b"old").unwrap();
10818
10819        match extract_non_seekable_stream_to_dir(
10820            std::io::Cursor::new(archive.bytes.clone()),
10821            &master_key(),
10822            &out,
10823            NonSeekableReaderOptions::default(),
10824            SafeExtractionOptions::default(),
10825        )
10826        .unwrap_err()
10827        {
10828            ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
10829            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10830        }
10831        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
10832
10833        extract_non_seekable_stream_to_dir(
10834            std::io::Cursor::new(archive.bytes),
10835            &master_key(),
10836            &out,
10837            NonSeekableReaderOptions::default(),
10838            SafeExtractionOptions {
10839                overwrite_existing: true,
10840            },
10841        )
10842        .unwrap();
10843        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
10844    }
10845
10846    #[test]
10847    fn live_non_seekable_list_stream_matches_seekable_final_view() {
10848        let archive = write_archive(
10849            &[
10850                RegularFile::new("a.txt", b"a"),
10851                RegularFile::new("b.txt", b"bb"),
10852            ],
10853            &master_key(),
10854            single_stream_options(),
10855        )
10856        .unwrap();
10857        let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
10858        let expected = seekable.list_files().unwrap();
10859
10860        let report = list_non_seekable_stream(
10861            std::io::Cursor::new(archive.bytes),
10862            &master_key(),
10863            NonSeekableReaderOptions::default(),
10864        )
10865        .unwrap();
10866
10867        assert_eq!(report.verification.file_count, 2);
10868        assert_eq!(report.entries, expected);
10869    }
10870
10871    #[test]
10872    fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
10873        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10874        let mut bad_flags = archive.bootstrap_sidecar.clone();
10875        rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
10876            header.flags |= 0x08;
10877        });
10878        assert_eq!(
10879            open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
10880                .unwrap_err(),
10881            FormatError::UnknownBootstrapSidecarFlags(0x0b)
10882        );
10883
10884        let mut trailing = archive.bootstrap_sidecar.clone();
10885        trailing.push(0);
10886        assert_eq!(
10887            open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
10888                .unwrap_err(),
10889            FormatError::NonCanonicalBootstrapSidecarLayout
10890        );
10891    }
10892
10893    #[test]
10894    fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
10895        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10896        let mut wrong_volume = archive.bootstrap_sidecar.clone();
10897        mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
10898            footer.volume_index = 1;
10899        });
10900        assert_eq!(
10901            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
10902                .unwrap_err(),
10903            FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
10904        );
10905
10906        let mut non_authoritative = archive.bootstrap_sidecar.clone();
10907        mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
10908            footer.is_authoritative = 0;
10909        });
10910        assert_eq!(
10911            open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
10912                .unwrap_err(),
10913            FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
10914        );
10915    }
10916
10917    #[test]
10918    fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
10919        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10920        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10921        let crypto_start = volume_header.crypto_header_offset as usize;
10922        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
10923        let crypto_header = CryptoHeader::parse(
10924            &archive.bytes[crypto_start..crypto_end],
10925            volume_header.crypto_header_length,
10926        )
10927        .unwrap();
10928        let subkeys = Subkeys::derive(
10929            &master_key(),
10930            &volume_header.archive_uuid,
10931            &volume_header.session_id,
10932        )
10933        .unwrap();
10934        let mut opened_header = volume_header;
10935        opened_header.volume_index = 1;
10936
10937        let parsed = parse_bootstrap_sidecar(
10938            &archive.bootstrap_sidecar,
10939            &opened_header,
10940            &crypto_header.fixed,
10941            &subkeys,
10942        )
10943        .unwrap();
10944
10945        assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
10946    }
10947
10948    #[test]
10949    fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
10950        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10951        let mut conflicting = archive.bootstrap_sidecar.clone();
10952        mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
10953            footer.index_root_first_block += 1;
10954        });
10955
10956        assert_eq!(
10957            open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
10958                .unwrap_err(),
10959            FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
10960        );
10961    }
10962
10963    #[test]
10964    fn sidecar_size_cap_counts_only_present_sparse_sections() {
10965        let mut crypto_header = test_crypto_header();
10966        crypto_header.has_dictionary = 1;
10967        crypto_header.index_root_fec_data_shards = 1;
10968        crypto_header.index_root_fec_parity_shards = 0;
10969        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10970        let header = BootstrapSidecarHeader {
10971            archive_uuid: [0x31; 16],
10972            session_id: [0x42; 16],
10973            flags: 0x04,
10974            manifest_footer_offset: 0,
10975            manifest_footer_length: 0,
10976            index_root_records_offset: 0,
10977            index_root_records_length: 0,
10978            dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10979            dictionary_records_length: record_len,
10980            sidecar_hmac: [0u8; 32],
10981            header_crc32c: 0,
10982        };
10983
10984        validate_sidecar_size_cap(
10985            &header,
10986            &crypto_header,
10987            BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
10988        )
10989        .unwrap();
10990        assert_eq!(
10991            validate_sidecar_size_cap(
10992                &header,
10993                &crypto_header,
10994                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
10995            )
10996            .unwrap_err(),
10997            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10998        );
10999    }
11000
11001    #[test]
11002    fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
11003        let mut crypto_header = test_crypto_header();
11004        crypto_header.index_root_fec_data_shards = 1;
11005        crypto_header.index_root_fec_parity_shards = 0;
11006        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
11007        let header = BootstrapSidecarHeader {
11008            archive_uuid: [0x31; 16],
11009            session_id: [0x42; 16],
11010            flags: 0x02,
11011            manifest_footer_offset: 0,
11012            manifest_footer_length: 0,
11013            index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
11014            index_root_records_length: record_len * 2,
11015            dictionary_records_offset: 0,
11016            dictionary_records_length: 0,
11017            sidecar_hmac: [0u8; 32],
11018            header_crc32c: 0,
11019        };
11020
11021        assert_eq!(
11022            validate_sidecar_size_cap(
11023                &header,
11024                &crypto_header,
11025                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
11026            )
11027            .unwrap_err(),
11028            FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
11029        );
11030    }
11031
11032    #[test]
11033    fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
11034        let mut crypto_header = test_crypto_header();
11035        crypto_header.block_size = u32::MAX;
11036        crypto_header.index_root_fec_data_shards = u16::MAX;
11037        crypto_header.index_root_fec_parity_shards = u16::MAX;
11038        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
11039        let max_records = crypto_header.index_root_fec_data_shards as u64
11040            + crypto_header.index_root_fec_parity_shards as u64;
11041        let max_section_len = max_records * record_len;
11042        let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
11043            + MANIFEST_FOOTER_LEN as u64
11044            + max_section_len
11045            + max_section_len;
11046        let header = BootstrapSidecarHeader {
11047            archive_uuid: [0x31; 16],
11048            session_id: [0x42; 16],
11049            flags: 0x01 | 0x02 | 0x04,
11050            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
11051            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
11052            index_root_records_offset: 0,
11053            index_root_records_length: max_section_len,
11054            dictionary_records_offset: 0,
11055            dictionary_records_length: max_section_len,
11056            sidecar_hmac: [0u8; 32],
11057            header_crc32c: 0,
11058        };
11059
11060        validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
11061        assert_eq!(
11062            validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
11063            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
11064        );
11065    }
11066
11067    #[test]
11068    fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
11069        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11070        let mut with_dictionary = archive.bootstrap_sidecar.clone();
11071        let header =
11072            BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11073                .unwrap();
11074        let record_len = sidecar_record_len(&with_dictionary);
11075        let first_record = header.index_root_records_offset as usize;
11076        let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
11077        let dictionary_offset = with_dictionary.len() as u64;
11078        with_dictionary.extend_from_slice(&copied_record);
11079        rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
11080            header.flags |= 0x04;
11081            header.dictionary_records_offset = dictionary_offset;
11082            header.dictionary_records_length = record_len as u64;
11083        });
11084
11085        assert_eq!(
11086            open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
11087                .unwrap_err(),
11088            FormatError::InvalidArchive(
11089                "bootstrap sidecar has dictionary records while has_dictionary is false"
11090            )
11091        );
11092    }
11093
11094    #[test]
11095    fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
11096        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11097        let mut missing = archive.bootstrap_sidecar.clone();
11098        let record_len = sidecar_record_len(&missing);
11099        let new_len = missing.len() - record_len;
11100        missing.truncate(new_len);
11101        rewrite_sidecar_header(&mut missing, &master_key(), |header| {
11102            header.index_root_records_length -= record_len as u64;
11103        });
11104        assert_eq!(
11105            open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
11106                .unwrap_err(),
11107            FormatError::InvalidArchive(
11108                "sidecar BlockRecord section does not match declared extent"
11109            )
11110        );
11111
11112        let mut duplicate = archive.bootstrap_sidecar.clone();
11113        mutate_sidecar_index_record(&mut duplicate, 1, |record| {
11114            record.block_index -= 1;
11115        });
11116        assert_eq!(
11117            open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
11118                .unwrap_err(),
11119            FormatError::InvalidArchive(
11120                "sidecar BlockRecord section has missing or duplicate blocks"
11121            )
11122        );
11123
11124        let mut misordered = archive.bootstrap_sidecar.clone();
11125        swap_sidecar_index_records(&mut misordered, 0, 1);
11126        assert_eq!(
11127            open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
11128                .unwrap_err(),
11129            FormatError::InvalidArchive(
11130                "sidecar BlockRecord section has missing or duplicate blocks"
11131            )
11132        );
11133
11134        let mut wrong_kind = archive.bootstrap_sidecar.clone();
11135        mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
11136            record.kind = BlockKind::PayloadData;
11137        });
11138        assert_eq!(
11139            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
11140                .unwrap_err(),
11141            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
11142        );
11143
11144        let mut wrong_last = archive.bootstrap_sidecar.clone();
11145        mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
11146            record.flags = 0;
11147        });
11148        assert_eq!(
11149            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
11150                .unwrap_err(),
11151            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
11152        );
11153    }
11154
11155    #[test]
11156    fn verify_helper_rejects_envelope_frame_coverage_gap() {
11157        let frames = BTreeMap::from([(
11158            0,
11159            FrameEntry {
11160                frame_index: 0,
11161                envelope_index: 0,
11162                offset_in_envelope: 0,
11163                compressed_size: 10,
11164                decompressed_size: 512,
11165                flags: 0,
11166                tar_stream_offset: 0,
11167            },
11168        )]);
11169        let envelopes = BTreeMap::from([(
11170            0,
11171            EnvelopeEntry {
11172                envelope_index: 0,
11173                first_block_index: 0,
11174                data_block_count: 1,
11175                parity_block_count: 1,
11176                encrypted_size: 4096,
11177                plaintext_size: 11,
11178                first_frame_index: 0,
11179                frame_count: 1,
11180            },
11181        )]);
11182
11183        assert_eq!(
11184            validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
11185            FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
11186        );
11187    }
11188
11189    #[test]
11190    fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
11191        assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
11192        assert_eq!(
11193            validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
11194            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11195        );
11196        assert_eq!(
11197            validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
11198            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11199        );
11200    }
11201
11202    #[test]
11203    fn verify_rejects_authenticated_content_hash_mismatch() {
11204        let options = WriterOptions {
11205            index_root_fec_parity_shards: 0,
11206            ..single_stream_options()
11207        };
11208        let archive = write_archive(
11209            &[RegularFile::new("content-hash.txt", b"hash covered")],
11210            &master_key(),
11211            options,
11212        )
11213        .unwrap();
11214        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11215
11216        let mut root = opened.index_root.clone();
11217        root.header.content_sha256 = [0xa5; 32];
11218        let root_plaintext = root.to_bytes();
11219        IndexRoot::parse(
11220            &root_plaintext,
11221            false,
11222            metadata_limits(&opened.crypto_header),
11223        )
11224        .unwrap();
11225        assert_eq!(
11226            root_plaintext.len() as u32,
11227            opened.manifest_footer.index_root_decompressed_size
11228        );
11229
11230        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11231        let mut next_block_index = opened.manifest_footer.index_root_first_block;
11232        let replacement = encrypt_test_object(
11233            &compressed_root,
11234            &opened.subkeys.index_root_key,
11235            &opened.subkeys.index_nonce_seed,
11236            b"idxroot",
11237            0,
11238            BlockKind::IndexRootData,
11239            &mut next_block_index,
11240            &opened.crypto_header,
11241            &opened.volume_header,
11242        );
11243        assert_eq!(
11244            replacement.extent.data_block_count,
11245            opened.manifest_footer.index_root_data_block_count
11246        );
11247        assert_eq!(
11248            replacement.extent.encrypted_size,
11249            opened.manifest_footer.index_root_encrypted_size
11250        );
11251
11252        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11253        let crypto_end = volume_header.crypto_header_offset as usize
11254            + volume_header.crypto_header_length as usize;
11255        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11256        let mut malformed = archive.bytes.clone();
11257        for record in replacement.records {
11258            let offset = crypto_end + record.block_index as usize * record_len;
11259            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11260        }
11261
11262        let reopened = open_archive(&malformed, &master_key()).unwrap();
11263        assert_eq!(
11264            reopened.verify().unwrap_err(),
11265            FormatError::InvalidArchive(
11266                "IndexRoot content_sha256 does not match decoded tar stream"
11267            )
11268        );
11269    }
11270
11271    #[test]
11272    fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
11273        let (mut path_mismatch, _) = multi_envelope_reader_fixture();
11274        rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
11275            path[0] = b'x';
11276        });
11277        assert_eq!(
11278            path_mismatch.verify().unwrap_err(),
11279            FormatError::InvalidArchive("tar member path does not match FileEntry path")
11280        );
11281
11282        let (mut size_mismatch, _) = multi_envelope_reader_fixture();
11283        rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
11284            file.file_data_size += 1;
11285        });
11286        assert_eq!(
11287            size_mismatch.verify().unwrap_err(),
11288            FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
11289        );
11290    }
11291
11292    #[test]
11293    fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
11294        let (mut opened, _) = multi_envelope_reader_fixture();
11295        let locating = opened.index_root.shards[0].clone();
11296        let mut duplicate = opened.load_index_shard(&locating).unwrap();
11297        duplicate.header.shard_index = 1;
11298        duplicate.frames[0].flags ^= 0x0000_0001;
11299        let duplicate_plaintext = duplicate.to_bytes();
11300        let mut next_block_index = opened
11301            .blocks
11302            .keys()
11303            .last()
11304            .copied()
11305            .map(|index| index + 1)
11306            .unwrap_or(0);
11307        let duplicate_object = encrypt_test_object(
11308            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11309            &opened.subkeys.index_shard_key,
11310            &opened.subkeys.index_nonce_seed,
11311            b"idxshard",
11312            1,
11313            BlockKind::IndexShardData,
11314            &mut next_block_index,
11315            &opened.crypto_header,
11316            &opened.volume_header,
11317        );
11318        insert_records(&mut opened.blocks, &duplicate_object.records);
11319        opened.index_root.shards.push(ShardEntry {
11320            shard_index: 1,
11321            first_block_index: duplicate_object.extent.first_block_index,
11322            data_block_count: duplicate_object.extent.data_block_count,
11323            parity_block_count: 0,
11324            encrypted_size: duplicate_object.extent.encrypted_size,
11325            decompressed_size: duplicate_plaintext.len() as u32,
11326            file_count: locating.file_count,
11327            first_path_hash: locating.first_path_hash,
11328            last_path_hash: locating.last_path_hash,
11329        });
11330        opened.index_root.header.file_count += locating.file_count as u64;
11331
11332        assert_eq!(
11333            opened.verify().unwrap_err(),
11334            FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
11335        );
11336    }
11337
11338    #[test]
11339    fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
11340        let (mut opened, _) = multi_envelope_reader_fixture();
11341        let locating = opened.index_root.shards[0].clone();
11342        let mut duplicate = opened.load_index_shard(&locating).unwrap();
11343        duplicate.header.shard_index = 1;
11344        duplicate.envelopes[0].first_block_index += 1;
11345        let duplicate_plaintext = duplicate.to_bytes();
11346        let mut next_block_index = opened
11347            .blocks
11348            .keys()
11349            .last()
11350            .copied()
11351            .map(|index| index + 1)
11352            .unwrap_or(0);
11353        let duplicate_object = encrypt_test_object(
11354            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11355            &opened.subkeys.index_shard_key,
11356            &opened.subkeys.index_nonce_seed,
11357            b"idxshard",
11358            1,
11359            BlockKind::IndexShardData,
11360            &mut next_block_index,
11361            &opened.crypto_header,
11362            &opened.volume_header,
11363        );
11364        insert_records(&mut opened.blocks, &duplicate_object.records);
11365        opened.index_root.shards.push(ShardEntry {
11366            shard_index: 1,
11367            first_block_index: duplicate_object.extent.first_block_index,
11368            data_block_count: duplicate_object.extent.data_block_count,
11369            parity_block_count: 0,
11370            encrypted_size: duplicate_object.extent.encrypted_size,
11371            decompressed_size: duplicate_plaintext.len() as u32,
11372            file_count: locating.file_count,
11373            first_path_hash: locating.first_path_hash,
11374            last_path_hash: locating.last_path_hash,
11375        });
11376        opened.index_root.header.file_count += locating.file_count as u64;
11377
11378        assert_eq!(
11379            opened.verify().unwrap_err(),
11380            FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
11381        );
11382    }
11383
11384    #[test]
11385    fn verify_rejects_non_contiguous_global_envelope_indexes() {
11386        let (mut opened, _) = multi_envelope_reader_fixture();
11387        replace_first_index_shard(&mut opened, |shard| {
11388            let frame = shard
11389                .frames
11390                .iter_mut()
11391                .find(|entry| entry.frame_index == 1)
11392                .unwrap();
11393            frame.envelope_index = 2;
11394
11395            let envelope = shard
11396                .envelopes
11397                .iter_mut()
11398                .find(|entry| entry.envelope_index == 1)
11399                .unwrap();
11400            envelope.envelope_index = 2;
11401        });
11402
11403        assert_eq!(
11404            opened.verify().unwrap_err(),
11405            FormatError::InvalidMetadata {
11406                structure: "EnvelopeEntry",
11407                reason: "global index coverage has a gap",
11408            }
11409        );
11410    }
11411
11412    #[test]
11413    fn verify_rejects_payload_object_extent_overlap() {
11414        let (mut opened, _) = multi_envelope_reader_fixture();
11415        replace_first_index_shard(&mut opened, |shard| {
11416            let first_block_index = shard.envelopes[0].first_block_index;
11417            shard.envelopes[1].first_block_index = first_block_index;
11418        });
11419
11420        assert_eq!(
11421            opened.verify().unwrap_err(),
11422            FormatError::InvalidArchive("encrypted object block ranges overlap")
11423        );
11424    }
11425
11426    #[test]
11427    fn verify_accepts_cross_shard_shared_envelope_frame_union() {
11428        let volume_header = test_volume_header();
11429        let crypto_header = test_crypto_header();
11430        let subkeys = Subkeys::derive(
11431            &master_key(),
11432            &volume_header.archive_uuid,
11433            &volume_header.session_id,
11434        )
11435        .unwrap();
11436        let mut next_block_index = 0u64;
11437        let mut blocks = BTreeMap::new();
11438
11439        let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
11440        let beta = test_member(b"beta.txt", b"beta cross shard\n");
11441        let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
11442        let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
11443        let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
11444        let envelope_plaintext =
11445            [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
11446        let payload = encrypt_test_object(
11447            &envelope_plaintext,
11448            &subkeys.enc_key,
11449            &subkeys.nonce_seed,
11450            b"envelope",
11451            0,
11452            BlockKind::PayloadData,
11453            &mut next_block_index,
11454            &crypto_header,
11455            &volume_header,
11456        );
11457        insert_records(&mut blocks, &payload.records);
11458
11459        let envelope = EnvelopeEntry {
11460            envelope_index: 0,
11461            first_block_index: payload.extent.first_block_index,
11462            data_block_count: payload.extent.data_block_count,
11463            parity_block_count: 0,
11464            encrypted_size: payload.extent.encrypted_size,
11465            plaintext_size: envelope_plaintext.len() as u32,
11466            first_frame_index: 0,
11467            frame_count: 2,
11468        };
11469        let frame0 = FrameEntry {
11470            frame_index: 0,
11471            envelope_index: 0,
11472            offset_in_envelope: 0,
11473            compressed_size: frame0_plaintext.len() as u32,
11474            decompressed_size: alpha.len() as u32,
11475            flags: 0x0000_0003,
11476            tar_stream_offset: 0,
11477        };
11478        let frame1 = FrameEntry {
11479            frame_index: 1,
11480            envelope_index: 0,
11481            offset_in_envelope: frame0_plaintext.len() as u32,
11482            compressed_size: frame1_plaintext.len() as u32,
11483            decompressed_size: beta.len() as u32,
11484            flags: 0x0000_0003,
11485            tar_stream_offset: alpha.len() as u64,
11486        };
11487
11488        let (shard0_plaintext, first0, last0) = build_test_index_shard(
11489            &[TestFileMeta {
11490                path: b"alpha.txt".to_vec(),
11491                frame_index: 0,
11492                tar_stream_offset: 0,
11493                member_group_size: alpha.len() as u64,
11494                file_data_size: b"alpha cross shard\n".len() as u64,
11495            }],
11496            &[frame0],
11497            std::slice::from_ref(&envelope),
11498        );
11499        let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
11500            &[TestFileMeta {
11501                path: b"beta.txt".to_vec(),
11502                frame_index: 1,
11503                tar_stream_offset: alpha.len() as u64,
11504                member_group_size: beta.len() as u64,
11505                file_data_size: b"beta cross shard\n".len() as u64,
11506            }],
11507            &[frame1],
11508            std::slice::from_ref(&envelope),
11509        );
11510        shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
11511
11512        let shard0 = encrypt_test_object(
11513            &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
11514            &subkeys.index_shard_key,
11515            &subkeys.index_nonce_seed,
11516            b"idxshard",
11517            0,
11518            BlockKind::IndexShardData,
11519            &mut next_block_index,
11520            &crypto_header,
11521            &volume_header,
11522        );
11523        let shard1 = encrypt_test_object(
11524            &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
11525            &subkeys.index_shard_key,
11526            &subkeys.index_nonce_seed,
11527            b"idxshard",
11528            1,
11529            BlockKind::IndexShardData,
11530            &mut next_block_index,
11531            &crypto_header,
11532            &volume_header,
11533        );
11534        insert_records(&mut blocks, &shard0.records);
11535        insert_records(&mut blocks, &shard1.records);
11536
11537        let index_root = IndexRoot {
11538            header: IndexRootHeader {
11539                frame_count: 2,
11540                envelope_count: 1,
11541                file_count: 2,
11542                payload_block_count: payload.extent.data_block_count as u64,
11543                tar_total_size: tar_stream.len() as u64,
11544                content_sha256: sha256_bytes(&tar_stream),
11545                ..IndexRootHeader::empty()
11546            },
11547            shards: vec![
11548                ShardEntry {
11549                    shard_index: 0,
11550                    first_block_index: shard0.extent.first_block_index,
11551                    data_block_count: shard0.extent.data_block_count,
11552                    parity_block_count: 0,
11553                    encrypted_size: shard0.extent.encrypted_size,
11554                    decompressed_size: shard0_plaintext.len() as u32,
11555                    file_count: 1,
11556                    first_path_hash: first0,
11557                    last_path_hash: last0,
11558                },
11559                ShardEntry {
11560                    shard_index: 1,
11561                    first_block_index: shard1.extent.first_block_index,
11562                    data_block_count: shard1.extent.data_block_count,
11563                    parity_block_count: 0,
11564                    encrypted_size: shard1.extent.encrypted_size,
11565                    decompressed_size: shard1_plaintext.len() as u32,
11566                    file_count: 1,
11567                    first_path_hash: first1,
11568                    last_path_hash: last1,
11569                },
11570            ],
11571            directory_hint_shards: Vec::new(),
11572        };
11573
11574        let index_root_plaintext = index_root.to_bytes();
11575        let index_root_object = encrypt_test_object(
11576            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
11577            &subkeys.index_root_key,
11578            &subkeys.index_nonce_seed,
11579            b"idxroot",
11580            0,
11581            BlockKind::IndexRootData,
11582            &mut next_block_index,
11583            &crypto_header,
11584            &volume_header,
11585        );
11586        insert_records(&mut blocks, &index_root_object.records);
11587
11588        let archive_uuid = volume_header.archive_uuid;
11589        let session_id = volume_header.session_id;
11590        let opened = OpenedArchive {
11591            options: ReaderOptions::default(),
11592            observed_archive_bytes: 1_000_000,
11593            observed_volume_count: 1,
11594            subkeys,
11595            blocks,
11596            lazy_blocks: None,
11597            crypto_header_bytes: Vec::new(),
11598            volume_header,
11599            crypto_header,
11600            manifest_footer: ManifestFooter {
11601                archive_uuid,
11602                session_id,
11603                volume_index: 0,
11604                is_authoritative: 1,
11605                total_volumes: 1,
11606                index_root_first_block: index_root_object.extent.first_block_index,
11607                index_root_data_block_count: index_root_object.extent.data_block_count,
11608                index_root_parity_block_count: 0,
11609                index_root_encrypted_size: index_root_object.extent.encrypted_size,
11610                index_root_decompressed_size: index_root_plaintext.len() as u32,
11611                manifest_hmac: [0u8; 32],
11612            },
11613            volume_trailer: Some(VolumeTrailer {
11614                archive_uuid,
11615                session_id,
11616                volume_index: 0,
11617                block_count: next_block_index,
11618                bytes_written: 0,
11619                manifest_footer_offset: 0,
11620                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
11621                closed_at_ns: 0,
11622                root_auth_footer_offset: 0,
11623                root_auth_footer_length: 0,
11624                root_auth_flags: 0,
11625                trailer_hmac: [0u8; 32],
11626            }),
11627            root_auth_footer: None,
11628            index_root,
11629            payload_dictionary: None,
11630        };
11631
11632        opened.verify().unwrap();
11633    }
11634
11635    #[test]
11636    fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
11637        let options = WriterOptions {
11638            index_root_fec_parity_shards: 0,
11639            ..single_stream_options()
11640        };
11641        let archive = write_archive(
11642            &[RegularFile::new("only.txt", b"only payload")],
11643            &master_key(),
11644            options,
11645        )
11646        .unwrap();
11647        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11648        assert!(opened.index_root.directory_hint_shards.is_empty());
11649
11650        let mut root = opened.index_root.clone();
11651        root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
11652        root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
11653        let root_plaintext = root.to_bytes();
11654        IndexRoot::parse(
11655            &root_plaintext,
11656            false,
11657            metadata_limits(&opened.crypto_header),
11658        )
11659        .unwrap();
11660        assert_eq!(
11661            root_plaintext.len() as u32,
11662            opened.manifest_footer.index_root_decompressed_size
11663        );
11664
11665        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11666        let mut next_block_index = opened.manifest_footer.index_root_first_block;
11667        let replacement = encrypt_test_object(
11668            &compressed_root,
11669            &opened.subkeys.index_root_key,
11670            &opened.subkeys.index_nonce_seed,
11671            b"idxroot",
11672            0,
11673            BlockKind::IndexRootData,
11674            &mut next_block_index,
11675            &opened.crypto_header,
11676            &opened.volume_header,
11677        );
11678        assert_eq!(
11679            replacement.extent.first_block_index,
11680            opened.manifest_footer.index_root_first_block
11681        );
11682        assert_eq!(
11683            replacement.extent.data_block_count,
11684            opened.manifest_footer.index_root_data_block_count
11685        );
11686        assert_eq!(
11687            replacement.extent.encrypted_size,
11688            opened.manifest_footer.index_root_encrypted_size
11689        );
11690
11691        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11692        let crypto_end = volume_header.crypto_header_offset as usize
11693            + volume_header.crypto_header_length as usize;
11694        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11695        let mut malformed = archive.bytes.clone();
11696        for record in replacement.records {
11697            let offset = crypto_end + record.block_index as usize * record_len;
11698            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11699        }
11700
11701        let reopened = open_archive(&malformed, &master_key()).unwrap();
11702        assert_eq!(
11703            reopened.index_root.header.file_count,
11704            DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
11705        );
11706        assert!(reopened.index_root.directory_hint_shards.is_empty());
11707
11708        assert_eq!(
11709            reopened.verify().unwrap_err(),
11710            FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
11711        );
11712    }
11713
11714    #[test]
11715    fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
11716        let mut map = DirectoryHintMap::new();
11717        add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
11718        add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
11719
11720        assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
11721        assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
11722        assert_eq!(
11723            map.get(b"foo/bar".as_slice()),
11724            Some(&BTreeSet::from([2, 4]))
11725        );
11726        assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
11727        assert!(!map.contains_key(b"foobar".as_slice()));
11728    }
11729
11730    #[test]
11731    fn directory_hint_validation_requires_exact_global_map() {
11732        let mut expected = DirectoryHintMap::new();
11733        add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
11734        add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
11735        let rows = sorted_directory_hint_rows(&expected);
11736        let table = directory_hint_table_from_rows(7, &rows, 2);
11737
11738        validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
11739            .unwrap();
11740
11741        let mut missing_root = expected.clone();
11742        missing_root.remove(&Vec::new());
11743        let missing_root_rows = sorted_directory_hint_rows(&missing_root);
11744        let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
11745        assert_eq!(
11746            validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
11747                .unwrap_err(),
11748            FormatError::InvalidArchive("directory hint map does not match decoded files")
11749        );
11750
11751        let mut expected_missing_directory_entry = expected.clone();
11752        expected_missing_directory_entry
11753            .get_mut(b"foo".as_slice())
11754            .unwrap()
11755            .remove(&1);
11756        assert_eq!(
11757            validate_directory_hint_tables_against_expected(
11758                std::slice::from_ref(&table),
11759                &expected_missing_directory_entry,
11760            )
11761            .unwrap_err(),
11762            FormatError::InvalidArchive("directory hint map does not match decoded files")
11763        );
11764
11765        let mut extra = expected.clone();
11766        extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
11767        let extra_rows = sorted_directory_hint_rows(&extra);
11768        let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
11769        assert_eq!(
11770            validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
11771            FormatError::InvalidArchive("directory hint map does not match decoded files")
11772        );
11773    }
11774
11775    #[test]
11776    fn directory_hint_validation_rejects_global_order_mismatch() {
11777        let mut expected = DirectoryHintMap::new();
11778        expected.insert(Vec::new(), BTreeSet::from([0]));
11779        expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
11780        let rows = sorted_directory_hint_rows(&expected);
11781        let first = directory_hint_table_from_rows(8, &rows[..1], 1);
11782        let second = directory_hint_table_from_rows(9, &rows[1..], 1);
11783
11784        assert_eq!(
11785            validate_directory_hint_tables_against_expected(&[second, first], &expected)
11786                .unwrap_err(),
11787            FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
11788        );
11789    }
11790
11791    #[test]
11792    fn object_extent_rejects_parity_above_class_cap() {
11793        let crypto_header = CryptoHeaderFixed {
11794            length: 0,
11795            compression_algo: CompressionAlgo::ZstdFramed,
11796            aead_algo: AeadAlgo::AesGcmSiv256,
11797            fec_algo: FecAlgo::ReedSolomonGF16,
11798            kdf_algo: KdfAlgo::Raw,
11799            chunk_size: 1024,
11800            envelope_target_size: 4096,
11801            block_size: 4096,
11802            fec_data_shards: 1,
11803            fec_parity_shards: 1,
11804            index_fec_data_shards: 1,
11805            index_fec_parity_shards: 1,
11806            index_root_fec_data_shards: 1,
11807            index_root_fec_parity_shards: 1,
11808            stripe_width: 1,
11809            volume_loss_tolerance: 0,
11810            bit_rot_buffer_pct: 0,
11811            has_dictionary: 0,
11812            max_path_length: 4096,
11813            expected_volume_size: 0,
11814        };
11815        let extent = ObjectExtent {
11816            first_block_index: 0,
11817            data_block_count: 1,
11818            parity_block_count: 2,
11819            encrypted_size: 4096,
11820        };
11821
11822        assert_eq!(
11823            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11824            FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
11825        );
11826    }
11827
11828    #[test]
11829    fn object_extent_rejects_parity_below_recoverability_requirement() {
11830        let crypto_header = CryptoHeaderFixed {
11831            length: 0,
11832            compression_algo: CompressionAlgo::ZstdFramed,
11833            aead_algo: AeadAlgo::AesGcmSiv256,
11834            fec_algo: FecAlgo::ReedSolomonGF16,
11835            kdf_algo: KdfAlgo::Raw,
11836            chunk_size: 1024,
11837            envelope_target_size: 4096,
11838            block_size: 4096,
11839            fec_data_shards: 1,
11840            fec_parity_shards: 1,
11841            index_fec_data_shards: 1,
11842            index_fec_parity_shards: 1,
11843            index_root_fec_data_shards: 1,
11844            index_root_fec_parity_shards: 1,
11845            stripe_width: 2,
11846            volume_loss_tolerance: 1,
11847            bit_rot_buffer_pct: 0,
11848            has_dictionary: 0,
11849            max_path_length: 4096,
11850            expected_volume_size: 0,
11851        };
11852        let extent = ObjectExtent {
11853            first_block_index: 0,
11854            data_block_count: 1,
11855            parity_block_count: 0,
11856            encrypted_size: 4096,
11857        };
11858
11859        assert_eq!(
11860            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11861            FormatError::InvalidArchive(
11862                "encrypted object parity does not match v41 compute_parity"
11863            )
11864        );
11865    }
11866
11867    #[test]
11868    fn encrypted_object_extent_matrix_rejects_overlaps() {
11869        let (opened, _) = multi_envelope_reader_fixture();
11870        let loaded_shard = opened
11871            .load_index_shard(&opened.index_root.shards[0])
11872            .unwrap();
11873        let base_envelopes = loaded_shard
11874            .envelopes
11875            .iter()
11876            .map(|entry| (entry.envelope_index, entry.clone()))
11877            .collect::<BTreeMap<_, _>>();
11878        let payload_start = loaded_shard.envelopes[0].first_block_index;
11879        let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
11880
11881        let mut payload_overlap = base_envelopes.clone();
11882        payload_overlap
11883            .get_mut(&loaded_shard.envelopes[1].envelope_index)
11884            .unwrap()
11885            .first_block_index = payload_start;
11886        assert_eq!(
11887            opened
11888                .validate_encrypted_object_block_ranges(&payload_overlap)
11889                .unwrap_err(),
11890            overlap
11891        );
11892
11893        let mut shard_overlap = opened.clone();
11894        let shard = shard_overlap.index_root.shards[0].clone();
11895        shard_overlap.index_root.shards.push(ShardEntry {
11896            shard_index: 1,
11897            ..shard
11898        });
11899        assert_eq!(
11900            shard_overlap
11901                .validate_encrypted_object_block_ranges(&base_envelopes)
11902                .unwrap_err(),
11903            overlap
11904        );
11905
11906        let mut dictionary_overlap = opened.clone();
11907        dictionary_overlap.crypto_header.has_dictionary = 1;
11908        dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
11909        dictionary_overlap
11910            .index_root
11911            .header
11912            .dictionary_data_block_count = 1;
11913        dictionary_overlap
11914            .index_root
11915            .header
11916            .dictionary_parity_block_count = 0;
11917        dictionary_overlap
11918            .index_root
11919            .header
11920            .dictionary_encrypted_size = 4096;
11921        dictionary_overlap
11922            .index_root
11923            .header
11924            .dictionary_decompressed_size = 128;
11925        assert_eq!(
11926            dictionary_overlap
11927                .validate_encrypted_object_block_ranges(&base_envelopes)
11928                .unwrap_err(),
11929            overlap
11930        );
11931
11932        let mut hint_overlap = opened.clone();
11933        hint_overlap
11934            .index_root
11935            .directory_hint_shards
11936            .push(DirectoryHintShardEntry {
11937                hint_shard_index: 0,
11938                first_dir_hash: [0; 8],
11939                last_dir_hash: [0; 8],
11940                first_block_index: payload_start,
11941                data_block_count: 1,
11942                parity_block_count: 0,
11943                encrypted_size: 4096,
11944                decompressed_size: 128,
11945                entry_count: 1,
11946            });
11947        assert_eq!(
11948            hint_overlap
11949                .validate_encrypted_object_block_ranges(&base_envelopes)
11950                .unwrap_err(),
11951            overlap
11952        );
11953    }
11954
11955    #[test]
11956    fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
11957        let volume_header = test_volume_header();
11958        let crypto_header = test_crypto_header();
11959        let subkeys = Subkeys::derive(
11960            &master_key(),
11961            &volume_header.archive_uuid,
11962            &volume_header.session_id,
11963        )
11964        .unwrap();
11965        let mut next_block_index = 0u64;
11966
11967        let index_root_payload = b"index root metadata object";
11968        let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
11969        assert_metadata_object_from_compressed(
11970            &{
11971                let mut bytes = index_root_compressed.clone();
11972                bytes.push(0);
11973                bytes
11974            },
11975            index_root_payload.len(),
11976            &subkeys,
11977            &volume_header,
11978            &crypto_header,
11979            &subkeys.index_root_key,
11980            &subkeys.index_nonce_seed,
11981            b"idxroot",
11982            0,
11983            BlockKind::IndexRootData,
11984            BlockKind::IndexRootParity,
11985            crypto_header.index_root_fec_data_shards,
11986            crypto_header.index_root_fec_parity_shards,
11987            &mut next_block_index,
11988            FormatError::TrailingBytesAfterZstdFrame,
11989        );
11990        assert_metadata_object_from_compressed(
11991            &index_root_compressed,
11992            index_root_payload.len() + 1,
11993            &subkeys,
11994            &volume_header,
11995            &crypto_header,
11996            &subkeys.index_root_key,
11997            &subkeys.index_nonce_seed,
11998            b"idxroot",
11999            0,
12000            BlockKind::IndexRootData,
12001            BlockKind::IndexRootParity,
12002            crypto_header.index_root_fec_data_shards,
12003            crypto_header.index_root_fec_parity_shards,
12004            &mut next_block_index,
12005            FormatError::ZstdDecompressedSizeMismatch {
12006                expected: index_root_payload.len() + 1,
12007                actual: index_root_payload.len(),
12008            },
12009        );
12010
12011        let index_shard_payload = b"index shard metadata object";
12012        let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
12013        assert_metadata_object_from_compressed(
12014            &{
12015                let mut bytes = index_shard_compressed.clone();
12016                bytes.push(0);
12017                bytes
12018            },
12019            index_shard_payload.len(),
12020            &subkeys,
12021            &volume_header,
12022            &crypto_header,
12023            &subkeys.index_shard_key,
12024            &subkeys.index_nonce_seed,
12025            b"idxshard",
12026            1,
12027            BlockKind::IndexShardData,
12028            BlockKind::IndexShardParity,
12029            crypto_header.index_fec_data_shards,
12030            crypto_header.index_fec_parity_shards,
12031            &mut next_block_index,
12032            FormatError::TrailingBytesAfterZstdFrame,
12033        );
12034        assert_metadata_object_from_compressed(
12035            &index_shard_compressed,
12036            index_shard_payload.len() + 1,
12037            &subkeys,
12038            &volume_header,
12039            &crypto_header,
12040            &subkeys.index_shard_key,
12041            &subkeys.index_nonce_seed,
12042            b"idxshard",
12043            1,
12044            BlockKind::IndexShardData,
12045            BlockKind::IndexShardParity,
12046            crypto_header.index_fec_data_shards,
12047            crypto_header.index_fec_parity_shards,
12048            &mut next_block_index,
12049            FormatError::ZstdDecompressedSizeMismatch {
12050                expected: index_shard_payload.len() + 1,
12051                actual: index_shard_payload.len(),
12052            },
12053        );
12054
12055        let directory_hint_payload = b"directory hint metadata object";
12056        let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
12057        assert_metadata_object_from_compressed(
12058            &{
12059                let mut bytes = directory_hint_compressed.clone();
12060                bytes.push(0);
12061                bytes
12062            },
12063            directory_hint_payload.len(),
12064            &subkeys,
12065            &volume_header,
12066            &crypto_header,
12067            &subkeys.dir_hint_key,
12068            &subkeys.index_nonce_seed,
12069            b"dirhint",
12070            0,
12071            BlockKind::DirectoryHintData,
12072            BlockKind::DirectoryHintParity,
12073            crypto_header.index_fec_data_shards,
12074            crypto_header.index_fec_parity_shards,
12075            &mut next_block_index,
12076            FormatError::TrailingBytesAfterZstdFrame,
12077        );
12078        assert_metadata_object_from_compressed(
12079            &directory_hint_compressed,
12080            directory_hint_payload.len() + 1,
12081            &subkeys,
12082            &volume_header,
12083            &crypto_header,
12084            &subkeys.dir_hint_key,
12085            &subkeys.index_nonce_seed,
12086            b"dirhint",
12087            0,
12088            BlockKind::DirectoryHintData,
12089            BlockKind::DirectoryHintParity,
12090            crypto_header.index_fec_data_shards,
12091            crypto_header.index_fec_parity_shards,
12092            &mut next_block_index,
12093            FormatError::ZstdDecompressedSizeMismatch {
12094                expected: directory_hint_payload.len() + 1,
12095                actual: directory_hint_payload.len(),
12096            },
12097        );
12098
12099        let dictionary_payload = b"dictionary metadata object";
12100        let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
12101        assert_metadata_object_from_compressed(
12102            &{
12103                let mut bytes = dictionary_compressed.clone();
12104                bytes.push(0);
12105                bytes
12106            },
12107            dictionary_payload.len(),
12108            &subkeys,
12109            &volume_header,
12110            &crypto_header,
12111            &subkeys.dictionary_key,
12112            &subkeys.index_nonce_seed,
12113            b"dict",
12114            0,
12115            BlockKind::DictionaryData,
12116            BlockKind::DictionaryParity,
12117            crypto_header.index_root_fec_data_shards,
12118            crypto_header.index_root_fec_parity_shards,
12119            &mut next_block_index,
12120            FormatError::TrailingBytesAfterZstdFrame,
12121        );
12122        assert_metadata_object_from_compressed(
12123            &dictionary_compressed,
12124            dictionary_payload.len() + 1,
12125            &subkeys,
12126            &volume_header,
12127            &crypto_header,
12128            &subkeys.dictionary_key,
12129            &subkeys.index_nonce_seed,
12130            b"dict",
12131            0,
12132            BlockKind::DictionaryData,
12133            BlockKind::DictionaryParity,
12134            crypto_header.index_root_fec_data_shards,
12135            crypto_header.index_root_fec_parity_shards,
12136            &mut next_block_index,
12137            FormatError::ZstdDecompressedSizeMismatch {
12138                expected: dictionary_payload.len() + 1,
12139                actual: dictionary_payload.len(),
12140            },
12141        );
12142    }
12143
12144    #[test]
12145    fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
12146        let volume_header = test_volume_header();
12147        let crypto_header = test_crypto_header();
12148        let subkeys = Subkeys::derive(
12149            &master_key(),
12150            &volume_header.archive_uuid,
12151            &volume_header.session_id,
12152        )
12153        .unwrap();
12154        let mut next_block_index = 0u64;
12155
12156        let index_root_payload = b"index root metadata object";
12157        let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
12158            index_root_payload,
12159            &subkeys,
12160            &volume_header,
12161            &crypto_header,
12162            &subkeys.index_root_key,
12163            &subkeys.index_nonce_seed,
12164            b"idxroot",
12165            0,
12166            BlockKind::IndexRootData,
12167            &mut next_block_index,
12168        );
12169        let mut index_root_extent = index_root_extent;
12170        index_root_extent.encrypted_size = index_root_extent
12171            .encrypted_size
12172            .saturating_add(crypto_header.block_size);
12173        assert_eq!(
12174            load_metadata_object_from_parts(
12175                &index_root_records,
12176                ObjectLoadContext::index_root(
12177                    &volume_header,
12178                    &crypto_header,
12179                    &subkeys,
12180                    index_root_extent,
12181                ),
12182                index_root_payload.len() as u32,
12183            )
12184            .unwrap_err(),
12185            FormatError::InvalidArchive(
12186                "encrypted object size is not data_block_count * block_size"
12187            )
12188        );
12189
12190        let index_shard_payload = b"index shard metadata object";
12191        let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
12192            index_shard_payload,
12193            &subkeys,
12194            &volume_header,
12195            &crypto_header,
12196            &subkeys.index_shard_key,
12197            &subkeys.index_nonce_seed,
12198            b"idxshard",
12199            1,
12200            BlockKind::IndexShardData,
12201            &mut next_block_index,
12202        );
12203        let mut index_shard_extent = index_shard_extent;
12204        index_shard_extent.encrypted_size = index_shard_extent
12205            .encrypted_size
12206            .saturating_add(crypto_header.block_size);
12207        assert_eq!(
12208            load_metadata_object_from_parts(
12209                &index_shard_records,
12210                ObjectLoadContext {
12211                    volume_header: &volume_header,
12212                    crypto_header: &crypto_header,
12213                    extent: index_shard_extent,
12214                    data_kind: BlockKind::IndexShardData,
12215                    parity_kind: BlockKind::IndexShardParity,
12216                    key: &subkeys.index_shard_key,
12217                    nonce_seed: &subkeys.index_nonce_seed,
12218                    domain: b"idxshard",
12219                    counter: 1,
12220                    class_data_shard_max: crypto_header.index_fec_data_shards,
12221                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
12222                },
12223                index_shard_payload.len() as u32,
12224            )
12225            .unwrap_err(),
12226            FormatError::InvalidArchive(
12227                "encrypted object size is not data_block_count * block_size"
12228            )
12229        );
12230
12231        let directory_hint_payload = b"directory hint metadata object";
12232        let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
12233            directory_hint_payload,
12234            &subkeys,
12235            &volume_header,
12236            &crypto_header,
12237            &subkeys.dir_hint_key,
12238            &subkeys.index_nonce_seed,
12239            b"dirhint",
12240            0,
12241            BlockKind::DirectoryHintData,
12242            &mut next_block_index,
12243        );
12244        let mut directory_hint_extent = directory_hint_extent;
12245        directory_hint_extent.encrypted_size = directory_hint_extent
12246            .encrypted_size
12247            .saturating_add(crypto_header.block_size);
12248        assert_eq!(
12249            load_metadata_object_from_parts(
12250                &directory_hint_records,
12251                ObjectLoadContext {
12252                    volume_header: &volume_header,
12253                    crypto_header: &crypto_header,
12254                    extent: directory_hint_extent,
12255                    data_kind: BlockKind::DirectoryHintData,
12256                    parity_kind: BlockKind::DirectoryHintParity,
12257                    key: &subkeys.dir_hint_key,
12258                    nonce_seed: &subkeys.index_nonce_seed,
12259                    domain: b"dirhint",
12260                    counter: 0,
12261                    class_data_shard_max: crypto_header.index_fec_data_shards,
12262                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
12263                },
12264                directory_hint_payload.len() as u32,
12265            )
12266            .unwrap_err(),
12267            FormatError::InvalidArchive(
12268                "encrypted object size is not data_block_count * block_size"
12269            )
12270        );
12271
12272        let dictionary_payload = b"dictionary metadata object";
12273        let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
12274            dictionary_payload,
12275            &subkeys,
12276            &volume_header,
12277            &crypto_header,
12278            &subkeys.dictionary_key,
12279            &subkeys.index_nonce_seed,
12280            b"dict",
12281            0,
12282            BlockKind::DictionaryData,
12283            &mut next_block_index,
12284        );
12285        let mut dictionary_extent = dictionary_extent;
12286        dictionary_extent.encrypted_size = dictionary_extent
12287            .encrypted_size
12288            .saturating_add(crypto_header.block_size);
12289        assert_eq!(
12290            load_metadata_object_from_parts(
12291                &dictionary_records,
12292                ObjectLoadContext {
12293                    volume_header: &volume_header,
12294                    crypto_header: &crypto_header,
12295                    extent: dictionary_extent,
12296                    data_kind: BlockKind::DictionaryData,
12297                    parity_kind: BlockKind::DictionaryParity,
12298                    key: &subkeys.dictionary_key,
12299                    nonce_seed: &subkeys.index_nonce_seed,
12300                    domain: b"dict",
12301                    counter: 0,
12302                    class_data_shard_max: crypto_header.index_root_fec_data_shards,
12303                    class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
12304                },
12305                dictionary_payload.len() as u32,
12306            )
12307            .unwrap_err(),
12308            FormatError::InvalidArchive(
12309                "encrypted object size is not data_block_count * block_size"
12310            )
12311        );
12312    }
12313
12314    #[test]
12315    fn opens_complete_multi_volume_archive() {
12316        let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
12317        let archive = write_archive(
12318            &files,
12319            &master_key(),
12320            WriterOptions {
12321                stripe_width: 2,
12322                volume_loss_tolerance: 1,
12323                ..single_stream_options()
12324            },
12325        )
12326        .unwrap();
12327        assert_eq!(archive.volumes.len(), 2);
12328
12329        let volume_refs = archive
12330            .volumes
12331            .iter()
12332            .map(Vec::as_slice)
12333            .collect::<Vec<_>>();
12334        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12335
12336        assert_eq!(opened.volume_header.stripe_width, 2);
12337        assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
12338        assert_eq!(
12339            opened.extract_file("alpha.txt").unwrap(),
12340            Some(b"hello from volume stripes".to_vec())
12341        );
12342        opened.verify().unwrap();
12343    }
12344
12345    #[test]
12346    fn recovers_from_one_missing_volume_when_parity_allows() {
12347        let files = [RegularFile::new("alpha.txt", b"recover me")];
12348        let archive = write_archive(
12349            &files,
12350            &master_key(),
12351            WriterOptions {
12352                stripe_width: 2,
12353                volume_loss_tolerance: 1,
12354                ..single_stream_options()
12355            },
12356        )
12357        .unwrap();
12358
12359        let recovered =
12360            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
12361        assert_eq!(
12362            recovered.extract_file("alpha.txt").unwrap(),
12363            Some(b"recover me".to_vec())
12364        );
12365        recovered.verify().unwrap();
12366    }
12367
12368    #[test]
12369    fn recovers_from_crc_corrupted_block_when_parity_allows() {
12370        let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
12371        let archive = write_archive(
12372            &files,
12373            &master_key(),
12374            WriterOptions {
12375                stripe_width: 2,
12376                volume_loss_tolerance: 1,
12377                ..single_stream_options()
12378            },
12379        )
12380        .unwrap();
12381        let mut volumes = archive.volumes.clone();
12382        corrupt_first_block_record_payload(&mut volumes[0]);
12383
12384        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12385        let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12386
12387        assert_eq!(
12388            recovered.extract_file("alpha.txt").unwrap(),
12389            Some(b"repair corrupt block".to_vec())
12390        );
12391        recovered.verify().unwrap();
12392    }
12393
12394    #[test]
12395    fn rejects_multi_volume_count_mismatch_without_tolerance() {
12396        let files = [RegularFile::new("alpha.txt", b"count check")];
12397        let archive = write_archive(
12398            &files,
12399            &master_key(),
12400            WriterOptions {
12401                stripe_width: 3,
12402                volume_loss_tolerance: 0,
12403                ..single_stream_options()
12404            },
12405        )
12406        .unwrap();
12407
12408        assert_eq!(
12409            open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
12410            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12411        );
12412    }
12413
12414    #[test]
12415    fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
12416        let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
12417        let archive = write_archive(
12418            &files,
12419            &master_key(),
12420            WriterOptions {
12421                stripe_width: 2,
12422                volume_loss_tolerance: 1,
12423                ..single_stream_options()
12424            },
12425        )
12426        .unwrap();
12427
12428        let mut bad_first = archive.volumes[0].clone();
12429        rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
12430            footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
12431        });
12432
12433        open_archive_volumes(
12434            &[bad_first.as_slice(), archive.volumes[1].as_slice()],
12435            &master_key(),
12436        )
12437        .unwrap();
12438    }
12439
12440    #[test]
12441    fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
12442        let files = [RegularFile::new("alpha.txt", b"repair meta root")];
12443        let archive = write_archive(
12444            &files,
12445            &master_key(),
12446            WriterOptions {
12447                stripe_width: 2,
12448                volume_loss_tolerance: 1,
12449                ..single_stream_options()
12450            },
12451        )
12452        .unwrap();
12453        let mut volumes = archive.volumes.clone();
12454
12455        let mut corrupted = false;
12456        for volume in &mut volumes {
12457            if let Some(slot) =
12458                block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
12459            {
12460                corrupt_block_record_payload_at_slot(volume, *slot);
12461                corrupted = true;
12462                break;
12463            }
12464        }
12465        assert!(corrupted, "expected an IndexRootData record");
12466
12467        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12468        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12469        assert_eq!(
12470            opened.extract_file("alpha.txt").unwrap(),
12471            Some(b"repair meta root".to_vec())
12472        );
12473        opened.verify().unwrap();
12474    }
12475
12476    #[test]
12477    fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
12478        let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
12479        let archive = write_archive(
12480            &files,
12481            &master_key(),
12482            WriterOptions {
12483                stripe_width: 2,
12484                volume_loss_tolerance: 1,
12485                ..single_stream_options()
12486            },
12487        )
12488        .unwrap();
12489        let mut volumes = archive.volumes.clone();
12490
12491        let mut corrupted = false;
12492        for volume in &mut volumes {
12493            if let Some(slot) =
12494                block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
12495            {
12496                corrupt_block_record_payload_at_slot(volume, *slot);
12497                corrupted = true;
12498                break;
12499            }
12500        }
12501        assert!(corrupted, "expected an IndexShardData record");
12502
12503        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12504        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12505        assert_eq!(
12506            opened.extract_file("alpha.txt").unwrap(),
12507            Some(b"repair meta shard".to_vec())
12508        );
12509        opened.verify().unwrap();
12510    }
12511
12512    #[test]
12513    fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
12514        let files = [RegularFile::new(
12515            "alpha.txt",
12516            b"bitrot parity is not volume loss",
12517        )];
12518        let archive = write_archive(
12519            &files,
12520            &master_key(),
12521            WriterOptions {
12522                stripe_width: 2,
12523                volume_loss_tolerance: 0,
12524                bit_rot_buffer_pct: 1,
12525                ..single_stream_options()
12526            },
12527        )
12528        .unwrap();
12529
12530        assert_eq!(
12531            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
12532            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12533        );
12534    }
12535
12536    #[test]
12537    fn repairs_crc_erasure_only_within_parity_budget() {
12538        let payload = pseudo_random_bytes(12_000);
12539        let archive = write_archive(
12540            &[RegularFile::new("rot.bin", &payload)],
12541            &master_key(),
12542            small_block_recovery_options(),
12543        )
12544        .unwrap();
12545        let payload_slots = first_payload_data_run_slots(&archive.bytes);
12546        assert!(
12547            payload_slots.len() >= 2,
12548            "fixture must contain a multi-block payload object"
12549        );
12550
12551        let mut one_erasure = archive.bytes.clone();
12552        corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
12553        let repaired = open_archive(&one_erasure, &master_key()).unwrap();
12554        assert_eq!(
12555            repaired.extract_file("rot.bin").unwrap(),
12556            Some(payload.clone())
12557        );
12558
12559        let mut two_erasures = archive.bytes.clone();
12560        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
12561        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
12562        let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
12563        assert_eq!(
12564            unrepaired.extract_file("rot.bin").unwrap_err(),
12565            FormatError::FecTooFewAvailableShards
12566        );
12567    }
12568
12569    #[test]
12570    fn verify_rejects_missing_required_object_block_extent() {
12571        let (mut opened, missing_block) = multi_envelope_reader_fixture();
12572        assert!(opened.blocks.remove(&missing_block).is_some());
12573
12574        assert_eq!(
12575            opened.verify().unwrap_err(),
12576            FormatError::FecTooFewAvailableShards
12577        );
12578    }
12579
12580    #[test]
12581    fn parity_crc_erasure_does_not_hide_authenticated_data() {
12582        let payload = pseudo_random_bytes(12_000);
12583        let archive = write_archive(
12584            &[RegularFile::new("parity-erasure.bin", &payload)],
12585            &master_key(),
12586            parity_rich_recovery_options(),
12587        )
12588        .unwrap();
12589        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12590        let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
12591        assert!(
12592            parity_slots.len() >= 2,
12593            "fixture must contain redundant parity shards"
12594        );
12595        let mut corrupted = archive.bytes;
12596        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12597        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
12598
12599        let opened = open_archive(&corrupted, &master_key()).unwrap();
12600        assert_eq!(
12601            opened.extract_file("parity-erasure.bin").unwrap(),
12602            Some(payload)
12603        );
12604        opened.verify().unwrap();
12605    }
12606
12607    #[test]
12608    fn repair_patches_restore_crc_erased_payload_block() {
12609        let payload = pseudo_random_bytes(12_000);
12610        let archive = write_archive(
12611            &[RegularFile::new("rot.bin", &payload)],
12612            &master_key(),
12613            small_block_recovery_options(),
12614        )
12615        .unwrap();
12616        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12617        let mut corrupted = archive.bytes.clone();
12618        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12619
12620        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12621        opened.verify().unwrap();
12622        let patches = opened.repair_patches().unwrap();
12623        assert_eq!(patches.len(), 1);
12624        apply_repair_patches(&mut corrupted, &patches);
12625
12626        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12627        repaired.verify().unwrap();
12628        assert!(repaired.repair_patches().unwrap().is_empty());
12629    }
12630
12631    #[test]
12632    fn repair_patches_restore_crc_erased_payload_parity_block() {
12633        let payload = pseudo_random_bytes(12_000);
12634        let archive = write_archive(
12635            &[RegularFile::new("parity-erasure.bin", &payload)],
12636            &master_key(),
12637            parity_rich_recovery_options(),
12638        )
12639        .unwrap();
12640        let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
12641        let mut corrupted = archive.bytes.clone();
12642        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
12643
12644        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12645        opened.verify().unwrap();
12646        let patches = opened.repair_patches().unwrap();
12647        assert_eq!(patches.len(), 1);
12648        apply_repair_patches(&mut corrupted, &patches);
12649
12650        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12651        repaired.verify().unwrap();
12652        assert!(repaired.repair_patches().unwrap().is_empty());
12653    }
12654
12655    #[test]
12656    fn recovers_physical_odd_block_size_from_cmra_authority() {
12657        let archive = write_archive(
12658            &[RegularFile::new("odd-block.txt", b"payload")],
12659            &master_key(),
12660            small_block_recovery_options(),
12661        )
12662        .unwrap();
12663        let mut malformed = archive.bytes;
12664        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
12665        let block_size_offset = volume_header.crypto_header_offset as usize + 24;
12666        malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
12667
12668        let opened = open_archive(&malformed, &master_key()).unwrap();
12669        assert_ne!(opened.crypto_header.block_size, 4097);
12670        assert_eq!(
12671            opened.extract_file("odd-block.txt").unwrap(),
12672            Some(b"payload".to_vec())
12673        );
12674        opened.verify().unwrap();
12675    }
12676
12677    #[test]
12678    fn repairs_structurally_malformed_payload_block_slots() {
12679        let payload = pseudo_random_bytes(12_000);
12680        let archive = write_archive(
12681            &[RegularFile::new("structural-block.bin", &payload)],
12682            &master_key(),
12683            small_block_recovery_options(),
12684        )
12685        .unwrap();
12686        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12687
12688        let mut bad_magic = archive.bytes.clone();
12689        corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
12690        assert_eq!(
12691            open_archive(&bad_magic, &master_key())
12692                .unwrap()
12693                .extract_file("structural-block.bin")
12694                .unwrap(),
12695            Some(payload.clone())
12696        );
12697
12698        let mut bad_reserved = archive.bytes;
12699        corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
12700        assert_eq!(
12701            open_archive(&bad_reserved, &master_key())
12702                .unwrap()
12703                .extract_file("structural-block.bin")
12704                .unwrap(),
12705            Some(payload)
12706        );
12707    }
12708
12709    #[test]
12710    fn repair_patches_restore_structurally_malformed_payload_block_slot() {
12711        let payload = pseudo_random_bytes(12_000);
12712        let archive = write_archive(
12713            &[RegularFile::new("structural-patch.bin", &payload)],
12714            &master_key(),
12715            small_block_recovery_options(),
12716        )
12717        .unwrap();
12718        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12719        let mut corrupted = archive.bytes.clone();
12720        corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
12721
12722        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12723        opened.verify().unwrap();
12724        assert_eq!(
12725            opened.extract_file("structural-patch.bin").unwrap(),
12726            Some(payload)
12727        );
12728        let patches = opened.repair_patches().unwrap();
12729        assert_eq!(patches.len(), 1);
12730        apply_repair_patches(&mut corrupted, &patches);
12731
12732        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12733        repaired.verify().unwrap();
12734        assert!(repaired.repair_patches().unwrap().is_empty());
12735    }
12736
12737    #[test]
12738    fn repairs_structurally_malformed_index_root_block_slot() {
12739        let archive = write_archive(
12740            &[RegularFile::new(
12741                "structural-index-root.txt",
12742                b"metadata repair",
12743            )],
12744            &master_key(),
12745            small_block_recovery_options(),
12746        )
12747        .unwrap();
12748        let index_root_slot =
12749            first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
12750        let mut corrupted = archive.bytes;
12751        corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
12752
12753        let opened = open_archive(&corrupted, &master_key()).unwrap();
12754        assert_eq!(
12755            opened.extract_file("structural-index-root.txt").unwrap(),
12756            Some(b"metadata repair".to_vec())
12757        );
12758        opened.verify().unwrap();
12759    }
12760
12761    #[test]
12762    fn rejects_parity_block_with_last_data_flag() {
12763        let archive = write_archive(
12764            &[RegularFile::new("parity-flag.txt", b"payload")],
12765            &master_key(),
12766            small_block_recovery_options(),
12767        )
12768        .unwrap();
12769        let parity_slot =
12770            first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
12771        let mut malformed = archive.bytes;
12772        mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
12773            record.flags = 0x01;
12774        });
12775
12776        assert_eq!(
12777            open_archive(&malformed, &master_key()).unwrap_err(),
12778            FormatError::ParityBlockHasLastDataFlag
12779        );
12780    }
12781
12782    #[test]
12783    fn rejects_missing_and_duplicate_payload_last_data_flags() {
12784        let payload = pseudo_random_bytes(12_000);
12785        let archive = write_archive(
12786            &[RegularFile::new("flags.bin", &payload)],
12787            &master_key(),
12788            small_block_recovery_options(),
12789        )
12790        .unwrap();
12791        let payload_slots = first_payload_data_run_slots(&archive.bytes);
12792        assert!(
12793            payload_slots.len() >= 2,
12794            "fixture must contain a multi-block payload object"
12795        );
12796
12797        let mut duplicate_last = archive.bytes.clone();
12798        mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
12799            record.flags = 0x01;
12800        });
12801        let opened = open_archive(&duplicate_last, &master_key()).unwrap();
12802        assert_eq!(
12803            opened.extract_file("flags.bin").unwrap_err(),
12804            FormatError::InvalidArchive("object last-data flag is not on the final data block")
12805        );
12806
12807        let mut missing_last = archive.bytes;
12808        mutate_block_record_at_slot(
12809            &mut missing_last,
12810            *payload_slots.last().unwrap(),
12811            |record| {
12812                record.flags = 0;
12813            },
12814        );
12815        let opened = open_archive(&missing_last, &master_key()).unwrap();
12816        assert_eq!(
12817            opened.extract_file("flags.bin").unwrap_err(),
12818            FormatError::InvalidArchive("object last-data flag is not on the final data block")
12819        );
12820    }
12821
12822    #[test]
12823    fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
12824        let files = [RegularFile::new(
12825            "footer-copy.txt",
12826            b"survives one bad footer",
12827        )];
12828        let archive = write_archive(
12829            &files,
12830            &master_key(),
12831            WriterOptions {
12832                stripe_width: 2,
12833                volume_loss_tolerance: 1,
12834                ..single_stream_options()
12835            },
12836        )
12837        .unwrap();
12838        let mut volumes = archive.volumes.clone();
12839        corrupt_manifest_footer_hmac(&mut volumes[0]);
12840
12841        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12842        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12843        assert_eq!(opened.manifest_footer.volume_index, 0);
12844        assert_eq!(opened.volume_header.volume_index, 0);
12845        assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
12846        assert_eq!(
12847            opened.extract_file("footer-copy.txt").unwrap(),
12848            Some(b"survives one bad footer".to_vec())
12849        );
12850        opened.verify().unwrap();
12851    }
12852
12853    #[test]
12854    fn manifest_footer_corruption_requires_trusted_sidecar() {
12855        let archive = write_archive(
12856            &[RegularFile::new("footer.txt", b"sidecar authority")],
12857            &master_key(),
12858            single_stream_options(),
12859        )
12860        .unwrap();
12861        let manifest_offset = terminal_material_offset(&archive.bytes);
12862        let mut corrupted = archive.bytes.clone();
12863        corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
12864        corrupt_v41_terminal_recovery(&mut corrupted);
12865
12866        assert!(open_archive(&corrupted, &master_key()).is_err());
12867
12868        let opened =
12869            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
12870                .unwrap();
12871        assert!(opened.volume_trailer.is_none());
12872        assert_eq!(
12873            opened.extract_file("footer.txt").unwrap(),
12874            Some(b"sidecar authority".to_vec())
12875        );
12876        opened.verify().unwrap();
12877    }
12878
12879    #[test]
12880    fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
12881        let archive = write_archive(
12882            &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
12883            &master_key(),
12884            single_stream_options(),
12885        )
12886        .unwrap();
12887        let strict_options = ReaderOptions {
12888            max_trailing_garbage_scan: 0,
12889            ..ReaderOptions::default()
12890        };
12891
12892        let manifest_offset = terminal_material_offset(&archive.bytes);
12893        for offset in [
12894            manifest_offset + 71,
12895            manifest_offset + MANIFEST_HMAC_COVERED_LEN,
12896        ] {
12897            let mut corrupted = archive.bytes.clone();
12898            corrupted[offset] ^= 0x01;
12899            open_archive(&corrupted, &master_key()).unwrap();
12900        }
12901
12902        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
12903        for offset in [
12904            trailer_offset + 75,
12905            trailer_offset + TRAILER_HMAC_COVERED_LEN,
12906        ] {
12907            let mut corrupted = archive.bytes.clone();
12908            corrupted[offset] ^= 0x01;
12909            OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
12910        }
12911
12912        let mut covered_sidecar = archive.bootstrap_sidecar.clone();
12913        let mut header =
12914            BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12915                .unwrap();
12916        header.manifest_footer_offset += 1;
12917        covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12918        assert_eq!(
12919            open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
12920                .unwrap_err(),
12921            FormatError::HmacMismatch {
12922                structure: "BootstrapSidecarHeader"
12923            }
12924        );
12925
12926        let mut tag_sidecar = archive.bootstrap_sidecar.clone();
12927        let mut header =
12928            BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12929        header.sidecar_hmac[0] ^= 1;
12930        tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12931        assert_eq!(
12932            open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
12933                .unwrap_err(),
12934            FormatError::HmacMismatch {
12935                structure: "BootstrapSidecarHeader"
12936            }
12937        );
12938
12939        let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
12940        let header =
12941            BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12942                .unwrap();
12943        let mut header_bytes = header.to_bytes();
12944        header_bytes[124] ^= 0x01;
12945        let crc = crc32c::crc32c(&header_bytes[..124]);
12946        header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
12947        non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12948        let opened = open_archive_with_bootstrap_sidecar(
12949            &archive.bytes,
12950            &non_covered_sidecar,
12951            &master_key(),
12952        )
12953        .unwrap();
12954        assert_eq!(
12955            opened.extract_file("hmac-boundary.txt").unwrap(),
12956            Some(b"boundary bytes".to_vec())
12957        );
12958    }
12959
12960    #[test]
12961    fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
12962        let archive = write_archive(
12963            &[RegularFile::new("volume-index.txt", b"identity")],
12964            &master_key(),
12965            single_stream_options(),
12966        )
12967        .unwrap();
12968
12969        let mut bad_trailer = archive.bytes.clone();
12970        rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
12971            trailer.volume_index = 1;
12972        });
12973        open_archive(&bad_trailer, &master_key()).unwrap();
12974
12975        let mut bad_manifest = archive.bytes;
12976        rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
12977            footer.volume_index = 1;
12978        });
12979        open_archive(&bad_manifest, &master_key()).unwrap();
12980    }
12981
12982    #[test]
12983    fn rejects_same_key_header_terminal_material_splice() {
12984        let first = write_archive(
12985            &[RegularFile::new("splice.txt", b"same shape")],
12986            &master_key(),
12987            single_stream_options(),
12988        )
12989        .unwrap();
12990        let second = write_archive(
12991            &[RegularFile::new("splice.txt", b"same shape")],
12992            &master_key(),
12993            single_stream_options(),
12994        )
12995        .unwrap();
12996        assert_ne!(first.archive_uuid, second.archive_uuid);
12997        assert_eq!(
12998            terminal_material_offset(&first.bytes),
12999            terminal_material_offset(&second.bytes)
13000        );
13001        assert_eq!(first.bytes.len(), second.bytes.len());
13002
13003        let terminal_offset = terminal_material_offset(&first.bytes);
13004        let mut spliced = first.bytes.clone();
13005        spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
13006
13007        assert_eq!(
13008            open_archive(&spliced, &master_key()).unwrap_err(),
13009            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13010        );
13011    }
13012
13013    #[test]
13014    fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
13015        let kdf_params = crate::crypto::KdfParams::Argon2id {
13016            t_cost: 1,
13017            m_cost_kib: 8,
13018            parallelism: 1,
13019            salt: b"0123456789abcdef".to_vec(),
13020        };
13021        let archive = write_archive_with_kdf(
13022            &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
13023            &master_key(),
13024            single_stream_options(),
13025            &kdf_params,
13026        )
13027        .unwrap();
13028        let mut mutated = archive.bytes.clone();
13029        let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
13030        let subkeys = Subkeys::derive(
13031            &master_key(),
13032            &volume_header.archive_uuid,
13033            &volume_header.session_id,
13034        )
13035        .unwrap();
13036
13037        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
13038            let crypto_region = image
13039                .regions
13040                .iter_mut()
13041                .find(|region| region.region_type == 2)
13042                .unwrap();
13043            let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
13044            let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
13045            crypto_region.bytes[salt_start] ^= 0x01;
13046            let hmac = compute_hmac(
13047                HmacDomain::CryptoHeader,
13048                &subkeys.mac_key,
13049                &volume_header.archive_uuid,
13050                &volume_header.session_id,
13051                &crypto_region.bytes[..hmac_offset],
13052            );
13053            crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
13054        });
13055
13056        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13057        let locator = final_recovery_locator(&mutated);
13058        let crypto_start = volume_header.crypto_header_offset as usize;
13059        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13060        let parsed_crypto = CryptoHeader::parse(
13061            &mutated[crypto_start..crypto_end],
13062            volume_header.crypto_header_length,
13063        )
13064        .unwrap();
13065        assert_eq!(
13066            parse_locator_cmra_candidate(
13067                &mutated,
13068                final_offset,
13069                locator,
13070                KeyHoldingTerminalContext {
13071                    subkeys: &subkeys,
13072                    volume_header: &volume_header,
13073                    crypto_header: &parsed_crypto.fixed,
13074                    crypto_header_bytes: &mutated[crypto_start..crypto_end],
13075                },
13076            )
13077            .unwrap_err(),
13078            FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
13079        );
13080        assert!(open_archive(&mutated, &master_key()).is_err());
13081    }
13082
13083    #[test]
13084    fn recovers_physical_crypto_header_splice_from_cmra_authority() {
13085        let base = WriterOptions {
13086            archive_uuid: Some([0x11; 16]),
13087            session_id: Some([0x22; 16]),
13088            ..small_block_recovery_options()
13089        };
13090        let same_archive = WriterOptions {
13091            archive_uuid: Some([0x11; 16]),
13092            session_id: Some([0x33; 16]),
13093            ..small_block_recovery_options()
13094        };
13095
13096        let first = write_archive(
13097            &[RegularFile::new("splice.txt", b"same shape")],
13098            &master_key(),
13099            base,
13100        )
13101        .unwrap();
13102        let second = write_archive(
13103            &[RegularFile::new("splice.txt", b"same shape")],
13104            &master_key(),
13105            same_archive,
13106        )
13107        .unwrap();
13108
13109        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
13110        let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
13111        let crypto_start = volume_header.crypto_header_offset as usize;
13112        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13113        let second_crypto_end = second_volume_header.crypto_header_offset as usize
13114            + second_volume_header.crypto_header_length as usize;
13115        assert_eq!(crypto_end, second_crypto_end);
13116
13117        let mut spliced = first.bytes.clone();
13118        spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
13119
13120        let opened = open_archive(&spliced, &master_key()).unwrap();
13121        assert_eq!(
13122            opened.crypto_header_bytes,
13123            first.bytes[crypto_start..crypto_end].to_vec()
13124        );
13125        assert_eq!(
13126            opened.extract_file("splice.txt").unwrap(),
13127            Some(b"same shape".to_vec())
13128        );
13129        opened.verify().unwrap();
13130    }
13131
13132    #[test]
13133    fn rejects_same_key_object_splice_with_session_mismatch() {
13134        let first = write_archive(
13135            &[RegularFile::new("splice.txt", b"same shape")],
13136            &master_key(),
13137            WriterOptions {
13138                archive_uuid: Some([0x11; 16]),
13139                session_id: Some([0x22; 16]),
13140                ..single_stream_options()
13141            },
13142        )
13143        .unwrap();
13144        let second = write_archive(
13145            &[RegularFile::new("splice.txt", b"same shape")],
13146            &master_key(),
13147            WriterOptions {
13148                archive_uuid: Some([0x11; 16]),
13149                session_id: Some([0x33; 16]),
13150                ..single_stream_options()
13151            },
13152        )
13153        .unwrap();
13154
13155        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
13156        let crypto_end = volume_header.crypto_header_offset as usize
13157            + volume_header.crypto_header_length as usize;
13158        let terminal_offset = terminal_material_offset(&first.bytes);
13159        let second_terminal_offset = terminal_material_offset(&second.bytes);
13160        assert_eq!(terminal_offset, second_terminal_offset);
13161
13162        let mut spliced = first.bytes.clone();
13163        spliced[crypto_end..terminal_offset]
13164            .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
13165
13166        assert_eq!(
13167            open_archive(&spliced, &master_key()).unwrap_err(),
13168            FormatError::AeadFailure
13169        );
13170    }
13171
13172    #[test]
13173    fn rejects_authenticated_trailer_pointer_and_count_mutations() {
13174        let archive = write_archive(
13175            &[RegularFile::new(
13176                "trailer-range.txt",
13177                b"authenticated ranges",
13178            )],
13179            &master_key(),
13180            single_stream_options(),
13181        )
13182        .unwrap();
13183        let strict_options = ReaderOptions {
13184            max_trailing_garbage_scan: 0,
13185            ..ReaderOptions::default()
13186        };
13187        let bytes = archive.bytes;
13188        let manifest_offset = terminal_material_offset(&bytes);
13189        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
13190
13191        let mut wrong_footer_length = bytes.clone();
13192        rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
13193            trailer.manifest_footer_length = 42;
13194        });
13195        OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
13196            .unwrap();
13197
13198        for (label, offset) in [
13199            (
13200                "offset before trailer by 1",
13201                manifest_offset.saturating_sub(1),
13202            ),
13203            ("offset after trailer", manifest_offset + 1),
13204            ("offset at stream start", 0),
13205            ("offset at trailer", trailer_offset),
13206            ("offset beyond trailer", trailer_offset + 4),
13207        ] {
13208            let mut wrong_footer_offset = bytes.clone();
13209            rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13210                trailer.manifest_footer_offset = offset as u64;
13211            });
13212            open_archive(&wrong_footer_offset, &master_key())
13213                .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
13214        }
13215
13216        let mut wrong_bytes_written = bytes.clone();
13217        rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
13218            trailer.bytes_written += 1;
13219        });
13220        open_archive(&wrong_bytes_written, &master_key()).unwrap();
13221
13222        let mut wrong_block_count = bytes.clone();
13223        rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
13224            trailer.block_count += 1;
13225        });
13226        open_archive(&wrong_block_count, &master_key()).unwrap();
13227
13228        let mut wrong_footer_offset = bytes.clone();
13229        rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13230            trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
13231        });
13232        open_archive(&wrong_footer_offset, &master_key()).unwrap();
13233    }
13234
13235    #[test]
13236    fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
13237        let archive = write_archive(
13238            &[RegularFile::new(
13239                "trailer-trailing-scan.txt",
13240                b"trailer scan boundaries",
13241            )],
13242            &master_key(),
13243            single_stream_options(),
13244        )
13245        .unwrap();
13246        let options = ReaderOptions {
13247            max_trailing_garbage_scan: 8,
13248            ..ReaderOptions::default()
13249        };
13250
13251        let mut within_scan = archive.bytes.clone();
13252        within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
13253        let opened =
13254            OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
13255        assert_eq!(
13256            opened.extract_file("trailer-trailing-scan.txt").unwrap(),
13257            Some(b"trailer scan boundaries".to_vec())
13258        );
13259
13260        let mut beyond_scan = archive.bytes.clone();
13261        beyond_scan.resize(
13262            beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
13263            0xAA,
13264        );
13265        assert_eq!(
13266            OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
13267            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13268        );
13269    }
13270
13271    #[test]
13272    fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
13273        let archive = write_archive(
13274            &[RegularFile::new("index-root-size.txt", b"extent size")],
13275            &master_key(),
13276            single_stream_options(),
13277        )
13278        .unwrap();
13279        let mut malformed = archive.bytes;
13280        let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
13281            .expect("archive should contain IndexRootData");
13282        mutate_block_record_at_slot(&mut malformed, slot, |record| {
13283            record.payload[0] ^= 0x55;
13284        });
13285
13286        assert_eq!(
13287            open_archive(&malformed, &master_key()).unwrap_err(),
13288            FormatError::AeadFailure
13289        );
13290    }
13291
13292    #[test]
13293    fn rejects_block_record_at_wrong_stripe_position() {
13294        let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
13295        let archive = write_archive(
13296            &files,
13297            &master_key(),
13298            WriterOptions {
13299                stripe_width: 2,
13300                volume_loss_tolerance: 1,
13301                ..single_stream_options()
13302            },
13303        )
13304        .unwrap();
13305        let mut volumes = archive.volumes.clone();
13306        mutate_first_block_record(&mut volumes[0], |record| {
13307            record.block_index += 2;
13308        });
13309
13310        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
13311        assert_eq!(
13312            open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
13313            FormatError::InvalidArchive("BlockRecord index does not match volume position")
13314        );
13315    }
13316
13317    #[test]
13318    fn rejects_decreasing_block_record_index_in_required_region() {
13319        let archive = write_archive(
13320            &[RegularFile::new("alpha.txt", b"decreasing block index")],
13321            &master_key(),
13322            single_stream_options(),
13323        )
13324        .unwrap();
13325        assert!(block_record_slots(&archive.bytes).len() >= 2);
13326
13327        let mut malformed = archive.bytes;
13328        mutate_block_record_at_slot(&mut malformed, 1, |record| {
13329            record.block_index = 0;
13330        });
13331
13332        assert_eq!(
13333            open_archive(&malformed, &master_key()).unwrap_err(),
13334            FormatError::InvalidArchive("BlockRecord index does not match volume position")
13335        );
13336    }
13337
13338    #[test]
13339    fn rejects_duplicate_authenticated_volume_indexes() {
13340        let files = [RegularFile::new("alpha.txt", b"duplicates")];
13341        let archive = write_archive(
13342            &files,
13343            &master_key(),
13344            WriterOptions {
13345                stripe_width: 2,
13346                volume_loss_tolerance: 1,
13347                ..single_stream_options()
13348            },
13349        )
13350        .unwrap();
13351
13352        assert_eq!(
13353            open_archive_volumes(
13354                &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
13355                &master_key()
13356            )
13357            .unwrap_err(),
13358            FormatError::InvalidArchive("duplicate authenticated volume index")
13359        );
13360    }
13361
13362    #[test]
13363    fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
13364        let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
13365        let archive = write_archive(
13366            &files,
13367            &master_key(),
13368            WriterOptions {
13369                stripe_width: 2,
13370                volume_loss_tolerance: 1,
13371                ..single_stream_options()
13372            },
13373        )
13374        .unwrap();
13375        let mut conflicting = archive.volumes[0].clone();
13376        corrupt_first_block_record_payload(&mut conflicting);
13377
13378        assert_eq!(
13379            open_archive_volumes(
13380                &[archive.volumes[0].as_slice(), conflicting.as_slice()],
13381                &master_key()
13382            )
13383            .unwrap_err(),
13384            FormatError::InvalidArchive("duplicate authenticated volume index")
13385        );
13386    }
13387
13388    fn directory_hint_table_from_rows(
13389        hint_shard_index: u64,
13390        rows: &[(Vec<u8>, Vec<u32>)],
13391        shard_count: u32,
13392    ) -> DirectoryHintTable {
13393        let mut entries = Vec::new();
13394        let mut shard_row_indexes = Vec::new();
13395        let mut string_pool = Vec::new();
13396
13397        for (path, rows) in rows {
13398            let path_offset = if path.is_empty() {
13399                0
13400            } else {
13401                let offset = string_pool.len() as u64;
13402                string_pool.extend_from_slice(path);
13403                offset
13404            };
13405            let shard_list_start_index = shard_row_indexes.len() as u32;
13406            shard_row_indexes.extend_from_slice(rows);
13407            entries.push(DirectoryHintEntry {
13408                dir_hash: hash_prefix(path),
13409                path_offset,
13410                path_length: path.len() as u32,
13411                shard_list_start_index,
13412                shard_count: rows.len() as u32,
13413            });
13414        }
13415
13416        let table_bytes =
13417            directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
13418        let locating = DirectoryHintShardEntry {
13419            hint_shard_index,
13420            first_dir_hash: hash_prefix(&rows.first().unwrap().0),
13421            last_dir_hash: hash_prefix(&rows.last().unwrap().0),
13422            first_block_index: 0,
13423            data_block_count: 1,
13424            parity_block_count: 0,
13425            encrypted_size: 4096,
13426            decompressed_size: table_bytes.len() as u32,
13427            entry_count: rows.len() as u64,
13428        };
13429        DirectoryHintTable::parse(
13430            &table_bytes,
13431            &locating,
13432            shard_count,
13433            MetadataLimits::default(),
13434        )
13435        .unwrap()
13436    }
13437
13438    fn directory_hint_table_bytes(
13439        hint_shard_index: u64,
13440        entries: Vec<DirectoryHintEntry>,
13441        shard_row_indexes: Vec<u32>,
13442        string_pool: Vec<u8>,
13443    ) -> Vec<u8> {
13444        let header_len = DirectoryHintTableHeader {
13445            version: 1,
13446            hint_shard_index,
13447            entry_count: 0,
13448            entry_table_offset: 0,
13449            shard_list_offset: 0,
13450            string_pool_offset: 0,
13451            string_pool_size: 0,
13452        }
13453        .to_bytes()
13454        .len();
13455        let entry_len = entries
13456            .first()
13457            .map(|entry| entry.to_bytes().len())
13458            .unwrap_or(0);
13459        let shard_list_offset = if entries.is_empty() {
13460            0
13461        } else {
13462            header_len + entries.len() * entry_len
13463        };
13464        let string_pool_offset = if string_pool.is_empty() {
13465            0
13466        } else {
13467            shard_list_offset + shard_row_indexes.len() * 4
13468        };
13469
13470        let header = DirectoryHintTableHeader {
13471            version: 1,
13472            hint_shard_index,
13473            entry_count: entries.len() as u64,
13474            entry_table_offset: if entries.is_empty() {
13475                0
13476            } else {
13477                header_len as u64
13478            },
13479            shard_list_offset: shard_list_offset as u64,
13480            string_pool_offset: string_pool_offset as u64,
13481            string_pool_size: string_pool.len() as u64,
13482        };
13483
13484        let mut out = Vec::new();
13485        out.extend_from_slice(&header.to_bytes());
13486        for entry in entries {
13487            out.extend_from_slice(&entry.to_bytes());
13488        }
13489        for row in shard_row_indexes {
13490            out.extend_from_slice(&row.to_le_bytes());
13491        }
13492        out.extend_from_slice(&string_pool);
13493        out
13494    }
13495
13496    fn corrupt_first_block_record_payload(volume: &mut [u8]) {
13497        let (record_offset, _) = first_block_record(volume);
13498        volume[record_offset + 16] ^= 0x55;
13499    }
13500
13501    fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
13502        let (record_offset, _) = block_record_at_slot(volume, slot);
13503        volume[record_offset + 16] ^= 0x55;
13504    }
13505
13506    fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
13507        for patch in patches {
13508            let offset = patch.record_offset as usize;
13509            let end = offset + patch.record_bytes.len();
13510            volume[offset..end].copy_from_slice(&patch.record_bytes);
13511        }
13512    }
13513
13514    fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
13515        let (record_offset, _) = block_record_at_slot(volume, slot);
13516        volume[record_offset] ^= 0x55;
13517    }
13518
13519    fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
13520        let (record_offset, _) = block_record_at_slot(volume, slot);
13521        volume[record_offset + 14] = 0x01;
13522    }
13523
13524    fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
13525        let manifest_offset = terminal_material_offset(volume);
13526        volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
13527    }
13528
13529    fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
13530        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13531        CriticalRecoveryLocator::parse(
13532            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13533        )
13534        .unwrap()
13535    }
13536
13537    fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
13538        let locator = final_recovery_locator(volume);
13539        let tuple = CmraDecoderTuple::from(locator);
13540        assert!(parity_shard_count < tuple.parity_shard_count);
13541        let cmra_offset = locator.cmra_offset as usize;
13542        let shard_size = tuple.shard_size as usize;
13543        let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
13544        let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
13545        let mut header = CriticalMetadataRecoveryHeader::parse(
13546            &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
13547        )
13548        .unwrap();
13549        header.parity_shard_count = parity_shard_count;
13550
13551        let mut cmra =
13552            Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
13553        cmra.extend_from_slice(&header.to_bytes());
13554        let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
13555        for row in 0..kept_rows {
13556            let start = rows_start + row * row_len;
13557            cmra.extend_from_slice(&volume[start..start + row_len]);
13558        }
13559
13560        let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
13561        out.extend_from_slice(&volume[..cmra_offset]);
13562        out.extend_from_slice(&cmra);
13563        let mut mirror = locator;
13564        mirror.locator_sequence = 1;
13565        mirror.cmra_length = cmra.len() as u32;
13566        mirror.cmra_parity_shard_count = parity_shard_count;
13567        out.extend_from_slice(&mirror.to_bytes());
13568        let final_locator = CriticalRecoveryLocator {
13569            locator_sequence: 0,
13570            ..mirror
13571        };
13572        out.extend_from_slice(&final_locator.to_bytes());
13573        out
13574    }
13575
13576    fn rewrite_public_cmra_image(
13577        volume: &mut [u8],
13578        mutate: impl FnOnce(&mut CriticalMetadataImage),
13579    ) {
13580        rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
13581    }
13582
13583    fn rewrite_cmra_image(
13584        volume: &mut [u8],
13585        mode: CmraRecoveryMode,
13586        mutate: impl FnOnce(&mut CriticalMetadataImage),
13587    ) {
13588        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13589        let locator = final_recovery_locator(volume);
13590        let tuple = CmraDecoderTuple::from(locator);
13591        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
13592        let mut image = recovered.image;
13593        mutate(&mut image);
13594        refresh_critical_image_region_digests(&mut image);
13595        let image_bytes = image.to_bytes().unwrap();
13596        assert_eq!(image_bytes.len(), tuple.image_length as usize);
13597
13598        let shard_size = tuple.shard_size as usize;
13599        let data_shard_count = tuple.data_shard_count as usize;
13600        let parity_shard_count = tuple.parity_shard_count as usize;
13601        assert!(image_bytes.len() <= data_shard_count * shard_size);
13602
13603        let mut data_shards = Vec::with_capacity(data_shard_count);
13604        for idx in 0..data_shard_count {
13605            let start = idx * shard_size;
13606            let end = (start + shard_size).min(image_bytes.len());
13607            let mut payload = vec![0u8; shard_size];
13608            if start < image_bytes.len() {
13609                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
13610            }
13611            data_shards.push(payload);
13612        }
13613        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
13614        let image_sha256 = sha256_bytes(&image_bytes);
13615
13616        let header = CriticalMetadataRecoveryHeader {
13617            shard_size: tuple.shard_size,
13618            data_shard_count: tuple.data_shard_count,
13619            parity_shard_count: tuple.parity_shard_count,
13620            image_length: tuple.image_length,
13621            archive_uuid_hint: locator.archive_uuid_hint,
13622            session_id_hint: locator.session_id_hint,
13623            volume_index_hint: locator.volume_index_hint,
13624            image_sha256,
13625            header_crc32c: 0,
13626        };
13627        let mut cmra = Vec::new();
13628        cmra.extend_from_slice(&header.to_bytes());
13629        for (idx, payload) in data_shards.into_iter().enumerate() {
13630            let payload_len = if idx + 1 == data_shard_count {
13631                image_bytes.len() - idx * shard_size
13632            } else {
13633                shard_size
13634            };
13635            cmra.extend_from_slice(
13636                &CriticalMetadataRecoveryShard {
13637                    shard_index: idx as u16,
13638                    shard_role: 0,
13639                    shard_payload_length: payload_len as u32,
13640                    payload,
13641                    shard_crc32c: 0,
13642                }
13643                .to_bytes(shard_size)
13644                .unwrap(),
13645            );
13646        }
13647        for (idx, payload) in parity_shards.into_iter().enumerate() {
13648            cmra.extend_from_slice(
13649                &CriticalMetadataRecoveryShard {
13650                    shard_index: (data_shard_count + idx) as u16,
13651                    shard_role: 1,
13652                    shard_payload_length: shard_size as u32,
13653                    payload,
13654                    shard_crc32c: 0,
13655                }
13656                .to_bytes(shard_size)
13657                .unwrap(),
13658            );
13659        }
13660        assert_eq!(cmra.len() as u64, recovered.cmra_length);
13661        let cmra_offset = locator.cmra_offset as usize;
13662        volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
13663
13664        rewrite_locator_image_sha(volume, final_offset, image_sha256);
13665        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13666        rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
13667    }
13668
13669    fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
13670        image.volume_header_sha256 = sha256_bytes(
13671            &image
13672                .regions
13673                .iter()
13674                .find(|region| region.region_type == 1)
13675                .unwrap()
13676                .bytes,
13677        );
13678        image.crypto_header_sha256 = sha256_bytes(
13679            &image
13680                .regions
13681                .iter()
13682                .find(|region| region.region_type == 2)
13683                .unwrap()
13684                .bytes,
13685        );
13686        image.manifest_footer_sha256 = sha256_bytes(
13687            &image
13688                .regions
13689                .iter()
13690                .find(|region| region.region_type == 3)
13691                .unwrap()
13692                .bytes,
13693        );
13694        image.root_auth_footer_sha256 = image
13695            .regions
13696            .iter()
13697            .find(|region| region.region_type == 4)
13698            .map(|region| sha256_bytes(&region.bytes))
13699            .unwrap_or([0u8; 32]);
13700        image.volume_trailer_sha256 = sha256_bytes(
13701            &image
13702                .regions
13703                .iter()
13704                .find(|region| region.region_type == 5)
13705                .unwrap()
13706                .bytes,
13707        );
13708    }
13709
13710    fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
13711        let mut locator =
13712            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
13713                .unwrap();
13714        locator.cmra_image_sha256 = image_sha256;
13715        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
13716    }
13717
13718    fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
13719        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13720        let final_locator = CriticalRecoveryLocator::parse(
13721            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13722        )
13723        .unwrap();
13724        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13725        volume[final_locator.cmra_offset as usize] ^= 0x55;
13726        volume[mirror_offset] ^= 0x55;
13727        volume[final_offset] ^= 0x55;
13728    }
13729
13730    fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
13731        let (record_offset, record_len) = first_block_record(volume);
13732        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13733        let mut record = BlockRecord::parse(
13734            &volume[record_offset..record_offset + record_len],
13735            block_size,
13736        )
13737        .unwrap();
13738        mutate(&mut record);
13739        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13740    }
13741
13742    fn mutate_block_record_at_slot(
13743        volume: &mut [u8],
13744        slot: usize,
13745        mutate: impl FnOnce(&mut BlockRecord),
13746    ) {
13747        let (record_offset, record_len) = block_record_at_slot(volume, slot);
13748        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13749        let mut record = BlockRecord::parse(
13750            &volume[record_offset..record_offset + record_len],
13751            block_size,
13752        )
13753        .unwrap();
13754        mutate(&mut record);
13755        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13756    }
13757
13758    fn first_block_record(volume: &[u8]) -> (usize, usize) {
13759        block_record_at_slot(volume, 0)
13760    }
13761
13762    fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
13763        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13764        let crypto_start = volume_header.crypto_header_offset as usize;
13765        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13766        let crypto_header = CryptoHeader::parse(
13767            &volume[crypto_start..crypto_end],
13768            volume_header.crypto_header_length,
13769        )
13770        .unwrap();
13771        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13772        let record_offset = crypto_end + slot * record_len;
13773        assert!(volume.len() >= record_offset + record_len);
13774        (record_offset, record_len)
13775    }
13776
13777    fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
13778        block_record_slots(volume)
13779            .into_iter()
13780            .enumerate()
13781            .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13782    }
13783
13784    fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
13785        block_record_slots(volume)
13786            .into_iter()
13787            .enumerate()
13788            .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13789            .collect()
13790    }
13791
13792    fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
13793        let mut slots = Vec::new();
13794        for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
13795            if record.kind == BlockKind::PayloadData {
13796                slots.push(slot);
13797            } else if !slots.is_empty() {
13798                break;
13799            }
13800        }
13801        slots
13802    }
13803
13804    fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
13805        envelope_entries_for_path(opened, path)
13806            .into_iter()
13807            .map(|entry| entry.envelope_index)
13808            .collect()
13809    }
13810
13811    fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
13812        let normalized =
13813            normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
13814        let located = opened.locate_index_file(&normalized).unwrap().unwrap();
13815        let file = &located.shard.files[located.file_index];
13816        frame_range_for_file(&located.shard, file)
13817            .unwrap()
13818            .into_iter()
13819            .map(|frame| {
13820                located
13821                    .shard
13822                    .envelopes
13823                    .iter()
13824                    .find(|entry| entry.envelope_index == frame.envelope_index)
13825                    .unwrap()
13826                    .clone()
13827            })
13828            .collect()
13829    }
13830
13831    fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
13832        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13833        let crypto_start = volume_header.crypto_header_offset as usize;
13834        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13835        let crypto_header = CryptoHeader::parse(
13836            &volume[crypto_start..crypto_end],
13837            volume_header.crypto_header_length,
13838        )
13839        .unwrap();
13840        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13841        let manifest_offset = terminal_material_offset(volume);
13842        assert_eq!((manifest_offset - crypto_end) % record_len, 0);
13843        let record_count = (manifest_offset - crypto_end) / record_len;
13844        (0..record_count)
13845            .map(|slot| {
13846                let offset = crypto_end + slot * record_len;
13847                let record = BlockRecord::parse(
13848                    &volume[offset..offset + record_len],
13849                    record_len - BLOCK_RECORD_FRAMING_LEN,
13850                )
13851                .unwrap();
13852                (offset, record_len, record)
13853            })
13854            .collect()
13855    }
13856
13857    fn rewrite_manifest_footer(
13858        volume: &mut [u8],
13859        master_key: &MasterKey,
13860        mutate: impl FnOnce(&mut ManifestFooter),
13861    ) {
13862        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13863        let offset = terminal_material_offset(volume);
13864        let mut footer =
13865            ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
13866        mutate(&mut footer);
13867        footer.manifest_hmac = [0u8; 32];
13868        let mut footer_bytes = footer.to_bytes();
13869        let subkeys = Subkeys::derive(
13870            master_key,
13871            &volume_header.archive_uuid,
13872            &volume_header.session_id,
13873        )
13874        .unwrap();
13875        footer.manifest_hmac = compute_hmac(
13876            HmacDomain::ManifestFooter,
13877            &subkeys.mac_key,
13878            &volume_header.archive_uuid,
13879            &volume_header.session_id,
13880            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
13881        );
13882        footer_bytes = footer.to_bytes();
13883        volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
13884    }
13885
13886    fn rewrite_volume_trailer(
13887        volume: &mut [u8],
13888        master_key: &MasterKey,
13889        mutate: impl FnOnce(&mut VolumeTrailer),
13890    ) {
13891        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13892        let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
13893        let mut trailer =
13894            VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
13895        mutate(&mut trailer);
13896        trailer.trailer_hmac = [0u8; 32];
13897        let mut trailer_bytes = trailer.to_bytes();
13898        let subkeys = Subkeys::derive(
13899            master_key,
13900            &volume_header.archive_uuid,
13901            &volume_header.session_id,
13902        )
13903        .unwrap();
13904        trailer.trailer_hmac = compute_hmac(
13905            HmacDomain::VolumeTrailer,
13906            &subkeys.mac_key,
13907            &volume_header.archive_uuid,
13908            &volume_header.session_id,
13909            &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
13910        );
13911        trailer_bytes = trailer.to_bytes();
13912        volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
13913    }
13914
13915    fn rewrite_sidecar_header(
13916        sidecar: &mut [u8],
13917        master_key: &MasterKey,
13918        mutate: impl FnOnce(&mut BootstrapSidecarHeader),
13919    ) {
13920        let mut header =
13921            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13922        mutate(&mut header);
13923        write_signed_sidecar_header(sidecar, master_key, &mut header);
13924    }
13925
13926    fn write_signed_sidecar_header(
13927        sidecar: &mut [u8],
13928        master_key: &MasterKey,
13929        header: &mut BootstrapSidecarHeader,
13930    ) {
13931        header.sidecar_hmac = [0u8; 32];
13932        let mut header_bytes = header.to_bytes();
13933        let subkeys =
13934            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
13935        header.sidecar_hmac = compute_hmac(
13936            HmacDomain::BootstrapSidecar,
13937            &subkeys.mac_key,
13938            &header.archive_uuid,
13939            &header.session_id,
13940            &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
13941        );
13942        header_bytes = header.to_bytes();
13943        sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
13944    }
13945
13946    fn sparse_bootstrap_sidecar(
13947        source: &[u8],
13948        master_key: &MasterKey,
13949        include_manifest: bool,
13950        include_index_root: bool,
13951        include_dictionary: bool,
13952    ) -> Vec<u8> {
13953        let source_header =
13954            BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13955        let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
13956        let mut header = BootstrapSidecarHeader {
13957            archive_uuid: source_header.archive_uuid,
13958            session_id: source_header.session_id,
13959            flags: 0,
13960            manifest_footer_offset: 0,
13961            manifest_footer_length: 0,
13962            index_root_records_offset: 0,
13963            index_root_records_length: 0,
13964            dictionary_records_offset: 0,
13965            dictionary_records_length: 0,
13966            sidecar_hmac: [0u8; 32],
13967            header_crc32c: 0,
13968        };
13969
13970        if include_manifest {
13971            assert!(source_header.has_manifest_footer());
13972            let (offset, length) = append_sidecar_section(
13973                source,
13974                &mut sidecar,
13975                source_header.manifest_footer_offset,
13976                source_header.manifest_footer_length as u64,
13977            );
13978            header.flags |= 0x01;
13979            header.manifest_footer_offset = offset;
13980            header.manifest_footer_length = length as u32;
13981        }
13982        if include_index_root {
13983            assert!(source_header.has_index_root_records());
13984            let (offset, length) = append_sidecar_section(
13985                source,
13986                &mut sidecar,
13987                source_header.index_root_records_offset,
13988                source_header.index_root_records_length,
13989            );
13990            header.flags |= 0x02;
13991            header.index_root_records_offset = offset;
13992            header.index_root_records_length = length;
13993        }
13994        if include_dictionary {
13995            assert!(source_header.has_dictionary_records());
13996            let (offset, length) = append_sidecar_section(
13997                source,
13998                &mut sidecar,
13999                source_header.dictionary_records_offset,
14000                source_header.dictionary_records_length,
14001            );
14002            header.flags |= 0x04;
14003            header.dictionary_records_offset = offset;
14004            header.dictionary_records_length = length;
14005        }
14006
14007        write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
14008        sidecar
14009    }
14010
14011    fn append_sidecar_section(
14012        source: &[u8],
14013        sidecar: &mut Vec<u8>,
14014        source_offset: u64,
14015        length: u64,
14016    ) -> (u64, u64) {
14017        let source_offset = source_offset as usize;
14018        let length = length as usize;
14019        let offset = sidecar.len() as u64;
14020        sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
14021        (offset, length as u64)
14022    }
14023
14024    fn mutate_sidecar_manifest(
14025        sidecar: &mut [u8],
14026        master_key: &MasterKey,
14027        mutate: impl FnOnce(&mut ManifestFooter),
14028    ) {
14029        let header =
14030            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14031        let offset = header.manifest_footer_offset as usize;
14032        let mut footer =
14033            ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
14034        mutate(&mut footer);
14035        footer.manifest_hmac = [0u8; 32];
14036        let mut footer_bytes = footer.to_bytes();
14037        let subkeys =
14038            Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
14039        footer.manifest_hmac = compute_hmac(
14040            HmacDomain::ManifestFooter,
14041            &subkeys.mac_key,
14042            &footer.archive_uuid,
14043            &footer.session_id,
14044            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
14045        );
14046        footer_bytes = footer.to_bytes();
14047        sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
14048    }
14049
14050    fn mutate_sidecar_index_record(
14051        sidecar: &mut [u8],
14052        record_index: usize,
14053        mutate: impl FnOnce(&mut BlockRecord),
14054    ) {
14055        let header =
14056            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14057        let record_len = sidecar_record_len(sidecar);
14058        let offset = header.index_root_records_offset as usize + record_index * record_len;
14059        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
14060        let mut record =
14061            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
14062        mutate(&mut record);
14063        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14064    }
14065
14066    fn mutate_sidecar_dictionary_record(
14067        sidecar: &mut [u8],
14068        record_index: usize,
14069        mutate: impl FnOnce(&mut BlockRecord),
14070    ) {
14071        let header =
14072            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14073        let record_len = sidecar_record_len(sidecar);
14074        let offset = header.dictionary_records_offset as usize + record_index * record_len;
14075        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
14076        let mut record =
14077            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
14078        mutate(&mut record);
14079        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14080    }
14081
14082    fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
14083        let header =
14084            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14085        let record_len = sidecar_record_len(sidecar);
14086        let left_offset = header.index_root_records_offset as usize + left * record_len;
14087        let right_offset = header.index_root_records_offset as usize + right * record_len;
14088        for idx in 0..record_len {
14089            sidecar.swap(left_offset + idx, right_offset + idx);
14090        }
14091    }
14092
14093    fn sidecar_record_len(sidecar: &[u8]) -> usize {
14094        let header =
14095            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14096        let footer_offset = header.manifest_footer_offset as usize;
14097        let footer =
14098            ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
14099                .unwrap();
14100        let index_record_count = footer.index_root_data_block_count as usize
14101            + footer.index_root_parity_block_count as usize;
14102        header.index_root_records_length as usize / index_record_count
14103    }
14104
14105    fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
14106        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
14107        assert_eq!(volume_header.volume_index, 0);
14108        assert_eq!(volume_header.stripe_width, 1);
14109        let crypto_start = volume_header.crypto_header_offset as usize;
14110        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14111        let crypto_header = CryptoHeader::parse(
14112            &volume[crypto_start..crypto_end],
14113            volume_header.crypto_header_length,
14114        )
14115        .unwrap();
14116        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14117        let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
14118        for offset in 0..record_count {
14119            let block_index = extent.first_block_index + offset;
14120            let record_offset = crypto_end + block_index as usize * record_len;
14121            volume[record_offset + 16] ^= 0x55;
14122        }
14123    }
14124
14125    fn terminal_material_offset(volume: &[u8]) -> usize {
14126        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
14127        let crypto_start = volume_header.crypto_header_offset as usize;
14128        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14129        let crypto_header = CryptoHeader::parse(
14130            &volume[crypto_start..crypto_end],
14131            volume_header.crypto_header_length,
14132        )
14133        .unwrap();
14134        let (_, offset, _) = parse_stream_block_prefix(
14135            volume,
14136            crypto_end,
14137            crypto_header.fixed.block_size as usize,
14138            &volume_header,
14139        )
14140        .unwrap();
14141        offset
14142    }
14143
14144    #[derive(Debug)]
14145    struct TestObject {
14146        extent: ObjectExtent,
14147        records: Vec<BlockRecord>,
14148    }
14149
14150    #[derive(Debug)]
14151    struct TestFileMeta {
14152        path: Vec<u8>,
14153        frame_index: u64,
14154        tar_stream_offset: u64,
14155        member_group_size: u64,
14156        file_data_size: u64,
14157    }
14158
14159    fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
14160        let volume_header = test_volume_header();
14161        let crypto_header = test_crypto_header();
14162        let subkeys = Subkeys::derive(
14163            &master_key(),
14164            &volume_header.archive_uuid,
14165            &volume_header.session_id,
14166        )
14167        .unwrap();
14168        let mut next_block_index = 0u64;
14169        let mut blocks = BTreeMap::new();
14170
14171        let healthy = test_member(b"healthy.txt", b"healthy payload\n");
14172        let broken = test_member(b"broken.txt", b"broken payload\n");
14173        let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
14174
14175        let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
14176        let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
14177
14178        let healthy_payload = encrypt_test_object(
14179            &healthy_frame,
14180            &subkeys.enc_key,
14181            &subkeys.nonce_seed,
14182            b"envelope",
14183            0,
14184            BlockKind::PayloadData,
14185            &mut next_block_index,
14186            &crypto_header,
14187            &volume_header,
14188        );
14189        let broken_payload = encrypt_test_object(
14190            &broken_frame,
14191            &subkeys.enc_key,
14192            &subkeys.nonce_seed,
14193            b"envelope",
14194            1,
14195            BlockKind::PayloadData,
14196            &mut next_block_index,
14197            &crypto_header,
14198            &volume_header,
14199        );
14200        let broken_payload_block = broken_payload.extent.first_block_index;
14201        insert_records(&mut blocks, &healthy_payload.records);
14202        insert_records(&mut blocks, &broken_payload.records);
14203
14204        let frames = vec![
14205            FrameEntry {
14206                frame_index: 0,
14207                envelope_index: 0,
14208                offset_in_envelope: 0,
14209                compressed_size: healthy_frame.len() as u32,
14210                decompressed_size: healthy.len() as u32,
14211                flags: 0x0000_0003,
14212                tar_stream_offset: 0,
14213            },
14214            FrameEntry {
14215                frame_index: 1,
14216                envelope_index: 1,
14217                offset_in_envelope: 0,
14218                compressed_size: broken_frame.len() as u32,
14219                decompressed_size: broken.len() as u32,
14220                flags: 0x0000_0003,
14221                tar_stream_offset: healthy.len() as u64,
14222            },
14223        ];
14224        let envelopes = vec![
14225            EnvelopeEntry {
14226                envelope_index: 0,
14227                first_block_index: healthy_payload.extent.first_block_index,
14228                data_block_count: healthy_payload.extent.data_block_count,
14229                parity_block_count: 0,
14230                encrypted_size: healthy_payload.extent.encrypted_size,
14231                plaintext_size: healthy_frame.len() as u32,
14232                first_frame_index: 0,
14233                frame_count: 1,
14234            },
14235            EnvelopeEntry {
14236                envelope_index: 1,
14237                first_block_index: broken_payload.extent.first_block_index,
14238                data_block_count: broken_payload.extent.data_block_count,
14239                parity_block_count: 0,
14240                encrypted_size: broken_payload.extent.encrypted_size,
14241                plaintext_size: broken_frame.len() as u32,
14242                first_frame_index: 1,
14243                frame_count: 1,
14244            },
14245        ];
14246        let files = vec![
14247            TestFileMeta {
14248                path: b"healthy.txt".to_vec(),
14249                frame_index: 0,
14250                tar_stream_offset: 0,
14251                member_group_size: healthy.len() as u64,
14252                file_data_size: b"healthy payload\n".len() as u64,
14253            },
14254            TestFileMeta {
14255                path: b"broken.txt".to_vec(),
14256                frame_index: 1,
14257                tar_stream_offset: healthy.len() as u64,
14258                member_group_size: broken.len() as u64,
14259                file_data_size: b"broken payload\n".len() as u64,
14260            },
14261        ];
14262
14263        let (index_shard_plaintext, first_path_hash, last_path_hash) =
14264            build_test_index_shard(&files, &frames, &envelopes);
14265        let index_shard = encrypt_test_object(
14266            &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
14267            &subkeys.index_shard_key,
14268            &subkeys.index_nonce_seed,
14269            b"idxshard",
14270            0,
14271            BlockKind::IndexShardData,
14272            &mut next_block_index,
14273            &crypto_header,
14274            &volume_header,
14275        );
14276        insert_records(&mut blocks, &index_shard.records);
14277
14278        let shard_entry = ShardEntry {
14279            shard_index: 0,
14280            first_block_index: index_shard.extent.first_block_index,
14281            data_block_count: index_shard.extent.data_block_count,
14282            parity_block_count: 0,
14283            encrypted_size: index_shard.extent.encrypted_size,
14284            decompressed_size: index_shard_plaintext.len() as u32,
14285            file_count: files.len() as u32,
14286            first_path_hash,
14287            last_path_hash,
14288        };
14289        let mut root_header = IndexRootHeader::empty();
14290        root_header.frame_count = frames.len() as u64;
14291        root_header.envelope_count = envelopes.len() as u64;
14292        root_header.file_count = files.len() as u64;
14293        root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
14294            + broken_payload.extent.data_block_count as u64;
14295        root_header.tar_total_size = tar_stream.len() as u64;
14296        root_header.content_sha256 = sha256_bytes(&tar_stream);
14297        let index_root = IndexRoot {
14298            header: root_header,
14299            shards: vec![shard_entry],
14300            directory_hint_shards: Vec::new(),
14301        };
14302
14303        let index_root_plaintext = index_root.to_bytes();
14304        let index_root_object = encrypt_test_object(
14305            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14306            &subkeys.index_root_key,
14307            &subkeys.index_nonce_seed,
14308            b"idxroot",
14309            0,
14310            BlockKind::IndexRootData,
14311            &mut next_block_index,
14312            &crypto_header,
14313            &volume_header,
14314        );
14315        insert_records(&mut blocks, &index_root_object.records);
14316
14317        let archive_uuid = volume_header.archive_uuid;
14318        let session_id = volume_header.session_id;
14319        let opened = OpenedArchive {
14320            options: ReaderOptions::default(),
14321            observed_archive_bytes: 1_000_000,
14322            observed_volume_count: 1,
14323            subkeys,
14324            blocks,
14325            lazy_blocks: None,
14326            crypto_header_bytes: Vec::new(),
14327            volume_header,
14328            crypto_header,
14329            manifest_footer: ManifestFooter {
14330                archive_uuid,
14331                session_id,
14332                volume_index: 0,
14333                is_authoritative: 1,
14334                total_volumes: 1,
14335                index_root_first_block: index_root_object.extent.first_block_index,
14336                index_root_data_block_count: index_root_object.extent.data_block_count,
14337                index_root_parity_block_count: 0,
14338                index_root_encrypted_size: index_root_object.extent.encrypted_size,
14339                index_root_decompressed_size: index_root_plaintext.len() as u32,
14340                manifest_hmac: [0u8; 32],
14341            },
14342            volume_trailer: Some(VolumeTrailer {
14343                archive_uuid,
14344                session_id,
14345                volume_index: 0,
14346                block_count: next_block_index,
14347                bytes_written: 0,
14348                manifest_footer_offset: 0,
14349                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14350                closed_at_ns: 0,
14351                root_auth_footer_offset: 0,
14352                root_auth_footer_length: 0,
14353                root_auth_flags: 0,
14354                trailer_hmac: [0u8; 32],
14355            }),
14356            root_auth_footer: None,
14357            index_root,
14358            payload_dictionary: None,
14359        };
14360        (opened, broken_payload_block)
14361    }
14362
14363    fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
14364        let locating = opened.index_root.shards[0].clone();
14365        let mut shard = opened.load_index_shard(&locating).unwrap();
14366        mutate(&mut shard);
14367        let plaintext = shard.to_bytes();
14368        let mut next_block_index = opened
14369            .blocks
14370            .keys()
14371            .last()
14372            .copied()
14373            .map(|index| index + 1)
14374            .unwrap_or(0);
14375        let replacement = encrypt_test_object(
14376            &compress_zstd_frame(&plaintext, 1).unwrap(),
14377            &opened.subkeys.index_shard_key,
14378            &opened.subkeys.index_nonce_seed,
14379            b"idxshard",
14380            locating.shard_index,
14381            BlockKind::IndexShardData,
14382            &mut next_block_index,
14383            &opened.crypto_header,
14384            &opened.volume_header,
14385        );
14386        insert_records(&mut opened.blocks, &replacement.records);
14387        opened.index_root.shards[0] = ShardEntry {
14388            shard_index: locating.shard_index,
14389            first_block_index: replacement.extent.first_block_index,
14390            data_block_count: replacement.extent.data_block_count,
14391            parity_block_count: 0,
14392            encrypted_size: replacement.extent.encrypted_size,
14393            decompressed_size: plaintext.len() as u32,
14394            file_count: shard.files.len() as u32,
14395            first_path_hash: shard.files.first().unwrap().path_hash,
14396            last_path_hash: shard.files.last().unwrap().path_hash,
14397        };
14398    }
14399
14400    fn rewrite_as_single_healthy_file(
14401        opened: &mut OpenedArchive,
14402        mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
14403    ) {
14404        let healthy_path = b"healthy.txt";
14405        let healthy_payload = b"healthy payload\n";
14406        let healthy_member = test_member(healthy_path, healthy_payload);
14407        replace_first_index_shard(opened, |shard| {
14408            let file_index = (0..shard.files.len())
14409                .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
14410                .unwrap();
14411            let mut file = shard.files[file_index].clone();
14412            let frame = shard
14413                .frames
14414                .iter()
14415                .find(|entry| entry.frame_index == 0)
14416                .unwrap()
14417                .clone();
14418            let envelope = shard
14419                .envelopes
14420                .iter()
14421                .find(|entry| entry.envelope_index == 0)
14422                .unwrap()
14423                .clone();
14424            let mut path = healthy_path.to_vec();
14425
14426            file.path_offset = 0;
14427            file.path_length = path.len() as u32;
14428            file.first_frame_index = 0;
14429            file.frame_count = 1;
14430            file.offset_in_first_frame_plaintext = 0;
14431            file.tar_member_group_size = healthy_member.len() as u64;
14432            file.file_data_size = healthy_payload.len() as u64;
14433            file.flags = 0;
14434            mutate(&mut file, &mut path);
14435            file.path_offset = 0;
14436            file.path_length = path.len() as u32;
14437            file.path_hash = hash_prefix(&path);
14438
14439            shard.files = vec![file];
14440            shard.frames = vec![frame];
14441            shard.envelopes = vec![envelope];
14442            shard.string_pool = path;
14443        });
14444
14445        opened.index_root.header.file_count = 1;
14446        opened.index_root.header.frame_count = 1;
14447        opened.index_root.header.envelope_count = 1;
14448        opened.index_root.header.payload_block_count = 1;
14449        opened.index_root.header.tar_total_size = healthy_member.len() as u64;
14450        opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
14451    }
14452
14453    fn test_volume_header() -> VolumeHeader {
14454        VolumeHeader {
14455            format_version: FORMAT_VERSION,
14456            volume_format_rev: VOLUME_FORMAT_REV,
14457            volume_index: 0,
14458            stripe_width: 1,
14459            archive_uuid: [0x31; 16],
14460            session_id: [0x42; 16],
14461            crypto_header_offset: VOLUME_HEADER_LEN as u32,
14462            crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
14463            header_crc32c: 0,
14464        }
14465    }
14466
14467    fn test_crypto_header() -> CryptoHeaderFixed {
14468        CryptoHeaderFixed {
14469            length: CRYPTO_HEADER_FIXED_LEN as u32,
14470            compression_algo: CompressionAlgo::ZstdFramed,
14471            aead_algo: AeadAlgo::AesGcmSiv256,
14472            fec_algo: FecAlgo::ReedSolomonGF16,
14473            kdf_algo: KdfAlgo::Raw,
14474            chunk_size: 4096,
14475            envelope_target_size: 8192,
14476            block_size: 4096,
14477            fec_data_shards: 4,
14478            fec_parity_shards: 0,
14479            index_fec_data_shards: 4,
14480            index_fec_parity_shards: 0,
14481            index_root_fec_data_shards: 4,
14482            index_root_fec_parity_shards: 0,
14483            stripe_width: 1,
14484            volume_loss_tolerance: 0,
14485            bit_rot_buffer_pct: 0,
14486            has_dictionary: 0,
14487            max_path_length: 4096,
14488            expected_volume_size: 0,
14489        }
14490    }
14491
14492    #[allow(clippy::too_many_arguments)]
14493    fn encrypt_test_object(
14494        plaintext: &[u8],
14495        key: &[u8; 32],
14496        nonce_seed: &[u8; 32],
14497        domain: &[u8],
14498        counter: u64,
14499        data_kind: BlockKind,
14500        next_block_index: &mut u64,
14501        crypto_header: &CryptoHeaderFixed,
14502        volume_header: &VolumeHeader,
14503    ) -> TestObject {
14504        let block_size = crypto_header.block_size as usize;
14505        let encrypted = encrypt_padded_aead_object(
14506            AeadObjectContext {
14507                algo: crypto_header.aead_algo,
14508                key,
14509                nonce_seed,
14510                domain,
14511                archive_uuid: &volume_header.archive_uuid,
14512                session_id: &volume_header.session_id,
14513                counter,
14514            },
14515            block_size,
14516            plaintext,
14517        )
14518        .unwrap();
14519        assert_eq!(encrypted.len() % block_size, 0);
14520
14521        let first_block_index = *next_block_index;
14522        let data_block_count = encrypted.len() / block_size;
14523        let records = encrypted
14524            .chunks(block_size)
14525            .enumerate()
14526            .map(|(index, payload)| BlockRecord {
14527                block_index: first_block_index + index as u64,
14528                kind: data_kind,
14529                flags: if index + 1 == data_block_count {
14530                    0x01
14531                } else {
14532                    0
14533                },
14534                payload: payload.to_vec(),
14535                record_crc32c: 0,
14536            })
14537            .collect::<Vec<_>>();
14538        *next_block_index += data_block_count as u64;
14539
14540        TestObject {
14541            extent: ObjectExtent {
14542                first_block_index,
14543                data_block_count: data_block_count as u32,
14544                parity_block_count: 0,
14545                encrypted_size: encrypted.len() as u32,
14546            },
14547            records,
14548        }
14549    }
14550
14551    fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
14552        for record in records {
14553            assert!(blocks.insert(record.block_index, record.clone()).is_none());
14554        }
14555    }
14556
14557    #[allow(clippy::too_many_arguments)]
14558    fn build_metadata_object_from_payload(
14559        payload: &[u8],
14560        _subkeys: &Subkeys,
14561        volume_header: &VolumeHeader,
14562        crypto_header: &CryptoHeaderFixed,
14563        key: &[u8; 32],
14564        nonce_seed: &[u8; 32],
14565        domain: &[u8],
14566        counter: u64,
14567        data_kind: BlockKind,
14568        next_block_index: &mut u64,
14569    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14570        let compressed = compress_zstd_frame(payload, 1).unwrap();
14571        build_metadata_object_from_compressed(
14572            &compressed,
14573            key,
14574            nonce_seed,
14575            domain,
14576            counter,
14577            data_kind,
14578            next_block_index,
14579            crypto_header,
14580            volume_header,
14581        )
14582    }
14583
14584    #[allow(clippy::too_many_arguments)]
14585    fn build_metadata_object_from_compressed(
14586        compressed: &[u8],
14587        key: &[u8; 32],
14588        nonce_seed: &[u8; 32],
14589        domain: &[u8],
14590        counter: u64,
14591        data_kind: BlockKind,
14592        next_block_index: &mut u64,
14593        crypto_header: &CryptoHeaderFixed,
14594        volume_header: &VolumeHeader,
14595    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14596        let object = encrypt_test_object(
14597            compressed,
14598            key,
14599            nonce_seed,
14600            domain,
14601            counter,
14602            data_kind,
14603            next_block_index,
14604            crypto_header,
14605            volume_header,
14606        );
14607
14608        let mut blocks = BTreeMap::new();
14609        for record in object.records {
14610            blocks.insert(record.block_index, record);
14611        }
14612        (object.extent, blocks)
14613    }
14614
14615    #[allow(clippy::too_many_arguments)]
14616    fn assert_metadata_object_from_compressed(
14617        compressed: &[u8],
14618        decompressed_size: usize,
14619        _subkeys: &Subkeys,
14620        volume_header: &VolumeHeader,
14621        crypto_header: &CryptoHeaderFixed,
14622        key: &[u8; 32],
14623        nonce_seed: &[u8; 32],
14624        domain: &[u8],
14625        counter: u64,
14626        data_kind: BlockKind,
14627        parity_kind: BlockKind,
14628        class_data_shards: u16,
14629        class_parity_shards: u16,
14630        next_block_index: &mut u64,
14631        expected: FormatError,
14632    ) {
14633        let (extent, blocks) = build_metadata_object_from_compressed(
14634            compressed,
14635            key,
14636            nonce_seed,
14637            domain,
14638            counter,
14639            data_kind,
14640            next_block_index,
14641            crypto_header,
14642            volume_header,
14643        );
14644        let error = load_metadata_object_from_parts(
14645            &blocks,
14646            ObjectLoadContext {
14647                volume_header,
14648                crypto_header,
14649                extent,
14650                data_kind,
14651                parity_kind,
14652                key,
14653                nonce_seed,
14654                domain,
14655                counter,
14656                class_data_shard_max: class_data_shards,
14657                class_parity_shard_max: class_parity_shards,
14658            },
14659            decompressed_size as u32,
14660        )
14661        .unwrap_err();
14662        assert_eq!(error, expected);
14663    }
14664
14665    fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
14666        let record = blocks.get_mut(&block_index).unwrap();
14667        assert_eq!(record.kind, BlockKind::PayloadData);
14668        record.payload[0] ^= 0x55;
14669    }
14670
14671    fn build_test_index_shard(
14672        files: &[TestFileMeta],
14673        frames: &[FrameEntry],
14674        envelopes: &[EnvelopeEntry],
14675    ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
14676        let mut sorted = files
14677            .iter()
14678            .map(|file| (hash_prefix(&file.path), file))
14679            .collect::<Vec<_>>();
14680        sorted.sort_by(|left, right| {
14681            (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
14682                right.0,
14683                right.1.path.as_slice(),
14684                right.1.tar_stream_offset,
14685            ))
14686        });
14687
14688        let mut string_pool = Vec::new();
14689        let mut file_entries = Vec::with_capacity(sorted.len());
14690        for (path_hash, file) in &sorted {
14691            let path_offset = string_pool.len() as u32;
14692            string_pool.extend_from_slice(&file.path);
14693            file_entries.push(FileEntry {
14694                path_hash: *path_hash,
14695                path_offset,
14696                path_length: file.path.len() as u32,
14697                first_frame_index: file.frame_index,
14698                frame_count: 1,
14699                offset_in_first_frame_plaintext: 0,
14700                tar_member_group_size: file.member_group_size,
14701                file_data_size: file.file_data_size,
14702                flags: 0,
14703            });
14704        }
14705
14706        let header = IndexShardHeader {
14707            version: 1,
14708            shard_index: 0,
14709            file_count: file_entries.len() as u32,
14710            frame_count: frames.len() as u32,
14711            envelope_count: envelopes.len() as u32,
14712            file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
14713            frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
14714                as u32,
14715            envelope_table_offset: (INDEX_SHARD_HEADER_LEN
14716                + file_entries.len() * FILE_ENTRY_LEN
14717                + frames.len() * FRAME_ENTRY_LEN) as u32,
14718            string_pool_offset: (INDEX_SHARD_HEADER_LEN
14719                + file_entries.len() * FILE_ENTRY_LEN
14720                + frames.len() * FRAME_ENTRY_LEN
14721                + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
14722            string_pool_size: string_pool.len() as u32,
14723        };
14724
14725        let mut bytes = Vec::new();
14726        bytes.extend_from_slice(&header.to_bytes());
14727        for entry in &file_entries {
14728            bytes.extend_from_slice(&entry.to_bytes());
14729        }
14730        for entry in frames {
14731            bytes.extend_from_slice(&entry.to_bytes());
14732        }
14733        for entry in envelopes {
14734            bytes.extend_from_slice(&entry.to_bytes());
14735        }
14736        bytes.extend_from_slice(&string_pool);
14737
14738        (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
14739    }
14740
14741    fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
14742        let mut out = Vec::new();
14743        out.extend_from_slice(&test_tar_header(path, data.len() as u64));
14744        out.extend_from_slice(data);
14745        out.resize(out.len() + padding_to_512(data.len()), 0);
14746        out
14747    }
14748
14749    fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
14750        let mut header = [0u8; 512];
14751        header[..path.len()].copy_from_slice(path);
14752        write_test_tar_octal(&mut header[100..108], 0o644);
14753        write_test_tar_octal(&mut header[108..116], 0);
14754        write_test_tar_octal(&mut header[116..124], 0);
14755        write_test_tar_octal(&mut header[124..136], size);
14756        write_test_tar_octal(&mut header[136..148], 0);
14757        header[148..156].fill(b' ');
14758        header[156] = b'0';
14759        header[257..263].copy_from_slice(b"ustar\0");
14760        header[263..265].copy_from_slice(b"00");
14761        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
14762        write_test_tar_checksum(&mut header[148..156], checksum);
14763        header
14764    }
14765
14766    fn write_test_tar_octal(field: &mut [u8], value: u64) {
14767        let digits = format!("{value:o}");
14768        field.fill(0);
14769        let start = field.len() - 1 - digits.len();
14770        field[..start].fill(b'0');
14771        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
14772    }
14773
14774    fn write_test_tar_checksum(field: &mut [u8], value: u64) {
14775        let digits = format!("{value:06o}");
14776        field[0..6].copy_from_slice(digits.as_bytes());
14777        field[6] = 0;
14778        field[7] = b' ';
14779    }
14780
14781    fn padding_to_512(len: usize) -> usize {
14782        let remainder = len % 512;
14783        if remainder == 0 {
14784            0
14785        } else {
14786            512 - remainder
14787        }
14788    }
14789}