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