Skip to main content

tzap_core/
reader.rs

1use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
2use std::fs::File;
3use std::io::{Read, Write};
4use std::sync::Arc;
5use std::thread;
6
7use sha2::{Digest, Sha256};
8
9use crate::compression::{decompress_exact_zstd_frame, validate_exact_zstd_frame};
10use crate::crypto::{
11    decrypt_padded_aead_object, verify_integrity_tag, AeadObjectContext, HmacDomain, KdfParams,
12    MasterKey, Subkeys,
13};
14use crate::fec::{encode_parity_gf16, repair_data_gf16};
15use crate::format::{
16    AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, VolumeFormatRevision,
17    BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
18    CRITICAL_METADATA_IMAGE_FIXED_LEN_V43, CRITICAL_METADATA_RECOVERY_HEADER_LEN,
19    CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN, CRITICAL_RECOVERY_LOCATOR_LEN,
20    CRYPTO_HEADER_HMAC_LEN, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN, MANIFEST_FOOTER_LEN, MASTER_KEY_LEN,
21    READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN, READER_MAX_KEY_WRAP_TABLE_LEN,
22    READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN, VOLUME_FORMAT_REV_44,
23    VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
24};
25use crate::metadata::{
26    hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
27    EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
28};
29use crate::non_seekable_reader::{
30    StreamedEnvelopeSummary, StreamedFrameSummary, StreamedPayloadSummary,
31};
32use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
33use crate::root_auth::{
34    archive_root_for_revision, critical_metadata_digest, data_block_merkle_root_for_revision,
35    fec_layout_digest_for_revision, index_digest_for_revision,
36    root_auth_descriptor_digest_for_revision, signer_identity_digest, ArchiveRootInputs,
37    CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
38};
39use crate::tar_model::{
40    parse_tar_member_group, restore_streaming_tar_member_group,
41    stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
42    MetadataDiagnostic, NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
43    TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
44    TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
45};
46use crate::wire::{
47    compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
48    CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
49    CryptoHeader, CryptoHeaderFixed, ExtensionTlv, KeyWrapTableV1, ManifestFooter,
50    RootAuthFooterV1, VolumeHeader, VolumeTrailer,
51};
52
53const TRAILER_HMAC_COVERED_LEN: usize = 96;
54const MANIFEST_HMAC_COVERED_LEN: usize = 104;
55const SIDECAR_HMAC_COVERED_LEN: usize = 92;
56const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
57const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
58const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
59const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
60
61fn default_jobs() -> usize {
62    std::thread::available_parallelism()
63        .map(|jobs| jobs.get())
64        .unwrap_or(1)
65}
66
67pub trait ArchiveReadAt: Send + Sync + 'static {
68    fn len(&self) -> Result<u64, FormatError>;
69    fn is_empty(&self) -> Result<bool, FormatError> {
70        Ok(self.len()? == 0)
71    }
72    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
73}
74
75pub type RecipientWrapCandidateMasterKey = [u8; MASTER_KEY_LEN];
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct RecipientWrapArchiveIdentity {
79    pub archive_uuid: [u8; 16],
80    pub session_id: [u8; 16],
81    pub format_version: u16,
82    pub volume_format_rev: u16,
83}
84
85#[derive(Debug, Clone, Copy)]
86pub struct RecipientWrapRecordContext<'a> {
87    pub archive_identity: RecipientWrapArchiveIdentity,
88    pub record: &'a crate::wire::RecipientRecordV1,
89}
90
91impl ArchiveReadAt for File {
92    fn len(&self) -> Result<u64, FormatError> {
93        self.metadata()
94            .map(|metadata| metadata.len())
95            .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
96    }
97
98    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
99        file_read_exact_at(self, offset, buf)
100    }
101}
102
103#[cfg(unix)]
104fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
105    use std::os::unix::fs::FileExt;
106
107    file_read_exact_at_with(offset, buf, |chunk, offset| file.read_at(chunk, offset))
108}
109
110#[cfg(windows)]
111fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
112    use std::os::windows::fs::FileExt;
113
114    file_read_exact_at_with(offset, buf, |chunk, offset| file.seek_read(chunk, offset))
115}
116
117#[cfg(any(unix, windows))]
118fn file_read_exact_at_with<F>(
119    mut offset: u64,
120    mut buf: &mut [u8],
121    mut read_at: F,
122) -> Result<(), FormatError>
123where
124    F: FnMut(&mut [u8], u64) -> std::io::Result<usize>,
125{
126    while !buf.is_empty() {
127        let read =
128            read_at(buf, offset).map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
129        if read == 0 {
130            return Err(FormatError::InvalidArchive("archive read failed"));
131        }
132        offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
133        let rest = std::mem::take(&mut buf).split_at_mut(read).1;
134        buf = rest;
135    }
136    Ok(())
137}
138
139#[cfg(not(any(unix, windows)))]
140fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
141    let mut file = file
142        .try_clone()
143        .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
144    std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
145        .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
146    file.read_exact(buf)
147        .map_err(|_| FormatError::InvalidArchive("archive read failed"))
148}
149
150impl ArchiveReadAt for Vec<u8> {
151    fn len(&self) -> Result<u64, FormatError> {
152        u64::try_from(self.len())
153            .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
154    }
155
156    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
157        let offset = to_usize(offset, "archive")?;
158        let end = checked_add(offset, buf.len(), "archive")?;
159        let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
160            structure: "archive",
161            expected: end,
162            actual: self.len(),
163        })?;
164        buf.copy_from_slice(source);
165        Ok(())
166    }
167}
168
169impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
170    fn len(&self) -> Result<u64, FormatError> {
171        (**self).len()
172    }
173
174    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
175        (**self).read_exact_at(offset, buf)
176    }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct ReaderOptions {
181    pub max_trailing_garbage_scan: usize,
182    pub max_verify_tar_size: usize,
183    pub max_total_extraction_size: u64,
184    pub jobs: usize,
185}
186
187impl Default for ReaderOptions {
188    fn default() -> Self {
189        Self {
190            max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
191            max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
192            max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
193            jobs: default_jobs(),
194        }
195    }
196}
197
198pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
199    if options.jobs == 0 {
200        return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
201    }
202    Ok(())
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct ArchiveEntry {
207    pub path: String,
208    pub file_data_size: u64,
209    pub kind: TarEntryKind,
210    pub mode: u32,
211    pub mtime: u64,
212    pub diagnostics: Vec<MetadataDiagnostic>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct ArchiveIndexEntry {
217    pub path: String,
218    pub file_data_size: u64,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct ExtractedArchiveMember {
223    pub path: String,
224    pub kind: TarEntryKind,
225    pub data: Vec<u8>,
226    pub link_target: Option<String>,
227    pub diagnostics: Vec<MetadataDiagnostic>,
228}
229
230/// Receives logical regular-file bytes while the archive reader extracts data.
231///
232/// Callbacks report uncompressed member payload bytes after they are accepted by
233/// the destination writer. Each selected file is capped by its authenticated
234/// `file_data_size`.
235pub trait ArchiveExtractProgressSink {
236    /// Reports newly extracted payload bytes for one archive member.
237    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
238}
239
240impl<F> ArchiveExtractProgressSink for F
241where
242    F: FnMut(&str, u64),
243{
244    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
245        self(archive_path, bytes);
246    }
247}
248
249#[derive(Debug, Clone)]
250pub struct OpenedArchive {
251    options: ReaderOptions,
252    observed_archive_bytes: u64,
253    observed_volume_count: u32,
254    subkeys: Subkeys,
255    blocks: BTreeMap<u64, BlockRecord>,
256    lazy_blocks: Option<Arc<SeekableBlockSource>>,
257    crypto_header_bytes: Vec<u8>,
258    pub volume_header: VolumeHeader,
259    pub crypto_header: CryptoHeaderFixed,
260    pub manifest_footer: ManifestFooter,
261    pub volume_trailer: Option<VolumeTrailer>,
262    pub root_auth_footer: Option<RootAuthFooterV1>,
263    pub index_root: IndexRoot,
264    payload_dictionary: Option<Vec<u8>>,
265}
266
267#[derive(Debug)]
268pub struct ArchiveContentVerification<'a> {
269    archive: &'a OpenedArchive,
270    mode: ContentVerificationMode,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274enum ContentVerificationMode {
275    Full,
276    Fast,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct ArchiveRepairPatch {
281    pub volume_index: u32,
282    pub block_index: u64,
283    pub record_offset: u64,
284    pub record_bytes: Vec<u8>,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum RootAuthDiagnostic {
289    RootAuthContentVerified,
290    RootAuthDeferredFullArchiveScanRequired,
291    AuthenticatedMetadataNotRootSigned,
292    RecoveryMarginNotRootAuthenticated,
293    ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
294    RecoveryMarginChecked,
295    RecoveryMarginFailed,
296    RecoveryMarginUnchecked,
297}
298
299impl RootAuthDiagnostic {
300    pub const fn label(self) -> &'static str {
301        match self {
302            Self::RootAuthContentVerified => "root_auth_content_verified",
303            Self::RootAuthDeferredFullArchiveScanRequired => {
304                "root_auth_deferred_full_archive_scan_required"
305            }
306            Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
307            Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
308            Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
309                "replicated_global_copy_unchecked_due_to_volume_loss"
310            }
311            Self::RecoveryMarginChecked => "recovery_margin_checked",
312            Self::RecoveryMarginFailed => "recovery_margin_failed",
313            Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
314        }
315    }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum PublicNoKeyDiagnostic {
320    PublicDataBlockCommitmentVerified,
321    PublicPhysicalCompletenessUnverified,
322    PublicRecoveryMarginUnchecked,
323}
324
325impl PublicNoKeyDiagnostic {
326    pub const fn label(self) -> &'static str {
327        match self {
328            Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
329            Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
330            Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
331        }
332    }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct RootAuthVerification {
337    pub format_version: u16,
338    pub volume_format_rev: u16,
339    pub archive_root: [u8; 32],
340    pub authenticator_id: u16,
341    pub signer_identity_type: u16,
342    pub signer_identity_bytes: Vec<u8>,
343    pub total_data_block_count: u64,
344    pub diagnostics: Vec<RootAuthDiagnostic>,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub struct PublicNoKeyVerification {
349    pub format_version: u16,
350    pub volume_format_rev: u16,
351    pub archive_root: [u8; 32],
352    pub authenticator_id: u16,
353    pub signer_identity_type: u16,
354    pub signer_identity_bytes: Vec<u8>,
355    pub total_data_block_count: u64,
356    pub diagnostics: Vec<PublicNoKeyDiagnostic>,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360struct RootAuthMaterial {
361    critical_metadata_digest: [u8; 32],
362    index_digest: [u8; 32],
363    fec_layout_digest: [u8; 32],
364    data_block_merkle_root: [u8; 32],
365    signer_identity_digest: [u8; 32],
366    archive_root: [u8; 32],
367    total_data_block_count: u64,
368}
369
370#[derive(Debug, Clone, Copy)]
371struct ObjectExtent {
372    first_block_index: u64,
373    data_block_count: u32,
374    parity_block_count: u32,
375    encrypted_size: u32,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379enum ParityReadPolicy {
380    Always,
381    RepairOnly,
382}
383
384pub(crate) struct StreamedArchiveOpenParts {
385    pub(crate) options: ReaderOptions,
386    pub(crate) observed_archive_bytes: u64,
387    pub(crate) subkeys: Subkeys,
388    pub(crate) blocks: BTreeMap<u64, BlockRecord>,
389    pub(crate) crypto_header_bytes: Vec<u8>,
390    pub(crate) volume_header: VolumeHeader,
391    pub(crate) crypto_header: CryptoHeaderFixed,
392    pub(crate) manifest_footer: ManifestFooter,
393    pub(crate) volume_trailer: VolumeTrailer,
394    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
395}
396
397#[derive(Clone, Copy)]
398struct WinningIndexEntry {
399    start: u64,
400    file_data_size: u64,
401    shard_index: usize,
402    file_index: usize,
403}
404
405struct LocatedIndexFile {
406    shard: IndexShard,
407    file_index: usize,
408    start: u64,
409}
410
411struct ExtractProgressWriter<'a, W> {
412    inner: &'a mut W,
413    archive_path: &'a str,
414    file_data_size: u64,
415    reported_bytes: u64,
416    progress: &'a mut dyn ArchiveExtractProgressSink,
417}
418
419impl<'a, W> ExtractProgressWriter<'a, W> {
420    fn new(
421        inner: &'a mut W,
422        archive_path: &'a str,
423        file_data_size: u64,
424        progress: &'a mut dyn ArchiveExtractProgressSink,
425    ) -> Self {
426        Self {
427            inner,
428            archive_path,
429            file_data_size,
430            reported_bytes: 0,
431            progress,
432        }
433    }
434
435    fn report(&mut self, bytes: u64) {
436        if bytes == 0 || self.file_data_size == 0 {
437            return;
438        }
439        let capped_next = self
440            .reported_bytes
441            .saturating_add(bytes)
442            .min(self.file_data_size);
443        let delta = capped_next.saturating_sub(self.reported_bytes);
444        if delta == 0 {
445            return;
446        }
447        self.reported_bytes = capped_next;
448        self.progress.file_bytes_extracted(self.archive_path, delta);
449    }
450}
451
452impl<W: Write> Write for ExtractProgressWriter<'_, W> {
453    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
454        let written = self.inner.write(buf)?;
455        self.report(written as u64);
456        Ok(written)
457    }
458
459    fn flush(&mut self) -> std::io::Result<()> {
460        self.inner.flush()
461    }
462}
463
464struct DecodedTarMemberGroupReader<'a> {
465    archive: &'a OpenedArchive,
466    shard: &'a IndexShard,
467    file: &'a FileEntry,
468    decompressor: zstd::bulk::Decompressor<'static>,
469    next_frame_offset: u64,
470    cached_envelope_index: Option<u64>,
471    cached_envelope_plaintext: Vec<u8>,
472    current_frame: Vec<u8>,
473    current_frame_offset: usize,
474    remaining_group_bytes: u64,
475}
476
477struct SeekableVolumeSource {
478    reader: Arc<dyn ArchiveReadAt>,
479    volume_index: u32,
480    block_records_start: u64,
481    block_count: u64,
482    record_len: u64,
483    block_size: usize,
484}
485
486impl std::fmt::Debug for SeekableVolumeSource {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        f.debug_struct("SeekableVolumeSource")
489            .field("volume_index", &self.volume_index)
490            .field("block_records_start", &self.block_records_start)
491            .field("block_count", &self.block_count)
492            .field("record_len", &self.record_len)
493            .field("block_size", &self.block_size)
494            .finish_non_exhaustive()
495    }
496}
497
498#[derive(Debug)]
499struct SeekableBlockSource {
500    stripe_width: u32,
501    volumes: Vec<Option<SeekableVolumeSource>>,
502}
503
504trait BlockProvider {
505    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
506}
507
508struct OpenedBlockProvider<'a> {
509    memory_blocks: &'a BTreeMap<u64, BlockRecord>,
510    lazy_blocks: Option<&'a SeekableBlockSource>,
511}
512
513impl SeekableBlockSource {
514    fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
515        if self.stripe_width == 0 {
516            return Err(FormatError::ZeroStripeWidth);
517        }
518        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
519            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
520        let Some(volume) = self
521            .volumes
522            .get(volume_index as usize)
523            .and_then(Option::as_ref)
524        else {
525            return Err(FormatError::InvalidArchive(
526                "repair output requires all archive volumes",
527            ));
528        };
529        let slot = block_index / self.stripe_width as u64;
530        if slot >= volume.block_count {
531            return Err(FormatError::InvalidArchive(
532                "BlockRecord global coverage has a gap",
533            ));
534        }
535        Ok((volume_index, volume.record_offset(slot)?))
536    }
537
538    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
539        if self.stripe_width == 0 {
540            return Err(FormatError::ZeroStripeWidth);
541        }
542        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
543            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
544        let Some(volume) = self
545            .volumes
546            .get(volume_index as usize)
547            .and_then(Option::as_ref)
548        else {
549            return Ok(None);
550        };
551        let slot = block_index / self.stripe_width as u64;
552        if slot >= volume.block_count {
553            return Ok(None);
554        }
555        match volume.read_slot(slot, block_index) {
556            Ok(record) => Ok(Some(record)),
557            Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
558            Err(err) => Err(err),
559        }
560    }
561
562    fn is_complete_volume_set(&self) -> bool {
563        self.volumes.iter().all(Option::is_some)
564    }
565
566    fn total_block_count(&self) -> Result<u64, FormatError> {
567        self.volumes
568            .iter()
569            .map(|volume| {
570                volume
571                    .as_ref()
572                    .map(|volume| volume.block_count)
573                    .ok_or(FormatError::InvalidArchive(
574                        "missing volume in complete set",
575                    ))
576            })
577            .try_fold(0u64, |sum, count| {
578                checked_u64_add(sum, count?, "BlockRecord count overflow")
579            })
580    }
581}
582
583impl SeekableVolumeSource {
584    fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
585        self.block_records_start
586            .checked_add(checked_u64_mul(
587                slot,
588                self.record_len,
589                "BlockRecord offset overflow",
590            )?)
591            .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
592    }
593
594    fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
595        let record_offset = self.record_offset(slot)?;
596        let raw = read_at_vec_unchecked(
597            self.reader.as_ref(),
598            record_offset,
599            usize::try_from(self.record_len)
600                .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
601        )?;
602        let record = BlockRecord::parse(&raw, self.block_size)?;
603        if record.block_index != expected_block_index {
604            return Err(FormatError::InvalidArchive(
605                "BlockRecord index does not match volume position",
606            ));
607        }
608        Ok(record)
609    }
610}
611
612impl BlockProvider for BTreeMap<u64, BlockRecord> {
613    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
614        Ok(self.get(&block_index).cloned())
615    }
616}
617
618impl BlockProvider for OpenedBlockProvider<'_> {
619    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
620        if let Some(record) = self.memory_blocks.get(&block_index) {
621            return Ok(Some(record.clone()));
622        }
623        match self.lazy_blocks {
624            Some(source) => source.block(block_index),
625            None => Ok(None),
626        }
627    }
628}
629
630fn subkeys_for_open(
631    master_key: Option<&MasterKey>,
632    aead_algo: AeadAlgo,
633    archive_uuid: &[u8; 16],
634    session_id: &[u8; 16],
635) -> Result<Subkeys, FormatError> {
636    if aead_algo.is_encrypted() {
637        Subkeys::derive(
638            master_key.ok_or(FormatError::KeyMaterialMismatch)?,
639            archive_uuid,
640            session_id,
641        )
642    } else {
643        Ok(Subkeys::unencrypted_placeholder())
644    }
645}
646
647type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
648pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
649const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
650    "fast full extract requires unique archive paths";
651
652fn parse_volume_format_dispatch(
653    volume_header: &VolumeHeader,
654) -> Result<VolumeFormatRevision, FormatError> {
655    let revision = volume_header.parse_volume_format_revision()?;
656    match revision {
657        VolumeFormatRevision::V43 | VolumeFormatRevision::V44 => Ok(revision),
658    }
659}
660
661#[derive(Debug)]
662struct PayloadIndexTables {
663    shards: Vec<IndexShard>,
664    file_count: u64,
665    frames: BTreeMap<u64, FrameEntry>,
666    envelopes: BTreeMap<u64, EnvelopeEntry>,
667}
668
669pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
670    OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
671}
672
673pub fn open_archive_with_recipient_wrap_resolver<F>(
674    bytes: &[u8],
675    resolver: F,
676) -> Result<OpenedArchive, FormatError>
677where
678    F: FnMut(
679        RecipientWrapRecordContext<'_>,
680    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
681{
682    OpenedArchive::open_with_recipient_wrap_resolver_options(
683        bytes,
684        resolver,
685        ReaderOptions::default(),
686    )
687}
688
689pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
690    require_unencrypted_volume_profile(bytes)?;
691    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
692    OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
693}
694
695pub fn open_archive_volumes(
696    volumes: &[&[u8]],
697    master_key: &MasterKey,
698) -> Result<OpenedArchive, FormatError> {
699    OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
700}
701
702pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
703    for volume in volumes {
704        require_unencrypted_volume_profile(volume)?;
705    }
706    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
707    OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
708}
709
710pub fn open_archive_with_bootstrap_sidecar(
711    bytes: &[u8],
712    bootstrap_sidecar: &[u8],
713    master_key: &MasterKey,
714) -> Result<OpenedArchive, FormatError> {
715    OpenedArchive::open_with_bootstrap_sidecar_options(
716        bytes,
717        bootstrap_sidecar,
718        master_key,
719        ReaderOptions::default(),
720    )
721}
722
723fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
724    if bytes.len() < VOLUME_HEADER_LEN {
725        return Err(FormatError::InvalidLength {
726            structure: "archive",
727            expected: VOLUME_HEADER_LEN,
728            actual: bytes.len(),
729        });
730    }
731    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
732    parse_volume_format_dispatch(&volume_header)?;
733    let crypto_start = volume_header.crypto_header_offset as usize;
734    let crypto_len = volume_header.crypto_header_length as usize;
735    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
736    let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
737    if crypto_header.fixed.aead_algo == AeadAlgo::None
738        && crypto_header.fixed.kdf_algo == KdfAlgo::None
739    {
740        Ok(())
741    } else {
742        Err(FormatError::KeyMaterialMismatch)
743    }
744}
745
746pub fn open_seekable_archive<R: ArchiveReadAt>(
747    reader: R,
748    master_key: &MasterKey,
749) -> Result<OpenedArchive, FormatError> {
750    OpenedArchive::open_seekable_volumes_with_options(
751        vec![reader],
752        master_key,
753        ReaderOptions::default(),
754    )
755}
756
757pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
758    readers: Vec<R>,
759    master_key: &MasterKey,
760) -> Result<OpenedArchive, FormatError> {
761    OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
762}
763
764pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
765    reader: R,
766    bootstrap_sidecar: &[u8],
767    master_key: &MasterKey,
768) -> Result<OpenedArchive, FormatError> {
769    open_seekable_archive_with_bootstrap_sidecar_options(
770        reader,
771        bootstrap_sidecar,
772        master_key,
773        ReaderOptions::default(),
774    )
775}
776
777pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
778    reader: R,
779    bootstrap_sidecar: &[u8],
780    master_key: &MasterKey,
781    options: ReaderOptions,
782) -> Result<OpenedArchive, FormatError> {
783    OpenedArchive::open_seekable_volumes_with_options_for_mode(
784        vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
785        master_key,
786        options,
787        Some(bootstrap_sidecar),
788    )
789}
790
791pub fn open_seekable_archive_with_recipient_wrap_resolver_options<R, F>(
792    reader: R,
793    resolver: F,
794    options: ReaderOptions,
795) -> Result<OpenedArchive, FormatError>
796where
797    R: ArchiveReadAt,
798    F: FnMut(
799        RecipientWrapRecordContext<'_>,
800    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
801{
802    OpenedArchive::open_seekable_with_recipient_wrap_resolver_options(reader, resolver, options)
803}
804
805pub fn open_seekable_archive_volumes_with_recipient_wrap_resolver_options<R, F>(
806    readers: Vec<R>,
807    resolver: F,
808    options: ReaderOptions,
809) -> Result<OpenedArchive, FormatError>
810where
811    R: ArchiveReadAt,
812    F: FnMut(
813        RecipientWrapRecordContext<'_>,
814    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
815{
816    OpenedArchive::open_seekable_volumes_with_recipient_wrap_resolver_options(
817        readers, resolver, options,
818    )
819}
820
821pub fn open_non_seekable_archive(
822    bytes: &[u8],
823    master_key: &MasterKey,
824    bootstrap_sidecar: Option<&[u8]>,
825) -> Result<OpenedArchive, FormatError> {
826    match bootstrap_sidecar {
827        Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
828            bytes,
829            sidecar,
830            master_key,
831            ReaderOptions::default(),
832            BootstrapSidecarUse::NonSeekableRandomAccess,
833        ),
834        None => Err(FormatError::ReaderUnsupported(
835            "non-seekable random access requires a bootstrap sidecar",
836        )),
837    }
838}
839
840pub fn public_no_key_verify_archive_with<F>(
841    bytes: &[u8],
842    verifier: F,
843) -> Result<PublicNoKeyVerification, FormatError>
844where
845    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
846{
847    public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
848}
849
850pub fn public_no_key_verify_volumes_with<F>(
851    volumes: &[&[u8]],
852    verifier: F,
853) -> Result<PublicNoKeyVerification, FormatError>
854where
855    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
856{
857    public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
858}
859
860/// Decode a single-volume, dictionary-free non-seekable archive image into tar
861/// bytes after authenticating its terminal ManifestFooter and VolumeTrailer.
862///
863/// This is a whole-buffer helper, not a live provisional-output API.
864/// Callers receive no decoded bytes if terminal authentication fails.
865pub fn sequential_extract_tar_stream(
866    bytes: &[u8],
867    master_key: &MasterKey,
868) -> Result<Vec<u8>, FormatError> {
869    sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
870}
871
872impl OpenedArchive {
873    fn block_provider(&self) -> OpenedBlockProvider<'_> {
874        OpenedBlockProvider {
875            memory_blocks: &self.blocks,
876            lazy_blocks: self.lazy_blocks.as_deref(),
877        }
878    }
879
880    fn missing_volume_count(&self) -> u32 {
881        self.crypto_header
882            .stripe_width
883            .saturating_sub(self.observed_volume_count)
884    }
885
886    fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
887        let mut diagnostics = vec![
888            RootAuthDiagnostic::RootAuthContentVerified,
889            RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
890            RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
891        ];
892        if self.missing_volume_count() > 0 {
893            diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
894        }
895        diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
896        diagnostics
897    }
898
899    pub fn open_with_options(
900        bytes: &[u8],
901        master_key: &MasterKey,
902        options: ReaderOptions,
903    ) -> Result<Self, FormatError> {
904        Self::open_volumes_with_options(&[bytes], master_key, options)
905    }
906
907    pub fn open_with_recipient_wrap_resolver_options<F>(
908        bytes: &[u8],
909        mut resolver: F,
910        options: ReaderOptions,
911    ) -> Result<Self, FormatError>
912    where
913        F: FnMut(
914            RecipientWrapRecordContext<'_>,
915        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
916    {
917        validate_reader_options(options)?;
918        let observed_archive_bytes = observed_archive_size(std::iter::once(bytes.len() as u64))?;
919        let parsed =
920            parse_seekable_volume_with_recipient_wrap_resolver(bytes, &mut resolver, options)?;
921        let ParsedSeekableVolume {
922            volume_header,
923            crypto_header,
924            crypto_header_bytes,
925            key_wrap_table_bytes: _,
926            subkeys,
927            manifest_footer,
928            manifest_footer_error,
929            root_auth_footer,
930            root_auth_footer_bytes: _,
931            volume_trailer,
932            blocks,
933            erased_block_indices,
934        } = parsed;
935        let manifest_footer = match manifest_footer {
936            Some(footer) => footer,
937            None => {
938                return Err(manifest_footer_error.unwrap_or(FormatError::InvalidArchive(
939                    "no authenticated ManifestFooter found",
940                )));
941            }
942        };
943        let observed_volume_count = 1;
944        let missing_volume_count = crypto_header
945            .stripe_width
946            .checked_sub(observed_volume_count)
947            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
948        if missing_volume_count > crypto_header.volume_loss_tolerance as u32 {
949            return Err(FormatError::InvalidArchive(
950                "missing volume count exceeds volume_loss_tolerance",
951            ));
952        }
953        if missing_volume_count == 0 {
954            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
955        }
956
957        let limits = metadata_limits(&crypto_header);
958        let index_root_plaintext = load_metadata_object_from_parts(
959            &blocks,
960            ObjectLoadContext::index_root(
961                &volume_header,
962                &crypto_header,
963                &subkeys,
964                ObjectExtent {
965                    first_block_index: manifest_footer.index_root_first_block,
966                    data_block_count: manifest_footer.index_root_data_block_count,
967                    parity_block_count: manifest_footer.index_root_parity_block_count,
968                    encrypted_size: manifest_footer.index_root_encrypted_size,
969                },
970            ),
971            manifest_footer.index_root_decompressed_size,
972        )?;
973        let index_root = IndexRoot::parse(
974            &index_root_plaintext,
975            crypto_header.has_dictionary != 0,
976            limits,
977        )?;
978        let payload_dictionary = load_archive_dictionary(
979            &blocks,
980            &subkeys,
981            &volume_header,
982            &crypto_header,
983            &index_root,
984        )?;
985
986        Ok(Self {
987            options,
988            observed_archive_bytes,
989            observed_volume_count,
990            subkeys,
991            blocks,
992            lazy_blocks: None,
993            crypto_header_bytes,
994            volume_header,
995            crypto_header,
996            manifest_footer,
997            volume_trailer: Some(volume_trailer),
998            root_auth_footer,
999            index_root,
1000            payload_dictionary,
1001        })
1002    }
1003
1004    pub fn open_seekable_with_recipient_wrap_resolver_options<R, F>(
1005        reader: R,
1006        mut resolver: F,
1007        options: ReaderOptions,
1008    ) -> Result<Self, FormatError>
1009    where
1010        R: ArchiveReadAt,
1011        F: FnMut(
1012            RecipientWrapRecordContext<'_>,
1013        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1014    {
1015        validate_reader_options(options)?;
1016        let reader = Arc::new(reader) as Arc<dyn ArchiveReadAt>;
1017        let observed_len = reader.len()?;
1018        let observed_archive_bytes = observed_archive_size([observed_len])?;
1019        let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1020            reader.clone(),
1021            &mut resolver,
1022            options,
1023        )?;
1024        let manifest_footer = match parsed.manifest_footer.take() {
1025            Some(footer) => footer,
1026            None => {
1027                return Err(parsed.manifest_footer_error.take().unwrap_or(
1028                    FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1029                ));
1030            }
1031        };
1032        let observed_volume_count = 1;
1033        let missing_volume_count = parsed
1034            .crypto_header
1035            .stripe_width
1036            .checked_sub(observed_volume_count)
1037            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1038        if missing_volume_count > parsed.crypto_header.volume_loss_tolerance as u32 {
1039            return Err(FormatError::InvalidArchive(
1040                "missing volume count exceeds volume_loss_tolerance",
1041            ));
1042        }
1043
1044        let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1045        let mut lazy_volume_slots = Vec::new();
1046        lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1047        let slot = parsed.volume_header.volume_index as usize;
1048        if slot >= lazy_volume_slots.len() {
1049            return Err(FormatError::InvalidArchive(
1050                "authenticated volume index exceeds stripe_width",
1051            ));
1052        }
1053        lazy_volume_slots[slot] = Some(SeekableVolumeSource {
1054            reader: parsed.reader.clone(),
1055            volume_index: parsed.volume_header.volume_index,
1056            block_records_start: parsed.block_records_start,
1057            block_count: parsed.volume_trailer.block_count,
1058            record_len,
1059            block_size: parsed.crypto_header.block_size as usize,
1060        });
1061        let lazy_source = Arc::new(SeekableBlockSource {
1062            stripe_width: parsed.crypto_header.stripe_width,
1063            volumes: lazy_volume_slots,
1064        });
1065        let blocks = BTreeMap::new();
1066        let block_provider = OpenedBlockProvider {
1067            memory_blocks: &blocks,
1068            lazy_blocks: Some(lazy_source.as_ref()),
1069        };
1070        let limits = metadata_limits(&parsed.crypto_header);
1071        let index_root_plaintext = load_metadata_object_from_parts(
1072            &block_provider,
1073            ObjectLoadContext::index_root(
1074                &parsed.volume_header,
1075                &parsed.crypto_header,
1076                &parsed.subkeys,
1077                index_root_extent_from_manifest(&manifest_footer),
1078            ),
1079            manifest_footer.index_root_decompressed_size,
1080        )?;
1081        let index_root = IndexRoot::parse(
1082            &index_root_plaintext,
1083            parsed.crypto_header.has_dictionary != 0,
1084            limits,
1085        )?;
1086        let block_provider = OpenedBlockProvider {
1087            memory_blocks: &blocks,
1088            lazy_blocks: Some(lazy_source.as_ref()),
1089        };
1090        let payload_dictionary = load_archive_dictionary(
1091            &block_provider,
1092            &parsed.subkeys,
1093            &parsed.volume_header,
1094            &parsed.crypto_header,
1095            &index_root,
1096        )?;
1097
1098        Ok(Self {
1099            options,
1100            observed_archive_bytes,
1101            observed_volume_count,
1102            subkeys: parsed.subkeys,
1103            blocks,
1104            lazy_blocks: Some(lazy_source),
1105            crypto_header_bytes: parsed.crypto_header_bytes,
1106            volume_header: parsed.volume_header,
1107            crypto_header: parsed.crypto_header,
1108            manifest_footer,
1109            volume_trailer: Some(parsed.volume_trailer),
1110            root_auth_footer: parsed.root_auth_footer,
1111            index_root,
1112            payload_dictionary,
1113        })
1114    }
1115
1116    pub fn open_seekable_volumes_with_recipient_wrap_resolver_options<R, F>(
1117        readers: Vec<R>,
1118        mut resolver: F,
1119        options: ReaderOptions,
1120    ) -> Result<Self, FormatError>
1121    where
1122        R: ArchiveReadAt,
1123        F: FnMut(
1124            RecipientWrapRecordContext<'_>,
1125        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1126    {
1127        validate_reader_options(options)?;
1128        if readers.is_empty() {
1129            return Err(FormatError::InvalidArchive("no volumes supplied"));
1130        }
1131        let readers = readers
1132            .into_iter()
1133            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1134            .collect::<Vec<_>>();
1135        let observed_archive_bytes = observed_archive_size(
1136            readers
1137                .iter()
1138                .map(|reader| reader.len())
1139                .collect::<Result<Vec<_>, _>>()?,
1140        )?;
1141        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1142        let mut manifest_authority: Option<ManifestFooter> = None;
1143        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1144        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1145        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1146        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1147        let mut saw_root_auth_absent = false;
1148        let mut first_manifest_footer_error: Option<FormatError> = None;
1149        let mut seen_volume_indexes = BTreeSet::new();
1150        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1151
1152        for reader in readers {
1153            let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1154                reader,
1155                &mut resolver,
1156                options,
1157            )?;
1158            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1159                return Err(FormatError::InvalidArchive(
1160                    "duplicate authenticated volume index",
1161                ));
1162            }
1163
1164            if let Some(first) = &first {
1165                validate_volume_set_member_metadata(
1166                    &first.volume_header,
1167                    &first.crypto_header,
1168                    &first.crypto_header_bytes,
1169                    &parsed.volume_header,
1170                    &parsed.crypto_header,
1171                    &parsed.crypto_header_bytes,
1172                )?;
1173                validate_key_wrap_table_bytes_match(
1174                    &first.key_wrap_table_bytes,
1175                    &parsed.key_wrap_table_bytes,
1176                )?;
1177            } else {
1178                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1179            }
1180
1181            if let Some(footer) = &parsed.manifest_footer {
1182                if let Some(authority) = &manifest_authority {
1183                    if !manifest_bootstrap_fields_match(authority, footer) {
1184                        return Err(FormatError::InvalidArchive(
1185                            "ManifestFooter bootstrap fields differ",
1186                        ));
1187                    }
1188                } else {
1189                    manifest_authority = Some(footer.clone());
1190                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1191                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1192                }
1193            } else if first_manifest_footer_error.is_none() {
1194                first_manifest_footer_error = parsed.manifest_footer_error.take();
1195            }
1196
1197            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1198                (Some(footer), Some(bytes)) => {
1199                    if saw_root_auth_absent {
1200                        return Err(FormatError::InvalidArchive(
1201                            "root-auth footer presence differs across volumes",
1202                        ));
1203                    }
1204                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1205                        if authority_bytes != bytes {
1206                            return Err(FormatError::InvalidArchive(
1207                                "RootAuthFooter copies differ",
1208                            ));
1209                        }
1210                    } else {
1211                        root_auth_authority = Some(footer.clone());
1212                        root_auth_authority_bytes = Some(bytes.clone());
1213                    }
1214                }
1215                (None, None) => {
1216                    if root_auth_authority_bytes.is_some() {
1217                        return Err(FormatError::InvalidArchive(
1218                            "root-auth footer presence differs across volumes",
1219                        ));
1220                    }
1221                    saw_root_auth_absent = true;
1222                }
1223                _ => {
1224                    return Err(FormatError::InvalidArchive(
1225                        "root-auth footer terminal state is inconsistent",
1226                    ));
1227                }
1228            }
1229
1230            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1231            let source = SeekableVolumeSource {
1232                reader: parsed.reader.clone(),
1233                volume_index: parsed.volume_header.volume_index,
1234                block_records_start: parsed.block_records_start,
1235                block_count: parsed.volume_trailer.block_count,
1236                record_len,
1237                block_size: parsed.crypto_header.block_size as usize,
1238            };
1239            let slot = parsed.volume_header.volume_index as usize;
1240            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1241            {
1242                return Err(FormatError::InvalidArchive(
1243                    "duplicate authenticated volume index",
1244                ));
1245            }
1246
1247            if first.is_none() {
1248                first = Some(parsed);
1249            }
1250        }
1251
1252        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1253        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1254            Some(err) => err,
1255            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1256        })?;
1257        let authority_volume_header = manifest_authority_volume_header.ok_or(
1258            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1259        )?;
1260        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1261            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1262        )?;
1263        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1264            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1265        let missing_volume_count = first
1266            .crypto_header
1267            .stripe_width
1268            .checked_sub(observed_volume_count)
1269            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1270        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1271            return Err(FormatError::InvalidArchive(
1272                "missing volume count exceeds volume_loss_tolerance",
1273            ));
1274        }
1275
1276        let blocks = BTreeMap::new();
1277        let lazy_source = Arc::new(SeekableBlockSource {
1278            stripe_width: first.crypto_header.stripe_width,
1279            volumes: lazy_volume_slots,
1280        });
1281        let block_provider = OpenedBlockProvider {
1282            memory_blocks: &blocks,
1283            lazy_blocks: Some(lazy_source.as_ref()),
1284        };
1285        let limits = metadata_limits(&first.crypto_header);
1286        let index_root_plaintext = load_metadata_object_from_parts(
1287            &block_provider,
1288            ObjectLoadContext::index_root(
1289                &first.volume_header,
1290                &first.crypto_header,
1291                &first.subkeys,
1292                index_root_extent_from_manifest(&manifest_footer),
1293            ),
1294            manifest_footer.index_root_decompressed_size,
1295        )?;
1296        let index_root = IndexRoot::parse(
1297            &index_root_plaintext,
1298            first.crypto_header.has_dictionary != 0,
1299            limits,
1300        )?;
1301        let block_provider = OpenedBlockProvider {
1302            memory_blocks: &blocks,
1303            lazy_blocks: Some(lazy_source.as_ref()),
1304        };
1305        let payload_dictionary = load_archive_dictionary(
1306            &block_provider,
1307            &first.subkeys,
1308            &first.volume_header,
1309            &first.crypto_header,
1310            &index_root,
1311        )?;
1312
1313        Ok(Self {
1314            options,
1315            observed_archive_bytes,
1316            observed_volume_count,
1317            subkeys: first.subkeys,
1318            blocks,
1319            lazy_blocks: Some(lazy_source),
1320            crypto_header_bytes: first.crypto_header_bytes,
1321            volume_header: authority_volume_header,
1322            crypto_header: first.crypto_header,
1323            manifest_footer,
1324            volume_trailer: Some(authority_volume_trailer),
1325            root_auth_footer: root_auth_authority,
1326            index_root,
1327            payload_dictionary,
1328        })
1329    }
1330
1331    pub fn open_volumes_with_options(
1332        volumes: &[&[u8]],
1333        master_key: &MasterKey,
1334        options: ReaderOptions,
1335    ) -> Result<Self, FormatError> {
1336        validate_reader_options(options)?;
1337        if volumes.is_empty() {
1338            return Err(FormatError::InvalidArchive("no volumes supplied"));
1339        }
1340
1341        let observed_archive_bytes =
1342            observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
1343        let mut first: Option<ParsedSeekableVolume> = None;
1344        let mut manifest_authority: Option<ManifestFooter> = None;
1345        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1346        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1347        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1348        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1349        let mut saw_root_auth_absent = false;
1350        let mut first_manifest_footer_error: Option<FormatError> = None;
1351        let mut seen_volume_indexes = BTreeSet::new();
1352        let mut blocks = BTreeMap::new();
1353        let mut erased_block_indices = BTreeSet::new();
1354
1355        for volume_bytes in volumes {
1356            let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
1357            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1358                return Err(FormatError::InvalidArchive(
1359                    "duplicate authenticated volume index",
1360                ));
1361            }
1362
1363            if let Some(first) = &first {
1364                validate_volume_set_member(first, &parsed)?;
1365            }
1366
1367            if let Some(footer) = &parsed.manifest_footer {
1368                if let Some(authority) = &manifest_authority {
1369                    if !manifest_bootstrap_fields_match(authority, footer) {
1370                        return Err(FormatError::InvalidArchive(
1371                            "ManifestFooter bootstrap fields differ",
1372                        ));
1373                    }
1374                } else {
1375                    manifest_authority = Some(footer.clone());
1376                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1377                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1378                }
1379            } else if first_manifest_footer_error.is_none() {
1380                first_manifest_footer_error = parsed.manifest_footer_error.take();
1381            }
1382
1383            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1384                (Some(footer), Some(bytes)) => {
1385                    if saw_root_auth_absent {
1386                        return Err(FormatError::InvalidArchive(
1387                            "root-auth footer presence differs across volumes",
1388                        ));
1389                    }
1390                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1391                        if authority_bytes != bytes {
1392                            return Err(FormatError::InvalidArchive(
1393                                "RootAuthFooter copies differ",
1394                            ));
1395                        }
1396                    } else {
1397                        root_auth_authority = Some(footer.clone());
1398                        root_auth_authority_bytes = Some(bytes.clone());
1399                    }
1400                }
1401                (None, None) => {
1402                    if root_auth_authority_bytes.is_some() {
1403                        return Err(FormatError::InvalidArchive(
1404                            "root-auth footer presence differs across volumes",
1405                        ));
1406                    }
1407                    saw_root_auth_absent = true;
1408                }
1409                _ => {
1410                    return Err(FormatError::InvalidArchive(
1411                        "root-auth footer terminal state is inconsistent",
1412                    ));
1413                }
1414            }
1415
1416            for (block_index, record) in &parsed.blocks {
1417                if blocks.insert(*block_index, record.clone()).is_some() {
1418                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1419                }
1420            }
1421            for block_index in &parsed.erased_block_indices {
1422                erased_block_indices.insert(*block_index);
1423            }
1424
1425            if first.is_none() {
1426                first = Some(parsed);
1427            }
1428        }
1429
1430        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1431        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1432            Some(err) => err,
1433            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1434        })?;
1435        let authority_volume_header = manifest_authority_volume_header.ok_or(
1436            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1437        )?;
1438        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1439            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1440        )?;
1441        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1442            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1443        let missing_volume_count = first
1444            .crypto_header
1445            .stripe_width
1446            .checked_sub(observed_volume_count)
1447            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1448        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1449            return Err(FormatError::InvalidArchive(
1450                "missing volume count exceeds volume_loss_tolerance",
1451            ));
1452        }
1453        if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
1454            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
1455        }
1456
1457        let limits = metadata_limits(&first.crypto_header);
1458        let index_root_plaintext = load_metadata_object_from_parts(
1459            &blocks,
1460            ObjectLoadContext::index_root(
1461                &first.volume_header,
1462                &first.crypto_header,
1463                &first.subkeys,
1464                ObjectExtent {
1465                    first_block_index: manifest_footer.index_root_first_block,
1466                    data_block_count: manifest_footer.index_root_data_block_count,
1467                    parity_block_count: manifest_footer.index_root_parity_block_count,
1468                    encrypted_size: manifest_footer.index_root_encrypted_size,
1469                },
1470            ),
1471            manifest_footer.index_root_decompressed_size,
1472        )?;
1473        let index_root = IndexRoot::parse(
1474            &index_root_plaintext,
1475            first.crypto_header.has_dictionary != 0,
1476            limits,
1477        )?;
1478        let payload_dictionary = load_archive_dictionary(
1479            &blocks,
1480            &first.subkeys,
1481            &first.volume_header,
1482            &first.crypto_header,
1483            &index_root,
1484        )?;
1485
1486        Ok(Self {
1487            options,
1488            observed_archive_bytes,
1489            observed_volume_count,
1490            subkeys: first.subkeys,
1491            blocks,
1492            lazy_blocks: None,
1493            crypto_header_bytes: first.crypto_header_bytes,
1494            volume_header: authority_volume_header,
1495            crypto_header: first.crypto_header,
1496            manifest_footer,
1497            volume_trailer: Some(authority_volume_trailer),
1498            root_auth_footer: root_auth_authority,
1499            index_root,
1500            payload_dictionary,
1501        })
1502    }
1503
1504    pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
1505        readers: Vec<R>,
1506        master_key: &MasterKey,
1507        options: ReaderOptions,
1508    ) -> Result<Self, FormatError> {
1509        let readers = readers
1510            .into_iter()
1511            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1512            .collect::<Vec<_>>();
1513        Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1514    }
1515
1516    fn open_seekable_volumes_with_options_for_mode(
1517        readers: Vec<Arc<dyn ArchiveReadAt>>,
1518        master_key: &MasterKey,
1519        options: ReaderOptions,
1520        bootstrap_sidecar: Option<&[u8]>,
1521    ) -> Result<Self, FormatError> {
1522        validate_reader_options(options)?;
1523        if readers.is_empty() {
1524            return Err(FormatError::InvalidArchive("no volumes supplied"));
1525        }
1526        if bootstrap_sidecar.is_some() && readers.len() > 1 {
1527            return Err(FormatError::ReaderUnsupported(
1528                "multi-volume inputs with bootstrap sidecar are not supported",
1529            ));
1530        }
1531
1532        let observed_archive_bytes = observed_archive_size(
1533            readers
1534                .iter()
1535                .map(|reader| reader.len())
1536                .collect::<Result<Vec<_>, _>>()?
1537                .into_iter()
1538                .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1539        )?;
1540        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1541        let mut manifest_authority: Option<ManifestFooter> = None;
1542        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1543        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1544        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1545        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1546        let mut saw_root_auth_absent = false;
1547        let mut first_manifest_footer_error: Option<FormatError> = None;
1548        let mut seen_volume_indexes = BTreeSet::new();
1549        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1550
1551        for reader in readers {
1552            let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1553            if bootstrap_sidecar.is_some() {
1554                validate_bootstrap_single_volume_input(
1555                    &parsed.volume_header,
1556                    &parsed.crypto_header,
1557                )?;
1558            }
1559            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1560                return Err(FormatError::InvalidArchive(
1561                    "duplicate authenticated volume index",
1562                ));
1563            }
1564
1565            if let Some(first) = &first {
1566                validate_volume_set_member_metadata(
1567                    &first.volume_header,
1568                    &first.crypto_header,
1569                    &first.crypto_header_bytes,
1570                    &parsed.volume_header,
1571                    &parsed.crypto_header,
1572                    &parsed.crypto_header_bytes,
1573                )?;
1574                validate_key_wrap_table_bytes_match(
1575                    &first.key_wrap_table_bytes,
1576                    &parsed.key_wrap_table_bytes,
1577                )?;
1578            } else {
1579                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1580            }
1581
1582            if let Some(footer) = &parsed.manifest_footer {
1583                if let Some(authority) = &manifest_authority {
1584                    if !manifest_bootstrap_fields_match(authority, footer) {
1585                        return Err(FormatError::InvalidArchive(
1586                            "ManifestFooter bootstrap fields differ",
1587                        ));
1588                    }
1589                } else {
1590                    manifest_authority = Some(footer.clone());
1591                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1592                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1593                }
1594            } else if first_manifest_footer_error.is_none() {
1595                first_manifest_footer_error = parsed.manifest_footer_error.take();
1596            }
1597
1598            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1599                (Some(footer), Some(bytes)) => {
1600                    if saw_root_auth_absent {
1601                        return Err(FormatError::InvalidArchive(
1602                            "root-auth footer presence differs across volumes",
1603                        ));
1604                    }
1605                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1606                        if authority_bytes != bytes {
1607                            return Err(FormatError::InvalidArchive(
1608                                "RootAuthFooter copies differ",
1609                            ));
1610                        }
1611                    } else {
1612                        root_auth_authority = Some(footer.clone());
1613                        root_auth_authority_bytes = Some(bytes.clone());
1614                    }
1615                }
1616                (None, None) => {
1617                    if root_auth_authority_bytes.is_some() {
1618                        return Err(FormatError::InvalidArchive(
1619                            "root-auth footer presence differs across volumes",
1620                        ));
1621                    }
1622                    saw_root_auth_absent = true;
1623                }
1624                _ => {
1625                    return Err(FormatError::InvalidArchive(
1626                        "root-auth footer terminal state is inconsistent",
1627                    ));
1628                }
1629            }
1630
1631            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1632            let source = SeekableVolumeSource {
1633                reader: parsed.reader.clone(),
1634                volume_index: parsed.volume_header.volume_index,
1635                block_records_start: parsed.block_records_start,
1636                block_count: parsed.volume_trailer.block_count,
1637                record_len,
1638                block_size: parsed.crypto_header.block_size as usize,
1639            };
1640            let slot = parsed.volume_header.volume_index as usize;
1641            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1642            {
1643                return Err(FormatError::InvalidArchive(
1644                    "duplicate authenticated volume index",
1645                ));
1646            }
1647
1648            if first.is_none() {
1649                first = Some(parsed);
1650            }
1651        }
1652
1653        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1654        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1655            Some(err) => err,
1656            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1657        })?;
1658        let authority_volume_header = manifest_authority_volume_header.ok_or(
1659            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1660        )?;
1661        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1662            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1663        )?;
1664        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1665            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1666        let missing_volume_count = first
1667            .crypto_header
1668            .stripe_width
1669            .checked_sub(observed_volume_count)
1670            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1671        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1672            return Err(FormatError::InvalidArchive(
1673                "missing volume count exceeds volume_loss_tolerance",
1674            ));
1675        }
1676
1677        let mut blocks = BTreeMap::new();
1678        let sidecar = if let Some(bytes) = bootstrap_sidecar {
1679            let sidecar = parse_bootstrap_sidecar(
1680                bytes,
1681                &first.volume_header,
1682                &first.crypto_header,
1683                &first.subkeys,
1684            )?;
1685            sidecar
1686                .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1687            if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1688                if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1689                    return Err(FormatError::InvalidArchive(
1690                        "bootstrap sidecar conflicts with terminal ManifestFooter",
1691                    ));
1692                }
1693            }
1694            Some((bytes, sidecar))
1695        } else {
1696            None
1697        };
1698
1699        if let Some((sidecar_bytes, sidecar)) = &sidecar {
1700            if let Some((offset, length)) = sidecar.index_root_records_section {
1701                let index_root_records = parse_sidecar_block_records(
1702                    sidecar_bytes,
1703                    first.crypto_header.block_size as usize,
1704                    SidecarBlockRecordsSection {
1705                        offset,
1706                        length,
1707                        extent: index_root_extent_from_manifest(&manifest_footer),
1708                        data_kind: BlockKind::IndexRootData,
1709                        parity_kind: BlockKind::IndexRootParity,
1710                        structure: "IndexRoot",
1711                    },
1712                )?;
1713                insert_sidecar_records(&mut blocks, index_root_records)?;
1714            }
1715        }
1716
1717        let lazy_source = Arc::new(SeekableBlockSource {
1718            stripe_width: first.crypto_header.stripe_width,
1719            volumes: lazy_volume_slots,
1720        });
1721        let block_provider = OpenedBlockProvider {
1722            memory_blocks: &blocks,
1723            lazy_blocks: Some(lazy_source.as_ref()),
1724        };
1725        let limits = metadata_limits(&first.crypto_header);
1726        let index_root_plaintext = load_metadata_object_from_parts(
1727            &block_provider,
1728            ObjectLoadContext::index_root(
1729                &first.volume_header,
1730                &first.crypto_header,
1731                &first.subkeys,
1732                index_root_extent_from_manifest(&manifest_footer),
1733            ),
1734            manifest_footer.index_root_decompressed_size,
1735        )?;
1736        let index_root = IndexRoot::parse(
1737            &index_root_plaintext,
1738            first.crypto_header.has_dictionary != 0,
1739            limits,
1740        )?;
1741        if first.crypto_header.has_dictionary != 0 {
1742            if let Some((sidecar_bytes, sidecar)) = &sidecar {
1743                if let Some((offset, length)) = sidecar.dictionary_records_section {
1744                    let dictionary_records = parse_sidecar_block_records(
1745                        sidecar_bytes,
1746                        first.crypto_header.block_size as usize,
1747                        SidecarBlockRecordsSection {
1748                            offset,
1749                            length,
1750                            extent: dictionary_extent_from_index_root(&index_root)?,
1751                            data_kind: BlockKind::DictionaryData,
1752                            parity_kind: BlockKind::DictionaryParity,
1753                            structure: "Dictionary",
1754                        },
1755                    )?;
1756                    insert_sidecar_records(&mut blocks, dictionary_records)?;
1757                }
1758            }
1759        }
1760        let block_provider = OpenedBlockProvider {
1761            memory_blocks: &blocks,
1762            lazy_blocks: Some(lazy_source.as_ref()),
1763        };
1764        let payload_dictionary = load_archive_dictionary(
1765            &block_provider,
1766            &first.subkeys,
1767            &first.volume_header,
1768            &first.crypto_header,
1769            &index_root,
1770        )?;
1771
1772        Ok(Self {
1773            options,
1774            observed_archive_bytes,
1775            observed_volume_count,
1776            subkeys: first.subkeys,
1777            blocks,
1778            lazy_blocks: Some(lazy_source),
1779            crypto_header_bytes: first.crypto_header_bytes,
1780            volume_header: authority_volume_header,
1781            crypto_header: first.crypto_header,
1782            manifest_footer,
1783            volume_trailer: Some(authority_volume_trailer),
1784            root_auth_footer: root_auth_authority,
1785            index_root,
1786            payload_dictionary,
1787        })
1788    }
1789
1790    pub fn open_with_bootstrap_sidecar_options(
1791        bytes: &[u8],
1792        bootstrap_sidecar: &[u8],
1793        master_key: &MasterKey,
1794        options: ReaderOptions,
1795    ) -> Result<Self, FormatError> {
1796        Self::open_with_bootstrap_sidecar_options_for_mode(
1797            bytes,
1798            bootstrap_sidecar,
1799            master_key,
1800            options,
1801            BootstrapSidecarUse::SeekableAssist,
1802        )
1803    }
1804
1805    fn open_with_bootstrap_sidecar_options_for_mode(
1806        bytes: &[u8],
1807        bootstrap_sidecar: &[u8],
1808        master_key: &MasterKey,
1809        options: ReaderOptions,
1810        sidecar_use: BootstrapSidecarUse,
1811    ) -> Result<Self, FormatError> {
1812        let observed_archive_bytes =
1813            observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1814        if bytes.len() < VOLUME_HEADER_LEN {
1815            return Err(FormatError::InvalidLength {
1816                structure: "archive",
1817                expected: VOLUME_HEADER_LEN,
1818                actual: bytes.len(),
1819            });
1820        }
1821
1822        let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1823        parse_volume_format_dispatch(&volume_header)?;
1824        let crypto_start = volume_header.crypto_header_offset as usize;
1825        let crypto_len = volume_header.crypto_header_length as usize;
1826        let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1827        let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1828        let subkeys = subkeys_for_open(
1829            Some(master_key),
1830            parsed_crypto.fixed.aead_algo,
1831            &volume_header.archive_uuid,
1832            &volume_header.session_id,
1833        )?;
1834        verify_integrity_tag(
1835            HmacDomain::CryptoHeader,
1836            parsed_crypto.fixed.aead_algo,
1837            volume_header.volume_format_rev,
1838            Some(&subkeys.mac_key),
1839            &volume_header.archive_uuid,
1840            &volume_header.session_id,
1841            parsed_crypto.hmac_covered_bytes,
1842            &parsed_crypto.header_hmac,
1843        )?;
1844        parsed_crypto.validate_extension_semantics()?;
1845        reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1846        validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1847        validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1848
1849        let sidecar = parse_bootstrap_sidecar(
1850            bootstrap_sidecar,
1851            &volume_header,
1852            &parsed_crypto.fixed,
1853            &subkeys,
1854        )?;
1855        sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1856        let block_records_start = startup_block_records_start(
1857            &volume_header,
1858            &parsed_crypto.kdf_params,
1859            |start, length| {
1860                let start = to_usize(start, "KeyWrapTableV1")?;
1861                Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
1862            },
1863        )?;
1864
1865        let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1866            bytes,
1867            to_usize(block_records_start, "BlockRecord")?,
1868            parsed_crypto.fixed.block_size as usize,
1869            &volume_header,
1870        )?;
1871        let terminal_material = match sidecar_use {
1872            BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1873                bytes,
1874                terminal_offset,
1875                observed_block_count,
1876                KeyHoldingTerminalContext {
1877                    subkeys: &subkeys,
1878                    volume_header: &volume_header,
1879                    crypto_header: &parsed_crypto.fixed,
1880                    crypto_header_bytes: crypto_bytes,
1881                },
1882                options,
1883            )?),
1884            BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1885                bytes,
1886                terminal_offset,
1887                observed_block_count,
1888                KeyHoldingTerminalContext {
1889                    subkeys: &subkeys,
1890                    volume_header: &volume_header,
1891                    crypto_header: &parsed_crypto.fixed,
1892                    crypto_header_bytes: crypto_bytes,
1893                },
1894                options,
1895            )
1896            .ok(),
1897        };
1898        let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1899        let manifest_authority = match sidecar_use {
1900            BootstrapSidecarUse::SeekableAssist => {
1901                let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1902                    "terminal ManifestFooter/VolumeTrailer is required",
1903                ))?;
1904                if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1905                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1906                        return Err(FormatError::InvalidArchive(
1907                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1908                        ));
1909                    }
1910                }
1911                terminal_manifest.clone()
1912            }
1913            BootstrapSidecarUse::NonSeekableRandomAccess => {
1914                let sidecar_manifest = sidecar
1915                    .manifest_footer
1916                    .as_ref()
1917                    .ok_or(FormatError::ReaderUnsupported(
1918                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1919                ))?;
1920                if let Some(terminal_manifest) = terminal_manifest {
1921                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1922                        return Err(FormatError::InvalidArchive(
1923                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1924                        ));
1925                    }
1926                }
1927                sidecar_manifest.clone()
1928            }
1929        };
1930        manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1931
1932        if let Some((offset, length)) = sidecar.index_root_records_section {
1933            let index_root_records = parse_sidecar_block_records(
1934                bootstrap_sidecar,
1935                parsed_crypto.fixed.block_size as usize,
1936                SidecarBlockRecordsSection {
1937                    offset,
1938                    length,
1939                    extent: index_root_extent_from_manifest(&manifest_authority),
1940                    data_kind: BlockKind::IndexRootData,
1941                    parity_kind: BlockKind::IndexRootParity,
1942                    structure: "IndexRoot",
1943                },
1944            )?;
1945            insert_sidecar_records(&mut blocks, index_root_records)?;
1946        }
1947
1948        let limits = metadata_limits(&parsed_crypto.fixed);
1949        let index_root_plaintext = load_metadata_object_from_parts(
1950            &blocks,
1951            ObjectLoadContext::index_root(
1952                &volume_header,
1953                &parsed_crypto.fixed,
1954                &subkeys,
1955                index_root_extent_from_manifest(&manifest_authority),
1956            ),
1957            manifest_authority.index_root_decompressed_size,
1958        )?;
1959        let index_root = IndexRoot::parse(
1960            &index_root_plaintext,
1961            parsed_crypto.fixed.has_dictionary != 0,
1962            limits,
1963        )?;
1964        if parsed_crypto.fixed.has_dictionary != 0 {
1965            if let Some((offset, length)) = sidecar.dictionary_records_section {
1966                let dictionary_records = parse_sidecar_block_records(
1967                    bootstrap_sidecar,
1968                    parsed_crypto.fixed.block_size as usize,
1969                    SidecarBlockRecordsSection {
1970                        offset,
1971                        length,
1972                        extent: dictionary_extent_from_index_root(&index_root)?,
1973                        data_kind: BlockKind::DictionaryData,
1974                        parity_kind: BlockKind::DictionaryParity,
1975                        structure: "dictionary",
1976                    },
1977                )?;
1978                insert_sidecar_records(&mut blocks, dictionary_records)?;
1979            }
1980        }
1981        let payload_dictionary = load_archive_dictionary(
1982            &blocks,
1983            &subkeys,
1984            &volume_header,
1985            &parsed_crypto.fixed,
1986            &index_root,
1987        )?;
1988
1989        Ok(Self {
1990            options,
1991            observed_archive_bytes,
1992            observed_volume_count: 1,
1993            subkeys,
1994            blocks,
1995            lazy_blocks: None,
1996            crypto_header_bytes: crypto_bytes.to_vec(),
1997            volume_header,
1998            crypto_header: parsed_crypto.fixed,
1999            manifest_footer: manifest_authority,
2000            volume_trailer: terminal_material
2001                .as_ref()
2002                .map(|(_, trailer, _)| trailer.clone()),
2003            root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
2004            index_root,
2005            payload_dictionary,
2006        })
2007    }
2008
2009    /// Return path and payload-size entries from encrypted index metadata only.
2010    ///
2011    /// Unlike [`Self::list_files`], this does not decode tar member groups, so
2012    /// it does not read or decrypt payload envelopes after the index shards are
2013    /// available.
2014    pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
2015        let shards = self.load_all_index_shards()?;
2016        final_index_entry_winners(&shards)?
2017            .into_iter()
2018            .map(|(path, winner)| {
2019                Ok(ArchiveIndexEntry {
2020                    path,
2021                    file_data_size: winner.file_data_size,
2022                })
2023            })
2024            .collect()
2025    }
2026
2027    /// Look up one archive path using encrypted index metadata only.
2028    pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
2029        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2030        self.locate_index_file(&normalized)?
2031            .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
2032            .transpose()
2033    }
2034
2035    pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
2036        let shards = self.load_all_index_shards()?;
2037        final_index_entry_winners(&shards)?
2038            .into_iter()
2039            .map(|(path, winner)| {
2040                let shard = &shards[winner.shard_index];
2041                let member =
2042                    self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
2043                Ok(ArchiveEntry {
2044                    path,
2045                    file_data_size: winner.file_data_size,
2046                    kind: member.kind,
2047                    mode: member.mode,
2048                    mtime: member.mtime,
2049                    diagnostics: member.diagnostics,
2050                })
2051            })
2052            .collect()
2053    }
2054
2055    /// Return only the regular-file payload bytes for `path`.
2056    ///
2057    /// This is a payload-only convenience for callers that do not need tar
2058    /// metadata fidelity diagnostics. Use [`Self::extract_file_with_diagnostics`]
2059    /// or [`Self::extract_member`] when unsupported local PAX/GNU metadata must
2060    /// be reported to users.
2061    pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
2062        self.extract_member(path)?
2063            .map(|member| {
2064                if member.kind != TarEntryKind::Regular {
2065                    return Err(FormatError::ReaderUnsupported(
2066                        "extract_file returns only regular file payloads",
2067                    ));
2068                }
2069                Ok(member.data)
2070            })
2071            .transpose()
2072    }
2073
2074    /// Return regular-file payload bytes together with parsed tar metadata
2075    /// diagnostics for `path`.
2076    pub fn extract_file_with_diagnostics(
2077        &self,
2078        path: &str,
2079    ) -> Result<Option<ExtractedRegularFile>, FormatError> {
2080        self.extract_member(path)?
2081            .map(|member| {
2082                if member.kind != TarEntryKind::Regular {
2083                    return Err(FormatError::ReaderUnsupported(
2084                        "extract_file_with_diagnostics returns only regular file payloads",
2085                    ));
2086                }
2087                Ok((member.data, member.diagnostics))
2088            })
2089            .transpose()
2090    }
2091
2092    /// Stream regular-file payload bytes for `path` into `writer`.
2093    ///
2094    /// This keeps extraction memory bounded by the selected payload envelope,
2095    /// one decompressed frame, and small tar metadata buffers. It returns the
2096    /// same metadata diagnostics as [`Self::extract_file_with_diagnostics`].
2097    pub fn extract_file_to_writer<W: Write>(
2098        &self,
2099        path: &str,
2100        writer: &mut W,
2101    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2102        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2103        self.locate_index_file(&normalized)?
2104            .map(|located| {
2105                self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
2106            })
2107            .transpose()
2108    }
2109
2110    /// Stream regular-file payload bytes for `path` into `writer` while
2111    /// reporting extracted logical payload bytes.
2112    pub fn extract_file_to_writer_with_progress<W: Write>(
2113        &self,
2114        path: &str,
2115        writer: &mut W,
2116        progress: &mut dyn ArchiveExtractProgressSink,
2117    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2118        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2119        self.locate_index_file(&normalized)?
2120            .map(|located| {
2121                self.stream_loaded_file_to_writer_with_progress(
2122                    &located.shard,
2123                    located.file_index,
2124                    writer,
2125                    progress,
2126                )
2127            })
2128            .transpose()
2129    }
2130
2131    pub fn extract_member(
2132        &self,
2133        path: &str,
2134    ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
2135        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2136        self.locate_index_file(&normalized)?
2137            .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
2138            .transpose()
2139    }
2140
2141    pub fn extract_file_to(
2142        &self,
2143        path: &str,
2144        root: &std::path::Path,
2145        options: SafeExtractionOptions,
2146    ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2147        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2148        self.locate_index_file(&normalized)?
2149            .map(|located| {
2150                self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
2151            })
2152            .transpose()
2153    }
2154
2155    pub fn extract_indexed_files_to(
2156        &self,
2157        root: &std::path::Path,
2158        options: SafeExtractionOptions,
2159        jobs: usize,
2160    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2161        if jobs == 0 {
2162            return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2163        }
2164
2165        let shards = self.load_all_index_shards()?;
2166        let entries = final_index_entry_winners(&shards)?.into_iter().collect();
2167        self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2168    }
2169
2170    pub fn verify(&self) -> Result<(), FormatError> {
2171        self.verify_content().map(|_| ())
2172    }
2173
2174    pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2175        self.verify_content_with_parity_policy(
2176            ParityReadPolicy::Always,
2177            ContentVerificationMode::Full,
2178        )
2179    }
2180
2181    pub fn verify_content_fast(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2182        if self.fast_verify_defers_payload_semantics() {
2183            self.verify_payload_record_integrity_only()?;
2184            return Ok(ArchiveContentVerification {
2185                archive: self,
2186                mode: ContentVerificationMode::Fast,
2187            });
2188        }
2189        self.verify_content_with_parity_policy(
2190            ParityReadPolicy::RepairOnly,
2191            ContentVerificationMode::Fast,
2192        )
2193    }
2194
2195    pub fn fast_verify_defers_payload_semantics(&self) -> bool {
2196        self.root_auth_footer.is_none()
2197            && self.crypto_header.has_dictionary == 0
2198            && !self.crypto_header.aead_algo.is_encrypted()
2199            && self.crypto_header.fec_parity_shards == 0
2200            && self.crypto_header.index_fec_parity_shards == 0
2201            && self.crypto_header.index_root_fec_parity_shards == 0
2202            && self.manifest_footer.index_root_parity_block_count == 0
2203    }
2204
2205    fn verify_payload_record_integrity_only(&self) -> Result<(), FormatError> {
2206        let tables = self.load_payload_index_tables()?;
2207        let block_provider = self.block_provider();
2208        let block_size = self.crypto_header.block_size as u64;
2209        for envelope in tables.envelopes.values() {
2210            if envelope.parity_block_count != 0 {
2211                return Err(FormatError::InvalidArchive(
2212                    "fast payload record scan requires zero parity",
2213                ));
2214            }
2215            let expected_encrypted_size = checked_u64_mul(
2216                envelope.data_block_count as u64,
2217                block_size,
2218                "payload envelope encrypted size",
2219            )?;
2220            if envelope.encrypted_size as u64 != expected_encrypted_size {
2221                return Err(FormatError::InvalidArchive(
2222                    "payload envelope encrypted_size mismatch",
2223                ));
2224            }
2225            for offset in 0..envelope.data_block_count {
2226                let block_index =
2227                    checked_u64_add(envelope.first_block_index, offset as u64, "payload")?;
2228                let record = block_provider
2229                    .block(block_index)?
2230                    .ok_or(FormatError::InvalidArchive("payload data block is missing"))?;
2231                if record.kind != BlockKind::PayloadData {
2232                    return Err(FormatError::InvalidArchive(
2233                        "payload data block has unexpected kind",
2234                    ));
2235                }
2236                let should_be_last = offset + 1 == envelope.data_block_count;
2237                if record.is_last_data() != should_be_last {
2238                    return Err(FormatError::InvalidArchive(
2239                        "payload last-data flag is not on the final data block",
2240                    ));
2241                }
2242            }
2243        }
2244        Ok(())
2245    }
2246
2247    fn verify_content_with_parity_policy(
2248        &self,
2249        parity_policy: ParityReadPolicy,
2250        mode: ContentVerificationMode,
2251    ) -> Result<ArchiveContentVerification<'_>, FormatError> {
2252        let tables = self.load_payload_index_tables()?;
2253        let streamed = self.scan_seekable_payload(
2254            &tables,
2255            u64::MAX,
2256            NoopTarStreamObserver,
2257            true,
2258            parity_policy,
2259        )?;
2260        self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
2261        Ok(ArchiveContentVerification {
2262            archive: self,
2263            mode,
2264        })
2265    }
2266
2267    pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
2268        let lazy_source = self
2269            .lazy_blocks
2270            .as_ref()
2271            .ok_or(FormatError::ReaderUnsupported(
2272                "repair output requires seekable archive input",
2273            ))?;
2274        if !lazy_source.is_complete_volume_set() {
2275            return Err(FormatError::ReaderUnsupported(
2276                "repair output requires all archive volumes",
2277            ));
2278        }
2279
2280        let shards = self.load_all_index_shards()?;
2281        let rows = self.root_auth_fec_layout_rows(&shards)?;
2282        let block_provider = self.block_provider();
2283        let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
2284        for row in rows.into_iter().filter(|row| row.present) {
2285            self.collect_repair_patches_for_object(
2286                &block_provider,
2287                lazy_source,
2288                row,
2289                &mut patches,
2290            )?;
2291        }
2292        Ok(patches.into_values().collect())
2293    }
2294
2295    pub fn extract_all_to(
2296        &self,
2297        root: &std::path::Path,
2298        options: SafeExtractionOptions,
2299    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2300        let tables = self.load_payload_index_tables()?;
2301        if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
2302            return Err(FormatError::ReaderUnsupported(
2303                FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
2304            ));
2305        }
2306
2307        let observer = TarStreamFilesystemRestoreObserver::new(root, options);
2308        let streamed = self.scan_seekable_payload(
2309            &tables,
2310            total_extraction_size_cap(self.options, self.observed_archive_bytes),
2311            observer,
2312            false,
2313            ParityReadPolicy::RepairOnly,
2314        )?;
2315        self.validate_streamed_payload_summary(&tables, &streamed, true, false)?;
2316        streamed
2317            .tar
2318            .members
2319            .into_iter()
2320            .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
2321            .collect()
2322    }
2323
2324    fn collect_repair_patches_for_object(
2325        &self,
2326        blocks: &impl BlockProvider,
2327        source: &SeekableBlockSource,
2328        row: FecLayoutObjectRow,
2329        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2330    ) -> Result<(), FormatError> {
2331        let (data_kind, parity_kind, data_max, parity_max) =
2332            self.fec_object_class_shape(row.object_class)?;
2333        let extent = ObjectExtent {
2334            first_block_index: row.first_block_index,
2335            data_block_count: row.data_block_count,
2336            parity_block_count: row.parity_block_count,
2337            encrypted_size: row.encrypted_size,
2338        };
2339        validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
2340
2341        let block_size = self.crypto_header.block_size as usize;
2342        let data_count = extent.data_block_count as usize;
2343        let parity_count = extent.parity_block_count as usize;
2344        let mut data_shards = Vec::with_capacity(data_count);
2345        let mut parity_shards = Vec::with_capacity(parity_count);
2346
2347        for offset in 0..data_count {
2348            let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2349            match blocks.block(block_index)? {
2350                Some(record) => {
2351                    if record.kind != data_kind {
2352                        return Err(FormatError::InvalidArchive(
2353                            "object data block has unexpected kind",
2354                        ));
2355                    }
2356                    let should_be_last = offset + 1 == data_count;
2357                    if record.is_last_data() != should_be_last {
2358                        return Err(FormatError::InvalidArchive(
2359                            "object last-data flag is not on the final data block",
2360                        ));
2361                    }
2362                    data_shards.push(Some(record.payload.clone()));
2363                }
2364                None => data_shards.push(None),
2365            }
2366        }
2367
2368        for offset in 0..parity_count {
2369            let block_index = checked_u64_add(
2370                extent.first_block_index,
2371                data_count as u64 + offset as u64,
2372                "object",
2373            )?;
2374            match blocks.block(block_index)? {
2375                Some(record) => {
2376                    if record.kind != parity_kind {
2377                        return Err(FormatError::InvalidArchive(
2378                            "object parity block has unexpected kind",
2379                        ));
2380                    }
2381                    if record.is_last_data() {
2382                        return Err(FormatError::InvalidArchive(
2383                            "object parity block has last-data flag",
2384                        ));
2385                    }
2386                    parity_shards.push(Some(record.payload.clone()));
2387                }
2388                None => parity_shards.push(None),
2389            }
2390        }
2391
2392        let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2393        for (offset, payload) in repaired_data.iter().enumerate() {
2394            if data_shards[offset].is_none() {
2395                let block_index =
2396                    checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2397                let flags = if offset + 1 == data_count { 0x01 } else { 0 };
2398                self.insert_repair_patch(
2399                    patches,
2400                    source,
2401                    block_index,
2402                    data_kind,
2403                    flags,
2404                    payload.clone(),
2405                )?;
2406            }
2407        }
2408
2409        if parity_count > 0 {
2410            let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
2411            for (offset, payload) in repaired_parity.into_iter().enumerate() {
2412                if parity_shards[offset].as_ref() != Some(&payload) {
2413                    let block_index = checked_u64_add(
2414                        extent.first_block_index,
2415                        data_count as u64 + offset as u64,
2416                        "object",
2417                    )?;
2418                    self.insert_repair_patch(
2419                        patches,
2420                        source,
2421                        block_index,
2422                        parity_kind,
2423                        0,
2424                        payload,
2425                    )?;
2426                }
2427            }
2428        }
2429
2430        Ok(())
2431    }
2432
2433    fn insert_repair_patch(
2434        &self,
2435        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2436        source: &SeekableBlockSource,
2437        block_index: u64,
2438        kind: BlockKind,
2439        flags: u8,
2440        payload: Vec<u8>,
2441    ) -> Result<(), FormatError> {
2442        let (volume_index, record_offset) = source.record_location(block_index)?;
2443        let record = BlockRecord {
2444            block_index,
2445            kind,
2446            flags,
2447            payload,
2448            record_crc32c: 0,
2449        };
2450        let patch = ArchiveRepairPatch {
2451            volume_index,
2452            block_index,
2453            record_offset,
2454            record_bytes: record.to_bytes(),
2455        };
2456        if let Some(existing) = patches.insert(block_index, patch.clone()) {
2457            if existing != patch {
2458                return Err(FormatError::InvalidArchive(
2459                    "conflicting repair patch for BlockRecord",
2460                ));
2461            }
2462        }
2463        Ok(())
2464    }
2465
2466    fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
2467        if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
2468            && self.index_root.directory_hint_shards.is_empty()
2469        {
2470            return Err(FormatError::InvalidArchive(
2471                "IndexRoot file_count requires directory hints",
2472            ));
2473        }
2474
2475        let shards = self.load_all_index_shards()?;
2476        let mut file_count = 0u64;
2477        let mut frames = BTreeMap::<u64, FrameEntry>::new();
2478        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2479
2480        for shard in &shards {
2481            file_count = file_count
2482                .checked_add(shard.files.len() as u64)
2483                .ok_or(FormatError::InvalidArchive("file count overflow"))?;
2484            for frame in &shard.frames {
2485                if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
2486                    if existing != *frame {
2487                        return Err(FormatError::InvalidArchive(
2488                            "duplicate FrameEntry rows do not match",
2489                        ));
2490                    }
2491                }
2492            }
2493            for envelope in &shard.envelopes {
2494                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2495                {
2496                    if existing != *envelope {
2497                        return Err(FormatError::InvalidArchive(
2498                            "duplicate EnvelopeEntry rows do not match",
2499                        ));
2500                    }
2501                }
2502            }
2503        }
2504        validate_global_file_table_order(&shards)?;
2505
2506        if file_count != self.index_root.header.file_count {
2507            return Err(FormatError::InvalidArchive(
2508                "IndexRoot file_count does not match decoded shards",
2509            ));
2510        }
2511        verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
2512        verify_dense_keys(
2513            &envelopes,
2514            self.index_root.header.envelope_count,
2515            "EnvelopeEntry",
2516        )?;
2517        validate_envelope_frame_coverage(&frames, &envelopes)?;
2518        self.validate_encrypted_object_block_ranges(&envelopes)?;
2519
2520        let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
2521            sum.checked_add(envelope.data_block_count as u64)
2522                .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2523        })?;
2524        if payload_block_count != self.index_root.header.payload_block_count {
2525            return Err(FormatError::InvalidArchive(
2526                "IndexRoot payload_block_count does not match envelopes",
2527            ));
2528        }
2529
2530        Ok(PayloadIndexTables {
2531            shards,
2532            file_count,
2533            frames,
2534            envelopes,
2535        })
2536    }
2537
2538    fn scan_seekable_payload<O: TarStreamObserver>(
2539        &self,
2540        tables: &PayloadIndexTables,
2541        extraction_cap: u64,
2542        observer: O,
2543        hash_content: bool,
2544        parity_policy: ParityReadPolicy,
2545    ) -> Result<StreamedPayloadSummary, FormatError> {
2546        let mut tar = TarStreamSummaryValidator::with_observer(
2547            self.crypto_header.max_path_length,
2548            extraction_cap,
2549            usize::MAX,
2550            self.index_root.header.file_count,
2551            observer,
2552        );
2553        let mut content_hasher = hash_content.then(Sha256::new);
2554        let mut streamed_frames = Vec::with_capacity(tables.frames.len());
2555        let streamed_envelopes = tables
2556            .envelopes
2557            .values()
2558            .map(|envelope| StreamedEnvelopeSummary {
2559                envelope_index: envelope.envelope_index,
2560                first_block_index: envelope.first_block_index,
2561                data_block_count: envelope.data_block_count,
2562                parity_block_count: envelope.parity_block_count,
2563                encrypted_size: envelope.encrypted_size,
2564                plaintext_size: envelope.plaintext_size,
2565                first_frame_index: envelope.first_frame_index,
2566                frame_count: envelope.frame_count,
2567            })
2568            .collect::<Vec<_>>();
2569        let mut cached_envelope_index = None;
2570        let mut cached_envelope_plaintext = Vec::new();
2571        let mut decompressor = self.new_payload_decompressor()?;
2572
2573        for frame in tables.frames.values() {
2574            let envelope =
2575                tables
2576                    .envelopes
2577                    .get(&frame.envelope_index)
2578                    .ok_or(FormatError::InvalidArchive(
2579                        "FrameEntry references missing EnvelopeEntry",
2580                    ))?;
2581            if cached_envelope_index != Some(envelope.envelope_index) {
2582                cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
2583                cached_envelope_index = Some(envelope.envelope_index);
2584            }
2585            let compressed = slice(
2586                &cached_envelope_plaintext,
2587                frame.offset_in_envelope as usize,
2588                frame.compressed_size as usize,
2589                "FrameEntry",
2590            )?;
2591            let tar_stream_offset = tar.tar_total_size();
2592            let decoded = self.decompress_payload_frame_with(
2593                &mut decompressor,
2594                compressed,
2595                frame.decompressed_size,
2596            )?;
2597            if decoded.is_empty() {
2598                return Err(FormatError::InvalidArchive(
2599                    "zstd payload frame decompressed to zero bytes",
2600                ));
2601            }
2602            if let Some(hasher) = &mut content_hasher {
2603                hasher.update(&decoded);
2604            }
2605            tar.observe(&decoded)?;
2606            streamed_frames.push(StreamedFrameSummary {
2607                frame_index: frame.frame_index,
2608                envelope_index: frame.envelope_index,
2609                offset_in_envelope: frame.offset_in_envelope,
2610                compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2611                    FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2612                })?,
2613                decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2614                    FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2615                })?,
2616                tar_stream_offset,
2617            });
2618        }
2619
2620        let mut content_sha256 = [0u8; 32];
2621        if let Some(hasher) = content_hasher {
2622            let digest = hasher.finalize();
2623            content_sha256.copy_from_slice(&digest);
2624        }
2625        Ok(StreamedPayloadSummary {
2626            tar: tar.finish()?,
2627            content_sha256,
2628            envelopes: streamed_envelopes,
2629            frames: streamed_frames,
2630        })
2631    }
2632
2633    fn validate_streamed_payload_summary(
2634        &self,
2635        tables: &PayloadIndexTables,
2636        streamed: &StreamedPayloadSummary,
2637        enforce_total_extraction_cap: bool,
2638        enforce_content_sha256: bool,
2639    ) -> Result<(), FormatError> {
2640        if enforce_total_extraction_cap
2641            && streamed.tar.total_extraction_size
2642                > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2643        {
2644            return Err(FormatError::ReaderUnsupported(
2645                "total extraction size exceeds configured cap",
2646            ));
2647        }
2648
2649        let streamed_payload_block_count =
2650            streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2651                sum.checked_add(envelope.data_block_count as u64)
2652                    .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2653            })?;
2654        if streamed_payload_block_count != self.index_root.header.payload_block_count {
2655            return Err(FormatError::InvalidArchive(
2656                "streamed payload block count does not match IndexRoot",
2657            ));
2658        }
2659
2660        if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2661            return Err(FormatError::InvalidArchive(
2662                "IndexRoot tar_total_size does not match streamed tar stream",
2663            ));
2664        }
2665        if enforce_content_sha256
2666            && streamed.content_sha256 != self.index_root.header.content_sha256
2667        {
2668            return Err(FormatError::InvalidArchive(
2669                "IndexRoot content_sha256 does not match decoded tar stream",
2670            ));
2671        }
2672
2673        let streamed_envelopes = streamed.envelope_map()?;
2674        for envelope in tables.envelopes.values() {
2675            let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2676                FormatError::InvalidArchive(
2677                    "metadata references missing streamed payload envelope",
2678                ),
2679            )?;
2680            if actual.first_block_index != envelope.first_block_index
2681                || actual.data_block_count != envelope.data_block_count
2682                || actual.parity_block_count != envelope.parity_block_count
2683                || actual.encrypted_size != envelope.encrypted_size
2684                || actual.plaintext_size != envelope.plaintext_size
2685                || actual.first_frame_index != envelope.first_frame_index
2686                || actual.frame_count != envelope.frame_count
2687            {
2688                return Err(FormatError::InvalidArchive(
2689                    "EnvelopeEntry does not match streamed payload envelope",
2690                ));
2691            }
2692        }
2693
2694        let streamed_frames = streamed.frame_map()?;
2695        for frame in tables.frames.values() {
2696            let actual =
2697                streamed_frames
2698                    .get(&frame.frame_index)
2699                    .ok_or(FormatError::InvalidArchive(
2700                        "metadata references missing streamed payload frame",
2701                    ))?;
2702            if actual.envelope_index != frame.envelope_index
2703                || actual.offset_in_envelope != frame.offset_in_envelope
2704                || actual.compressed_size != frame.compressed_size
2705                || actual.decompressed_size != frame.decompressed_size
2706                || actual.tar_stream_offset != frame.tar_stream_offset
2707                || streamed.frame_flags(actual)? != frame.flags
2708            {
2709                return Err(FormatError::InvalidArchive(
2710                    "FrameEntry does not match streamed payload frame",
2711                ));
2712            }
2713        }
2714
2715        let streamed_members = streamed.member_start_map()?;
2716        if streamed.tar.members.len() as u64 != tables.file_count {
2717            return Err(FormatError::InvalidArchive(
2718                "streamed tar member count does not match decoded shards",
2719            ));
2720        }
2721        let mut file_extents = Vec::new();
2722        let mut directory_hint_map = DirectoryHintMap::new();
2723        for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2724            let shard_row_index = u32::try_from(shard_row_index)
2725                .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2726            for idx in 0..shard.files.len() {
2727                let file = &shard.files[idx];
2728                let start =
2729                    shard
2730                        .tar_member_group_start(idx)
2731                        .ok_or(FormatError::InvalidArchive(
2732                            "FileEntry tar member start is missing",
2733                        ))?;
2734                file_extents.push((start, file.tar_member_group_size));
2735                let path = shard
2736                    .file_path(idx)
2737                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2738                let member = streamed_members
2739                    .get(&start)
2740                    .ok_or(FormatError::InvalidArchive(
2741                        "FileEntry tar member start is missing from streamed tar",
2742                    ))?;
2743                if member.path != path {
2744                    return Err(FormatError::InvalidArchive(
2745                        "tar member path does not match FileEntry path",
2746                    ));
2747                }
2748                if member.logical_size != file.file_data_size {
2749                    return Err(FormatError::InvalidArchive(
2750                        "tar member size does not match FileEntry file_data_size",
2751                    ));
2752                }
2753                if member.group_size != file.tar_member_group_size {
2754                    return Err(FormatError::InvalidArchive(
2755                        "FileEntry does not match streamed tar member",
2756                    ));
2757                }
2758                add_expected_directory_hint_rows(
2759                    &mut directory_hint_map,
2760                    shard_row_index,
2761                    path,
2762                    member.kind,
2763                );
2764            }
2765        }
2766        validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2767        if !self.index_root.directory_hint_shards.is_empty() {
2768            let hint_tables = self.load_all_directory_hint_tables()?;
2769            validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2770        }
2771
2772        Ok(())
2773    }
2774
2775    pub(crate) fn from_streamed_parts(
2776        parts: StreamedArchiveOpenParts,
2777    ) -> Result<Self, FormatError> {
2778        let limits = metadata_limits(&parts.crypto_header);
2779        let index_root_plaintext = load_metadata_object_from_parts(
2780            &parts.blocks,
2781            ObjectLoadContext::index_root(
2782                &parts.volume_header,
2783                &parts.crypto_header,
2784                &parts.subkeys,
2785                ObjectExtent {
2786                    first_block_index: parts.manifest_footer.index_root_first_block,
2787                    data_block_count: parts.manifest_footer.index_root_data_block_count,
2788                    parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2789                    encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2790                },
2791            ),
2792            parts.manifest_footer.index_root_decompressed_size,
2793        )?;
2794        let index_root = IndexRoot::parse(
2795            &index_root_plaintext,
2796            parts.crypto_header.has_dictionary != 0,
2797            limits,
2798        )?;
2799        let payload_dictionary = load_archive_dictionary(
2800            &parts.blocks,
2801            &parts.subkeys,
2802            &parts.volume_header,
2803            &parts.crypto_header,
2804            &index_root,
2805        )?;
2806
2807        Ok(Self {
2808            options: parts.options,
2809            observed_archive_bytes: parts.observed_archive_bytes,
2810            observed_volume_count: 1,
2811            subkeys: parts.subkeys,
2812            blocks: parts.blocks,
2813            lazy_blocks: None,
2814            crypto_header_bytes: parts.crypto_header_bytes,
2815            volume_header: parts.volume_header,
2816            crypto_header: parts.crypto_header,
2817            manifest_footer: parts.manifest_footer,
2818            volume_trailer: Some(parts.volume_trailer),
2819            root_auth_footer: parts.root_auth_footer,
2820            index_root,
2821            payload_dictionary,
2822        })
2823    }
2824
2825    pub(crate) fn verify_streamed_payload_summary(
2826        &self,
2827        streamed: &StreamedPayloadSummary,
2828    ) -> Result<(), FormatError> {
2829        let tables = self.load_payload_index_tables()?;
2830        self.validate_streamed_payload_summary(&tables, streamed, true, true)
2831    }
2832
2833    pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2834    where
2835        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2836    {
2837        let content_verification = self.verify_content()?;
2838        self.verify_root_auth_with_verified_content(&content_verification, verifier)
2839    }
2840
2841    pub fn verify_root_auth_with_verified_content<F>(
2842        &self,
2843        content_verification: &ArchiveContentVerification<'_>,
2844        mut verifier: F,
2845    ) -> Result<RootAuthVerification, FormatError>
2846    where
2847        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2848    {
2849        if !std::ptr::eq(content_verification.archive, self) {
2850            return Err(FormatError::InvalidArchive(
2851                "content verification does not match archive",
2852            ));
2853        }
2854        if content_verification.mode != ContentVerificationMode::Full {
2855            return Err(FormatError::ReaderUnsupported(
2856                "RootAuth verification requires full archive content verification",
2857            ));
2858        }
2859        let footer = self
2860            .root_auth_footer
2861            .as_ref()
2862            .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2863        let material = self.recompute_root_auth_material(footer)?;
2864        if material.critical_metadata_digest != footer.critical_metadata_digest
2865            || material.index_digest != footer.index_digest
2866            || material.fec_layout_digest != footer.fec_layout_digest
2867            || material.data_block_merkle_root != footer.data_block_merkle_root
2868            || material.signer_identity_digest != footer.signer_identity_digest
2869            || material.archive_root != footer.archive_root
2870            || material.total_data_block_count != footer.total_data_block_count
2871        {
2872            return Err(FormatError::InvalidArchive(
2873                "RootAuthFooter commitments do not match recomputed archive root",
2874            ));
2875        }
2876        if !verifier(footer, &material.archive_root)? {
2877            return Err(FormatError::InvalidArchive(
2878                "root-auth authenticator verification failed",
2879            ));
2880        }
2881        Ok(RootAuthVerification {
2882            format_version: footer.format_version,
2883            volume_format_rev: footer.volume_format_rev,
2884            archive_root: material.archive_root,
2885            authenticator_id: footer.authenticator_id,
2886            signer_identity_type: footer.signer_identity_type,
2887            signer_identity_bytes: footer.signer_identity_bytes.clone(),
2888            total_data_block_count: footer.total_data_block_count,
2889            diagnostics: self.root_auth_success_diagnostics(),
2890        })
2891    }
2892
2893    fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2894        parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2895            self.load_index_shard(entry)
2896        })
2897    }
2898
2899    fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2900        let block_provider = self.block_provider();
2901        let plaintext = load_metadata_object_from_parts(
2902            &block_provider,
2903            ObjectLoadContext::index_shard(
2904                &self.volume_header,
2905                &self.crypto_header,
2906                &self.subkeys,
2907                entry,
2908            ),
2909            entry.decompressed_size,
2910        )?;
2911        IndexShard::parse(&plaintext, entry, self.metadata_limits())
2912    }
2913
2914    fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2915        parallel_map_ref(
2916            &self.index_root.directory_hint_shards,
2917            self.options.jobs,
2918            |entry| self.load_directory_hint_table(entry),
2919        )
2920    }
2921
2922    fn load_directory_hint_table(
2923        &self,
2924        entry: &DirectoryHintShardEntry,
2925    ) -> Result<DirectoryHintTable, FormatError> {
2926        let block_provider = self.block_provider();
2927        let plaintext = load_metadata_object_from_parts(
2928            &block_provider,
2929            ObjectLoadContext::directory_hint(
2930                &self.volume_header,
2931                &self.crypto_header,
2932                &self.subkeys,
2933                entry,
2934            ),
2935            entry.decompressed_size,
2936        )?;
2937        DirectoryHintTable::parse(
2938            &plaintext,
2939            entry,
2940            self.index_root.header.shard_count,
2941            self.metadata_limits(),
2942        )
2943    }
2944
2945    fn load_payload_envelope(
2946        &self,
2947        envelope: &EnvelopeEntry,
2948        parity_policy: ParityReadPolicy,
2949    ) -> Result<Vec<u8>, FormatError> {
2950        let block_provider = self.block_provider();
2951        let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2952            &block_provider,
2953            ObjectLoadContext::payload(
2954                &self.volume_header,
2955                &self.crypto_header,
2956                &self.subkeys,
2957                envelope,
2958            ),
2959            parity_policy,
2960        )?;
2961        if plaintext.len() != envelope.plaintext_size as usize {
2962            return Err(FormatError::InvalidArchive(
2963                "payload envelope plaintext_size mismatch",
2964            ));
2965        }
2966        Ok(plaintext)
2967    }
2968
2969    fn locate_index_file(
2970        &self,
2971        normalized: &[u8],
2972    ) -> Result<Option<LocatedIndexFile>, FormatError> {
2973        let candidate_indexes = self
2974            .index_root
2975            .candidate_shards_for_path(normalized, self.metadata_limits())?;
2976        let mut winner: Option<LocatedIndexFile> = None;
2977
2978        for row_index in candidate_indexes {
2979            let locating =
2980                self.index_root
2981                    .shards
2982                    .get(row_index)
2983                    .ok_or(FormatError::InvalidArchive(
2984                        "candidate shard row is out of bounds",
2985                    ))?;
2986            let shard = self.load_index_shard(locating)?;
2987            if let Some(file_index) = shard.lookup_file_index(normalized) {
2988                let start =
2989                    shard
2990                        .tar_member_group_start(file_index)
2991                        .ok_or(FormatError::InvalidArchive(
2992                            "FileEntry tar member start is missing",
2993                        ))?;
2994                if winner
2995                    .as_ref()
2996                    .map(|existing| start > existing.start)
2997                    .unwrap_or(true)
2998                {
2999                    winner = Some(LocatedIndexFile {
3000                        shard,
3001                        file_index,
3002                        start,
3003                    });
3004                }
3005            }
3006        }
3007
3008        Ok(winner)
3009    }
3010
3011    fn extract_loaded_member(
3012        &self,
3013        shard: &IndexShard,
3014        file_index: usize,
3015    ) -> Result<ExtractedArchiveMember, FormatError> {
3016        let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
3017        Ok(ExtractedArchiveMember {
3018            path: utf8_path(&member.path)?,
3019            kind: member.kind,
3020            data: member.data,
3021            link_target: member
3022                .link_target
3023                .map(|target| utf8_path(&target))
3024                .transpose()?,
3025            diagnostics: member.diagnostics,
3026        })
3027    }
3028
3029    fn extract_loaded_owned_tar_member(
3030        &self,
3031        shard: &IndexShard,
3032        file_index: usize,
3033    ) -> Result<OwnedTarMember, FormatError> {
3034        self.decode_loaded_owned_tar_member(shard, file_index, true)
3035    }
3036
3037    fn stream_loaded_file_to_writer<W: Write>(
3038        &self,
3039        shard: &IndexShard,
3040        file_index: usize,
3041        writer: &mut W,
3042    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3043        let file = shard
3044            .files
3045            .get(file_index)
3046            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3047        self.validate_total_extraction_size(file.file_data_size)?;
3048        let expected_path = shard
3049            .file_path(file_index)
3050            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3051        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3052        stream_regular_tar_member_group_to_writer(
3053            &mut reader,
3054            expected_path,
3055            file.file_data_size,
3056            file.tar_member_group_size,
3057            self.crypto_header.max_path_length,
3058            writer,
3059        )
3060    }
3061
3062    fn stream_loaded_file_to_writer_with_progress<W: Write>(
3063        &self,
3064        shard: &IndexShard,
3065        file_index: usize,
3066        writer: &mut W,
3067        progress: &mut dyn ArchiveExtractProgressSink,
3068    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3069        let file = shard
3070            .files
3071            .get(file_index)
3072            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3073        self.validate_total_extraction_size(file.file_data_size)?;
3074        let expected_path = shard
3075            .file_path(file_index)
3076            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3077        let archive_path = utf8_path(expected_path)?;
3078        let mut progress_writer =
3079            ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
3080        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3081        stream_regular_tar_member_group_to_writer(
3082            &mut reader,
3083            expected_path,
3084            file.file_data_size,
3085            file.tar_member_group_size,
3086            self.crypto_header.max_path_length,
3087            &mut progress_writer,
3088        )
3089    }
3090
3091    fn stream_loaded_file_to_path(
3092        &self,
3093        shard: &IndexShard,
3094        file_index: usize,
3095        root: &std::path::Path,
3096        options: SafeExtractionOptions,
3097    ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3098        let file = shard
3099            .files
3100            .get(file_index)
3101            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3102        self.validate_total_extraction_size(file.file_data_size)?;
3103        let expected_path = shard
3104            .file_path(file_index)
3105            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3106        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3107        restore_streaming_tar_member_group(
3108            root,
3109            expected_path,
3110            file.file_data_size,
3111            file.tar_member_group_size,
3112            self.crypto_header.max_path_length,
3113            options,
3114            &mut reader,
3115        )
3116        .map_err(format_error_from_extract_error)
3117    }
3118
3119    fn extract_winning_index_entries_to(
3120        &self,
3121        shards: &[IndexShard],
3122        entries: Vec<(String, WinningIndexEntry)>,
3123        root: &std::path::Path,
3124        options: SafeExtractionOptions,
3125        jobs: usize,
3126    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
3127        if entries.is_empty() {
3128            return Ok(Vec::new());
3129        }
3130        if jobs <= 1 || entries.len() <= 1 {
3131            return entries
3132                .into_iter()
3133                .map(|(path, entry)| {
3134                    let shard =
3135                        shards
3136                            .get(entry.shard_index)
3137                            .ok_or(FormatError::InvalidArchive(
3138                                "winning FileEntry shard is out of bounds",
3139                            ))?;
3140                    let diagnostics =
3141                        self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
3142                    Ok((path, diagnostics))
3143                })
3144                .collect();
3145        }
3146
3147        let worker_count = jobs.min(entries.len());
3148        let chunk_size = entries.len().div_ceil(worker_count);
3149        std::thread::scope(|scope| {
3150            let handles = entries
3151                .chunks(chunk_size)
3152                .map(|chunk| {
3153                    scope.spawn(move || {
3154                        let mut out = Vec::with_capacity(chunk.len());
3155                        for (path, entry) in chunk {
3156                            let shard = shards.get(entry.shard_index).ok_or(
3157                                FormatError::InvalidArchive(
3158                                    "winning FileEntry shard is out of bounds",
3159                                ),
3160                            )?;
3161                            let diagnostics = self.stream_loaded_file_to_path(
3162                                shard,
3163                                entry.file_index,
3164                                root,
3165                                options,
3166                            )?;
3167                            out.push((path.clone(), diagnostics));
3168                        }
3169                        Ok(out)
3170                    })
3171                })
3172                .collect::<Vec<_>>();
3173            let mut out = Vec::new();
3174            for handle in handles {
3175                let mut chunk = handle
3176                    .join()
3177                    .map_err(|_| FormatError::ReaderUnsupported("extract worker panicked"))??;
3178                out.append(&mut chunk);
3179            }
3180            Ok(out)
3181        })
3182    }
3183
3184    fn decode_loaded_owned_tar_member(
3185        &self,
3186        shard: &IndexShard,
3187        file_index: usize,
3188        enforce_extraction_cap: bool,
3189    ) -> Result<OwnedTarMember, FormatError> {
3190        let file = shard
3191            .files
3192            .get(file_index)
3193            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3194        if enforce_extraction_cap {
3195            self.validate_total_extraction_size(file.file_data_size)?;
3196        }
3197        let expected_path = shard
3198            .file_path(file_index)
3199            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3200        let frames = frame_range_for_file(shard, file)?;
3201        let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
3202        let mut decoded = Vec::new();
3203
3204        for frame in frames {
3205            let envelope = shard
3206                .envelopes
3207                .iter()
3208                .find(|entry| entry.envelope_index == frame.envelope_index)
3209                .ok_or(FormatError::InvalidArchive(
3210                    "FrameEntry references missing EnvelopeEntry",
3211                ))?;
3212            if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
3213                entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
3214            }
3215            let envelope_plaintext = envelope_cache
3216                .get(&envelope.envelope_index)
3217                .expect("inserted above");
3218            let compressed = slice(
3219                envelope_plaintext,
3220                frame.offset_in_envelope as usize,
3221                frame.compressed_size as usize,
3222                "FrameEntry",
3223            )?;
3224            decoded.extend_from_slice(
3225                &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
3226            );
3227        }
3228
3229        let offset = file.offset_in_first_frame_plaintext as usize;
3230        let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
3231        let group = slice(&decoded, offset, group_len, "FileEntry")?;
3232        let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
3233        if member.path != expected_path {
3234            return Err(FormatError::InvalidArchive(
3235                "tar member path does not match FileEntry path",
3236            ));
3237        }
3238        if member.logical_size != file.file_data_size {
3239            return Err(FormatError::InvalidArchive(
3240                "tar member size does not match FileEntry file_data_size",
3241            ));
3242        }
3243        Ok(member.to_owned_member())
3244    }
3245
3246    fn metadata_limits(&self) -> MetadataLimits {
3247        metadata_limits(&self.crypto_header)
3248    }
3249
3250    fn recompute_root_auth_material(
3251        &self,
3252        footer: &RootAuthFooterV1,
3253    ) -> Result<RootAuthMaterial, FormatError> {
3254        if footer.format_version != self.volume_header.format_version {
3255            return Err(FormatError::InvalidArchive(
3256                "RootAuthFooter format_version differs from authenticated VolumeHeader",
3257            ));
3258        }
3259        if footer.volume_format_rev != self.volume_header.volume_format_rev {
3260            return Err(FormatError::InvalidArchive(
3261                "RootAuthFooter volume_format_rev differs from authenticated VolumeHeader",
3262            ));
3263        }
3264        let format_version = self.volume_header.format_version;
3265        let volume_format_rev = self.volume_header.volume_format_rev;
3266        let footer_length = footer.footer_length()?;
3267        let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
3268            format_version,
3269            volume_format_rev,
3270            footer.authenticator_id,
3271            footer.signer_identity_type,
3272            &footer.signer_identity_bytes,
3273            u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3274                FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3275            })?,
3276            footer_length,
3277        )?;
3278        let signer_identity_digest =
3279            signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3280        let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
3281        let crypto_pre_hmac_len = self
3282            .crypto_header_bytes
3283            .len()
3284            .checked_sub(CRYPTO_HEADER_HMAC_LEN)
3285            .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
3286        let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
3287            archive_uuid: self.volume_header.archive_uuid,
3288            session_id: self.volume_header.session_id,
3289            format_version,
3290            volume_format_rev,
3291            stripe_width: self.crypto_header.stripe_width,
3292            total_volumes: self.manifest_footer.total_volumes,
3293            compression_algo: self.crypto_header.compression_algo,
3294            aead_algo: self.crypto_header.aead_algo,
3295            fec_algo: self.crypto_header.fec_algo,
3296            kdf_algo: self.crypto_header.kdf_algo,
3297            crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
3298            chunk_size: self.crypto_header.chunk_size,
3299            envelope_target_size: self.crypto_header.envelope_target_size,
3300            block_size: self.crypto_header.block_size,
3301            fec_data_shards: self.crypto_header.fec_data_shards,
3302            fec_parity_shards: self.crypto_header.fec_parity_shards,
3303            index_fec_data_shards: self.crypto_header.index_fec_data_shards,
3304            index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
3305            index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
3306            index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
3307            volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
3308            bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
3309            has_dictionary: self.crypto_header.has_dictionary,
3310            manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
3311            index_root_first_block: self.manifest_footer.index_root_first_block,
3312            index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
3313            index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
3314            index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
3315            index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
3316            root_auth_descriptor_digest,
3317        })?;
3318        let index_root_plaintext = self.index_root.to_bytes();
3319        let index_digest =
3320            index_digest_for_revision(format_version, volume_format_rev, &index_root_plaintext)?;
3321        let shards = self.load_all_index_shards()?;
3322        let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
3323        let fec_layout_digest =
3324            fec_layout_digest_for_revision(format_version, volume_format_rev, &fec_layout_rows)?;
3325        let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
3326        let total_data_block_count = u64::try_from(data_leaves.len())
3327            .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
3328        let data_block_merkle_root =
3329            data_block_merkle_root_for_revision(format_version, volume_format_rev, &data_leaves)?;
3330        let archive_root = archive_root_for_revision(ArchiveRootInputs {
3331            archive_uuid: self.volume_header.archive_uuid,
3332            session_id: self.volume_header.session_id,
3333            format_version,
3334            volume_format_rev,
3335            compression_algo: self.crypto_header.compression_algo,
3336            aead_algo: self.crypto_header.aead_algo,
3337            fec_algo: self.crypto_header.fec_algo,
3338            kdf_algo: self.crypto_header.kdf_algo,
3339            critical_metadata_digest,
3340            index_digest,
3341            fec_layout_digest,
3342            total_data_block_count,
3343            data_block_merkle_root,
3344            root_auth_descriptor_digest,
3345            signer_identity_digest,
3346        })?;
3347        Ok(RootAuthMaterial {
3348            critical_metadata_digest,
3349            index_digest,
3350            fec_layout_digest,
3351            data_block_merkle_root,
3352            signer_identity_digest,
3353            archive_root,
3354            total_data_block_count,
3355        })
3356    }
3357
3358    fn root_auth_fec_layout_rows(
3359        &self,
3360        shards: &[IndexShard],
3361    ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
3362        let mut rows = Vec::new();
3363        rows.push(FecLayoutObjectRow {
3364            object_class: 1,
3365            present: true,
3366            object_id: 0,
3367            first_block_index: self.manifest_footer.index_root_first_block,
3368            data_block_count: self.manifest_footer.index_root_data_block_count,
3369            parity_block_count: self.manifest_footer.index_root_parity_block_count,
3370            encrypted_size: self.manifest_footer.index_root_encrypted_size,
3371            plain_size: self.manifest_footer.index_root_decompressed_size,
3372        });
3373        if self.crypto_header.has_dictionary != 0 {
3374            rows.push(FecLayoutObjectRow {
3375                object_class: 2,
3376                present: true,
3377                object_id: 0,
3378                first_block_index: self.index_root.header.dictionary_first_block,
3379                data_block_count: self.index_root.header.dictionary_data_block_count,
3380                parity_block_count: self.index_root.header.dictionary_parity_block_count,
3381                encrypted_size: self.index_root.header.dictionary_encrypted_size,
3382                plain_size: self.index_root.header.dictionary_decompressed_size,
3383            });
3384        } else {
3385            rows.push(FecLayoutObjectRow {
3386                object_class: 2,
3387                present: false,
3388                object_id: 0,
3389                first_block_index: 0,
3390                data_block_count: 0,
3391                parity_block_count: 0,
3392                encrypted_size: 0,
3393                plain_size: 0,
3394            });
3395        }
3396        for entry in &self.index_root.shards {
3397            rows.push(FecLayoutObjectRow {
3398                object_class: 3,
3399                present: true,
3400                object_id: entry.shard_index,
3401                first_block_index: entry.first_block_index,
3402                data_block_count: entry.data_block_count,
3403                parity_block_count: entry.parity_block_count,
3404                encrypted_size: entry.encrypted_size,
3405                plain_size: entry.decompressed_size,
3406            });
3407        }
3408        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
3409        for shard in shards {
3410            for envelope in &shard.envelopes {
3411                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
3412                {
3413                    if existing != *envelope {
3414                        return Err(FormatError::InvalidArchive(
3415                            "duplicate EnvelopeEntry rows do not match",
3416                        ));
3417                    }
3418                }
3419            }
3420        }
3421        for envelope in envelopes.values() {
3422            rows.push(FecLayoutObjectRow {
3423                object_class: 4,
3424                present: true,
3425                object_id: envelope.envelope_index,
3426                first_block_index: envelope.first_block_index,
3427                data_block_count: envelope.data_block_count,
3428                parity_block_count: envelope.parity_block_count,
3429                encrypted_size: envelope.encrypted_size,
3430                plain_size: envelope.plaintext_size,
3431            });
3432        }
3433        for entry in &self.index_root.directory_hint_shards {
3434            rows.push(FecLayoutObjectRow {
3435                object_class: 5,
3436                present: true,
3437                object_id: entry.hint_shard_index,
3438                first_block_index: entry.first_block_index,
3439                data_block_count: entry.data_block_count,
3440                parity_block_count: entry.parity_block_count,
3441                encrypted_size: entry.encrypted_size,
3442                plain_size: entry.decompressed_size,
3443            });
3444        }
3445        Ok(rows)
3446    }
3447
3448    fn fec_object_class_shape(
3449        &self,
3450        object_class: u8,
3451    ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
3452        match object_class {
3453            1 => Ok((
3454                BlockKind::IndexRootData,
3455                BlockKind::IndexRootParity,
3456                self.crypto_header.index_root_fec_data_shards,
3457                self.crypto_header.index_root_fec_parity_shards,
3458            )),
3459            2 => Ok((
3460                BlockKind::DictionaryData,
3461                BlockKind::DictionaryParity,
3462                self.crypto_header.index_root_fec_data_shards,
3463                self.crypto_header.index_root_fec_parity_shards,
3464            )),
3465            3 => Ok((
3466                BlockKind::IndexShardData,
3467                BlockKind::IndexShardParity,
3468                self.crypto_header.index_fec_data_shards,
3469                self.crypto_header.index_fec_parity_shards,
3470            )),
3471            4 => Ok((
3472                BlockKind::PayloadData,
3473                BlockKind::PayloadParity,
3474                self.crypto_header.fec_data_shards,
3475                self.crypto_header.fec_parity_shards,
3476            )),
3477            5 => Ok((
3478                BlockKind::DirectoryHintData,
3479                BlockKind::DirectoryHintParity,
3480                self.crypto_header.index_fec_data_shards,
3481                self.crypto_header.index_fec_parity_shards,
3482            )),
3483            _ => Err(FormatError::InvalidArchive(
3484                "unknown root-auth FEC row class",
3485            )),
3486        }
3487    }
3488
3489    fn root_auth_data_block_leaves(
3490        &self,
3491        rows: &[FecLayoutObjectRow],
3492    ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
3493        let block_provider = self.block_provider();
3494        let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
3495        let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
3496            let row = **row;
3497            let (data_kind, parity_kind, data_max, parity_max) =
3498                self.fec_object_class_shape(row.object_class)?;
3499            let extent = ObjectExtent {
3500                first_block_index: row.first_block_index,
3501                data_block_count: row.data_block_count,
3502                parity_block_count: row.parity_block_count,
3503                encrypted_size: row.encrypted_size,
3504            };
3505            let repaired = load_repaired_object_data_shards_from_parts(
3506                &block_provider,
3507                &self.crypto_header,
3508                extent,
3509                data_kind,
3510                parity_kind,
3511                data_max,
3512                parity_max,
3513            )?;
3514            let mut leaves = Vec::new();
3515            for (offset, payload) in repaired.into_iter().enumerate() {
3516                leaves.push(DataBlockMerkleLeaf {
3517                    block_index: checked_u64_add(
3518                        row.first_block_index,
3519                        offset as u64,
3520                        "root-auth data block",
3521                    )?,
3522                    kind: data_kind,
3523                    flags: if offset + 1 == row.data_block_count as usize {
3524                        0x01
3525                    } else {
3526                        0
3527                    },
3528                    payload,
3529                });
3530            }
3531            Ok(leaves)
3532        })?;
3533        let mut leaves = Vec::new();
3534        for mut chunk in chunks {
3535            leaves.append(&mut chunk);
3536        }
3537        leaves.sort_by_key(|leaf| leaf.block_index);
3538        Ok(leaves)
3539    }
3540
3541    fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
3542        let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
3543        if logical_size > cap {
3544            return Err(FormatError::ReaderUnsupported(
3545                "total extraction size exceeds configured cap",
3546            ));
3547        }
3548        Ok(())
3549    }
3550
3551    fn decompress_payload_frame(
3552        &self,
3553        compressed: &[u8],
3554        decompressed_size: u32,
3555    ) -> Result<Vec<u8>, FormatError> {
3556        let mut decompressor = self.new_payload_decompressor()?;
3557        self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
3558    }
3559
3560    fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
3561        match &self.payload_dictionary {
3562            Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
3563            None => zstd::bulk::Decompressor::new(),
3564        }
3565        .map_err(|_| FormatError::ZstdDecompressionFailure)
3566    }
3567
3568    fn decompress_payload_frame_with(
3569        &self,
3570        decompressor: &mut zstd::bulk::Decompressor<'static>,
3571        compressed: &[u8],
3572        decompressed_size: u32,
3573    ) -> Result<Vec<u8>, FormatError> {
3574        validate_exact_zstd_frame(compressed)?;
3575        let expected = decompressed_size as usize;
3576        let decoded = decompressor
3577            .decompress(compressed, expected)
3578            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
3579        if decoded.len() != expected {
3580            return Err(FormatError::ZstdDecompressedSizeMismatch {
3581                expected,
3582                actual: decoded.len(),
3583            });
3584        }
3585        Ok(decoded)
3586    }
3587
3588    fn validate_encrypted_object_block_ranges(
3589        &self,
3590        envelopes: &BTreeMap<u64, EnvelopeEntry>,
3591    ) -> Result<(), FormatError> {
3592        let mut ranges = Vec::new();
3593        ranges.push(object_block_range(
3594            self.manifest_footer.index_root_first_block,
3595            self.manifest_footer.index_root_data_block_count,
3596            self.manifest_footer.index_root_parity_block_count,
3597            "IndexRoot",
3598        )?);
3599        for shard in &self.index_root.shards {
3600            ranges.push(object_block_range(
3601                shard.first_block_index,
3602                shard.data_block_count,
3603                shard.parity_block_count,
3604                "IndexShard",
3605            )?);
3606        }
3607        for hint in &self.index_root.directory_hint_shards {
3608            ranges.push(object_block_range(
3609                hint.first_block_index,
3610                hint.data_block_count,
3611                hint.parity_block_count,
3612                "DirectoryHintShardEntry",
3613            )?);
3614        }
3615        if self.crypto_header.has_dictionary != 0 {
3616            ranges.push(object_block_range(
3617                self.index_root.header.dictionary_first_block,
3618                self.index_root.header.dictionary_data_block_count,
3619                self.index_root.header.dictionary_parity_block_count,
3620                "dictionary",
3621            )?);
3622        }
3623        for envelope in envelopes.values() {
3624            ranges.push(object_block_range(
3625                envelope.first_block_index,
3626                envelope.data_block_count,
3627                envelope.parity_block_count,
3628                "EnvelopeEntry",
3629            )?);
3630        }
3631        validate_non_overlapping_object_ranges(&mut ranges)?;
3632        if let Some(source) = &self.lazy_blocks {
3633            if source.is_complete_volume_set() {
3634                validate_exact_coverage_ranges_u64(
3635                    &mut ranges,
3636                    source.total_block_count()?,
3637                    "encrypted object block ranges do not cover complete archive exactly",
3638                )?;
3639            }
3640        }
3641        Ok(())
3642    }
3643}
3644
3645impl<'a> DecodedTarMemberGroupReader<'a> {
3646    fn new(
3647        archive: &'a OpenedArchive,
3648        shard: &'a IndexShard,
3649        file: &'a FileEntry,
3650    ) -> Result<Self, FormatError> {
3651        Ok(Self {
3652            archive,
3653            shard,
3654            file,
3655            decompressor: archive.new_payload_decompressor()?,
3656            next_frame_offset: 0,
3657            cached_envelope_index: None,
3658            cached_envelope_plaintext: Vec::new(),
3659            current_frame: Vec::new(),
3660            current_frame_offset: 0,
3661            remaining_group_bytes: file.tar_member_group_size,
3662        })
3663    }
3664
3665    fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3666        while self.current_frame_offset >= self.current_frame.len() {
3667            if self.next_frame_offset >= self.file.frame_count as u64 {
3668                return Err(
3669                    FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3670                );
3671            }
3672            let frame_index = self
3673                .file
3674                .first_frame_index
3675                .checked_add(self.next_frame_offset)
3676                .ok_or(FormatError::InvalidArchive(
3677                    "FileEntry frame range overflow",
3678                ))?;
3679            let frame = frame_by_index(self.shard, frame_index)?;
3680            let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3681            if self.cached_envelope_index != Some(envelope.envelope_index) {
3682                self.cached_envelope_plaintext = self
3683                    .archive
3684                    .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3685                self.cached_envelope_index = Some(envelope.envelope_index);
3686            }
3687            let compressed = slice(
3688                &self.cached_envelope_plaintext,
3689                frame.offset_in_envelope as usize,
3690                frame.compressed_size as usize,
3691                "FrameEntry",
3692            )?;
3693            let decoded = self.archive.decompress_payload_frame_with(
3694                &mut self.decompressor,
3695                compressed,
3696                frame.decompressed_size,
3697            )?;
3698            let offset = if self.next_frame_offset == 0 {
3699                self.file.offset_in_first_frame_plaintext as usize
3700            } else {
3701                0
3702            };
3703            if offset > decoded.len() {
3704                return Err(FormatError::InvalidArchive(
3705                    "offset in first frame is outside the first referenced frame",
3706                )
3707                .into());
3708            }
3709            self.next_frame_offset += 1;
3710            self.current_frame = decoded;
3711            self.current_frame_offset = offset;
3712        }
3713        Ok(())
3714    }
3715}
3716
3717impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3718    fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3719        if buf.is_empty() {
3720            return Ok(0);
3721        }
3722        if self.remaining_group_bytes == 0 {
3723            return Ok(0);
3724        }
3725        self.ensure_frame_available()?;
3726        let available = self.current_frame.len() - self.current_frame_offset;
3727        let len = available
3728            .min(buf.len())
3729            .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3730        if len == 0 {
3731            return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3732        }
3733        buf[..len].copy_from_slice(
3734            &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3735        );
3736        self.current_frame_offset += len;
3737        self.remaining_group_bytes -= len as u64;
3738        Ok(len)
3739    }
3740}
3741
3742fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3743    shard
3744        .frames
3745        .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3746        .map(|idx| &shard.frames[idx])
3747        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3748}
3749
3750fn envelope_by_index(
3751    shard: &IndexShard,
3752    envelope_index: u64,
3753) -> Result<&EnvelopeEntry, FormatError> {
3754    shard
3755        .envelopes
3756        .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3757        .map(|idx| &shard.envelopes[idx])
3758        .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3759}
3760
3761fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3762    match err {
3763        ExtractError::Format(err) => err,
3764        ExtractError::Output(_) => {
3765            FormatError::FilesystemExtractionFailed("failed to write regular file")
3766        }
3767    }
3768}
3769
3770fn final_index_entry_winners(
3771    shards: &[IndexShard],
3772) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3773    let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3774    for (shard_index, shard) in shards.iter().enumerate() {
3775        for (idx, file) in shard.files.iter().enumerate() {
3776            let path = utf8_path(
3777                shard
3778                    .file_path(idx)
3779                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3780            )?;
3781            let start = shard
3782                .tar_member_group_start(idx)
3783                .ok_or(FormatError::InvalidArchive(
3784                    "FileEntry tar member start is missing",
3785                ))?;
3786            if let Some(winner) = final_entries.get_mut(&path) {
3787                if start >= winner.start {
3788                    winner.start = start;
3789                    winner.file_data_size = file.file_data_size;
3790                    winner.shard_index = shard_index;
3791                    winner.file_index = idx;
3792                }
3793            } else {
3794                final_entries.insert(
3795                    path,
3796                    WinningIndexEntry {
3797                        start,
3798                        file_data_size: file.file_data_size,
3799                        shard_index,
3800                        file_index: idx,
3801                    },
3802                );
3803            }
3804        }
3805    }
3806    Ok(final_entries)
3807}
3808
3809fn archive_index_entry_from_loaded_file(
3810    shard: &IndexShard,
3811    file_index: usize,
3812) -> Result<ArchiveIndexEntry, FormatError> {
3813    let file = shard
3814        .files
3815        .get(file_index)
3816        .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3817    let path = utf8_path(
3818        shard
3819            .file_path(file_index)
3820            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3821    )?;
3822    Ok(ArchiveIndexEntry {
3823        path,
3824        file_data_size: file.file_data_size,
3825    })
3826}
3827
3828#[derive(Debug)]
3829struct ParsedSeekableVolume {
3830    volume_header: VolumeHeader,
3831    crypto_header: CryptoHeaderFixed,
3832    crypto_header_bytes: Vec<u8>,
3833    key_wrap_table_bytes: Option<Vec<u8>>,
3834    subkeys: Subkeys,
3835    manifest_footer: Option<ManifestFooter>,
3836    manifest_footer_error: Option<FormatError>,
3837    root_auth_footer: Option<RootAuthFooterV1>,
3838    root_auth_footer_bytes: Option<Vec<u8>>,
3839    volume_trailer: VolumeTrailer,
3840    blocks: BTreeMap<u64, BlockRecord>,
3841    erased_block_indices: BTreeSet<u64>,
3842}
3843
3844struct ParsedSeekableReadAtVolume {
3845    reader: Arc<dyn ArchiveReadAt>,
3846    volume_header: VolumeHeader,
3847    crypto_header: CryptoHeaderFixed,
3848    crypto_header_bytes: Vec<u8>,
3849    key_wrap_table_bytes: Option<Vec<u8>>,
3850    subkeys: Subkeys,
3851    manifest_footer: Option<ManifestFooter>,
3852    manifest_footer_error: Option<FormatError>,
3853    root_auth_footer: Option<RootAuthFooterV1>,
3854    root_auth_footer_bytes: Option<Vec<u8>>,
3855    volume_trailer: VolumeTrailer,
3856    block_records_start: u64,
3857}
3858
3859struct ParsedOpenPrefix {
3860    volume_header: VolumeHeader,
3861    crypto_header: CryptoHeaderFixed,
3862    crypto_header_bytes: Vec<u8>,
3863    key_wrap_table_bytes: Option<Vec<u8>>,
3864    block_records_start: u64,
3865    subkeys: Subkeys,
3866}
3867
3868struct ParsedReadAtOpenPrefix {
3869    volume_header: VolumeHeader,
3870    crypto_header: CryptoHeaderFixed,
3871    crypto_header_bytes: Vec<u8>,
3872    key_wrap_table_bytes: Option<Vec<u8>>,
3873    block_records_start: u64,
3874    subkeys: Subkeys,
3875}
3876
3877pub(crate) struct StartupKeyWrapTable {
3878    pub(crate) table: KeyWrapTableV1,
3879    pub(crate) bytes: Vec<u8>,
3880    pub(crate) block_records_start: u64,
3881}
3882
3883pub(crate) fn startup_block_records_start(
3884    volume_header: &VolumeHeader,
3885    kdf_params: &KdfParams,
3886    read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3887) -> Result<u64, FormatError> {
3888    Ok(
3889        startup_key_wrap_table(volume_header, kdf_params, read_key_wrap_table)?
3890            .map(|startup| startup.block_records_start)
3891            .unwrap_or_else(|| {
3892                volume_header.crypto_header_offset as u64
3893                    + volume_header.crypto_header_length as u64
3894            }),
3895    )
3896}
3897
3898pub(crate) fn startup_key_wrap_table(
3899    volume_header: &VolumeHeader,
3900    kdf_params: &KdfParams,
3901    mut read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3902) -> Result<Option<StartupKeyWrapTable>, FormatError> {
3903    let crypto_end = checked_u64_add(
3904        volume_header.crypto_header_offset as u64,
3905        volume_header.crypto_header_length as u64,
3906        "CryptoHeader",
3907    )?;
3908    // v43 is still supported by the reader for legacy compatibility.
3909    // Key-wrap table recovery is only enabled for v44 archives.
3910    if volume_header.volume_format_rev == VOLUME_FORMAT_REV_44 {
3911        let &KdfParams::RecipientWrap {
3912            key_wrap_table_length,
3913            ..
3914        } = kdf_params
3915        else {
3916            return Ok(None);
3917        };
3918        let key_wrap_table_length_usize =
3919            to_usize(u64::from(key_wrap_table_length), "KeyWrapTableV1 length")?;
3920        let key_wrap_table_bytes = read_key_wrap_table(crypto_end, key_wrap_table_length_usize)?;
3921        Ok(Some(parse_startup_key_wrap_table_bytes(
3922            volume_header,
3923            kdf_params,
3924            key_wrap_table_bytes,
3925        )?))
3926    } else if matches!(kdf_params, KdfParams::RecipientWrap { .. }) {
3927        Err(FormatError::InvalidArchive(
3928            "RecipientWrap KdfParams require volume_format_rev 44",
3929        ))
3930    } else {
3931        Ok(None)
3932    }
3933}
3934
3935pub(crate) fn parse_startup_key_wrap_table_bytes(
3936    volume_header: &VolumeHeader,
3937    kdf_params: &KdfParams,
3938    key_wrap_table_bytes: Vec<u8>,
3939) -> Result<StartupKeyWrapTable, FormatError> {
3940    let crypto_end = checked_u64_add(
3941        volume_header.crypto_header_offset as u64,
3942        volume_header.crypto_header_length as u64,
3943        "CryptoHeader",
3944    )?;
3945    let KdfParams::RecipientWrap {
3946        key_wrap_table_length,
3947        key_wrap_table_record_count,
3948        key_wrap_table_digest,
3949        ..
3950    } = kdf_params
3951    else {
3952        return Err(FormatError::KeyMaterialMismatch);
3953    };
3954    let key_wrap_table = KeyWrapTableV1::parse(
3955        &key_wrap_table_bytes,
3956        &volume_header.archive_uuid,
3957        &volume_header.session_id,
3958        *key_wrap_table_length,
3959        *key_wrap_table_record_count,
3960    )?;
3961    if compute_key_wrap_table_digest(*key_wrap_table_length, &key_wrap_table_bytes)
3962        != *key_wrap_table_digest
3963    {
3964        return Err(FormatError::IntegrityDigestMismatch {
3965            structure: "KeyWrapTableV1",
3966        });
3967    }
3968    let block_records_start = checked_u64_add(
3969        crypto_end,
3970        key_wrap_table.table_length as u64,
3971        "KeyWrapTableV1",
3972    )?;
3973    Ok(StartupKeyWrapTable {
3974        table: key_wrap_table,
3975        bytes: key_wrap_table_bytes,
3976        block_records_start,
3977    })
3978}
3979
3980fn parse_seekable_volume(
3981    bytes: &[u8],
3982    master_key: &MasterKey,
3983    options: ReaderOptions,
3984) -> Result<ParsedSeekableVolume, FormatError> {
3985    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3986        return Err(FormatError::InvalidLength {
3987            structure: "archive",
3988            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3989            actual: bytes.len(),
3990        });
3991    }
3992
3993    let prefix = match parse_open_prefix(bytes, master_key) {
3994        Ok(prefix) => prefix,
3995        Err(prefix_err) => {
3996            if matches!(
3997                prefix_err,
3998                FormatError::UnsupportedVolumeFormatRevision { .. }
3999            ) {
4000                return Err(prefix_err);
4001            }
4002            if matches!(prefix_err, FormatError::KeyMaterialMismatch)
4003                && prefix_uses_recipient_wrap(bytes)
4004            {
4005                return Err(prefix_err);
4006            }
4007            return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
4008                .or(Err(prefix_err));
4009        }
4010    };
4011    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4012    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4013        Ok(parsed) => Ok(parsed),
4014        Err(prefix_err) => {
4015            match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
4016                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4017                    Ok(recovered)
4018                }
4019                Ok(_) | Err(_) => Err(prefix_err),
4020            }
4021        }
4022    }
4023}
4024
4025fn parse_seekable_volume_with_recipient_wrap_resolver<F>(
4026    bytes: &[u8],
4027    resolver: &mut F,
4028    options: ReaderOptions,
4029) -> Result<ParsedSeekableVolume, FormatError>
4030where
4031    F: FnMut(
4032        RecipientWrapRecordContext<'_>,
4033    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4034{
4035    let prefix = match parse_open_prefix_with_recipient_wrap_resolver(bytes, resolver) {
4036        Ok(prefix) => prefix,
4037        Err(prefix_err) => {
4038            if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4039                return Err(prefix_err);
4040            }
4041            return parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4042                bytes, resolver, options,
4043            )
4044            .or(Err(prefix_err));
4045        }
4046    };
4047    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4048    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4049        Ok(parsed) => Ok(parsed),
4050        Err(prefix_err) => {
4051            match parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4052                bytes, resolver, options,
4053            ) {
4054                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4055                    Ok(recovered)
4056                }
4057                Ok(_) | Err(_) => Err(prefix_err),
4058            }
4059        }
4060    }
4061}
4062
4063fn parse_seekable_volume_with_prefix(
4064    bytes: &[u8],
4065    prefix: ParsedOpenPrefix,
4066    options: ReaderOptions,
4067) -> Result<ParsedSeekableVolume, FormatError> {
4068    let ParsedOpenPrefix {
4069        volume_header,
4070        crypto_header,
4071        crypto_header_bytes,
4072        key_wrap_table_bytes,
4073        block_records_start,
4074        subkeys,
4075    } = prefix;
4076    let crypto_bytes = crypto_header_bytes.as_slice();
4077
4078    let terminal = locate_v41_terminal(
4079        bytes,
4080        KeyHoldingTerminalContext {
4081            subkeys: &subkeys,
4082            volume_header: &volume_header,
4083            crypto_header: &crypto_header,
4084            crypto_header_bytes: crypto_bytes,
4085        },
4086        options,
4087    )?;
4088    finish_parse_seekable_volume(
4089        bytes,
4090        volume_header,
4091        crypto_header,
4092        crypto_header_bytes,
4093        key_wrap_table_bytes,
4094        block_records_start,
4095        subkeys,
4096        terminal,
4097    )
4098}
4099
4100fn parse_seekable_volume_from_recovered_terminal(
4101    bytes: &[u8],
4102    master_key: &MasterKey,
4103    options: ReaderOptions,
4104) -> Result<ParsedSeekableVolume, FormatError> {
4105    let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
4106    parse_volume_format_dispatch(&authority.volume_header)?;
4107    let startup_key_wrap_table = startup_key_wrap_table(
4108        &authority.volume_header,
4109        &authority.kdf_params,
4110        |start, length| {
4111            let start = to_usize(start, "KeyWrapTableV1")?;
4112            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4113        },
4114    )?;
4115    let crypto_end = checked_u64_add(
4116        authority.volume_header.crypto_header_offset as u64,
4117        authority.volume_header.crypto_header_length as u64,
4118        "CryptoHeader",
4119    )?;
4120    let (key_wrap_table_bytes, block_records_start) = startup_key_wrap_table
4121        .map(|startup| (Some(startup.bytes), startup.block_records_start))
4122        .unwrap_or((None, crypto_end));
4123    finish_parse_seekable_volume(
4124        bytes,
4125        authority.volume_header,
4126        authority.crypto_header,
4127        authority.crypto_header_bytes,
4128        key_wrap_table_bytes,
4129        block_records_start,
4130        authority.subkeys,
4131        authority.terminal,
4132    )
4133}
4134
4135fn parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4136    bytes: &[u8],
4137    resolver: &mut F,
4138    options: ReaderOptions,
4139) -> Result<ParsedSeekableVolume, FormatError>
4140where
4141    F: FnMut(
4142        RecipientWrapRecordContext<'_>,
4143    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4144{
4145    let authority = locate_v41_recipient_wrap_terminal_authority(bytes, resolver, options)?;
4146    finish_parse_seekable_volume(
4147        bytes,
4148        authority.volume_header,
4149        authority.crypto_header,
4150        authority.crypto_header_bytes,
4151        Some(authority.key_wrap_table_bytes),
4152        authority.block_records_start,
4153        authority.subkeys,
4154        authority.terminal,
4155    )
4156}
4157
4158fn recipient_wrap_prefix_error_precludes_recovery(error: &FormatError) -> bool {
4159    matches!(
4160        error,
4161        FormatError::UnsupportedVolumeFormatRevision { .. }
4162            | FormatError::ReaderUnsupported(_)
4163            | FormatError::InvalidArchive(
4164                "VolumeHeader and CryptoHeader stripe_width differ"
4165                    | "fec_parity_shards does not match v41 compute_parity"
4166                    | "index_fec_parity_shards does not match v41 compute_parity"
4167                    | "index_root_fec_parity_shards does not match v41 compute_parity"
4168            )
4169    )
4170}
4171
4172fn parse_open_prefix(
4173    bytes: &[u8],
4174    master_key: &MasterKey,
4175) -> Result<ParsedOpenPrefix, FormatError> {
4176    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4177    parse_volume_format_dispatch(&volume_header)?;
4178    let crypto_start = volume_header.crypto_header_offset as usize;
4179    let crypto_len = volume_header.crypto_header_length as usize;
4180    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4181    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4182    if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4183        return Err(FormatError::KeyMaterialMismatch);
4184    }
4185    let subkeys = subkeys_for_open(
4186        Some(master_key),
4187        parsed_crypto.fixed.aead_algo,
4188        &volume_header.archive_uuid,
4189        &volume_header.session_id,
4190    )?;
4191    verify_integrity_tag(
4192        HmacDomain::CryptoHeader,
4193        parsed_crypto.fixed.aead_algo,
4194        volume_header.volume_format_rev,
4195        Some(&subkeys.mac_key),
4196        &volume_header.archive_uuid,
4197        &volume_header.session_id,
4198        parsed_crypto.hmac_covered_bytes,
4199        &parsed_crypto.header_hmac,
4200    )?;
4201    parsed_crypto.validate_extension_semantics()?;
4202    validate_seekable_supported_volume(
4203        &volume_header,
4204        &parsed_crypto.fixed,
4205        &parsed_crypto.extensions,
4206    )?;
4207    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4208    let block_records_start = startup_block_records_start(
4209        &volume_header,
4210        &parsed_crypto.kdf_params,
4211        |start, length| {
4212            let start = to_usize(start, "KeyWrapTableV1")?;
4213            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4214        },
4215    )?;
4216    let crypto_header = parsed_crypto.fixed.clone();
4217    Ok(ParsedOpenPrefix {
4218        volume_header,
4219        crypto_header,
4220        crypto_header_bytes: crypto_bytes.to_vec(),
4221        key_wrap_table_bytes: None,
4222        block_records_start,
4223        subkeys,
4224    })
4225}
4226
4227fn prefix_uses_recipient_wrap(bytes: &[u8]) -> bool {
4228    let Ok(volume_header_bytes) = slice(bytes, 0, VOLUME_HEADER_LEN, "archive") else {
4229        return false;
4230    };
4231    let Ok(volume_header) = VolumeHeader::parse(volume_header_bytes) else {
4232        return false;
4233    };
4234    let crypto_start = volume_header.crypto_header_offset as usize;
4235    let crypto_len = volume_header.crypto_header_length as usize;
4236    let Ok(crypto_bytes) = slice(bytes, crypto_start, crypto_len, "CryptoHeader") else {
4237        return false;
4238    };
4239    let Ok(parsed_crypto) = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)
4240    else {
4241        return false;
4242    };
4243    matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4244}
4245
4246fn parse_open_prefix_with_recipient_wrap_resolver<F>(
4247    bytes: &[u8],
4248    resolver: &mut F,
4249) -> Result<ParsedOpenPrefix, FormatError>
4250where
4251    F: FnMut(
4252        RecipientWrapRecordContext<'_>,
4253    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4254{
4255    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4256    parse_volume_format_dispatch(&volume_header)?;
4257    let crypto_start = volume_header.crypto_header_offset as usize;
4258    let crypto_len = volume_header.crypto_header_length as usize;
4259    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4260    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4261    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4262        || !parsed_crypto.fixed.aead_algo.is_encrypted()
4263    {
4264        return Err(FormatError::KeyMaterialMismatch);
4265    }
4266
4267    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4268    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4269
4270    let startup_key_wrap_table = startup_key_wrap_table(
4271        &volume_header,
4272        &parsed_crypto.kdf_params,
4273        |start, length| {
4274            let start = to_usize(start, "KeyWrapTableV1")?;
4275            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4276        },
4277    )?
4278    .ok_or(FormatError::KeyMaterialMismatch)?;
4279    let key_wrap_table = startup_key_wrap_table.table;
4280    let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4281    let block_records_start = startup_key_wrap_table.block_records_start;
4282
4283    let subkeys = recipient_wrap_subkeys_from_table(
4284        &volume_header,
4285        &parsed_crypto,
4286        &key_wrap_table,
4287        resolver,
4288    )?;
4289    parsed_crypto.validate_extension_semantics()?;
4290    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4291
4292    let crypto_header = parsed_crypto.fixed.clone();
4293    Ok(ParsedOpenPrefix {
4294        volume_header,
4295        crypto_header,
4296        crypto_header_bytes: crypto_bytes.to_vec(),
4297        key_wrap_table_bytes,
4298        block_records_start,
4299        subkeys,
4300    })
4301}
4302
4303pub(crate) fn recipient_wrap_subkeys_from_table<F>(
4304    volume_header: &VolumeHeader,
4305    parsed_crypto: &CryptoHeader<'_>,
4306    key_wrap_table: &KeyWrapTableV1,
4307    resolver: &mut F,
4308) -> Result<Subkeys, FormatError>
4309where
4310    F: FnMut(
4311            RecipientWrapRecordContext<'_>,
4312        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
4313        + ?Sized,
4314{
4315    let archive_identity = RecipientWrapArchiveIdentity {
4316        archive_uuid: volume_header.archive_uuid,
4317        session_id: volume_header.session_id,
4318        format_version: volume_header.format_version,
4319        volume_format_rev: volume_header.volume_format_rev,
4320    };
4321
4322    for record in &key_wrap_table.recipient_records {
4323        let candidates = resolver(RecipientWrapRecordContext {
4324            archive_identity,
4325            record,
4326        })?;
4327        for candidate in candidates {
4328            let master_key = MasterKey::from_raw_key(&candidate)?;
4329            let subkeys = subkeys_for_open(
4330                Some(&master_key),
4331                parsed_crypto.fixed.aead_algo,
4332                &volume_header.archive_uuid,
4333                &volume_header.session_id,
4334            )?;
4335            if verify_integrity_tag(
4336                HmacDomain::CryptoHeader,
4337                parsed_crypto.fixed.aead_algo,
4338                volume_header.volume_format_rev,
4339                Some(&subkeys.mac_key),
4340                &volume_header.archive_uuid,
4341                &volume_header.session_id,
4342                parsed_crypto.hmac_covered_bytes,
4343                &parsed_crypto.header_hmac,
4344            )
4345            .is_ok()
4346            {
4347                return Ok(subkeys);
4348            }
4349        }
4350    }
4351    Err(FormatError::KeyMaterialMismatch)
4352}
4353
4354#[allow(clippy::too_many_arguments)]
4355fn finish_parse_seekable_volume(
4356    bytes: &[u8],
4357    volume_header: VolumeHeader,
4358    crypto_header: CryptoHeaderFixed,
4359    crypto_header_bytes: Vec<u8>,
4360    key_wrap_table_bytes: Option<Vec<u8>>,
4361    block_records_start: u64,
4362    subkeys: Subkeys,
4363    terminal: V41Terminal,
4364) -> Result<ParsedSeekableVolume, FormatError> {
4365    let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
4366    let volume_trailer = terminal.volume_trailer.clone();
4367    validate_trailer_identity(&volume_header, &volume_trailer)?;
4368
4369    let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
4370    let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
4371    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4372        if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
4373            || volume_trailer
4374                .root_auth_footer_offset
4375                .checked_add(volume_trailer.root_auth_footer_length as u64)
4376                .ok_or(FormatError::InvalidArchive(
4377                    "RootAuthFooter terminal boundary overflow",
4378                ))?
4379                != trailer_offset as u64
4380        {
4381            return Err(FormatError::InvalidArchive(
4382                "RootAuthFooter does not sit before selected trailer",
4383            ));
4384        }
4385    } else if manifest_end != trailer_offset {
4386        return Err(FormatError::InvalidArchive(
4387            "ManifestFooter does not end at selected trailer",
4388        ));
4389    }
4390    let manifest_bytes = &terminal.manifest_footer_bytes;
4391    let (manifest_footer, manifest_footer_error) =
4392        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4393        {
4394            Ok(footer) => (Some(footer), None),
4395            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4396            Err(err) => return Err(err),
4397        };
4398
4399    let block_region = parse_block_region(
4400        bytes,
4401        to_usize(block_records_start, "BlockRecord")?,
4402        manifest_offset,
4403        crypto_header.block_size as usize,
4404        &volume_header,
4405        &volume_trailer,
4406    )?;
4407
4408    Ok(ParsedSeekableVolume {
4409        volume_header,
4410        crypto_header,
4411        crypto_header_bytes,
4412        key_wrap_table_bytes,
4413        subkeys,
4414        manifest_footer,
4415        manifest_footer_error,
4416        root_auth_footer: terminal.root_auth_footer,
4417        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4418        volume_trailer,
4419        blocks: block_region.blocks,
4420        erased_block_indices: block_region.erased_block_indices,
4421    })
4422}
4423
4424fn parse_seekable_read_at_volume(
4425    reader: Arc<dyn ArchiveReadAt>,
4426    master_key: &MasterKey,
4427    options: ReaderOptions,
4428) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4429    let observed_len = reader.len()?;
4430    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4431        return Err(FormatError::InvalidLength {
4432            structure: "archive",
4433            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4434            actual: to_usize(observed_len, "archive")?,
4435        });
4436    }
4437
4438    let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
4439        Ok(prefix) => prefix,
4440        Err(prefix_err) => {
4441            if matches!(
4442                prefix_err,
4443                FormatError::UnsupportedVolumeFormatRevision { .. }
4444            ) {
4445                return Err(prefix_err);
4446            }
4447            return parse_seekable_read_at_volume_from_recovered_terminal(
4448                reader,
4449                observed_len,
4450                master_key,
4451                options,
4452            )
4453            .or(Err(prefix_err));
4454        }
4455    };
4456    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4457    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4458        Ok(parsed) => Ok(parsed),
4459        Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
4460            reader,
4461            observed_len,
4462            master_key,
4463            options,
4464        ) {
4465            Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4466                Ok(recovered)
4467            }
4468            Ok(_) | Err(_) => Err(prefix_err),
4469        },
4470    }
4471}
4472
4473fn parse_seekable_read_at_volume_with_recipient_wrap_resolver<F>(
4474    reader: Arc<dyn ArchiveReadAt>,
4475    resolver: &mut F,
4476    options: ReaderOptions,
4477) -> Result<ParsedSeekableReadAtVolume, FormatError>
4478where
4479    F: FnMut(
4480        RecipientWrapRecordContext<'_>,
4481    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4482{
4483    let observed_len = reader.len()?;
4484    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4485        return Err(FormatError::InvalidLength {
4486            structure: "archive",
4487            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4488            actual: to_usize(observed_len, "archive")?,
4489        });
4490    }
4491
4492    let prefix = match parse_read_at_open_prefix_with_recipient_wrap_resolver(
4493        reader.as_ref(),
4494        resolver,
4495    ) {
4496        Ok(prefix) => prefix,
4497        Err(prefix_err) => {
4498            if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4499                return Err(prefix_err);
4500            }
4501            return parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4502                reader,
4503                observed_len,
4504                resolver,
4505                options,
4506            )
4507            .or(Err(prefix_err));
4508        }
4509    };
4510    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4511    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4512        Ok(parsed) => Ok(parsed),
4513        Err(prefix_err) => {
4514            match parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4515                reader,
4516                observed_len,
4517                resolver,
4518                options,
4519            ) {
4520                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4521                    Ok(recovered)
4522                }
4523                Ok(_) | Err(_) => Err(prefix_err),
4524            }
4525        }
4526    }
4527}
4528
4529fn parse_seekable_read_at_volume_with_prefix(
4530    reader: Arc<dyn ArchiveReadAt>,
4531    observed_len: u64,
4532    prefix: ParsedReadAtOpenPrefix,
4533    options: ReaderOptions,
4534) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4535    let ParsedReadAtOpenPrefix {
4536        volume_header,
4537        crypto_header,
4538        crypto_header_bytes,
4539        key_wrap_table_bytes,
4540        block_records_start,
4541        subkeys,
4542    } = prefix;
4543
4544    let terminal = locate_v41_terminal_read_at(
4545        reader.as_ref(),
4546        observed_len,
4547        KeyHoldingTerminalContext {
4548            subkeys: &subkeys,
4549            volume_header: &volume_header,
4550            crypto_header: &crypto_header,
4551            crypto_header_bytes: &crypto_header_bytes,
4552        },
4553        options,
4554    )?;
4555    finish_parse_seekable_read_at_volume(
4556        reader,
4557        volume_header,
4558        crypto_header,
4559        crypto_header_bytes,
4560        key_wrap_table_bytes,
4561        block_records_start,
4562        subkeys,
4563        terminal,
4564    )
4565}
4566
4567fn parse_seekable_read_at_volume_from_recovered_terminal(
4568    reader: Arc<dyn ArchiveReadAt>,
4569    observed_len: u64,
4570    master_key: &MasterKey,
4571    options: ReaderOptions,
4572) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4573    let authority =
4574        locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
4575    parse_volume_format_dispatch(&authority.volume_header)?;
4576    if matches!(authority.kdf_params, KdfParams::RecipientWrap { .. }) {
4577        return Err(FormatError::KeyMaterialMismatch);
4578    }
4579    let block_records_start = startup_block_records_start(
4580        &authority.volume_header,
4581        &authority.kdf_params,
4582        |start, length| read_at_vec(reader.as_ref(), start, length, "KeyWrapTableV1"),
4583    )?;
4584    finish_parse_seekable_read_at_volume(
4585        reader,
4586        authority.volume_header,
4587        authority.crypto_header,
4588        authority.crypto_header_bytes,
4589        None,
4590        block_records_start,
4591        authority.subkeys,
4592        authority.terminal,
4593    )
4594}
4595
4596fn parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4597    reader: Arc<dyn ArchiveReadAt>,
4598    observed_len: u64,
4599    resolver: &mut F,
4600    options: ReaderOptions,
4601) -> Result<ParsedSeekableReadAtVolume, FormatError>
4602where
4603    F: FnMut(
4604        RecipientWrapRecordContext<'_>,
4605    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4606{
4607    let authority = locate_v41_recipient_wrap_terminal_authority_read_at(
4608        reader.as_ref(),
4609        observed_len,
4610        resolver,
4611        options,
4612    )?;
4613    finish_parse_seekable_read_at_volume(
4614        reader,
4615        authority.volume_header,
4616        authority.crypto_header,
4617        authority.crypto_header_bytes,
4618        Some(authority.key_wrap_table_bytes),
4619        authority.block_records_start,
4620        authority.subkeys,
4621        authority.terminal,
4622    )
4623}
4624
4625fn parse_read_at_open_prefix(
4626    reader: &dyn ArchiveReadAt,
4627    master_key: &MasterKey,
4628) -> Result<ParsedReadAtOpenPrefix, FormatError> {
4629    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4630    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4631    parse_volume_format_dispatch(&volume_header)?;
4632    let crypto_start = volume_header.crypto_header_offset as u64;
4633    let crypto_len = volume_header.crypto_header_length as u64;
4634    let crypto_bytes = read_at_vec(
4635        reader,
4636        crypto_start,
4637        to_usize(crypto_len, "CryptoHeader")?,
4638        "CryptoHeader",
4639    )?;
4640    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4641    if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4642        return Err(FormatError::KeyMaterialMismatch);
4643    }
4644    let subkeys = subkeys_for_open(
4645        Some(master_key),
4646        parsed_crypto.fixed.aead_algo,
4647        &volume_header.archive_uuid,
4648        &volume_header.session_id,
4649    )?;
4650    verify_integrity_tag(
4651        HmacDomain::CryptoHeader,
4652        parsed_crypto.fixed.aead_algo,
4653        volume_header.volume_format_rev,
4654        Some(&subkeys.mac_key),
4655        &volume_header.archive_uuid,
4656        &volume_header.session_id,
4657        parsed_crypto.hmac_covered_bytes,
4658        &parsed_crypto.header_hmac,
4659    )?;
4660    parsed_crypto.validate_extension_semantics()?;
4661    validate_seekable_supported_volume(
4662        &volume_header,
4663        &parsed_crypto.fixed,
4664        &parsed_crypto.extensions,
4665    )?;
4666    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4667    let block_records_start = startup_block_records_start(
4668        &volume_header,
4669        &parsed_crypto.kdf_params,
4670        |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4671    )?;
4672    let crypto_header = parsed_crypto.fixed.clone();
4673    drop(parsed_crypto);
4674    Ok(ParsedReadAtOpenPrefix {
4675        volume_header,
4676        crypto_header,
4677        crypto_header_bytes: crypto_bytes,
4678        key_wrap_table_bytes: None,
4679        block_records_start,
4680        subkeys,
4681    })
4682}
4683
4684fn parse_read_at_open_prefix_with_recipient_wrap_resolver<F>(
4685    reader: &dyn ArchiveReadAt,
4686    resolver: &mut F,
4687) -> Result<ParsedReadAtOpenPrefix, FormatError>
4688where
4689    F: FnMut(
4690        RecipientWrapRecordContext<'_>,
4691    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4692{
4693    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4694    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4695    parse_volume_format_dispatch(&volume_header)?;
4696    let crypto_start = volume_header.crypto_header_offset as u64;
4697    let crypto_len = volume_header.crypto_header_length as u64;
4698    let crypto_bytes = read_at_vec(
4699        reader,
4700        crypto_start,
4701        to_usize(crypto_len, "CryptoHeader")?,
4702        "CryptoHeader",
4703    )?;
4704    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4705    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4706        || !parsed_crypto.fixed.aead_algo.is_encrypted()
4707    {
4708        return Err(FormatError::KeyMaterialMismatch);
4709    }
4710
4711    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4712    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4713
4714    let startup_key_wrap_table = startup_key_wrap_table(
4715        &volume_header,
4716        &parsed_crypto.kdf_params,
4717        |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4718    )?
4719    .ok_or(FormatError::KeyMaterialMismatch)?;
4720    let key_wrap_table = startup_key_wrap_table.table;
4721    let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4722    let block_records_start = startup_key_wrap_table.block_records_start;
4723
4724    let subkeys = recipient_wrap_subkeys_from_table(
4725        &volume_header,
4726        &parsed_crypto,
4727        &key_wrap_table,
4728        resolver,
4729    )?;
4730    parsed_crypto.validate_extension_semantics()?;
4731    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4732
4733    let crypto_header = parsed_crypto.fixed.clone();
4734    Ok(ParsedReadAtOpenPrefix {
4735        volume_header,
4736        crypto_header,
4737        crypto_header_bytes: crypto_bytes,
4738        key_wrap_table_bytes,
4739        block_records_start,
4740        subkeys,
4741    })
4742}
4743
4744#[allow(clippy::too_many_arguments)]
4745fn finish_parse_seekable_read_at_volume(
4746    reader: Arc<dyn ArchiveReadAt>,
4747    volume_header: VolumeHeader,
4748    crypto_header: CryptoHeaderFixed,
4749    crypto_header_bytes: Vec<u8>,
4750    key_wrap_table_bytes: Option<Vec<u8>>,
4751    block_records_start: u64,
4752    subkeys: Subkeys,
4753    terminal: V41Terminal,
4754) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4755    let volume_trailer = terminal.volume_trailer.clone();
4756    validate_trailer_identity(&volume_header, &volume_trailer)?;
4757
4758    let manifest_offset = volume_trailer.manifest_footer_offset;
4759    let manifest_end = checked_u64_add(
4760        manifest_offset,
4761        MANIFEST_FOOTER_LEN as u64,
4762        "ManifestFooter",
4763    )?;
4764    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4765        if volume_trailer.root_auth_footer_offset != manifest_end
4766            || volume_trailer
4767                .root_auth_footer_offset
4768                .checked_add(volume_trailer.root_auth_footer_length as u64)
4769                .ok_or(FormatError::InvalidArchive(
4770                    "RootAuthFooter terminal boundary overflow",
4771                ))?
4772                != terminal.image.volume_trailer_offset
4773        {
4774            return Err(FormatError::InvalidArchive(
4775                "RootAuthFooter does not sit before selected trailer",
4776            ));
4777        }
4778    } else if manifest_end != terminal.image.volume_trailer_offset {
4779        return Err(FormatError::InvalidArchive(
4780            "ManifestFooter does not end at selected trailer",
4781        ));
4782    }
4783    validate_seekable_block_region_layout(
4784        block_records_start,
4785        manifest_offset,
4786        crypto_header.block_size as usize,
4787        &volume_trailer,
4788    )?;
4789
4790    let manifest_bytes = &terminal.manifest_footer_bytes;
4791    let (manifest_footer, manifest_footer_error) =
4792        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4793        {
4794            Ok(footer) => (Some(footer), None),
4795            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4796            Err(err) => return Err(err),
4797        };
4798
4799    Ok(ParsedSeekableReadAtVolume {
4800        reader,
4801        volume_header,
4802        crypto_header,
4803        crypto_header_bytes,
4804        key_wrap_table_bytes,
4805        subkeys,
4806        manifest_footer,
4807        manifest_footer_error,
4808        root_auth_footer: terminal.root_auth_footer,
4809        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4810        volume_trailer,
4811        block_records_start,
4812    })
4813}
4814
4815#[derive(Debug)]
4816struct ParsedPublicNoKeyVolume {
4817    volume_header: VolumeHeader,
4818    crypto_header: CryptoHeaderFixed,
4819    kdf_params: KdfParams,
4820    root_auth_footer: RootAuthFooterV1,
4821    root_auth_footer_bytes: Vec<u8>,
4822    blocks: BTreeMap<u64, BlockRecord>,
4823}
4824
4825pub fn public_no_key_verify_volumes_with_options<F>(
4826    volumes: &[&[u8]],
4827    mut verifier: F,
4828    options: ReaderOptions,
4829) -> Result<PublicNoKeyVerification, FormatError>
4830where
4831    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
4832{
4833    validate_reader_options(options)?;
4834    if volumes.is_empty() {
4835        return Err(FormatError::InvalidArchive("no volumes supplied"));
4836    }
4837    let mut parsed = Vec::with_capacity(volumes.len());
4838    for volume in volumes {
4839        parsed.push(parse_public_no_key_volume(volume, options)?);
4840    }
4841    let first = parsed
4842        .first()
4843        .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
4844    if parsed.len() != first.crypto_header.stripe_width as usize {
4845        return Err(FormatError::ReaderUnsupported(
4846            "public no-key verification requires a complete volume set",
4847        ));
4848    }
4849
4850    let mut seen_volume_indexes = BTreeSet::new();
4851    let mut blocks = BTreeMap::new();
4852    for volume in &parsed {
4853        if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
4854            || volume.volume_header.session_id != first.volume_header.session_id
4855            || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
4856            || !public_kdf_profiles_agree(&volume.kdf_params, &first.kdf_params)
4857        {
4858            return Err(FormatError::InvalidArchive(
4859                "public no-key volume global metadata differs",
4860            ));
4861        }
4862        if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
4863            return Err(FormatError::InvalidArchive(
4864                "public no-key RootAuthFooter copies differ",
4865            ));
4866        }
4867        if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
4868            return Err(FormatError::InvalidArchive(
4869                "duplicate public no-key volume index",
4870            ));
4871        }
4872        for (block_index, record) in &volume.blocks {
4873            if blocks.insert(*block_index, record.clone()).is_some() {
4874                return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
4875            }
4876        }
4877    }
4878    validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
4879
4880    let footer = &first.root_auth_footer;
4881    let mut data_leaves = blocks
4882        .values()
4883        .filter(|record| record.kind.is_data())
4884        .map(|record| DataBlockMerkleLeaf {
4885            block_index: record.block_index,
4886            kind: record.kind,
4887            flags: record.flags,
4888            payload: record.payload.clone(),
4889        })
4890        .collect::<Vec<_>>();
4891    data_leaves.sort_by_key(|leaf| leaf.block_index);
4892    let total_data_block_count = u64::try_from(data_leaves.len())
4893        .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
4894    let observed_data_root = data_block_merkle_root_for_revision(
4895        footer.format_version,
4896        footer.volume_format_rev,
4897        &data_leaves,
4898    )?;
4899    if total_data_block_count != footer.total_data_block_count
4900        || observed_data_root != footer.data_block_merkle_root
4901    {
4902        return Err(FormatError::InvalidArchive(
4903            "public no-key data-block commitment mismatch",
4904        ));
4905    }
4906    let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
4907    if archive_root != footer.archive_root {
4908        return Err(FormatError::InvalidArchive(
4909            "public no-key archive_root mismatch",
4910        ));
4911    }
4912    if !verifier(footer, &archive_root)? {
4913        return Err(FormatError::InvalidArchive(
4914            "public no-key authenticator verification failed",
4915        ));
4916    }
4917    Ok(PublicNoKeyVerification {
4918        format_version: footer.format_version,
4919        volume_format_rev: footer.volume_format_rev,
4920        archive_root,
4921        authenticator_id: footer.authenticator_id,
4922        signer_identity_type: footer.signer_identity_type,
4923        signer_identity_bytes: footer.signer_identity_bytes.clone(),
4924        total_data_block_count,
4925        diagnostics: vec![
4926            PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
4927            PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
4928            PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
4929        ],
4930    })
4931}
4932
4933fn parse_public_no_key_volume(
4934    bytes: &[u8],
4935    options: ReaderOptions,
4936) -> Result<ParsedPublicNoKeyVolume, FormatError> {
4937    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
4938        return Err(FormatError::InvalidLength {
4939            structure: "archive",
4940            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4941            actual: bytes.len(),
4942        });
4943    }
4944    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4945    parse_volume_format_dispatch(&volume_header)?;
4946    let crypto_start = volume_header.crypto_header_offset as usize;
4947    let crypto_len = volume_header.crypto_header_length as usize;
4948    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
4949    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4950    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4951    parsed_crypto.validate_extension_semantics()?;
4952    validate_seekable_supported_volume(
4953        &volume_header,
4954        &parsed_crypto.fixed,
4955        &parsed_crypto.extensions,
4956    )?;
4957    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4958
4959    let terminal = locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto, options)?;
4960    let block_records_start = match &parsed_crypto.kdf_params {
4961        KdfParams::RecipientWrap {
4962            key_wrap_table_length,
4963            ..
4964        } => checked_add(
4965            crypto_end,
4966            *key_wrap_table_length as usize,
4967            "KeyWrapTableV1",
4968        )?,
4969        _ => crypto_end,
4970    };
4971    let block_region = parse_public_block_observation(
4972        bytes,
4973        block_records_start,
4974        &terminal.image,
4975        parsed_crypto.fixed.block_size as usize,
4976        &volume_header,
4977    )?;
4978    Ok(ParsedPublicNoKeyVolume {
4979        volume_header,
4980        crypto_header: parsed_crypto.fixed,
4981        kdf_params: parsed_crypto.kdf_params,
4982        root_auth_footer: terminal.root_auth_footer,
4983        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4984        blocks: block_region,
4985    })
4986}
4987
4988fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
4989    left.length == right.length
4990        && left.stripe_width == right.stripe_width
4991        && left.block_size == right.block_size
4992        && left.compression_algo == right.compression_algo
4993        && left.aead_algo == right.aead_algo
4994        && left.fec_algo == right.fec_algo
4995        && left.kdf_algo == right.kdf_algo
4996}
4997
4998fn public_kdf_profiles_agree(left: &KdfParams, right: &KdfParams) -> bool {
4999    match (left, right) {
5000        (
5001            KdfParams::RecipientWrap {
5002                key_wrap_table_length: left_length,
5003                key_wrap_table_record_count: left_count,
5004                key_wrap_table_version: left_version,
5005                key_wrap_table_digest: left_digest,
5006            },
5007            KdfParams::RecipientWrap {
5008                key_wrap_table_length: right_length,
5009                key_wrap_table_record_count: right_count,
5010                key_wrap_table_version: right_version,
5011                key_wrap_table_digest: right_digest,
5012            },
5013        ) => {
5014            left_length == right_length
5015                && left_count == right_count
5016                && left_version == right_version
5017                && left_digest == right_digest
5018        }
5019        (KdfParams::RecipientWrap { .. }, _) | (_, KdfParams::RecipientWrap { .. }) => false,
5020        _ => true,
5021    }
5022}
5023
5024fn recompute_public_archive_root(
5025    footer: &RootAuthFooterV1,
5026    crypto_header: &CryptoHeaderFixed,
5027) -> Result<[u8; 32], FormatError> {
5028    let descriptor_digest = root_auth_descriptor_digest_for_revision(
5029        footer.format_version,
5030        footer.volume_format_rev,
5031        footer.authenticator_id,
5032        footer.signer_identity_type,
5033        &footer.signer_identity_bytes,
5034        u32::try_from(footer.authenticator_value.len()).map_err(|_| {
5035            FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
5036        })?,
5037        footer.footer_length()?,
5038    )?;
5039    let signer_digest =
5040        signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
5041    if signer_digest != footer.signer_identity_digest {
5042        return Err(FormatError::InvalidArchive(
5043            "public no-key signer identity digest mismatch",
5044        ));
5045    }
5046    archive_root_for_revision(ArchiveRootInputs {
5047        archive_uuid: footer.archive_uuid,
5048        session_id: footer.session_id,
5049        format_version: footer.format_version,
5050        volume_format_rev: footer.volume_format_rev,
5051        compression_algo: crypto_header.compression_algo,
5052        aead_algo: crypto_header.aead_algo,
5053        fec_algo: crypto_header.fec_algo,
5054        kdf_algo: crypto_header.kdf_algo,
5055        critical_metadata_digest: footer.critical_metadata_digest,
5056        index_digest: footer.index_digest,
5057        fec_layout_digest: footer.fec_layout_digest,
5058        total_data_block_count: footer.total_data_block_count,
5059        data_block_merkle_root: footer.data_block_merkle_root,
5060        root_auth_descriptor_digest: descriptor_digest,
5061        signer_identity_digest: signer_digest,
5062    })
5063}
5064
5065fn parse_valid_manifest_footer(
5066    volume_header: &VolumeHeader,
5067    crypto_header: &CryptoHeaderFixed,
5068    subkeys: &Subkeys,
5069    manifest_bytes: &[u8],
5070) -> Result<ManifestFooter, FormatError> {
5071    let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5072    validate_manifest_footer(
5073        volume_header,
5074        crypto_header,
5075        &manifest_footer,
5076        subkeys,
5077        volume_header.volume_format_rev,
5078        manifest_bytes,
5079    )?;
5080    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5081    Ok(manifest_footer)
5082}
5083
5084fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
5085    matches!(
5086        error,
5087        FormatError::BadMagic {
5088            structure: "ManifestFooter",
5089        } | FormatError::NonZeroReserved {
5090            structure: "ManifestFooter",
5091        } | FormatError::InvalidAuthoritativeFlag(_)
5092            | FormatError::HmacMismatch {
5093                structure: "ManifestFooter",
5094            }
5095            | FormatError::IntegrityDigestMismatch {
5096                structure: "ManifestFooter",
5097            }
5098    )
5099}
5100
5101fn validate_seekable_supported_volume(
5102    volume_header: &VolumeHeader,
5103    crypto_header: &CryptoHeaderFixed,
5104    extensions: &[ExtensionTlv<'_>],
5105) -> Result<(), FormatError> {
5106    reject_unsupported_raw_stream_profile(extensions)?;
5107    if crypto_header.stripe_width != volume_header.stripe_width {
5108        return Err(FormatError::InvalidArchive(
5109            "VolumeHeader and CryptoHeader stripe_width differ",
5110        ));
5111    }
5112    Ok(())
5113}
5114
5115pub(crate) fn validate_crypto_class_parity_exactness(
5116    crypto_header: &CryptoHeaderFixed,
5117) -> Result<(), FormatError> {
5118    let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
5119    if crypto_header.fec_parity_shards as u32 != fec {
5120        return Err(FormatError::InvalidArchive(
5121            "fec_parity_shards does not match v41 compute_parity",
5122        ));
5123    }
5124    let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
5125    if crypto_header.index_fec_parity_shards as u32 != index {
5126        return Err(FormatError::InvalidArchive(
5127            "index_fec_parity_shards does not match v41 compute_parity",
5128        ));
5129    }
5130    let index_root = required_object_parity(
5131        crypto_header.index_root_fec_data_shards as u64,
5132        crypto_header,
5133    )?;
5134    if crypto_header.index_root_fec_parity_shards as u32 != index_root {
5135        return Err(FormatError::InvalidArchive(
5136            "index_root_fec_parity_shards does not match v41 compute_parity",
5137        ));
5138    }
5139    Ok(())
5140}
5141
5142fn validate_volume_set_member(
5143    first: &ParsedSeekableVolume,
5144    candidate: &ParsedSeekableVolume,
5145) -> Result<(), FormatError> {
5146    validate_volume_set_member_metadata(
5147        &first.volume_header,
5148        &first.crypto_header,
5149        &first.crypto_header_bytes,
5150        &candidate.volume_header,
5151        &candidate.crypto_header,
5152        &candidate.crypto_header_bytes,
5153    )?;
5154    validate_key_wrap_table_bytes_match(
5155        &first.key_wrap_table_bytes,
5156        &candidate.key_wrap_table_bytes,
5157    )
5158}
5159
5160fn validate_key_wrap_table_bytes_match(
5161    first_key_wrap_table_bytes: &Option<Vec<u8>>,
5162    candidate_key_wrap_table_bytes: &Option<Vec<u8>>,
5163) -> Result<(), FormatError> {
5164    if candidate_key_wrap_table_bytes != first_key_wrap_table_bytes {
5165        return Err(FormatError::InvalidArchive("KeyWrapTableV1 copies differ"));
5166    }
5167    Ok(())
5168}
5169
5170fn validate_volume_set_member_metadata(
5171    first_volume_header: &VolumeHeader,
5172    first_crypto_header: &CryptoHeaderFixed,
5173    first_crypto_header_bytes: &[u8],
5174    candidate_volume_header: &VolumeHeader,
5175    candidate_crypto_header: &CryptoHeaderFixed,
5176    candidate_crypto_header_bytes: &[u8],
5177) -> Result<(), FormatError> {
5178    if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
5179        || candidate_volume_header.session_id != first_volume_header.session_id
5180    {
5181        return Err(FormatError::InvalidArchive(
5182            "mixed archive or session IDs in volume set",
5183        ));
5184    }
5185    if candidate_crypto_header_bytes != first_crypto_header_bytes
5186        || candidate_crypto_header != first_crypto_header
5187    {
5188        return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
5189    }
5190    Ok(())
5191}
5192
5193pub(crate) fn manifest_bootstrap_fields_match(
5194    left: &ManifestFooter,
5195    right: &ManifestFooter,
5196) -> bool {
5197    left.archive_uuid == right.archive_uuid
5198        && left.session_id == right.session_id
5199        && left.is_authoritative == right.is_authoritative
5200        && left.total_volumes == right.total_volumes
5201        && left.index_root_first_block == right.index_root_first_block
5202        && left.index_root_data_block_count == right.index_root_data_block_count
5203        && left.index_root_parity_block_count == right.index_root_parity_block_count
5204        && left.index_root_encrypted_size == right.index_root_encrypted_size
5205        && left.index_root_decompressed_size == right.index_root_decompressed_size
5206}
5207
5208fn validate_complete_global_block_coverage(
5209    blocks: &BTreeMap<u64, BlockRecord>,
5210    erased_block_indices: &BTreeSet<u64>,
5211) -> Result<(), FormatError> {
5212    let mut expected = 0u64;
5213    let mut block_iter = blocks.keys().copied().peekable();
5214    let mut erasure_iter = erased_block_indices.iter().copied().peekable();
5215
5216    loop {
5217        let next_block = block_iter.peek().copied();
5218        let next_erasure = erasure_iter.peek().copied();
5219        let next = match (next_block, next_erasure) {
5220            (Some(block), Some(erasure)) if block == erasure => {
5221                return Err(FormatError::InvalidArchive(
5222                    "BlockRecord index is both present and erased",
5223                ));
5224            }
5225            (Some(block), Some(erasure)) => block.min(erasure),
5226            (Some(block), None) => block,
5227            (None, Some(erasure)) => erasure,
5228            (None, None) => return Ok(()),
5229        };
5230
5231        if next != expected {
5232            return Err(FormatError::InvalidArchive(
5233                "complete volume set has missing global blocks",
5234            ));
5235        }
5236        if next_block == Some(next) {
5237            block_iter.next();
5238        }
5239        if next_erasure == Some(next) {
5240            erasure_iter.next();
5241        }
5242        expected = expected
5243            .checked_add(1)
5244            .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
5245    }
5246}
5247
5248#[derive(Debug)]
5249struct V41Terminal {
5250    image: CriticalMetadataImage,
5251    manifest_footer_bytes: Vec<u8>,
5252    root_auth_footer_bytes: Option<Vec<u8>>,
5253    root_auth_footer: Option<RootAuthFooterV1>,
5254    volume_trailer: VolumeTrailer,
5255}
5256
5257pub(crate) struct SequentialTerminalMaterial {
5258    pub(crate) manifest_footer: ManifestFooter,
5259    pub(crate) volume_trailer: VolumeTrailer,
5260    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
5261}
5262
5263#[derive(Debug)]
5264struct V41PublicTerminal {
5265    image: CriticalMetadataImage,
5266    root_auth_footer_bytes: Vec<u8>,
5267    root_auth_footer: RootAuthFooterV1,
5268}
5269
5270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5271struct CmraDecoderTuple {
5272    shard_size: u32,
5273    data_shard_count: u16,
5274    parity_shard_count: u16,
5275    image_length: u32,
5276    image_sha256: [u8; 32],
5277}
5278
5279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5280struct CmraIdentityHints {
5281    archive_uuid: [u8; 16],
5282    session_id: [u8; 16],
5283    volume_index: u32,
5284}
5285
5286impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
5287    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5288        Self {
5289            shard_size: header.shard_size,
5290            data_shard_count: header.data_shard_count,
5291            parity_shard_count: header.parity_shard_count,
5292            image_length: header.image_length,
5293            image_sha256: header.image_sha256,
5294        }
5295    }
5296}
5297
5298impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
5299    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5300        Self {
5301            archive_uuid: header.archive_uuid_hint,
5302            session_id: header.session_id_hint,
5303            volume_index: header.volume_index_hint,
5304        }
5305    }
5306}
5307
5308impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
5309    fn from(locator: CriticalRecoveryLocator) -> Self {
5310        Self {
5311            shard_size: locator.cmra_shard_size,
5312            data_shard_count: locator.cmra_data_shard_count,
5313            parity_shard_count: locator.cmra_parity_shard_count,
5314            image_length: locator.cmra_image_length,
5315            image_sha256: locator.cmra_image_sha256,
5316        }
5317    }
5318}
5319
5320impl From<CriticalRecoveryLocator> for CmraIdentityHints {
5321    fn from(locator: CriticalRecoveryLocator) -> Self {
5322        Self {
5323            archive_uuid: locator.archive_uuid_hint,
5324            session_id: locator.session_id_hint,
5325            volume_index: locator.volume_index_hint,
5326        }
5327    }
5328}
5329
5330#[derive(Debug)]
5331struct RecoveredCmra {
5332    image: CriticalMetadataImage,
5333    tuple: CmraDecoderTuple,
5334    header_hints: Option<CmraIdentityHints>,
5335    cmra_length: u64,
5336}
5337
5338#[derive(Debug)]
5339struct TerminalCandidate {
5340    terminal: V41Terminal,
5341    anchor: usize,
5342    locator_sequence: Option<u32>,
5343    cmra_offset: u64,
5344    cmra_length: u64,
5345}
5346
5347#[derive(Debug)]
5348struct PublicTerminalCandidate {
5349    terminal: V41PublicTerminal,
5350    anchor: usize,
5351    cmra_offset: u64,
5352    cmra_length: u64,
5353}
5354
5355#[derive(Debug)]
5356struct RecoveredTerminalAuthority {
5357    terminal: V41Terminal,
5358    volume_header: VolumeHeader,
5359    crypto_header: CryptoHeaderFixed,
5360    crypto_header_bytes: Vec<u8>,
5361    subkeys: Subkeys,
5362    kdf_params: KdfParams,
5363}
5364
5365#[derive(Debug)]
5366struct RecoveredRecipientWrapTerminalAuthority {
5367    terminal: V41Terminal,
5368    volume_header: VolumeHeader,
5369    crypto_header: CryptoHeaderFixed,
5370    crypto_header_bytes: Vec<u8>,
5371    key_wrap_table_bytes: Vec<u8>,
5372    block_records_start: u64,
5373    subkeys: Subkeys,
5374}
5375
5376#[derive(Debug)]
5377struct TerminalAuthorityCandidate {
5378    authority: RecoveredTerminalAuthority,
5379    anchor: usize,
5380    cmra_offset: u64,
5381    cmra_length: u64,
5382}
5383
5384#[derive(Debug)]
5385struct RecipientWrapTerminalAuthorityCandidate {
5386    authority: RecoveredRecipientWrapTerminalAuthority,
5387    anchor: usize,
5388    cmra_offset: u64,
5389    cmra_length: u64,
5390}
5391
5392#[derive(Debug, Clone, Copy)]
5393enum CmraRecoveryMode {
5394    KeyHolding,
5395    PublicNoKey,
5396}
5397
5398#[derive(Clone, Copy)]
5399pub(crate) struct KeyHoldingTerminalContext<'a> {
5400    pub(crate) subkeys: &'a Subkeys,
5401    pub(crate) volume_header: &'a VolumeHeader,
5402    pub(crate) crypto_header: &'a CryptoHeaderFixed,
5403    pub(crate) crypto_header_bytes: &'a [u8],
5404}
5405
5406fn locate_v41_terminal(
5407    bytes: &[u8],
5408    context: KeyHoldingTerminalContext<'_>,
5409    options: ReaderOptions,
5410) -> Result<V41Terminal, FormatError> {
5411    locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
5412}
5413
5414fn locate_v41_terminal_read_at(
5415    reader: &dyn ArchiveReadAt,
5416    len: u64,
5417    context: KeyHoldingTerminalContext<'_>,
5418    options: ReaderOptions,
5419) -> Result<V41Terminal, FormatError> {
5420    let mut candidates = Vec::new();
5421    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5422        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5423        collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
5424    }
5425    if len >= LOCATOR_PAIR_LEN as u64 {
5426        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5427        collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
5428    }
5429
5430    if candidates.is_empty() {
5431        let scan = max_critical_recovery_scan(options)? as u64;
5432        let scan_start = len.saturating_sub(scan);
5433        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5434        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5435        let mut offset = tail.len().saturating_sub(4);
5436        while offset < tail.len() {
5437            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5438            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5439                collect_v41_locator_candidate_read_at(
5440                    reader,
5441                    absolute_offset,
5442                    2,
5443                    context,
5444                    &mut candidates,
5445                );
5446            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5447                if let Ok(candidate) =
5448                    parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
5449                {
5450                    candidates.push(candidate);
5451                }
5452            }
5453            if offset == 0 {
5454                break;
5455            }
5456            offset -= 1;
5457        }
5458    }
5459
5460    choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5461}
5462
5463fn locate_v41_terminal_authority(
5464    bytes: &[u8],
5465    master_key: &MasterKey,
5466    options: ReaderOptions,
5467) -> Result<RecoveredTerminalAuthority, FormatError> {
5468    let mut candidates = Vec::new();
5469    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5470        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5471        collect_v41_locator_authority_candidate(
5472            bytes,
5473            final_offset,
5474            0,
5475            master_key,
5476            &mut candidates,
5477        );
5478    }
5479    if bytes.len() >= LOCATOR_PAIR_LEN {
5480        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5481        collect_v41_locator_authority_candidate(
5482            bytes,
5483            mirror_offset,
5484            1,
5485            master_key,
5486            &mut candidates,
5487        );
5488    }
5489
5490    if candidates.is_empty() {
5491        let scan = max_critical_recovery_scan(options)?;
5492        let scan_start = bytes.len().saturating_sub(scan);
5493        let mut offset = bytes.len().saturating_sub(4);
5494        while offset >= scan_start {
5495            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5496                collect_v41_locator_authority_candidate(
5497                    bytes,
5498                    offset,
5499                    2,
5500                    master_key,
5501                    &mut candidates,
5502                );
5503            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5504                if let Ok(candidate) =
5505                    parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
5506                {
5507                    candidates.push(candidate);
5508                }
5509            }
5510            if offset == 0 {
5511                break;
5512            }
5513            offset -= 1;
5514        }
5515    }
5516
5517    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5518}
5519
5520fn locate_v41_recipient_wrap_terminal_authority<F>(
5521    bytes: &[u8],
5522    resolver: &mut F,
5523    options: ReaderOptions,
5524) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5525where
5526    F: FnMut(
5527        RecipientWrapRecordContext<'_>,
5528    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5529{
5530    let mut candidates = Vec::new();
5531    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5532        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5533        collect_v41_recipient_wrap_locator_authority_candidate(
5534            bytes,
5535            final_offset,
5536            0,
5537            resolver,
5538            &mut candidates,
5539        );
5540    }
5541    if bytes.len() >= LOCATOR_PAIR_LEN {
5542        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5543        collect_v41_recipient_wrap_locator_authority_candidate(
5544            bytes,
5545            mirror_offset,
5546            1,
5547            resolver,
5548            &mut candidates,
5549        );
5550    }
5551
5552    if candidates.is_empty() {
5553        let scan = max_critical_recovery_scan(options)?;
5554        let scan_start = bytes.len().saturating_sub(scan);
5555        let mut offset = bytes.len().saturating_sub(4);
5556        while offset >= scan_start {
5557            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5558                collect_v41_recipient_wrap_locator_authority_candidate(
5559                    bytes,
5560                    offset,
5561                    2,
5562                    resolver,
5563                    &mut candidates,
5564                );
5565            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5566                if let Ok(candidate) = parse_locatorless_cmra_recipient_wrap_authority_candidate(
5567                    bytes, offset, resolver,
5568                ) {
5569                    candidates.push(candidate);
5570                }
5571            }
5572            if offset == 0 {
5573                break;
5574            }
5575            offset -= 1;
5576        }
5577    }
5578
5579    choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5580        .map(|candidate| candidate.authority)
5581}
5582
5583fn locate_v41_recipient_wrap_terminal_authority_read_at<F>(
5584    reader: &dyn ArchiveReadAt,
5585    len: u64,
5586    resolver: &mut F,
5587    options: ReaderOptions,
5588) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5589where
5590    F: FnMut(
5591        RecipientWrapRecordContext<'_>,
5592    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5593{
5594    let mut candidates = Vec::new();
5595    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5596        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5597        collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5598            reader,
5599            final_offset,
5600            0,
5601            resolver,
5602            &mut candidates,
5603        );
5604    }
5605    if len >= LOCATOR_PAIR_LEN as u64 {
5606        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5607        collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5608            reader,
5609            mirror_offset,
5610            1,
5611            resolver,
5612            &mut candidates,
5613        );
5614    }
5615
5616    if candidates.is_empty() {
5617        let scan = max_critical_recovery_scan(options)? as u64;
5618        let scan_start = len.saturating_sub(scan);
5619        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5620        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5621        let mut offset = tail.len().saturating_sub(4);
5622        while offset < tail.len() {
5623            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5624            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5625                collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5626                    reader,
5627                    absolute_offset,
5628                    2,
5629                    resolver,
5630                    &mut candidates,
5631                );
5632            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5633                if let Ok(candidate) =
5634                    parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at(
5635                        reader,
5636                        absolute_offset,
5637                        resolver,
5638                    )
5639                {
5640                    candidates.push(candidate);
5641                }
5642            }
5643            if offset == 0 {
5644                break;
5645            }
5646            offset -= 1;
5647        }
5648    }
5649
5650    choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5651        .map(|candidate| candidate.authority)
5652}
5653
5654fn locate_v41_terminal_authority_read_at(
5655    reader: &dyn ArchiveReadAt,
5656    len: u64,
5657    master_key: &MasterKey,
5658    options: ReaderOptions,
5659) -> Result<RecoveredTerminalAuthority, FormatError> {
5660    let mut candidates = Vec::new();
5661    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5662        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5663        collect_v41_locator_authority_candidate_read_at(
5664            reader,
5665            final_offset,
5666            0,
5667            master_key,
5668            &mut candidates,
5669        );
5670    }
5671    if len >= LOCATOR_PAIR_LEN as u64 {
5672        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5673        collect_v41_locator_authority_candidate_read_at(
5674            reader,
5675            mirror_offset,
5676            1,
5677            master_key,
5678            &mut candidates,
5679        );
5680    }
5681
5682    if candidates.is_empty() {
5683        let scan = max_critical_recovery_scan(options)? as u64;
5684        let scan_start = len.saturating_sub(scan);
5685        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5686        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5687        let mut offset = tail.len().saturating_sub(4);
5688        while offset < tail.len() {
5689            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5690            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5691                collect_v41_locator_authority_candidate_read_at(
5692                    reader,
5693                    absolute_offset,
5694                    2,
5695                    master_key,
5696                    &mut candidates,
5697                );
5698            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5699                if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
5700                    reader,
5701                    absolute_offset,
5702                    master_key,
5703                ) {
5704                    candidates.push(candidate);
5705                }
5706            }
5707            if offset == 0 {
5708                break;
5709            }
5710            offset -= 1;
5711        }
5712    }
5713
5714    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5715}
5716
5717fn locate_v41_terminal_candidate(
5718    bytes: &[u8],
5719    context: KeyHoldingTerminalContext<'_>,
5720    options: ReaderOptions,
5721) -> Result<TerminalCandidate, FormatError> {
5722    let mut candidates = Vec::new();
5723    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5724        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5725        collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
5726    }
5727    if bytes.len() >= LOCATOR_PAIR_LEN {
5728        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5729        collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
5730    }
5731
5732    if candidates.is_empty() {
5733        let scan = max_critical_recovery_scan(options)?;
5734        let scan_start = bytes.len().saturating_sub(scan);
5735        let mut offset = bytes.len().saturating_sub(4);
5736        while offset >= scan_start {
5737            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5738                collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
5739            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5740                if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
5741                    candidates.push(candidate);
5742                }
5743            }
5744            if offset == 0 {
5745                break;
5746            }
5747            offset -= 1;
5748        }
5749    }
5750
5751    choose_v41_terminal_candidate(candidates)
5752}
5753
5754fn locate_v41_public_terminal(
5755    bytes: &[u8],
5756    volume_header: &VolumeHeader,
5757    crypto_header: &CryptoHeader<'_>,
5758    options: ReaderOptions,
5759) -> Result<V41PublicTerminal, FormatError> {
5760    let mut candidates = Vec::new();
5761    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5762        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5763        collect_v41_public_locator_candidate(
5764            bytes,
5765            final_offset,
5766            0,
5767            volume_header,
5768            crypto_header,
5769            &mut candidates,
5770        );
5771    }
5772    if bytes.len() >= LOCATOR_PAIR_LEN {
5773        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5774        collect_v41_public_locator_candidate(
5775            bytes,
5776            mirror_offset,
5777            1,
5778            volume_header,
5779            crypto_header,
5780            &mut candidates,
5781        );
5782    }
5783
5784    if candidates.is_empty() {
5785        let scan = max_critical_recovery_scan(options)?;
5786        let scan_start = bytes.len().saturating_sub(scan);
5787        let mut offset = bytes.len().saturating_sub(4);
5788        while offset >= scan_start {
5789            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5790                collect_v41_public_locator_candidate(
5791                    bytes,
5792                    offset,
5793                    2,
5794                    volume_header,
5795                    crypto_header,
5796                    &mut candidates,
5797                );
5798            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5799                if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
5800                    bytes,
5801                    offset,
5802                    volume_header,
5803                    crypto_header,
5804                ) {
5805                    candidates.push(candidate);
5806                }
5807            }
5808            if offset == 0 {
5809                break;
5810            }
5811            offset -= 1;
5812        }
5813    }
5814
5815    choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5816}
5817
5818fn collect_v41_locator_candidate(
5819    bytes: &[u8],
5820    offset: usize,
5821    expected_sequence: u32,
5822    context: KeyHoldingTerminalContext<'_>,
5823    candidates: &mut Vec<TerminalCandidate>,
5824) {
5825    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5826        return;
5827    };
5828    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5829        return;
5830    };
5831    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5832        return;
5833    }
5834    if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
5835        candidates.push(candidate);
5836    }
5837}
5838
5839fn collect_v41_locator_candidate_read_at(
5840    reader: &dyn ArchiveReadAt,
5841    offset: u64,
5842    expected_sequence: u32,
5843    context: KeyHoldingTerminalContext<'_>,
5844    candidates: &mut Vec<TerminalCandidate>,
5845) {
5846    let Ok(raw) = read_at_vec(
5847        reader,
5848        offset,
5849        CRITICAL_RECOVERY_LOCATOR_LEN,
5850        "CriticalRecoveryLocator",
5851    ) else {
5852        return;
5853    };
5854    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5855        return;
5856    };
5857    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5858        return;
5859    }
5860    if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
5861        candidates.push(candidate);
5862    }
5863}
5864
5865fn collect_v41_locator_authority_candidate(
5866    bytes: &[u8],
5867    offset: usize,
5868    expected_sequence: u32,
5869    master_key: &MasterKey,
5870    candidates: &mut Vec<TerminalAuthorityCandidate>,
5871) {
5872    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5873        return;
5874    };
5875    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5876        return;
5877    };
5878    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5879        return;
5880    }
5881    if let Ok(candidate) =
5882        parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
5883    {
5884        candidates.push(candidate);
5885    }
5886}
5887
5888fn collect_v41_recipient_wrap_locator_authority_candidate<F>(
5889    bytes: &[u8],
5890    offset: usize,
5891    expected_sequence: u32,
5892    resolver: &mut F,
5893    candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
5894) where
5895    F: FnMut(
5896        RecipientWrapRecordContext<'_>,
5897    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5898{
5899    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5900        return;
5901    };
5902    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5903        return;
5904    };
5905    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5906        return;
5907    }
5908    if let Ok(candidate) =
5909        parse_locator_cmra_recipient_wrap_authority_candidate(bytes, offset, locator, resolver)
5910    {
5911        candidates.push(candidate);
5912    }
5913}
5914
5915fn collect_v41_recipient_wrap_locator_authority_candidate_read_at<F>(
5916    reader: &dyn ArchiveReadAt,
5917    offset: u64,
5918    expected_sequence: u32,
5919    resolver: &mut F,
5920    candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
5921) where
5922    F: FnMut(
5923        RecipientWrapRecordContext<'_>,
5924    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5925{
5926    let Ok(raw) = read_at_vec(
5927        reader,
5928        offset,
5929        CRITICAL_RECOVERY_LOCATOR_LEN,
5930        "CriticalRecoveryLocator",
5931    ) else {
5932        return;
5933    };
5934    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5935        return;
5936    };
5937    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5938        return;
5939    }
5940    if let Ok(candidate) = parse_locator_cmra_recipient_wrap_authority_candidate_read_at(
5941        reader, offset, locator, resolver,
5942    ) {
5943        candidates.push(candidate);
5944    }
5945}
5946
5947fn collect_v41_locator_authority_candidate_read_at(
5948    reader: &dyn ArchiveReadAt,
5949    offset: u64,
5950    expected_sequence: u32,
5951    master_key: &MasterKey,
5952    candidates: &mut Vec<TerminalAuthorityCandidate>,
5953) {
5954    let Ok(raw) = read_at_vec(
5955        reader,
5956        offset,
5957        CRITICAL_RECOVERY_LOCATOR_LEN,
5958        "CriticalRecoveryLocator",
5959    ) else {
5960        return;
5961    };
5962    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5963        return;
5964    };
5965    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5966        return;
5967    }
5968    if let Ok(candidate) =
5969        parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
5970    {
5971        candidates.push(candidate);
5972    }
5973}
5974
5975fn collect_v41_public_locator_candidate(
5976    bytes: &[u8],
5977    offset: usize,
5978    expected_sequence: u32,
5979    volume_header: &VolumeHeader,
5980    crypto_header: &CryptoHeader<'_>,
5981    candidates: &mut Vec<PublicTerminalCandidate>,
5982) {
5983    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5984        return;
5985    };
5986    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5987        return;
5988    };
5989    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5990        return;
5991    }
5992    if let Ok(candidate) =
5993        parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
5994    {
5995        candidates.push(candidate);
5996    }
5997}
5998
5999fn choose_v41_terminal_candidate(
6000    mut candidates: Vec<TerminalCandidate>,
6001) -> Result<TerminalCandidate, FormatError> {
6002    candidates.sort_by_key(|candidate| candidate.anchor);
6003    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6004        "no valid v41 CMRA candidate found",
6005    ))?;
6006    if let Some(previous) = candidates.last() {
6007        if previous.anchor == winner.anchor
6008            && (previous.cmra_offset != winner.cmra_offset
6009                || previous.cmra_length != winner.cmra_length)
6010        {
6011            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6012        }
6013    }
6014    Ok(winner)
6015}
6016
6017fn choose_v41_terminal_authority_candidate(
6018    mut candidates: Vec<TerminalAuthorityCandidate>,
6019) -> Result<TerminalAuthorityCandidate, FormatError> {
6020    candidates.sort_by_key(|candidate| candidate.anchor);
6021    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6022        "no valid v41 CMRA candidate found",
6023    ))?;
6024    if let Some(previous) = candidates.last() {
6025        if previous.anchor == winner.anchor
6026            && (previous.cmra_offset != winner.cmra_offset
6027                || previous.cmra_length != winner.cmra_length)
6028        {
6029            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6030        }
6031    }
6032    Ok(winner)
6033}
6034
6035fn choose_v41_recipient_wrap_terminal_authority_candidate(
6036    mut candidates: Vec<RecipientWrapTerminalAuthorityCandidate>,
6037) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError> {
6038    candidates.sort_by_key(|candidate| candidate.anchor);
6039    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6040        "no valid v41 CMRA candidate found",
6041    ))?;
6042    if let Some(previous) = candidates.last() {
6043        if previous.anchor == winner.anchor
6044            && (previous.cmra_offset != winner.cmra_offset
6045                || previous.cmra_length != winner.cmra_length)
6046        {
6047            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6048        }
6049    }
6050    Ok(winner)
6051}
6052
6053fn choose_v41_public_terminal_candidate(
6054    mut candidates: Vec<PublicTerminalCandidate>,
6055) -> Result<PublicTerminalCandidate, FormatError> {
6056    candidates.sort_by_key(|candidate| candidate.anchor);
6057    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6058        "no valid v41 public CMRA candidate found",
6059    ))?;
6060    if let Some(previous) = candidates.last() {
6061        if previous.anchor == winner.anchor
6062            && (previous.cmra_offset != winner.cmra_offset
6063                || previous.cmra_length != winner.cmra_length)
6064        {
6065            return Err(FormatError::InvalidArchive(
6066                "ambiguous v41 public CMRA candidates",
6067            ));
6068        }
6069    }
6070    Ok(winner)
6071}
6072
6073fn parse_locator_cmra_candidate(
6074    bytes: &[u8],
6075    locator_offset: usize,
6076    locator: CriticalRecoveryLocator,
6077    context: KeyHoldingTerminalContext<'_>,
6078) -> Result<TerminalCandidate, FormatError> {
6079    let tuple = CmraDecoderTuple::from(locator);
6080    validate_cmra_decoder_tuple(tuple)?;
6081    let expected_cmra_length = cmra_serialized_length(tuple)?;
6082    if locator.cmra_length as u64 != expected_cmra_length {
6083        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6084    }
6085    validate_locator_position(locator_offset, locator)?;
6086    let recovered = recover_cmra(
6087        bytes,
6088        locator.cmra_offset,
6089        Some(tuple),
6090        CmraRecoveryMode::KeyHolding,
6091    )?;
6092    if recovered.tuple != tuple {
6093        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6094    }
6095    if expected_cmra_length != recovered.cmra_length {
6096        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6097    }
6098    validate_locator_image_boundary(locator, &recovered.image)?;
6099    validate_cmra_identity_hints(
6100        recovered.header_hints,
6101        Some(CmraIdentityHints::from(locator)),
6102        &recovered.image,
6103    )?;
6104    let terminal =
6105        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
6106    Ok(TerminalCandidate {
6107        terminal,
6108        anchor: locator_offset
6109            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6110            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6111        locator_sequence: Some(locator.locator_sequence),
6112        cmra_offset: locator.cmra_offset,
6113        cmra_length: recovered.cmra_length,
6114    })
6115}
6116
6117fn parse_locator_cmra_candidate_read_at(
6118    reader: &dyn ArchiveReadAt,
6119    locator_offset: u64,
6120    locator: CriticalRecoveryLocator,
6121    context: KeyHoldingTerminalContext<'_>,
6122) -> Result<TerminalCandidate, FormatError> {
6123    let tuple = CmraDecoderTuple::from(locator);
6124    validate_cmra_decoder_tuple(tuple)?;
6125    let expected_cmra_length = cmra_serialized_length(tuple)?;
6126    if locator.cmra_length as u64 != expected_cmra_length {
6127        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6128    }
6129    validate_locator_position(
6130        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6131        locator,
6132    )?;
6133    let recovered = recover_cmra_read_at(
6134        reader,
6135        locator.cmra_offset,
6136        Some(tuple),
6137        CmraRecoveryMode::KeyHolding,
6138    )?;
6139    if recovered.tuple != tuple {
6140        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6141    }
6142    if expected_cmra_length != recovered.cmra_length {
6143        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6144    }
6145    validate_locator_image_boundary(locator, &recovered.image)?;
6146    validate_cmra_identity_hints(
6147        recovered.header_hints,
6148        Some(CmraIdentityHints::from(locator)),
6149        &recovered.image,
6150    )?;
6151    let terminal = validate_recovered_terminal_read_at(
6152        recovered.image,
6153        recovered.tuple,
6154        reader,
6155        context,
6156        false,
6157    )?;
6158    Ok(TerminalCandidate {
6159        terminal,
6160        anchor: to_usize(
6161            checked_u64_add(
6162                locator_offset,
6163                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6164                "locator anchor overflow",
6165            )?,
6166            "locator anchor overflow",
6167        )?,
6168        locator_sequence: Some(locator.locator_sequence),
6169        cmra_offset: locator.cmra_offset,
6170        cmra_length: recovered.cmra_length,
6171    })
6172}
6173
6174fn parse_locator_cmra_authority_candidate(
6175    bytes: &[u8],
6176    locator_offset: usize,
6177    locator: CriticalRecoveryLocator,
6178    master_key: &MasterKey,
6179) -> Result<TerminalAuthorityCandidate, FormatError> {
6180    let tuple = CmraDecoderTuple::from(locator);
6181    validate_cmra_decoder_tuple(tuple)?;
6182    let expected_cmra_length = cmra_serialized_length(tuple)?;
6183    if locator.cmra_length as u64 != expected_cmra_length {
6184        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6185    }
6186    validate_locator_position(locator_offset, locator)?;
6187    let recovered = recover_cmra(
6188        bytes,
6189        locator.cmra_offset,
6190        Some(tuple),
6191        CmraRecoveryMode::KeyHolding,
6192    )?;
6193    if recovered.tuple != tuple {
6194        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6195    }
6196    if expected_cmra_length != recovered.cmra_length {
6197        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6198    }
6199    validate_locator_image_boundary(locator, &recovered.image)?;
6200    validate_cmra_identity_hints(
6201        recovered.header_hints,
6202        Some(CmraIdentityHints::from(locator)),
6203        &recovered.image,
6204    )?;
6205    let cmra_length = recovered.cmra_length;
6206    let authority =
6207        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6208    Ok(TerminalAuthorityCandidate {
6209        authority,
6210        anchor: locator_offset
6211            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6212            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6213        cmra_offset: locator.cmra_offset,
6214        cmra_length,
6215    })
6216}
6217
6218fn parse_locator_cmra_recipient_wrap_authority_candidate<F>(
6219    bytes: &[u8],
6220    locator_offset: usize,
6221    locator: CriticalRecoveryLocator,
6222    resolver: &mut F,
6223) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6224where
6225    F: FnMut(
6226        RecipientWrapRecordContext<'_>,
6227    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6228{
6229    let tuple = CmraDecoderTuple::from(locator);
6230    validate_cmra_decoder_tuple(tuple)?;
6231    let expected_cmra_length = cmra_serialized_length(tuple)?;
6232    if locator.cmra_length as u64 != expected_cmra_length {
6233        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6234    }
6235    validate_locator_position(locator_offset, locator)?;
6236    let recovered = recover_cmra(
6237        bytes,
6238        locator.cmra_offset,
6239        Some(tuple),
6240        CmraRecoveryMode::KeyHolding,
6241    )?;
6242    if recovered.tuple != tuple {
6243        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6244    }
6245    if expected_cmra_length != recovered.cmra_length {
6246        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6247    }
6248    validate_locator_image_boundary(locator, &recovered.image)?;
6249    validate_cmra_identity_hints(
6250        recovered.header_hints,
6251        Some(CmraIdentityHints::from(locator)),
6252        &recovered.image,
6253    )?;
6254    let cmra_length = recovered.cmra_length;
6255    let authority = validate_recovered_recipient_wrap_terminal_authority(
6256        recovered.image,
6257        recovered.tuple,
6258        resolver,
6259        false,
6260    )?;
6261    Ok(RecipientWrapTerminalAuthorityCandidate {
6262        authority,
6263        anchor: locator_offset
6264            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6265            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6266        cmra_offset: locator.cmra_offset,
6267        cmra_length,
6268    })
6269}
6270
6271fn parse_locator_cmra_recipient_wrap_authority_candidate_read_at<F>(
6272    reader: &dyn ArchiveReadAt,
6273    locator_offset: u64,
6274    locator: CriticalRecoveryLocator,
6275    resolver: &mut F,
6276) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6277where
6278    F: FnMut(
6279        RecipientWrapRecordContext<'_>,
6280    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6281{
6282    let tuple = CmraDecoderTuple::from(locator);
6283    validate_cmra_decoder_tuple(tuple)?;
6284    let expected_cmra_length = cmra_serialized_length(tuple)?;
6285    if locator.cmra_length as u64 != expected_cmra_length {
6286        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6287    }
6288    validate_locator_position(
6289        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6290        locator,
6291    )?;
6292    let recovered = recover_cmra_read_at(
6293        reader,
6294        locator.cmra_offset,
6295        Some(tuple),
6296        CmraRecoveryMode::KeyHolding,
6297    )?;
6298    if recovered.tuple != tuple {
6299        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6300    }
6301    if expected_cmra_length != recovered.cmra_length {
6302        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6303    }
6304    validate_locator_image_boundary(locator, &recovered.image)?;
6305    validate_cmra_identity_hints(
6306        recovered.header_hints,
6307        Some(CmraIdentityHints::from(locator)),
6308        &recovered.image,
6309    )?;
6310    let cmra_length = recovered.cmra_length;
6311    let authority = validate_recovered_recipient_wrap_terminal_authority(
6312        recovered.image,
6313        recovered.tuple,
6314        resolver,
6315        false,
6316    )?;
6317    Ok(RecipientWrapTerminalAuthorityCandidate {
6318        authority,
6319        anchor: to_usize(
6320            checked_u64_add(
6321                locator_offset,
6322                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6323                "locator anchor overflow",
6324            )?,
6325            "locator anchor overflow",
6326        )?,
6327        cmra_offset: locator.cmra_offset,
6328        cmra_length,
6329    })
6330}
6331
6332fn parse_locator_cmra_authority_candidate_read_at(
6333    reader: &dyn ArchiveReadAt,
6334    locator_offset: u64,
6335    locator: CriticalRecoveryLocator,
6336    master_key: &MasterKey,
6337) -> Result<TerminalAuthorityCandidate, FormatError> {
6338    let tuple = CmraDecoderTuple::from(locator);
6339    validate_cmra_decoder_tuple(tuple)?;
6340    let expected_cmra_length = cmra_serialized_length(tuple)?;
6341    if locator.cmra_length as u64 != expected_cmra_length {
6342        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6343    }
6344    validate_locator_position(
6345        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6346        locator,
6347    )?;
6348    let recovered = recover_cmra_read_at(
6349        reader,
6350        locator.cmra_offset,
6351        Some(tuple),
6352        CmraRecoveryMode::KeyHolding,
6353    )?;
6354    if recovered.tuple != tuple {
6355        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6356    }
6357    if expected_cmra_length != recovered.cmra_length {
6358        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6359    }
6360    validate_locator_image_boundary(locator, &recovered.image)?;
6361    validate_cmra_identity_hints(
6362        recovered.header_hints,
6363        Some(CmraIdentityHints::from(locator)),
6364        &recovered.image,
6365    )?;
6366    let cmra_length = recovered.cmra_length;
6367    let authority =
6368        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6369    Ok(TerminalAuthorityCandidate {
6370        authority,
6371        anchor: to_usize(
6372            checked_u64_add(
6373                locator_offset,
6374                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6375                "locator anchor overflow",
6376            )?,
6377            "locator anchor overflow",
6378        )?,
6379        cmra_offset: locator.cmra_offset,
6380        cmra_length,
6381    })
6382}
6383
6384fn parse_public_locator_cmra_candidate(
6385    bytes: &[u8],
6386    locator_offset: usize,
6387    locator: CriticalRecoveryLocator,
6388    volume_header: &VolumeHeader,
6389    crypto_header: &CryptoHeader<'_>,
6390) -> Result<PublicTerminalCandidate, FormatError> {
6391    let tuple = CmraDecoderTuple::from(locator);
6392    validate_cmra_decoder_tuple(tuple)?;
6393    let expected_cmra_length = cmra_serialized_length(tuple)?;
6394    if locator.cmra_length as u64 != expected_cmra_length {
6395        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6396    }
6397    validate_locator_position(locator_offset, locator)?;
6398    let recovered = recover_cmra(
6399        bytes,
6400        locator.cmra_offset,
6401        Some(tuple),
6402        CmraRecoveryMode::PublicNoKey,
6403    )?;
6404    if recovered.tuple != tuple {
6405        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6406    }
6407    if expected_cmra_length != recovered.cmra_length {
6408        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6409    }
6410    validate_locator_image_boundary(locator, &recovered.image)?;
6411    validate_cmra_identity_hints(
6412        recovered.header_hints,
6413        Some(CmraIdentityHints::from(locator)),
6414        &recovered.image,
6415    )?;
6416    let terminal = validate_recovered_public_terminal(
6417        recovered.image,
6418        bytes,
6419        volume_header,
6420        crypto_header,
6421        false,
6422    )?;
6423    Ok(PublicTerminalCandidate {
6424        terminal,
6425        anchor: locator_offset
6426            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6427            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6428        cmra_offset: locator.cmra_offset,
6429        cmra_length: recovered.cmra_length,
6430    })
6431}
6432
6433fn parse_locatorless_cmra_candidate(
6434    bytes: &[u8],
6435    cmra_offset: usize,
6436    context: KeyHoldingTerminalContext<'_>,
6437) -> Result<TerminalCandidate, FormatError> {
6438    let recovered = recover_cmra(
6439        bytes,
6440        cmra_offset as u64,
6441        None,
6442        CmraRecoveryMode::KeyHolding,
6443    )?;
6444    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6445        return Err(FormatError::InvalidArchive(
6446            "locatorless CMRA boundary mismatch",
6447        ));
6448    }
6449    if recovered
6450        .image
6451        .volume_trailer_offset
6452        .checked_add(VOLUME_TRAILER_LEN as u64)
6453        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6454        != cmra_offset as u64
6455    {
6456        return Err(FormatError::InvalidArchive(
6457            "locatorless trailer boundary mismatch",
6458        ));
6459    }
6460    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6461    let terminal =
6462        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
6463    Ok(TerminalCandidate {
6464        terminal,
6465        anchor: cmra_offset
6466            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6467            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6468        locator_sequence: None,
6469        cmra_offset: cmra_offset as u64,
6470        cmra_length: recovered.cmra_length,
6471    })
6472}
6473
6474fn parse_locatorless_cmra_candidate_read_at(
6475    reader: &dyn ArchiveReadAt,
6476    cmra_offset: u64,
6477    context: KeyHoldingTerminalContext<'_>,
6478) -> Result<TerminalCandidate, FormatError> {
6479    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6480    if recovered.image.body_bytes_before_cmra != cmra_offset {
6481        return Err(FormatError::InvalidArchive(
6482            "locatorless CMRA boundary mismatch",
6483        ));
6484    }
6485    if recovered
6486        .image
6487        .volume_trailer_offset
6488        .checked_add(VOLUME_TRAILER_LEN as u64)
6489        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6490        != cmra_offset
6491    {
6492        return Err(FormatError::InvalidArchive(
6493            "locatorless trailer boundary mismatch",
6494        ));
6495    }
6496    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6497    let terminal = validate_recovered_terminal_read_at(
6498        recovered.image,
6499        recovered.tuple,
6500        reader,
6501        context,
6502        true,
6503    )?;
6504    Ok(TerminalCandidate {
6505        terminal,
6506        anchor: to_usize(
6507            checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
6508            "CMRA anchor overflow",
6509        )?,
6510        locator_sequence: None,
6511        cmra_offset,
6512        cmra_length: recovered.cmra_length,
6513    })
6514}
6515
6516fn parse_locatorless_cmra_authority_candidate(
6517    bytes: &[u8],
6518    cmra_offset: usize,
6519    master_key: &MasterKey,
6520) -> Result<TerminalAuthorityCandidate, FormatError> {
6521    let recovered = recover_cmra(
6522        bytes,
6523        cmra_offset as u64,
6524        None,
6525        CmraRecoveryMode::KeyHolding,
6526    )?;
6527    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6528        return Err(FormatError::InvalidArchive(
6529            "locatorless CMRA boundary mismatch",
6530        ));
6531    }
6532    if recovered
6533        .image
6534        .volume_trailer_offset
6535        .checked_add(VOLUME_TRAILER_LEN as u64)
6536        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6537        != cmra_offset as u64
6538    {
6539        return Err(FormatError::InvalidArchive(
6540            "locatorless trailer boundary mismatch",
6541        ));
6542    }
6543    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6544    let cmra_length = recovered.cmra_length;
6545    let authority =
6546        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6547    Ok(TerminalAuthorityCandidate {
6548        authority,
6549        anchor: cmra_offset
6550            .checked_add(to_usize(cmra_length, "CMRA")?)
6551            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6552        cmra_offset: cmra_offset as u64,
6553        cmra_length,
6554    })
6555}
6556
6557fn parse_locatorless_cmra_recipient_wrap_authority_candidate<F>(
6558    bytes: &[u8],
6559    cmra_offset: usize,
6560    resolver: &mut F,
6561) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6562where
6563    F: FnMut(
6564        RecipientWrapRecordContext<'_>,
6565    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6566{
6567    let recovered = recover_cmra(
6568        bytes,
6569        cmra_offset as u64,
6570        None,
6571        CmraRecoveryMode::KeyHolding,
6572    )?;
6573    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6574        return Err(FormatError::InvalidArchive(
6575            "locatorless CMRA boundary mismatch",
6576        ));
6577    }
6578    if recovered
6579        .image
6580        .volume_trailer_offset
6581        .checked_add(VOLUME_TRAILER_LEN as u64)
6582        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6583        != cmra_offset as u64
6584    {
6585        return Err(FormatError::InvalidArchive(
6586            "locatorless trailer boundary mismatch",
6587        ));
6588    }
6589    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6590    let cmra_length = recovered.cmra_length;
6591    let authority = validate_recovered_recipient_wrap_terminal_authority(
6592        recovered.image,
6593        recovered.tuple,
6594        resolver,
6595        true,
6596    )?;
6597    Ok(RecipientWrapTerminalAuthorityCandidate {
6598        authority,
6599        anchor: cmra_offset
6600            .checked_add(to_usize(cmra_length, "CMRA")?)
6601            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6602        cmra_offset: cmra_offset as u64,
6603        cmra_length,
6604    })
6605}
6606
6607fn parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at<F>(
6608    reader: &dyn ArchiveReadAt,
6609    cmra_offset: u64,
6610    resolver: &mut F,
6611) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6612where
6613    F: FnMut(
6614        RecipientWrapRecordContext<'_>,
6615    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6616{
6617    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6618    if recovered.image.body_bytes_before_cmra != cmra_offset {
6619        return Err(FormatError::InvalidArchive(
6620            "locatorless CMRA boundary mismatch",
6621        ));
6622    }
6623    if recovered
6624        .image
6625        .volume_trailer_offset
6626        .checked_add(VOLUME_TRAILER_LEN as u64)
6627        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6628        != cmra_offset
6629    {
6630        return Err(FormatError::InvalidArchive(
6631            "locatorless trailer boundary mismatch",
6632        ));
6633    }
6634    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6635    let cmra_length = recovered.cmra_length;
6636    let authority = validate_recovered_recipient_wrap_terminal_authority(
6637        recovered.image,
6638        recovered.tuple,
6639        resolver,
6640        true,
6641    )?;
6642    Ok(RecipientWrapTerminalAuthorityCandidate {
6643        authority,
6644        anchor: to_usize(
6645            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6646            "CMRA anchor overflow",
6647        )?,
6648        cmra_offset,
6649        cmra_length,
6650    })
6651}
6652
6653fn parse_locatorless_cmra_authority_candidate_read_at(
6654    reader: &dyn ArchiveReadAt,
6655    cmra_offset: u64,
6656    master_key: &MasterKey,
6657) -> Result<TerminalAuthorityCandidate, FormatError> {
6658    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6659    if recovered.image.body_bytes_before_cmra != cmra_offset {
6660        return Err(FormatError::InvalidArchive(
6661            "locatorless CMRA boundary mismatch",
6662        ));
6663    }
6664    if recovered
6665        .image
6666        .volume_trailer_offset
6667        .checked_add(VOLUME_TRAILER_LEN as u64)
6668        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6669        != cmra_offset
6670    {
6671        return Err(FormatError::InvalidArchive(
6672            "locatorless trailer boundary mismatch",
6673        ));
6674    }
6675    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6676    let cmra_length = recovered.cmra_length;
6677    let authority =
6678        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6679    Ok(TerminalAuthorityCandidate {
6680        authority,
6681        anchor: to_usize(
6682            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6683            "CMRA anchor overflow",
6684        )?,
6685        cmra_offset,
6686        cmra_length,
6687    })
6688}
6689
6690fn parse_public_locatorless_cmra_candidate(
6691    bytes: &[u8],
6692    cmra_offset: usize,
6693    volume_header: &VolumeHeader,
6694    crypto_header: &CryptoHeader<'_>,
6695) -> Result<PublicTerminalCandidate, FormatError> {
6696    let recovered = recover_cmra(
6697        bytes,
6698        cmra_offset as u64,
6699        None,
6700        CmraRecoveryMode::PublicNoKey,
6701    )?;
6702    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6703        return Err(FormatError::InvalidArchive(
6704            "locatorless CMRA boundary mismatch",
6705        ));
6706    }
6707    if recovered
6708        .image
6709        .volume_trailer_offset
6710        .checked_add(VOLUME_TRAILER_LEN as u64)
6711        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6712        != cmra_offset as u64
6713    {
6714        return Err(FormatError::InvalidArchive(
6715            "locatorless trailer boundary mismatch",
6716        ));
6717    }
6718    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6719    let terminal = validate_recovered_public_terminal(
6720        recovered.image,
6721        bytes,
6722        volume_header,
6723        crypto_header,
6724        true,
6725    )?;
6726    Ok(PublicTerminalCandidate {
6727        terminal,
6728        anchor: cmra_offset
6729            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6730            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6731        cmra_offset: cmra_offset as u64,
6732        cmra_length: recovered.cmra_length,
6733    })
6734}
6735
6736fn validate_locator_position(
6737    locator_offset: usize,
6738    locator: CriticalRecoveryLocator,
6739) -> Result<(), FormatError> {
6740    if locator.cmra_offset != locator.body_bytes_before_cmra {
6741        return Err(FormatError::InvalidArchive(
6742            "locator CMRA boundary mismatch",
6743        ));
6744    }
6745    if locator
6746        .volume_trailer_offset
6747        .checked_add(VOLUME_TRAILER_LEN as u64)
6748        .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
6749        != locator.cmra_offset
6750    {
6751        return Err(FormatError::InvalidArchive(
6752            "locator trailer boundary mismatch",
6753        ));
6754    }
6755    let expected_offset = match locator.locator_sequence {
6756        1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
6757        0 => locator
6758            .cmra_offset
6759            .checked_add(locator.cmra_length as u64)
6760            .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
6761        _ => None,
6762    }
6763    .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
6764    if expected_offset != locator_offset as u64 {
6765        return Err(FormatError::InvalidArchive(
6766            "locator position does not match sequence",
6767        ));
6768    }
6769    Ok(())
6770}
6771
6772fn validate_locator_image_boundary(
6773    locator: CriticalRecoveryLocator,
6774    image: &CriticalMetadataImage,
6775) -> Result<(), FormatError> {
6776    if locator.volume_format_rev != image.volume_format_rev
6777        || locator.volume_trailer_offset != image.volume_trailer_offset
6778        || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
6779        || image
6780            .volume_trailer_offset
6781            .checked_add(VOLUME_TRAILER_LEN as u64)
6782            .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
6783            != locator.cmra_offset
6784    {
6785        return Err(FormatError::InvalidArchive(
6786            "locator and CMRA image boundaries differ",
6787        ));
6788    }
6789    Ok(())
6790}
6791
6792fn validate_cmra_identity_hints(
6793    header_hints: Option<CmraIdentityHints>,
6794    locator_hints: Option<CmraIdentityHints>,
6795    image: &CriticalMetadataImage,
6796) -> Result<(), FormatError> {
6797    if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
6798        if header != locator {
6799            return Err(FormatError::InvalidArchive(
6800                "CMRA header and locator identity hints differ",
6801            ));
6802        }
6803    }
6804    for hints in [header_hints, locator_hints].into_iter().flatten() {
6805        if hints.archive_uuid != image.archive_uuid
6806            || hints.session_id != image.session_id
6807            || hints.volume_index != image.volume_index
6808        {
6809            return Err(FormatError::InvalidArchive(
6810                "CMRA identity hints do not match recovered image",
6811            ));
6812        }
6813    }
6814    Ok(())
6815}
6816
6817fn recover_cmra(
6818    bytes: &[u8],
6819    cmra_offset: u64,
6820    locator_tuple: Option<CmraDecoderTuple>,
6821    mode: CmraRecoveryMode,
6822) -> Result<RecoveredCmra, FormatError> {
6823    let offset = to_usize(cmra_offset, "CMRA")?;
6824    let header_bytes = slice(
6825        bytes,
6826        offset,
6827        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6828        "CriticalMetadataRecoveryHeader",
6829    )?;
6830    let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
6831    validate_cmra_decoder_tuple(tuple)?;
6832    let cmra_length = cmra_serialized_length(tuple)?;
6833    let cmra_len = to_usize(cmra_length, "CMRA")?;
6834    let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
6835    recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
6836}
6837
6838fn recover_cmra_read_at(
6839    reader: &dyn ArchiveReadAt,
6840    cmra_offset: u64,
6841    locator_tuple: Option<CmraDecoderTuple>,
6842    mode: CmraRecoveryMode,
6843) -> Result<RecoveredCmra, FormatError> {
6844    let header_bytes = read_at_vec(
6845        reader,
6846        cmra_offset,
6847        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6848        "CriticalMetadataRecoveryHeader",
6849    )?;
6850    let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
6851    validate_cmra_decoder_tuple(tuple)?;
6852    let cmra_length = cmra_serialized_length(tuple)?;
6853    let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
6854    recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
6855}
6856
6857fn recover_cmra_header_tuple(
6858    header_bytes: &[u8],
6859    locator_tuple: Option<CmraDecoderTuple>,
6860) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
6861    let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
6862    Ok(match (parsed_header, locator_tuple) {
6863        (Ok(header), Some(locator_tuple)) => {
6864            let header_tuple = CmraDecoderTuple::from(header);
6865            if header_tuple != locator_tuple {
6866                return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6867            }
6868            (locator_tuple, Some(CmraIdentityHints::from(header)))
6869        }
6870        (Ok(header), None) => (
6871            CmraDecoderTuple::from(header),
6872            Some(CmraIdentityHints::from(header)),
6873        ),
6874        (Err(_), Some(tuple)) => (tuple, None),
6875        (Err(err), _) => return Err(err),
6876    })
6877}
6878
6879fn recover_cmra_from_bytes(
6880    cmra_bytes: &[u8],
6881    tuple: CmraDecoderTuple,
6882    header_hints: Option<CmraIdentityHints>,
6883    cmra_length: u64,
6884    mode: CmraRecoveryMode,
6885) -> Result<RecoveredCmra, FormatError> {
6886    let shard_size = tuple.shard_size as usize;
6887    let mut data_shards = vec![None; tuple.data_shard_count as usize];
6888    let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
6889    let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
6890    for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
6891        let raw = slice(
6892            cmra_bytes,
6893            cursor,
6894            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
6895            "CriticalMetadataRecoveryShard",
6896        )?;
6897        let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
6898        if let Some(shard) = shard {
6899            validate_cmra_shard(&shard, idx, tuple)?;
6900            if shard.shard_role == 0 {
6901                let data_slot = data_shards
6902                    .get_mut(idx)
6903                    .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
6904                *data_slot = Some(shard.payload);
6905            } else {
6906                let parity_idx = idx - tuple.data_shard_count as usize;
6907                let parity_slot =
6908                    parity_shards
6909                        .get_mut(parity_idx)
6910                        .ok_or(FormatError::InvalidArchive(
6911                            "CMRA parity shard out of range",
6912                        ))?;
6913                *parity_slot = Some(shard.payload);
6914            }
6915        }
6916        cursor = checked_add(
6917            cursor,
6918            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
6919            "CriticalMetadataRecoveryShard",
6920        )?;
6921    }
6922    let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
6923    let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
6924    for shard in repaired {
6925        image_bytes.extend_from_slice(&shard);
6926    }
6927    image_bytes.truncate(tuple.image_length as usize);
6928    if sha256_bytes(&image_bytes) != tuple.image_sha256 {
6929        return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
6930    }
6931    let image = CriticalMetadataImage::parse(&image_bytes)?;
6932    validate_critical_metadata_image(&image, mode)?;
6933    Ok(RecoveredCmra {
6934        image,
6935        tuple,
6936        header_hints,
6937        cmra_length,
6938    })
6939}
6940
6941fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
6942    let shard_size = tuple.shard_size as u64;
6943    if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
6944        return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
6945    }
6946    let image_length = tuple.image_length as u64;
6947    let min = critical_image_min();
6948    let cap = critical_image_cap()?;
6949    if image_length < min || image_length > cap {
6950        return Err(FormatError::InvalidArchive(
6951            "CMRA image_length is outside bounds",
6952        ));
6953    }
6954    let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
6955    if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
6956        return Err(FormatError::InvalidArchive(
6957            "CMRA data_shard_count does not match image length",
6958        ));
6959    }
6960    let max_parity = 2u64.max(ceil_div_u64(
6961        checked_u64_mul(
6962            expected_data_shards,
6963            READER_MAX_CMRA_PARITY_PCT as u64,
6964            "CMRA parity overflow",
6965        )?,
6966        100,
6967    )?);
6968    if tuple.parity_shard_count as u64 > max_parity {
6969        return Err(FormatError::ReaderResourceLimitExceeded {
6970            field: "CMRA parity shard count",
6971            cap: max_parity,
6972            actual: tuple.parity_shard_count as u64,
6973        });
6974    }
6975    let total = expected_data_shards
6976        .checked_add(tuple.parity_shard_count as u64)
6977        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
6978    if total > 65_535 {
6979        return Err(FormatError::FecTooManyShards(total as usize));
6980    }
6981    Ok(())
6982}
6983
6984fn validate_cmra_writer_parity_lower_bound(
6985    tuple: CmraDecoderTuple,
6986    bit_rot_buffer_pct: u8,
6987) -> Result<(), FormatError> {
6988    let min_parity = 2u64.max(ceil_div_u64(
6989        checked_u64_mul(
6990            tuple.data_shard_count as u64,
6991            bit_rot_buffer_pct as u64,
6992            "CMRA parity lower-bound overflow",
6993        )?,
6994        100,
6995    )?);
6996    if (tuple.parity_shard_count as u64) < min_parity {
6997        return Err(FormatError::InvalidArchive(
6998            "CMRA parity shard count is below authenticated bit-rot lower bound",
6999        ));
7000    }
7001    Ok(())
7002}
7003
7004fn validate_cmra_shard(
7005    shard: &CriticalMetadataRecoveryShard,
7006    serialized_idx: usize,
7007    tuple: CmraDecoderTuple,
7008) -> Result<(), FormatError> {
7009    if shard.shard_index as usize != serialized_idx {
7010        return Err(FormatError::InvalidArchive(
7011            "CMRA shards are not in canonical order",
7012        ));
7013    }
7014    let data_count = tuple.data_shard_count as usize;
7015    let shard_size = tuple.shard_size as usize;
7016    if serialized_idx < data_count {
7017        if shard.shard_role != 0 {
7018            return Err(FormatError::InvalidArchive(
7019                "CMRA data shard has wrong role",
7020            ));
7021        }
7022        let expected_len = if serialized_idx + 1 == data_count {
7023            let used = tuple.image_length as usize - serialized_idx * shard_size;
7024            if used == 0 {
7025                shard_size
7026            } else {
7027                used
7028            }
7029        } else {
7030            shard_size
7031        };
7032        if shard.shard_payload_length as usize != expected_len {
7033            return Err(FormatError::InvalidArchive(
7034                "CMRA data shard payload length is non-canonical",
7035            ));
7036        }
7037        if serialized_idx + 1 == data_count
7038            && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
7039        {
7040            return Err(FormatError::InvalidArchive(
7041                "CMRA final data shard padding is non-zero",
7042            ));
7043        }
7044    } else {
7045        if shard.shard_role != 1 {
7046            return Err(FormatError::InvalidArchive(
7047                "CMRA parity shard has wrong role",
7048            ));
7049        }
7050        if shard.shard_payload_length as usize != shard_size {
7051            return Err(FormatError::InvalidArchive(
7052                "CMRA parity shard payload length is non-canonical",
7053            ));
7054        }
7055    }
7056    Ok(())
7057}
7058
7059fn validate_critical_metadata_image(
7060    image: &CriticalMetadataImage,
7061    mode: CmraRecoveryMode,
7062) -> Result<(), FormatError> {
7063    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7064    let key_wrap_layout_present = image.layout_flags & 0x0000_0002 != 0;
7065    let key_wrap_region = image.region(6);
7066    let key_wrap_present = if image.volume_format_rev == VOLUME_FORMAT_REV_44 {
7067        if key_wrap_layout_present != key_wrap_region.is_some() {
7068            return Err(FormatError::InvalidArchive(
7069                "CriticalMetadataImage key-wrap layout flag mismatch",
7070            ));
7071        }
7072        key_wrap_layout_present
7073    } else {
7074        if key_wrap_layout_present
7075            || key_wrap_region.is_some()
7076            || image.key_wrap_table_offset != 0
7077            || image.key_wrap_table_length != 0
7078            || image.key_wrap_table_sha256 != [0u8; 32]
7079        {
7080            return Err(FormatError::InvalidArchive(
7081                "v43 CriticalMetadataImage cannot contain KeyWrapTableV1",
7082            ));
7083        }
7084        false
7085    };
7086    if image.volume_header_offset != 0
7087        || image.volume_header_length != VOLUME_HEADER_LEN as u32
7088        || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
7089        || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7090        || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
7091        || image.body_bytes_before_cmra
7092            != image
7093                .volume_trailer_offset
7094                .checked_add(VOLUME_TRAILER_LEN as u64)
7095                .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7096    {
7097        return Err(FormatError::InvalidArchive(
7098            "CriticalMetadataImage fixed layout is invalid",
7099        ));
7100    }
7101    if root_auth_present {
7102        if image.root_auth_footer_offset == 0
7103            || image.root_auth_footer_length == 0
7104            || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7105        {
7106            return Err(FormatError::InvalidArchive(
7107                "CriticalMetadataImage root-auth range is invalid",
7108            ));
7109        }
7110    } else if image.root_auth_footer_offset != 0
7111        || image.root_auth_footer_length != 0
7112        || image.root_auth_footer_sha256 != [0u8; 32]
7113    {
7114        return Err(FormatError::InvalidArchive(
7115            "CriticalMetadataImage root-auth fields must be zero when absent",
7116        ));
7117    }
7118    let block_record_len = image_block_record_len_from_region(image)?;
7119    let block_record_len_u64 = u64::try_from(block_record_len)
7120        .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
7121    match mode {
7122        CmraRecoveryMode::KeyHolding => {
7123            let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
7124                FormatError::InvalidArchive("BlockRecord region length overflow"),
7125            )?;
7126            if image.block_records_length != expected_len {
7127                return Err(FormatError::InvalidArchive(
7128                    "CriticalMetadataImage terminal equations are invalid",
7129                ));
7130            }
7131        }
7132        CmraRecoveryMode::PublicNoKey => {
7133            if image.block_records_length % block_record_len_u64 != 0 {
7134                return Err(FormatError::InvalidArchive(
7135                    "CriticalMetadataImage BlockRecord region is not aligned",
7136                ));
7137            }
7138        }
7139    }
7140    let crypto_header_end = image
7141        .crypto_header_offset
7142        .checked_add(image.crypto_header_length as u64)
7143        .ok_or(FormatError::InvalidArchive(
7144            "CryptoHeader boundary overflow",
7145        ))?;
7146    let expected_block_records_offset = if key_wrap_present {
7147        let key_wrap_region = key_wrap_region.ok_or(FormatError::InvalidArchive(
7148            "missing CriticalMetadataImage key-wrap region",
7149        ))?;
7150        if image.key_wrap_table_offset != crypto_header_end
7151            || image.key_wrap_table_length == 0
7152            || key_wrap_region.offset != image.key_wrap_table_offset
7153            || key_wrap_region.bytes.len() != image.key_wrap_table_length as usize
7154        {
7155            return Err(FormatError::InvalidArchive(
7156                "CriticalMetadataImage key-wrap region is malformed",
7157            ));
7158        }
7159        image
7160            .key_wrap_table_offset
7161            .checked_add(image.key_wrap_table_length as u64)
7162            .ok_or(FormatError::InvalidArchive(
7163                "KeyWrapTableV1 boundary overflow",
7164            ))?
7165    } else {
7166        if image.key_wrap_table_offset != 0
7167            || image.key_wrap_table_length != 0
7168            || image.key_wrap_table_sha256 != [0u8; 32]
7169        {
7170            return Err(FormatError::InvalidArchive(
7171                "CriticalMetadataImage key-wrap fields must be zero when absent",
7172            ));
7173        }
7174        crypto_header_end
7175    };
7176    if image.block_records_offset != expected_block_records_offset
7177        || image.manifest_footer_offset
7178            != image
7179                .block_records_offset
7180                .checked_add(image.block_records_length)
7181                .ok_or(FormatError::InvalidArchive(
7182                    "ManifestFooter boundary overflow",
7183                ))?
7184    {
7185        return Err(FormatError::InvalidArchive(
7186            "CriticalMetadataImage terminal equations are invalid",
7187        ));
7188    }
7189    let manifest_end = image
7190        .manifest_footer_offset
7191        .checked_add(MANIFEST_FOOTER_LEN as u64)
7192        .ok_or(FormatError::InvalidArchive(
7193            "RootAuthFooter boundary overflow",
7194        ))?;
7195    if root_auth_present {
7196        if image.root_auth_footer_offset != manifest_end
7197            || image
7198                .root_auth_footer_offset
7199                .checked_add(image.root_auth_footer_length as u64)
7200                .ok_or(FormatError::InvalidArchive(
7201                    "VolumeTrailer boundary overflow",
7202                ))?
7203                != image.volume_trailer_offset
7204        {
7205            return Err(FormatError::InvalidArchive(
7206                "CriticalMetadataImage root-auth terminal equations are invalid",
7207            ));
7208        }
7209    } else if image.volume_trailer_offset != manifest_end {
7210        return Err(FormatError::InvalidArchive(
7211            "CriticalMetadataImage unsigned terminal equations are invalid",
7212        ));
7213    }
7214    let expected_types: &[u16] = match (key_wrap_present, root_auth_present) {
7215        (false, false) => &[1, 2, 3, 5],
7216        (false, true) => &[1, 2, 3, 4, 5],
7217        (true, false) => &[1, 2, 6, 3, 5],
7218        (true, true) => &[1, 2, 6, 3, 4, 5],
7219    };
7220    if image.regions.len() != expected_types.len()
7221        || image
7222            .regions
7223            .iter()
7224            .map(|region| region.region_type)
7225            .ne(expected_types.iter().copied())
7226    {
7227        return Err(FormatError::InvalidArchive(
7228            "CriticalMetadataImage regions are not canonical",
7229        ));
7230    }
7231    validate_image_region(
7232        image,
7233        1,
7234        image.volume_header_offset,
7235        image.volume_header_length,
7236    )?;
7237    validate_image_region(
7238        image,
7239        2,
7240        image.crypto_header_offset,
7241        image.crypto_header_length,
7242    )?;
7243    validate_image_region(
7244        image,
7245        3,
7246        image.manifest_footer_offset,
7247        image.manifest_footer_length,
7248    )?;
7249    if key_wrap_present {
7250        validate_image_region(
7251            image,
7252            6,
7253            image.key_wrap_table_offset,
7254            image.key_wrap_table_length,
7255        )?;
7256    }
7257    if root_auth_present {
7258        validate_image_region(
7259            image,
7260            4,
7261            image.root_auth_footer_offset,
7262            image.root_auth_footer_length,
7263        )?;
7264    }
7265    validate_image_region(
7266        image,
7267        5,
7268        image.volume_trailer_offset,
7269        image.volume_trailer_length,
7270    )?;
7271    if sha256_region(image, 1)? != image.volume_header_sha256
7272        || sha256_region(image, 2)? != image.crypto_header_sha256
7273        || (key_wrap_present && sha256_region(image, 6)? != image.key_wrap_table_sha256)
7274        || (!key_wrap_present && image.key_wrap_table_sha256 != [0u8; 32])
7275        || sha256_region(image, 3)? != image.manifest_footer_sha256
7276        || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
7277        || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
7278        || sha256_region(image, 5)? != image.volume_trailer_sha256
7279    {
7280        return Err(FormatError::InvalidArchive(
7281            "CriticalMetadataImage region digest mismatch",
7282        ));
7283    }
7284    Ok(())
7285}
7286
7287fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
7288    let crypto_region = image
7289        .region(2)
7290        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7291    let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7292    crypto.fixed.validate_supported_profile()?;
7293    Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
7294}
7295
7296fn validate_image_region(
7297    image: &CriticalMetadataImage,
7298    region_type: u16,
7299    offset: u64,
7300    length: u32,
7301) -> Result<(), FormatError> {
7302    let region = image
7303        .region(region_type)
7304        .ok_or(FormatError::InvalidArchive(
7305            "missing CriticalMetadataImage region",
7306        ))?;
7307    if region.offset != offset || region.bytes.len() != length as usize {
7308        return Err(FormatError::InvalidArchive(
7309            "CriticalMetadataImage region range mismatch",
7310        ));
7311    }
7312    Ok(())
7313}
7314
7315fn validate_image_identity(
7316    image: &CriticalMetadataImage,
7317    volume_header: &VolumeHeader,
7318    crypto_header: &CryptoHeaderFixed,
7319) -> Result<(), FormatError> {
7320    if image.volume_format_rev != volume_header.volume_format_rev
7321        || image.archive_uuid != volume_header.archive_uuid
7322        || image.session_id != volume_header.session_id
7323        || image.volume_index != volume_header.volume_index
7324        || image.stripe_width != volume_header.stripe_width
7325        || image.stripe_width != crypto_header.stripe_width
7326    {
7327        return Err(FormatError::InvalidArchive(
7328            "CriticalMetadataImage identity does not match selected volume",
7329        ));
7330    }
7331    Ok(())
7332}
7333
7334fn validate_image_key_wrap_table(
7335    image: &CriticalMetadataImage,
7336    volume_header: &VolumeHeader,
7337    kdf_params: &KdfParams,
7338) -> Result<(), FormatError> {
7339    match kdf_params {
7340        KdfParams::RecipientWrap {
7341            key_wrap_table_length,
7342            key_wrap_table_record_count,
7343            key_wrap_table_digest,
7344            ..
7345        } => {
7346            if image.volume_format_rev != VOLUME_FORMAT_REV_44
7347                || image.layout_flags & 0x0000_0002 == 0
7348                || image.key_wrap_table_length != *key_wrap_table_length
7349            {
7350                return Err(FormatError::InvalidArchive(
7351                    "CriticalMetadataImage key-wrap fields do not match KdfParams",
7352                ));
7353            }
7354            let region = image.region(6).ok_or(FormatError::InvalidArchive(
7355                "missing CriticalMetadataImage key-wrap region",
7356            ))?;
7357            if region.offset != image.key_wrap_table_offset
7358                || region.bytes.len() != *key_wrap_table_length as usize
7359            {
7360                return Err(FormatError::InvalidArchive(
7361                    "CriticalMetadataImage key-wrap region is malformed",
7362                ));
7363            }
7364            if compute_key_wrap_table_digest(*key_wrap_table_length, &region.bytes)
7365                != *key_wrap_table_digest
7366            {
7367                return Err(FormatError::IntegrityDigestMismatch {
7368                    structure: "KeyWrapTableV1",
7369                });
7370            }
7371            KeyWrapTableV1::parse(
7372                &region.bytes,
7373                &volume_header.archive_uuid,
7374                &volume_header.session_id,
7375                *key_wrap_table_length,
7376                *key_wrap_table_record_count,
7377            )?;
7378            Ok(())
7379        }
7380        _ => {
7381            if image.layout_flags & 0x0000_0002 != 0
7382                || image.region(6).is_some()
7383                || image.key_wrap_table_offset != 0
7384                || image.key_wrap_table_length != 0
7385                || image.key_wrap_table_sha256 != [0u8; 32]
7386            {
7387                return Err(FormatError::InvalidArchive(
7388                    "CriticalMetadataImage key-wrap fields must be zero when absent",
7389                ));
7390            }
7391            Ok(())
7392        }
7393    }
7394}
7395
7396fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
7397    Ok(sha256_bytes(
7398        &image
7399            .region(region_type)
7400            .ok_or(FormatError::InvalidArchive(
7401                "missing CriticalMetadataImage region",
7402            ))?
7403            .bytes,
7404    ))
7405}
7406
7407fn validate_recovered_terminal_authority(
7408    image: CriticalMetadataImage,
7409    tuple: CmraDecoderTuple,
7410    master_key: &MasterKey,
7411    require_cmra_boundary_magic: bool,
7412) -> Result<RecoveredTerminalAuthority, FormatError> {
7413    let volume_header_region = image
7414        .region(1)
7415        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7416    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7417    let crypto_region = image
7418        .region(2)
7419        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7420    let crypto_header_bytes = crypto_region.bytes.clone();
7421    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7422    let kdf_params = parsed_crypto.kdf_params.clone();
7423    let subkeys = subkeys_for_open(
7424        Some(master_key),
7425        parsed_crypto.fixed.aead_algo,
7426        &volume_header.archive_uuid,
7427        &volume_header.session_id,
7428    )?;
7429    verify_integrity_tag(
7430        HmacDomain::CryptoHeader,
7431        parsed_crypto.fixed.aead_algo,
7432        volume_header.volume_format_rev,
7433        Some(&subkeys.mac_key),
7434        &volume_header.archive_uuid,
7435        &volume_header.session_id,
7436        parsed_crypto.hmac_covered_bytes,
7437        &parsed_crypto.header_hmac,
7438    )?;
7439    parsed_crypto.validate_extension_semantics()?;
7440    validate_seekable_supported_volume(
7441        &volume_header,
7442        &parsed_crypto.fixed,
7443        &parsed_crypto.extensions,
7444    )?;
7445    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7446    let crypto_header = parsed_crypto.fixed.clone();
7447    if crypto_header.bit_rot_buffer_pct == 0 {
7448        return Err(FormatError::InvalidArchive(
7449            "CMRA startup recovery requires a nonzero bit-rot budget",
7450        ));
7451    }
7452    drop(parsed_crypto);
7453
7454    let terminal = validate_recovered_terminal_inner(
7455        image,
7456        tuple,
7457        require_cmra_boundary_magic,
7458        true,
7459        KeyHoldingTerminalContext {
7460            subkeys: &subkeys,
7461            volume_header: &volume_header,
7462            crypto_header: &crypto_header,
7463            crypto_header_bytes: &crypto_header_bytes,
7464        },
7465    )?;
7466    Ok(RecoveredTerminalAuthority {
7467        terminal,
7468        volume_header,
7469        crypto_header,
7470        crypto_header_bytes,
7471        subkeys,
7472        kdf_params,
7473    })
7474}
7475
7476fn validate_recovered_recipient_wrap_terminal_authority<F>(
7477    image: CriticalMetadataImage,
7478    tuple: CmraDecoderTuple,
7479    resolver: &mut F,
7480    require_cmra_boundary_magic: bool,
7481) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
7482where
7483    F: FnMut(
7484        RecipientWrapRecordContext<'_>,
7485    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
7486{
7487    let volume_header_region = image
7488        .region(1)
7489        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7490    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7491    let crypto_region = image
7492        .region(2)
7493        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7494    let crypto_header_bytes = crypto_region.bytes.clone();
7495    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7496    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
7497        || !parsed_crypto.fixed.aead_algo.is_encrypted()
7498    {
7499        return Err(FormatError::KeyMaterialMismatch);
7500    }
7501    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
7502    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7503    if parsed_crypto.fixed.bit_rot_buffer_pct == 0 {
7504        return Err(FormatError::InvalidArchive(
7505            "CMRA startup recovery requires a nonzero bit-rot budget",
7506        ));
7507    }
7508    validate_cmra_writer_parity_lower_bound(tuple, parsed_crypto.fixed.bit_rot_buffer_pct)?;
7509    validate_image_key_wrap_table(&image, &volume_header, &parsed_crypto.kdf_params)?;
7510    let key_wrap_region = image.region(6).ok_or(FormatError::InvalidArchive(
7511        "missing CriticalMetadataImage key-wrap region",
7512    ))?;
7513    let startup_key_wrap_table = parse_startup_key_wrap_table_bytes(
7514        &volume_header,
7515        &parsed_crypto.kdf_params,
7516        key_wrap_region.bytes.clone(),
7517    )?;
7518    let subkeys = recipient_wrap_subkeys_from_table(
7519        &volume_header,
7520        &parsed_crypto,
7521        &startup_key_wrap_table.table,
7522        resolver,
7523    )?;
7524    parsed_crypto.validate_extension_semantics()?;
7525    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
7526    let crypto_header = parsed_crypto.fixed.clone();
7527
7528    let terminal = validate_recovered_terminal_inner(
7529        image,
7530        tuple,
7531        require_cmra_boundary_magic,
7532        true,
7533        KeyHoldingTerminalContext {
7534            subkeys: &subkeys,
7535            volume_header: &volume_header,
7536            crypto_header: &crypto_header,
7537            crypto_header_bytes: &crypto_header_bytes,
7538        },
7539    )?;
7540    Ok(RecoveredRecipientWrapTerminalAuthority {
7541        terminal,
7542        volume_header,
7543        crypto_header,
7544        crypto_header_bytes,
7545        key_wrap_table_bytes: startup_key_wrap_table.bytes,
7546        block_records_start: startup_key_wrap_table.block_records_start,
7547        subkeys,
7548    })
7549}
7550
7551fn validate_recovered_terminal(
7552    image: CriticalMetadataImage,
7553    tuple: CmraDecoderTuple,
7554    bytes: &[u8],
7555    context: KeyHoldingTerminalContext<'_>,
7556    require_cmra_boundary_magic: bool,
7557) -> Result<V41Terminal, FormatError> {
7558    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7559    let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
7560    validate_recovered_terminal_inner(
7561        image,
7562        tuple,
7563        require_cmra_boundary_magic,
7564        cmra_boundary_magic_ok,
7565        context,
7566    )
7567}
7568
7569fn validate_recovered_terminal_read_at(
7570    image: CriticalMetadataImage,
7571    tuple: CmraDecoderTuple,
7572    reader: &dyn ArchiveReadAt,
7573    context: KeyHoldingTerminalContext<'_>,
7574    require_cmra_boundary_magic: bool,
7575) -> Result<V41Terminal, FormatError> {
7576    let mut magic = [0u8; 4];
7577    reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
7578    validate_recovered_terminal_inner(
7579        image,
7580        tuple,
7581        require_cmra_boundary_magic,
7582        magic == *b"TZCR",
7583        context,
7584    )
7585}
7586
7587fn validate_recovered_terminal_inner(
7588    image: CriticalMetadataImage,
7589    tuple: CmraDecoderTuple,
7590    require_cmra_boundary_magic: bool,
7591    cmra_boundary_magic_ok: bool,
7592    context: KeyHoldingTerminalContext<'_>,
7593) -> Result<V41Terminal, FormatError> {
7594    let subkeys = context.subkeys;
7595    let volume_header = context.volume_header;
7596    let crypto_header = context.crypto_header;
7597    let volume_header_region = image
7598        .region(1)
7599        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7600    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7601    if &recovered_volume_header != volume_header {
7602        return Err(FormatError::InvalidArchive(
7603            "CMRA VolumeHeader differs from parsed VolumeHeader",
7604        ));
7605    }
7606    validate_image_identity(&image, volume_header, crypto_header)?;
7607    let crypto_region = image
7608        .region(2)
7609        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7610    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7611    if recovered_crypto.fixed != *crypto_header {
7612        return Err(FormatError::InvalidArchive(
7613            "CMRA CryptoHeader differs from parsed CryptoHeader",
7614        ));
7615    }
7616    let recovered_pre_hmac_len = crypto_region
7617        .bytes
7618        .len()
7619        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7620        .ok_or(FormatError::InvalidArchive(
7621            "CMRA CryptoHeader is too short",
7622        ))?;
7623    let parsed_pre_hmac_len = context
7624        .crypto_header_bytes
7625        .len()
7626        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7627        .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
7628    if recovered_pre_hmac_len != parsed_pre_hmac_len
7629        || crypto_region.bytes[..recovered_pre_hmac_len]
7630            != context.crypto_header_bytes[..parsed_pre_hmac_len]
7631    {
7632        return Err(FormatError::InvalidArchive(
7633            "CMRA CryptoHeader differs from parsed CryptoHeader",
7634        ));
7635    }
7636    verify_integrity_tag(
7637        HmacDomain::CryptoHeader,
7638        recovered_crypto.fixed.aead_algo,
7639        volume_header.volume_format_rev,
7640        Some(&subkeys.mac_key),
7641        &volume_header.archive_uuid,
7642        &volume_header.session_id,
7643        recovered_crypto.hmac_covered_bytes,
7644        &recovered_crypto.header_hmac,
7645    )?;
7646    validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
7647    recovered_crypto.validate_extension_semantics()?;
7648    validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7649
7650    let manifest_region = image
7651        .region(3)
7652        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7653    let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
7654    validate_manifest_footer(
7655        volume_header,
7656        crypto_header,
7657        &manifest_footer,
7658        subkeys,
7659        volume_header.volume_format_rev,
7660        &manifest_region.bytes,
7661    )?;
7662    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
7663
7664    let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
7665        let root_auth_region = image
7666            .region(4)
7667            .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7668        let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7669        if footer.format_version != volume_header.format_version
7670            || footer.volume_format_rev != volume_header.volume_format_rev
7671        {
7672            return Err(FormatError::InvalidArchive(
7673                "RootAuthFooter format/revision does not match VolumeHeader",
7674            ));
7675        }
7676        if footer.archive_uuid != volume_header.archive_uuid
7677            || footer.session_id != volume_header.session_id
7678            || footer.footer_length()? != image.root_auth_footer_length
7679        {
7680            return Err(FormatError::InvalidArchive(
7681                "RootAuthFooter identity or length does not match terminal image",
7682            ));
7683        }
7684        Some(footer)
7685    } else {
7686        None
7687    };
7688
7689    let trailer_region = image
7690        .region(5)
7691        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7692    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7693    verify_integrity_tag(
7694        HmacDomain::VolumeTrailer,
7695        crypto_header.aead_algo,
7696        volume_header.volume_format_rev,
7697        Some(&subkeys.mac_key),
7698        &volume_header.archive_uuid,
7699        &volume_header.session_id,
7700        &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
7701        &trailer.trailer_hmac,
7702    )?;
7703    validate_trailer_identity(volume_header, &trailer)?;
7704    validate_v41_trailer_equations(&image, &trailer)?;
7705
7706    if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
7707        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7708    }
7709
7710    let manifest_footer_bytes = manifest_region.bytes.clone();
7711    let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
7712    Ok(V41Terminal {
7713        image,
7714        manifest_footer_bytes,
7715        root_auth_footer_bytes,
7716        root_auth_footer,
7717        volume_trailer: trailer,
7718    })
7719}
7720
7721fn validate_recovered_public_terminal(
7722    image: CriticalMetadataImage,
7723    bytes: &[u8],
7724    volume_header: &VolumeHeader,
7725    public_crypto_header: &CryptoHeader<'_>,
7726    require_cmra_boundary_magic: bool,
7727) -> Result<V41PublicTerminal, FormatError> {
7728    if image.layout_flags & 0x0000_0001 == 0 {
7729        return Err(FormatError::ReaderUnsupported(
7730            "public no-key verification requires root-auth",
7731        ));
7732    }
7733    let volume_header_region = image
7734        .region(1)
7735        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7736    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7737    if &recovered_volume_header != volume_header {
7738        return Err(FormatError::InvalidArchive(
7739            "CMRA VolumeHeader differs from parsed VolumeHeader",
7740        ));
7741    }
7742    validate_image_identity(&image, volume_header, &public_crypto_header.fixed)?;
7743    let crypto_region = image
7744        .region(2)
7745        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7746    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7747    if !public_crypto_headers_agree(&recovered_crypto.fixed, &public_crypto_header.fixed)
7748        || !public_kdf_profiles_agree(
7749            &recovered_crypto.kdf_params,
7750            &public_crypto_header.kdf_params,
7751        )
7752    {
7753        return Err(FormatError::InvalidArchive(
7754            "CMRA CryptoHeader differs from parsed CryptoHeader",
7755        ));
7756    }
7757    recovered_crypto.validate_extension_semantics()?;
7758    validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7759
7760    image
7761        .region(3)
7762        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7763
7764    let root_auth_region = image
7765        .region(4)
7766        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7767    let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7768    if root_auth_footer.format_version != volume_header.format_version
7769        || root_auth_footer.volume_format_rev != volume_header.volume_format_rev
7770    {
7771        return Err(FormatError::InvalidArchive(
7772            "public RootAuthFooter format/revision does not match VolumeHeader",
7773        ));
7774    }
7775    if root_auth_footer.archive_uuid != volume_header.archive_uuid
7776        || root_auth_footer.session_id != volume_header.session_id
7777        || root_auth_footer.footer_length()? != image.root_auth_footer_length
7778    {
7779        return Err(FormatError::InvalidArchive(
7780            "public RootAuthFooter identity or length does not match terminal image",
7781        ));
7782    }
7783
7784    let trailer_region = image
7785        .region(5)
7786        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7787    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7788    validate_trailer_identity(volume_header, &trailer)?;
7789    validate_v41_public_trailer_profile(&image, &trailer)?;
7790
7791    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7792    if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
7793        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7794    }
7795
7796    let root_auth_footer_bytes = root_auth_region.bytes.clone();
7797    Ok(V41PublicTerminal {
7798        image,
7799        root_auth_footer_bytes,
7800        root_auth_footer,
7801    })
7802}
7803
7804fn validate_v41_trailer_equations(
7805    image: &CriticalMetadataImage,
7806    trailer: &VolumeTrailer,
7807) -> Result<(), FormatError> {
7808    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7809    if trailer.bytes_written != image.volume_trailer_offset
7810        || trailer.manifest_footer_offset != image.manifest_footer_offset
7811        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7812        || trailer.block_count != image.block_count
7813    {
7814        return Err(FormatError::InvalidArchive(
7815            "VolumeTrailer does not match v41 terminal layout",
7816        ));
7817    }
7818    if root_auth_present {
7819        if trailer.root_auth_flags != 0x0000_0001
7820            || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7821            || trailer.root_auth_footer_length != image.root_auth_footer_length
7822            || image.root_auth_footer_offset
7823                != image
7824                    .manifest_footer_offset
7825                    .checked_add(MANIFEST_FOOTER_LEN as u64)
7826                    .ok_or(FormatError::InvalidArchive(
7827                        "RootAuthFooter trailer boundary overflow",
7828                    ))?
7829            || image
7830                .root_auth_footer_offset
7831                .checked_add(image.root_auth_footer_length as u64)
7832                .ok_or(FormatError::InvalidArchive(
7833                    "RootAuthFooter trailer boundary overflow",
7834                ))?
7835                != image.volume_trailer_offset
7836        {
7837            return Err(FormatError::InvalidArchive(
7838                "VolumeTrailer root-auth fields do not match v41 terminal layout",
7839            ));
7840        }
7841    } else if trailer.root_auth_footer_offset != 0
7842        || trailer.root_auth_footer_length != 0
7843        || trailer.root_auth_flags != 0
7844    {
7845        return Err(FormatError::InvalidArchive(
7846            "VolumeTrailer root-auth fields must be zero when absent",
7847        ));
7848    }
7849    Ok(())
7850}
7851
7852fn validate_v41_public_trailer_profile(
7853    image: &CriticalMetadataImage,
7854    trailer: &VolumeTrailer,
7855) -> Result<(), FormatError> {
7856    if trailer.bytes_written != image.volume_trailer_offset
7857        || trailer.manifest_footer_offset != image.manifest_footer_offset
7858        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7859    {
7860        return Err(FormatError::InvalidArchive(
7861            "VolumeTrailer does not match v41 public terminal layout",
7862        ));
7863    }
7864    if trailer.root_auth_flags != 0x0000_0001
7865        || trailer.root_auth_footer_offset == 0
7866        || trailer.root_auth_footer_length == 0
7867        || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7868        || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7869        || trailer.root_auth_footer_length != image.root_auth_footer_length
7870        || image.root_auth_footer_offset
7871            != image
7872                .manifest_footer_offset
7873                .checked_add(MANIFEST_FOOTER_LEN as u64)
7874                .ok_or(FormatError::InvalidArchive(
7875                    "RootAuthFooter trailer boundary overflow",
7876                ))?
7877        || image
7878            .root_auth_footer_offset
7879            .checked_add(image.root_auth_footer_length as u64)
7880            .ok_or(FormatError::InvalidArchive(
7881                "RootAuthFooter trailer boundary overflow",
7882            ))?
7883            != image.volume_trailer_offset
7884    {
7885        return Err(FormatError::InvalidArchive(
7886            "VolumeTrailer root-auth fields do not match v41 public terminal layout",
7887        ));
7888    }
7889    Ok(())
7890}
7891
7892fn critical_image_min() -> u64 {
7893    const MIN_CRYPTO_HEADER_LEN: u64 = 116;
7894    CRITICAL_METADATA_IMAGE_FIXED_LEN_V43 as u64
7895        + 4 * SERIALIZED_REGION_HEADER_LEN as u64
7896        + VOLUME_HEADER_LEN as u64
7897        + MIN_CRYPTO_HEADER_LEN
7898        + MANIFEST_FOOTER_LEN as u64
7899        + VOLUME_TRAILER_LEN as u64
7900        + IMAGE_CRC_LEN as u64
7901}
7902
7903fn critical_image_cap() -> Result<u64, FormatError> {
7904    [
7905        CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
7906        6 * SERIALIZED_REGION_HEADER_LEN as u64,
7907        VOLUME_HEADER_LEN as u64,
7908        READER_MAX_CRYPTO_HEADER_LEN as u64,
7909        READER_MAX_KEY_WRAP_TABLE_LEN as u64,
7910        MANIFEST_FOOTER_LEN as u64,
7911        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
7912        VOLUME_TRAILER_LEN as u64,
7913        IMAGE_CRC_LEN as u64,
7914    ]
7915    .into_iter()
7916    .try_fold(0u64, |total, value| {
7917        total
7918            .checked_add(value)
7919            .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
7920    })
7921}
7922
7923fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
7924    let shard_total = (tuple.data_shard_count as u64)
7925        .checked_add(tuple.parity_shard_count as u64)
7926        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
7927    let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
7928        .checked_add(tuple.shard_size as u64)
7929        .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
7930    checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
7931        .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
7932        .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
7933}
7934
7935fn cmra_worst_case_cap() -> Result<u64, FormatError> {
7936    let cap = critical_image_cap()?;
7937    let mut worst = 0u64;
7938    let mut shard_size = 512u64;
7939    while shard_size <= 4096 {
7940        let data = ceil_div_u64(cap, shard_size)?;
7941        let parity = 2u64.max(ceil_div_u64(
7942            checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
7943            100,
7944        )?);
7945        let tuple = CmraDecoderTuple {
7946            shard_size: shard_size as u32,
7947            data_shard_count: u16::try_from(data)
7948                .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
7949            parity_shard_count: u16::try_from(parity)
7950                .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
7951            image_length: u32::try_from(cap)
7952                .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
7953            image_sha256: [0u8; 32],
7954        };
7955        worst = worst.max(cmra_serialized_length(tuple)?);
7956        shard_size += 2;
7957    }
7958    Ok(worst)
7959}
7960
7961pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
7962    let total = [
7963        MANIFEST_FOOTER_LEN as u64,
7964        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
7965        VOLUME_TRAILER_LEN as u64,
7966        cmra_worst_case_cap()?,
7967        LOCATOR_PAIR_LEN as u64,
7968    ]
7969    .into_iter()
7970    .try_fold(0u64, |sum, value| {
7971        sum.checked_add(value)
7972            .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
7973    })?;
7974    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
7975}
7976
7977fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
7978    let worst = cmra_worst_case_cap()?;
7979    let total = options
7980        .max_trailing_garbage_scan
7981        .try_into()
7982        .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
7983        .and_then(|scan: u64| {
7984            scan.checked_add(worst)
7985                .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
7986                .ok_or(FormatError::InvalidArchive("scan cap overflow"))
7987        })?;
7988    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
7989}
7990
7991fn validate_bootstrap_single_volume_input(
7992    volume_header: &VolumeHeader,
7993    crypto_header: &CryptoHeaderFixed,
7994) -> Result<(), FormatError> {
7995    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
7996        return Err(FormatError::ReaderUnsupported(
7997            "bootstrap sidecar reader supports only single-volume archive input",
7998        ));
7999    }
8000    if crypto_header.stripe_width != volume_header.stripe_width {
8001        return Err(FormatError::InvalidArchive(
8002            "VolumeHeader and CryptoHeader stripe_width differ",
8003        ));
8004    }
8005    Ok(())
8006}
8007
8008#[derive(Debug)]
8009struct ParsedBootstrapSidecar {
8010    manifest_footer: Option<ManifestFooter>,
8011    index_root_records_section: Option<(u64, u64)>,
8012    dictionary_records_section: Option<(u64, u64)>,
8013}
8014
8015pub(crate) struct NonSeekableBootstrapMaterial {
8016    pub(crate) manifest_footer: ManifestFooter,
8017    pub(crate) payload_dictionary: Option<Vec<u8>>,
8018}
8019
8020#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8021enum BootstrapSidecarUse {
8022    SeekableAssist,
8023    NonSeekableRandomAccess,
8024}
8025
8026impl ParsedBootstrapSidecar {
8027    fn require_sections_for(
8028        &self,
8029        sidecar_use: BootstrapSidecarUse,
8030        crypto_header: &CryptoHeaderFixed,
8031    ) -> Result<(), FormatError> {
8032        if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
8033            if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
8034                return Err(FormatError::ReaderUnsupported(
8035                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8036                ));
8037            }
8038            if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
8039                return Err(FormatError::ReaderUnsupported(
8040                    "dictionary bootstrap required",
8041                ));
8042            }
8043        }
8044        Ok(())
8045    }
8046}
8047
8048pub(crate) fn parse_non_seekable_bootstrap_material(
8049    bootstrap_sidecar: &[u8],
8050    volume_header: &VolumeHeader,
8051    crypto_header: &CryptoHeaderFixed,
8052    subkeys: &Subkeys,
8053) -> Result<NonSeekableBootstrapMaterial, FormatError> {
8054    validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
8055    let sidecar =
8056        parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
8057    sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
8058    let manifest_footer = sidecar
8059        .manifest_footer
8060        .clone()
8061        .ok_or(FormatError::ReaderUnsupported(
8062            "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8063        ))?;
8064
8065    let mut blocks = BTreeMap::new();
8066    let (offset, length) =
8067        sidecar
8068            .index_root_records_section
8069            .ok_or(FormatError::ReaderUnsupported(
8070                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8071            ))?;
8072    let index_root_records = parse_sidecar_block_records(
8073        bootstrap_sidecar,
8074        crypto_header.block_size as usize,
8075        SidecarBlockRecordsSection {
8076            offset,
8077            length,
8078            extent: index_root_extent_from_manifest(&manifest_footer),
8079            data_kind: BlockKind::IndexRootData,
8080            parity_kind: BlockKind::IndexRootParity,
8081            structure: "IndexRoot",
8082        },
8083    )?;
8084    insert_sidecar_records(&mut blocks, index_root_records)?;
8085
8086    let limits = metadata_limits(crypto_header);
8087    let index_root_plaintext = load_metadata_object_from_parts(
8088        &blocks,
8089        ObjectLoadContext::index_root(
8090            volume_header,
8091            crypto_header,
8092            subkeys,
8093            index_root_extent_from_manifest(&manifest_footer),
8094        ),
8095        manifest_footer.index_root_decompressed_size,
8096    )?;
8097    let index_root = IndexRoot::parse(
8098        &index_root_plaintext,
8099        crypto_header.has_dictionary != 0,
8100        limits,
8101    )?;
8102
8103    if crypto_header.has_dictionary != 0 {
8104        let (offset, length) =
8105            sidecar
8106                .dictionary_records_section
8107                .ok_or(FormatError::ReaderUnsupported(
8108                    "dictionary bootstrap required",
8109                ))?;
8110        let dictionary_records = parse_sidecar_block_records(
8111            bootstrap_sidecar,
8112            crypto_header.block_size as usize,
8113            SidecarBlockRecordsSection {
8114                offset,
8115                length,
8116                extent: dictionary_extent_from_index_root(&index_root)?,
8117                data_kind: BlockKind::DictionaryData,
8118                parity_kind: BlockKind::DictionaryParity,
8119                structure: "dictionary",
8120            },
8121        )?;
8122        insert_sidecar_records(&mut blocks, dictionary_records)?;
8123    }
8124    let payload_dictionary =
8125        load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
8126
8127    Ok(NonSeekableBootstrapMaterial {
8128        manifest_footer,
8129        payload_dictionary,
8130    })
8131}
8132
8133fn parse_bootstrap_sidecar(
8134    bytes: &[u8],
8135    volume_header: &VolumeHeader,
8136    crypto_header: &CryptoHeaderFixed,
8137    subkeys: &Subkeys,
8138) -> Result<ParsedBootstrapSidecar, FormatError> {
8139    let header_bytes = slice(
8140        bytes,
8141        0,
8142        BOOTSTRAP_SIDECAR_HEADER_LEN,
8143        "BootstrapSidecarHeader",
8144    )?;
8145    let header = BootstrapSidecarHeader::parse(header_bytes)?;
8146    if header.archive_uuid != volume_header.archive_uuid
8147        || header.session_id != volume_header.session_id
8148    {
8149        return Err(FormatError::InvalidArchive(
8150            "bootstrap sidecar identity does not match VolumeHeader",
8151        ));
8152    }
8153    verify_integrity_tag(
8154        HmacDomain::BootstrapSidecar,
8155        crypto_header.aead_algo,
8156        volume_header.volume_format_rev,
8157        Some(&subkeys.mac_key),
8158        &volume_header.archive_uuid,
8159        &volume_header.session_id,
8160        &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
8161        &header.sidecar_hmac,
8162    )?;
8163    header.validate_packed_layout(bytes.len() as u64)?;
8164    validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
8165
8166    if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
8167        return Err(FormatError::InvalidArchive(
8168            "bootstrap sidecar has dictionary records while has_dictionary is false",
8169        ));
8170    }
8171
8172    let manifest_footer = if header.has_manifest_footer() {
8173        let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
8174        let manifest_bytes = slice(
8175            bytes,
8176            manifest_offset,
8177            MANIFEST_FOOTER_LEN,
8178            "ManifestFooter",
8179        )?;
8180        let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
8181        validate_sidecar_manifest_footer(
8182            volume_header,
8183            crypto_header,
8184            &manifest_footer,
8185            subkeys,
8186            volume_header.volume_format_rev,
8187            manifest_bytes,
8188        )?;
8189        manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
8190        Some(manifest_footer)
8191    } else {
8192        None
8193    };
8194
8195    Ok(ParsedBootstrapSidecar {
8196        manifest_footer,
8197        index_root_records_section: header.has_index_root_records().then_some((
8198            header.index_root_records_offset,
8199            header.index_root_records_length,
8200        )),
8201        dictionary_records_section: header.has_dictionary_records().then_some((
8202            header.dictionary_records_offset,
8203            header.dictionary_records_length,
8204        )),
8205    })
8206}
8207
8208fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
8209    ObjectExtent {
8210        first_block_index: manifest_footer.index_root_first_block,
8211        data_block_count: manifest_footer.index_root_data_block_count,
8212        parity_block_count: manifest_footer.index_root_parity_block_count,
8213        encrypted_size: manifest_footer.index_root_encrypted_size,
8214    }
8215}
8216
8217fn insert_sidecar_records(
8218    blocks: &mut BTreeMap<u64, BlockRecord>,
8219    records: Vec<BlockRecord>,
8220) -> Result<(), FormatError> {
8221    for record in records {
8222        if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
8223            if existing != record {
8224                return Err(FormatError::InvalidArchive(
8225                    "bootstrap sidecar conflicts with volume BlockRecord",
8226                ));
8227            }
8228        }
8229    }
8230    Ok(())
8231}
8232
8233fn validate_sidecar_manifest_footer(
8234    volume_header: &VolumeHeader,
8235    crypto_header: &CryptoHeaderFixed,
8236    footer: &ManifestFooter,
8237    subkeys: &Subkeys,
8238    volume_format_rev: u16,
8239    raw: &[u8],
8240) -> Result<(), FormatError> {
8241    if footer.archive_uuid != volume_header.archive_uuid
8242        || footer.session_id != volume_header.session_id
8243    {
8244        return Err(FormatError::InvalidArchive(
8245            "sidecar ManifestFooter identity does not match VolumeHeader",
8246        ));
8247    }
8248    if footer.volume_index != 0 {
8249        return Err(FormatError::InvalidArchive(
8250            "sidecar ManifestFooter volume_index must be zero",
8251        ));
8252    }
8253    if footer.total_volumes != crypto_header.stripe_width {
8254        return Err(FormatError::InvalidArchive(
8255            "sidecar ManifestFooter total_volumes does not match stripe_width",
8256        ));
8257    }
8258    if footer.is_authoritative != 1 {
8259        return Err(FormatError::InvalidArchive(
8260            "sidecar ManifestFooter is not authoritative",
8261        ));
8262    }
8263    verify_integrity_tag(
8264        HmacDomain::ManifestFooter,
8265        crypto_header.aead_algo,
8266        volume_format_rev,
8267        Some(&subkeys.mac_key),
8268        &volume_header.archive_uuid,
8269        &volume_header.session_id,
8270        &raw[..MANIFEST_HMAC_COVERED_LEN],
8271        &footer.manifest_hmac,
8272    )
8273}
8274
8275fn validate_sidecar_size_cap(
8276    header: &BootstrapSidecarHeader,
8277    crypto_header: &CryptoHeaderFixed,
8278    file_size: u64,
8279) -> Result<(), FormatError> {
8280    let record_len = checked_u64_add(
8281        crypto_header.block_size as u64,
8282        BLOCK_RECORD_FRAMING_LEN as u64,
8283        "bootstrap sidecar cap overflow",
8284    )?;
8285    let max_index_records = crypto_header.index_root_fec_data_shards as u64
8286        + crypto_header.index_root_fec_parity_shards as u64;
8287    let max_record_section_bytes = checked_u64_mul(
8288        max_index_records,
8289        record_len,
8290        "bootstrap sidecar cap overflow",
8291    )?;
8292    if header.index_root_records_length % record_len != 0 {
8293        return Err(FormatError::InvalidArchive(
8294            "bootstrap sidecar IndexRoot records length is not aligned",
8295        ));
8296    }
8297    if header.index_root_records_length / record_len > max_index_records {
8298        return Err(FormatError::InvalidArchive(
8299            "bootstrap sidecar IndexRoot records exceed resource cap",
8300        ));
8301    }
8302    if header.dictionary_records_length % record_len != 0 {
8303        return Err(FormatError::InvalidArchive(
8304            "bootstrap sidecar dictionary records length is not aligned",
8305        ));
8306    }
8307    if header.dictionary_records_length / record_len > max_index_records {
8308        return Err(FormatError::InvalidArchive(
8309            "bootstrap sidecar dictionary records exceed resource cap",
8310        ));
8311    }
8312
8313    let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
8314    if header.has_manifest_footer() {
8315        cap = cap
8316            .checked_add(MANIFEST_FOOTER_LEN as u64)
8317            .ok_or(FormatError::InvalidArchive(
8318                "bootstrap sidecar cap overflow",
8319            ))?;
8320    }
8321    if header.has_index_root_records() {
8322        cap = checked_u64_add(
8323            cap,
8324            max_record_section_bytes,
8325            "bootstrap sidecar cap overflow",
8326        )?;
8327    }
8328    if header.has_dictionary_records() {
8329        cap = checked_u64_add(
8330            cap,
8331            max_record_section_bytes,
8332            "bootstrap sidecar cap overflow",
8333        )?;
8334    }
8335    if file_size > cap {
8336        return Err(FormatError::InvalidArchive(
8337            "bootstrap sidecar exceeds resource cap",
8338        ));
8339    }
8340    Ok(())
8341}
8342
8343#[derive(Debug, Clone, Copy)]
8344struct SidecarBlockRecordsSection {
8345    offset: u64,
8346    length: u64,
8347    extent: ObjectExtent,
8348    data_kind: BlockKind,
8349    parity_kind: BlockKind,
8350    structure: &'static str,
8351}
8352
8353fn parse_sidecar_block_records(
8354    sidecar_bytes: &[u8],
8355    block_size: usize,
8356    section: SidecarBlockRecordsSection,
8357) -> Result<Vec<BlockRecord>, FormatError> {
8358    let record_len = block_size
8359        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8360        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8361    if section.length % record_len as u64 != 0 {
8362        return Err(FormatError::InvalidArchive(
8363            "sidecar BlockRecord section is not aligned",
8364        ));
8365    }
8366    let expected_count =
8367        section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
8368    let actual_count = usize::try_from(section.length / record_len as u64)
8369        .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
8370    if actual_count != expected_count {
8371        return Err(FormatError::InvalidArchive(
8372            "sidecar BlockRecord section does not match declared extent",
8373        ));
8374    }
8375    let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
8376    let raw = slice(
8377        sidecar_bytes,
8378        start,
8379        to_usize(section.length, "BootstrapSidecarHeader")?,
8380        "BootstrapSidecarHeader",
8381    )?;
8382    let mut records = Vec::with_capacity(expected_count);
8383
8384    for idx in 0..expected_count {
8385        let record = BlockRecord::parse(
8386            slice(raw, idx * record_len, record_len, "BlockRecord")?,
8387            block_size,
8388        )?;
8389        let expected_block_index = checked_u64_add(
8390            section.extent.first_block_index,
8391            idx as u64,
8392            section.structure,
8393        )?;
8394        if record.block_index != expected_block_index {
8395            return Err(FormatError::InvalidArchive(
8396                "sidecar BlockRecord section has missing or duplicate blocks",
8397            ));
8398        }
8399        let expected_kind = if idx < section.extent.data_block_count as usize {
8400            section.data_kind
8401        } else {
8402            section.parity_kind
8403        };
8404        if record.kind != expected_kind {
8405            return Err(FormatError::InvalidArchive(
8406                "sidecar BlockRecord section has wrong kind",
8407            ));
8408        }
8409        let should_be_last = idx + 1 == section.extent.data_block_count as usize;
8410        if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
8411        {
8412            return Err(FormatError::InvalidArchive(
8413                "sidecar BlockRecord section has wrong last-data flag",
8414            ));
8415        }
8416        records.push(record);
8417    }
8418
8419    Ok(records)
8420}
8421
8422fn validate_trailer_identity(
8423    volume_header: &VolumeHeader,
8424    trailer: &VolumeTrailer,
8425) -> Result<(), FormatError> {
8426    if trailer.archive_uuid != volume_header.archive_uuid
8427        || trailer.session_id != volume_header.session_id
8428        || trailer.volume_index != volume_header.volume_index
8429    {
8430        return Err(FormatError::InvalidArchive(
8431            "VolumeTrailer identity does not match VolumeHeader",
8432        ));
8433    }
8434    Ok(())
8435}
8436
8437fn validate_manifest_footer(
8438    volume_header: &VolumeHeader,
8439    crypto_header: &CryptoHeaderFixed,
8440    footer: &ManifestFooter,
8441    subkeys: &Subkeys,
8442    volume_format_rev: u16,
8443    raw: &[u8],
8444) -> Result<(), FormatError> {
8445    if footer.archive_uuid != volume_header.archive_uuid
8446        || footer.session_id != volume_header.session_id
8447        || footer.volume_index != volume_header.volume_index
8448    {
8449        return Err(FormatError::InvalidArchive(
8450            "ManifestFooter identity does not match VolumeHeader",
8451        ));
8452    }
8453    if footer.total_volumes != volume_header.stripe_width {
8454        return Err(FormatError::InvalidArchive(
8455            "ManifestFooter total_volumes does not match stripe_width",
8456        ));
8457    }
8458    if footer.is_authoritative != 1 {
8459        return Err(FormatError::InvalidArchive(
8460            "ManifestFooter is not authoritative",
8461        ));
8462    }
8463    verify_integrity_tag(
8464        HmacDomain::ManifestFooter,
8465        crypto_header.aead_algo,
8466        volume_format_rev,
8467        Some(&subkeys.mac_key),
8468        &volume_header.archive_uuid,
8469        &volume_header.session_id,
8470        &raw[..MANIFEST_HMAC_COVERED_LEN],
8471        &footer.manifest_hmac,
8472    )
8473}
8474
8475#[derive(Debug)]
8476struct ParsedBlockRegion {
8477    blocks: BTreeMap<u64, BlockRecord>,
8478    erased_block_indices: BTreeSet<u64>,
8479}
8480
8481fn parse_block_region(
8482    bytes: &[u8],
8483    start: usize,
8484    end: usize,
8485    block_size: usize,
8486    volume_header: &VolumeHeader,
8487    trailer: &VolumeTrailer,
8488) -> Result<ParsedBlockRegion, FormatError> {
8489    if end < start {
8490        return Err(FormatError::InvalidArchive(
8491            "ManifestFooter starts before BlockRecord region",
8492        ));
8493    }
8494    let record_len = block_size
8495        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8496        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8497    let region_len = end - start;
8498    if region_len % record_len != 0 {
8499        return Err(FormatError::InvalidArchive(
8500            "BlockRecord region length is not aligned",
8501        ));
8502    }
8503    let observed_count = region_len / record_len;
8504    if observed_count as u64 != trailer.block_count {
8505        return Err(FormatError::InvalidArchive(
8506            "VolumeTrailer block_count does not match BlockRecord region",
8507        ));
8508    }
8509
8510    let mut blocks = BTreeMap::new();
8511    let mut erased_block_indices = BTreeSet::new();
8512    for idx in 0..observed_count {
8513        let offset = start + idx * record_len;
8514        let expected_block_index = checked_u64_add(
8515            volume_header.volume_index as u64,
8516            checked_u64_mul(
8517                idx as u64,
8518                volume_header.stripe_width as u64,
8519                "BlockRecord index overflow",
8520            )?,
8521            "BlockRecord index overflow",
8522        )?;
8523        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8524        match BlockRecord::parse(raw, block_size) {
8525            Ok(record) => {
8526                if record.block_index != expected_block_index {
8527                    return Err(FormatError::InvalidArchive(
8528                        "BlockRecord index does not match volume position",
8529                    ));
8530                }
8531                if blocks.insert(record.block_index, record).is_some() {
8532                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8533                }
8534            }
8535            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
8536                if !erased_block_indices.insert(expected_block_index) {
8537                    return Err(FormatError::InvalidArchive(
8538                        "duplicate erased BlockRecord index",
8539                    ));
8540                }
8541            }
8542            Err(err) => return Err(err),
8543        }
8544    }
8545
8546    Ok(ParsedBlockRegion {
8547        blocks,
8548        erased_block_indices,
8549    })
8550}
8551
8552fn validate_seekable_block_region_layout(
8553    start: u64,
8554    end: u64,
8555    block_size: usize,
8556    trailer: &VolumeTrailer,
8557) -> Result<(), FormatError> {
8558    if end < start {
8559        return Err(FormatError::InvalidArchive(
8560            "ManifestFooter starts before BlockRecord region",
8561        ));
8562    }
8563    let record_len = block_record_len(block_size)?;
8564    let region_len = end - start;
8565    if region_len % record_len != 0 {
8566        return Err(FormatError::InvalidArchive(
8567            "BlockRecord region length is not aligned",
8568        ));
8569    }
8570    let observed_count = region_len / record_len;
8571    if observed_count != trailer.block_count {
8572        return Err(FormatError::InvalidArchive(
8573            "VolumeTrailer block_count does not match BlockRecord region",
8574        ));
8575    }
8576    Ok(())
8577}
8578
8579fn parse_public_block_observation(
8580    bytes: &[u8],
8581    start: usize,
8582    image: &CriticalMetadataImage,
8583    block_size: usize,
8584    volume_header: &VolumeHeader,
8585) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
8586    let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
8587    if start != image_start {
8588        return Err(FormatError::InvalidArchive(
8589            "public BlockRecord observation start mismatch",
8590        ));
8591    }
8592    let scan_limit_u64 = image
8593        .block_records_offset
8594        .checked_add(image.block_records_length)
8595        .ok_or(FormatError::InvalidArchive(
8596            "public BlockRecord observation limit overflow",
8597        ))?;
8598    if scan_limit_u64 != image.manifest_footer_offset {
8599        return Err(FormatError::InvalidArchive(
8600            "public BlockRecord observation limit mismatch",
8601        ));
8602    }
8603    let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
8604    if scan_limit < start {
8605        return Err(FormatError::InvalidArchive(
8606            "public BlockRecord observation limit before start",
8607        ));
8608    }
8609    let record_len = block_size
8610        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8611        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8612    let region_len = scan_limit - start;
8613    if region_len % record_len != 0 {
8614        return Err(FormatError::InvalidArchive(
8615            "public BlockRecord observation window is not aligned",
8616        ));
8617    }
8618
8619    let mut blocks = BTreeMap::new();
8620    let mut offset = start;
8621    let mut observed_slot = 0u64;
8622    while offset < scan_limit {
8623        let magic_end = checked_add(offset, 4, "BlockRecord")?;
8624        if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
8625            break;
8626        }
8627        let record_end = checked_add(offset, record_len, "BlockRecord")?;
8628        if record_end > scan_limit {
8629            return Err(FormatError::InvalidArchive(
8630                "public BlockRecord observation slot is incomplete",
8631            ));
8632        }
8633        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8634        let record = BlockRecord::parse(raw, block_size)?;
8635        let expected_block_index = checked_u64_add(
8636            volume_header.volume_index as u64,
8637            checked_u64_mul(
8638                observed_slot,
8639                volume_header.stripe_width as u64,
8640                "BlockRecord index overflow",
8641            )?,
8642            "BlockRecord index overflow",
8643        )?;
8644        if record.block_index != expected_block_index {
8645            return Err(FormatError::InvalidArchive(
8646                "public BlockRecord index does not match volume position",
8647            ));
8648        }
8649        if blocks.insert(record.block_index, record).is_some() {
8650            return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8651        }
8652        offset = record_end;
8653        observed_slot = observed_slot
8654            .checked_add(1)
8655            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8656    }
8657
8658    let mut scan = if offset < scan_limit {
8659        checked_add(offset, record_len, "BlockRecord")?
8660    } else {
8661        scan_limit
8662    };
8663    while scan < scan_limit {
8664        let magic_end = checked_add(scan, 4, "BlockRecord")?;
8665        let record_end = checked_add(scan, record_len, "BlockRecord")?;
8666        if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
8667            let raw = slice(bytes, scan, record_len, "BlockRecord")?;
8668            if BlockRecord::parse(raw, block_size).is_ok() {
8669                return Err(FormatError::InvalidArchive(
8670                    "public observation has ambiguous extra BlockRecord",
8671                ));
8672            }
8673        }
8674        scan = record_end;
8675    }
8676
8677    Ok(blocks)
8678}
8679
8680pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
8681    matches!(
8682        error,
8683        FormatError::BadCrc {
8684            structure: "BlockRecord",
8685        } | FormatError::BadMagic {
8686            structure: "BlockRecord",
8687        } | FormatError::NonZeroReserved {
8688            structure: "BlockRecord",
8689        }
8690    )
8691}
8692
8693fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
8694    let len = block_size
8695        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8696        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8697    u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
8698}
8699
8700fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
8701    lhs.checked_mul(rhs)
8702        .ok_or(FormatError::InvalidArchive(reason))
8703}
8704
8705fn parse_stream_block_prefix(
8706    bytes: &[u8],
8707    start: usize,
8708    block_size: usize,
8709    volume_header: &VolumeHeader,
8710) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
8711    let record_len = block_size
8712        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8713        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8714    let mut blocks = BTreeMap::new();
8715    let mut offset = start;
8716    let mut observed_block_count = 0u64;
8717
8718    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
8719        let expected_block_index =
8720            expected_stream_block_index(volume_header, observed_block_count)?;
8721        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8722        match BlockRecord::parse(raw, block_size) {
8723            Ok(record) => {
8724                if record.block_index != expected_block_index {
8725                    return Err(FormatError::InvalidArchive(
8726                        "BlockRecord index does not match stream position",
8727                    ));
8728                }
8729                if blocks.insert(record.block_index, record).is_some() {
8730                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8731                }
8732            }
8733            Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
8734            Err(err) => return Err(err),
8735        }
8736        offset = checked_add(offset, record_len, "BlockRecord")?;
8737        observed_block_count = observed_block_count
8738            .checked_add(1)
8739            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8740    }
8741
8742    Ok((blocks, offset, observed_block_count))
8743}
8744
8745pub(crate) fn expected_stream_block_index(
8746    volume_header: &VolumeHeader,
8747    observed_block_count: u64,
8748) -> Result<u64, FormatError> {
8749    checked_u64_add(
8750        volume_header.volume_index as u64,
8751        checked_u64_mul(
8752            observed_block_count,
8753            volume_header.stripe_width as u64,
8754            "BlockRecord index overflow",
8755        )?,
8756        "BlockRecord index overflow",
8757    )
8758}
8759
8760fn parse_sequential_block_or_erasure(
8761    bytes: &[u8],
8762    offset: usize,
8763    record_len: usize,
8764    block_size: usize,
8765    volume_header: &VolumeHeader,
8766    observed_block_count: u64,
8767) -> Result<Option<BlockRecord>, FormatError> {
8768    let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
8769    let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8770    match BlockRecord::parse(raw, block_size) {
8771        Ok(record) => {
8772            if record.block_index != expected_block_index {
8773                return Err(FormatError::InvalidArchive(
8774                    "BlockRecord index does not match stream position",
8775                ));
8776            }
8777            Ok(Some(record))
8778        }
8779        Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
8780        Err(err) => Err(err),
8781    }
8782}
8783
8784fn parse_terminal_material(
8785    bytes: &[u8],
8786    manifest_offset: usize,
8787    observed_block_count: u64,
8788    context: KeyHoldingTerminalContext<'_>,
8789    options: ReaderOptions,
8790) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
8791    let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
8792    if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
8793        return Err(FormatError::InvalidArchive(
8794            "sequential terminal does not end at EOF",
8795        ));
8796    }
8797    let terminal = candidate.terminal;
8798    if terminal.image.manifest_footer_offset != manifest_offset as u64 {
8799        return Err(FormatError::InvalidArchive(
8800            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8801        ));
8802    }
8803    if terminal.volume_trailer.block_count != observed_block_count {
8804        return Err(FormatError::InvalidArchive(
8805            "VolumeTrailer block_count does not match observed stream",
8806        ));
8807    }
8808    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8809    Ok((
8810        manifest_footer,
8811        terminal.volume_trailer,
8812        terminal.root_auth_footer,
8813    ))
8814}
8815
8816pub(crate) fn parse_terminal_material_read_at(
8817    reader: &dyn ArchiveReadAt,
8818    input_len: u64,
8819    manifest_offset: u64,
8820    observed_block_count: u64,
8821    context: KeyHoldingTerminalContext<'_>,
8822) -> Result<SequentialTerminalMaterial, FormatError> {
8823    let mut candidates = Vec::new();
8824    if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
8825        collect_v41_locator_candidate_read_at(
8826            reader,
8827            input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
8828            0,
8829            context,
8830            &mut candidates,
8831        );
8832    }
8833    if input_len >= LOCATOR_PAIR_LEN as u64 {
8834        collect_v41_locator_candidate_read_at(
8835            reader,
8836            input_len - LOCATOR_PAIR_LEN as u64,
8837            1,
8838            context,
8839            &mut candidates,
8840        );
8841    }
8842
8843    let candidate = choose_v41_terminal_candidate(candidates)?;
8844    if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
8845        return Err(FormatError::InvalidArchive(
8846            "sequential terminal does not end at EOF",
8847        ));
8848    }
8849    let terminal = candidate.terminal;
8850    if terminal.image.manifest_footer_offset != manifest_offset {
8851        return Err(FormatError::InvalidArchive(
8852            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8853        ));
8854    }
8855    if terminal.volume_trailer.block_count != observed_block_count {
8856        return Err(FormatError::InvalidArchive(
8857            "VolumeTrailer block_count does not match observed stream",
8858        ));
8859    }
8860    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8861    Ok(SequentialTerminalMaterial {
8862        manifest_footer,
8863        volume_trailer: terminal.volume_trailer,
8864        root_auth_footer: terminal.root_auth_footer,
8865    })
8866}
8867
8868fn terminal_candidate_reaches_eof(
8869    candidate: &TerminalCandidate,
8870    input_len: usize,
8871) -> Result<bool, FormatError> {
8872    let expected_end =
8873        match candidate.locator_sequence {
8874            Some(0) => candidate.anchor,
8875            Some(1) => candidate
8876                .anchor
8877                .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
8878                .ok_or(FormatError::InvalidArchive(
8879                    "terminal EOF boundary overflow",
8880                ))?,
8881            None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
8882                FormatError::InvalidArchive("terminal EOF boundary overflow"),
8883            )?,
8884            Some(_) => {
8885                return Err(FormatError::InvalidArchive(
8886                    "invalid terminal locator sequence",
8887                ))
8888            }
8889        };
8890    Ok(expected_end == input_len)
8891}
8892
8893#[derive(Debug, Default)]
8894struct PendingSequentialEnvelope {
8895    data_shards: Vec<Option<Vec<u8>>>,
8896    parity_shards: Vec<Option<Vec<u8>>>,
8897    saw_last_data: bool,
8898    awaiting_tentative_parity: bool,
8899}
8900
8901impl PendingSequentialEnvelope {
8902    fn is_empty(&self) -> bool {
8903        self.data_shards.is_empty() && self.parity_shards.is_empty()
8904    }
8905}
8906
8907fn handle_sequential_payload_erasure(
8908    pending: &mut PendingSequentialEnvelope,
8909    crypto_header: &CryptoHeaderFixed,
8910    metadata_seen: bool,
8911) -> Result<(), FormatError> {
8912    if metadata_seen || pending.saw_last_data {
8913        return Err(FormatError::BadCrc {
8914            structure: "BlockRecord",
8915        });
8916    }
8917    if !sequential_payload_parity_is_guaranteed(crypto_header) {
8918        return Err(FormatError::BadCrc {
8919            structure: "BlockRecord",
8920        });
8921    }
8922    pending.data_shards.push(None);
8923    pending.awaiting_tentative_parity = true;
8924    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
8925        return Err(FormatError::InvalidArchive(
8926            "sequential payload envelope exceeds data-shard cap",
8927        ));
8928    }
8929    Ok(())
8930}
8931
8932fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
8933    crypto_header.fec_parity_shards > 0
8934        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
8935}
8936
8937fn sequential_extract_tar_stream_with_options(
8938    bytes: &[u8],
8939    master_key: &MasterKey,
8940    options: ReaderOptions,
8941) -> Result<Vec<u8>, FormatError> {
8942    validate_reader_options(options)?;
8943    if bytes.len() < VOLUME_HEADER_LEN {
8944        return Err(FormatError::InvalidLength {
8945            structure: "archive",
8946            expected: VOLUME_HEADER_LEN,
8947            actual: bytes.len(),
8948        });
8949    }
8950
8951    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
8952    let crypto_start = volume_header.crypto_header_offset as usize;
8953    let crypto_len = volume_header.crypto_header_length as usize;
8954    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
8955    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
8956    let subkeys = subkeys_for_open(
8957        Some(master_key),
8958        parsed_crypto.fixed.aead_algo,
8959        &volume_header.archive_uuid,
8960        &volume_header.session_id,
8961    )?;
8962    verify_integrity_tag(
8963        HmacDomain::CryptoHeader,
8964        parsed_crypto.fixed.aead_algo,
8965        volume_header.volume_format_rev,
8966        Some(&subkeys.mac_key),
8967        &volume_header.archive_uuid,
8968        &volume_header.session_id,
8969        parsed_crypto.hmac_covered_bytes,
8970        &parsed_crypto.header_hmac,
8971    )?;
8972    parsed_crypto.validate_extension_semantics()?;
8973    validate_sequential_supported_volume(
8974        &volume_header,
8975        &parsed_crypto.fixed,
8976        &parsed_crypto.extensions,
8977    )?;
8978    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
8979    let block_records_start = startup_block_records_start(
8980        &volume_header,
8981        &parsed_crypto.kdf_params,
8982        |start, length| {
8983            let start = to_usize(start, "KeyWrapTableV1")?;
8984            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
8985        },
8986    )?;
8987
8988    let block_size = parsed_crypto.fixed.block_size as usize;
8989    let record_len = block_size
8990        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8991        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8992    let mut offset = to_usize(block_records_start, "BlockRecord")?;
8993    let mut observed_block_count = 0u64;
8994    let mut metadata_seen = false;
8995    let mut pending = PendingSequentialEnvelope::default();
8996    let mut next_envelope_index = 0u64;
8997    let mut tar_stream = Vec::new();
8998    let max_tar_stream_size = options.max_verify_tar_size;
8999    let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
9000    let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
9001    let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
9002        parsed_crypto.fixed.max_path_length,
9003        total_extraction_cap,
9004    );
9005
9006    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9007        let record = parse_sequential_block_or_erasure(
9008            bytes,
9009            offset,
9010            record_len,
9011            block_size,
9012            &volume_header,
9013            observed_block_count,
9014        )?;
9015        observed_block_count = observed_block_count
9016            .checked_add(1)
9017            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9018        let Some(record) = record else {
9019            handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
9020            offset = checked_add(offset, record_len, "BlockRecord")?;
9021            continue;
9022        };
9023
9024        match record.kind {
9025            BlockKind::PayloadData => {
9026                if metadata_seen {
9027                    return Err(FormatError::InvalidArchive(
9028                        "payload BlockRecord appears after metadata",
9029                    ));
9030                }
9031                if pending.awaiting_tentative_parity {
9032                    return Err(FormatError::InvalidArchive(
9033                        "sequential payload envelope boundary is ambiguous after CRC erasure",
9034                    ));
9035                }
9036                if pending.saw_last_data {
9037                    finalize_sequential_envelope(
9038                        &mut pending,
9039                        SequentialEnvelopeDecodeContext {
9040                            crypto_header: &parsed_crypto.fixed,
9041                            subkeys: &subkeys,
9042                            volume_header: &volume_header,
9043                            next_envelope_index: &mut next_envelope_index,
9044                            tar_stream: &mut tar_stream,
9045                            max_tar_stream_size,
9046                            tar_stream_total_validator: &mut tar_stream_total_validator,
9047                        },
9048                    )?;
9049                }
9050                let is_last_data = record.is_last_data();
9051                pending.data_shards.push(Some(record.payload));
9052                if is_last_data {
9053                    pending.saw_last_data = true;
9054                }
9055                if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
9056                    return Err(FormatError::InvalidArchive(
9057                        "sequential payload envelope exceeds data-shard cap",
9058                    ));
9059                }
9060            }
9061            BlockKind::PayloadParity => {
9062                if metadata_seen {
9063                    return Err(FormatError::InvalidArchive(
9064                        "payload parity BlockRecord appears after metadata",
9065                    ));
9066                }
9067                if pending.awaiting_tentative_parity {
9068                    pending.awaiting_tentative_parity = false;
9069                    pending.saw_last_data = true;
9070                } else if pending.data_shards.is_empty() || !pending.saw_last_data {
9071                    return Err(FormatError::InvalidArchive(
9072                        "payload parity appears before envelope data is complete",
9073                    ));
9074                }
9075                pending.parity_shards.push(Some(record.payload));
9076                if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
9077                    return Err(FormatError::InvalidArchive(
9078                        "sequential payload envelope exceeds parity-shard cap",
9079                    ));
9080                }
9081            }
9082            _ => {
9083                if !pending.is_empty() {
9084                    finalize_sequential_envelope(
9085                        &mut pending,
9086                        SequentialEnvelopeDecodeContext {
9087                            crypto_header: &parsed_crypto.fixed,
9088                            subkeys: &subkeys,
9089                            volume_header: &volume_header,
9090                            next_envelope_index: &mut next_envelope_index,
9091                            tar_stream: &mut tar_stream,
9092                            max_tar_stream_size,
9093                            tar_stream_total_validator: &mut tar_stream_total_validator,
9094                        },
9095                    )?;
9096                }
9097                metadata_seen = true;
9098            }
9099        }
9100
9101        offset = checked_add(offset, record_len, "BlockRecord")?;
9102    }
9103
9104    if !pending.is_empty() {
9105        finalize_sequential_envelope(
9106            &mut pending,
9107            SequentialEnvelopeDecodeContext {
9108                crypto_header: &parsed_crypto.fixed,
9109                subkeys: &subkeys,
9110                volume_header: &volume_header,
9111                next_envelope_index: &mut next_envelope_index,
9112                tar_stream: &mut tar_stream,
9113                max_tar_stream_size,
9114                tar_stream_total_validator: &mut tar_stream_total_validator,
9115            },
9116        )?;
9117    }
9118
9119    parse_terminal_material(
9120        bytes,
9121        offset,
9122        observed_block_count,
9123        KeyHoldingTerminalContext {
9124            subkeys: &subkeys,
9125            volume_header: &volume_header,
9126            crypto_header: &parsed_crypto.fixed,
9127            crypto_header_bytes: crypto_bytes,
9128        },
9129        options,
9130    )?;
9131    // This public helper is intentionally whole-buffer: decoded payload bytes
9132    // stay internal until terminal ManifestFooter and VolumeTrailer HMACs pass.
9133    validate_tar_stream_total_extraction_size(
9134        &tar_stream,
9135        parsed_crypto.fixed.max_path_length,
9136        total_extraction_cap,
9137    )?;
9138    Ok(tar_stream)
9139}
9140
9141fn validate_sequential_supported_volume(
9142    volume_header: &VolumeHeader,
9143    crypto_header: &CryptoHeaderFixed,
9144    extensions: &[ExtensionTlv<'_>],
9145) -> Result<(), FormatError> {
9146    reject_unsupported_raw_stream_profile(extensions)?;
9147    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
9148        return Err(FormatError::ReaderUnsupported(
9149            "sequential reader supports only single-volume archive input",
9150        ));
9151    }
9152    if crypto_header.stripe_width != volume_header.stripe_width {
9153        return Err(FormatError::InvalidArchive(
9154            "VolumeHeader and CryptoHeader stripe_width differ",
9155        ));
9156    }
9157    if crypto_header.has_dictionary != 0 {
9158        return Err(FormatError::ReaderUnsupported(
9159            "dictionary bootstrap required for non-seekable sequential extraction",
9160        ));
9161    }
9162    Ok(())
9163}
9164
9165struct SequentialEnvelopeDecodeContext<'a> {
9166    crypto_header: &'a CryptoHeaderFixed,
9167    subkeys: &'a Subkeys,
9168    volume_header: &'a VolumeHeader,
9169    next_envelope_index: &'a mut u64,
9170    tar_stream: &'a mut Vec<u8>,
9171    max_tar_stream_size: usize,
9172    tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
9173}
9174
9175fn finalize_sequential_envelope(
9176    pending: &mut PendingSequentialEnvelope,
9177    context: SequentialEnvelopeDecodeContext<'_>,
9178) -> Result<(), FormatError> {
9179    if !pending.saw_last_data {
9180        return Err(FormatError::InvalidArchive(
9181            "sequential payload envelope is missing last-data flag",
9182        ));
9183    }
9184    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
9185        return Err(FormatError::InvalidArchive(
9186            "sequential payload envelope exceeds data-shard cap",
9187        ));
9188    }
9189    if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
9190        return Err(FormatError::InvalidArchive(
9191            "sequential payload envelope exceeds parity-shard cap",
9192        ));
9193    }
9194    let required_parity =
9195        required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
9196    if pending.parity_shards.len() < required_parity as usize {
9197        return Err(FormatError::InvalidArchive(
9198            "sequential payload envelope has insufficient parity for recovery settings",
9199        ));
9200    }
9201
9202    let repaired = repair_data_gf16(
9203        &pending.data_shards,
9204        &pending.parity_shards,
9205        context.crypto_header.block_size as usize,
9206    )?;
9207    let mut encrypted =
9208        Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
9209    for shard in repaired {
9210        encrypted.extend_from_slice(&shard);
9211    }
9212    let plaintext = decrypt_padded_aead_object(
9213        AeadObjectContext {
9214            algo: context.crypto_header.aead_algo,
9215            key: &context.subkeys.enc_key,
9216            nonce_seed: &context.subkeys.nonce_seed,
9217            domain: b"envelope",
9218            archive_uuid: &context.volume_header.archive_uuid,
9219            session_id: &context.volume_header.session_id,
9220            counter: *context.next_envelope_index,
9221        },
9222        &encrypted,
9223    )?;
9224    decode_concatenated_zstd_frames_with_cap(
9225        &plaintext,
9226        None,
9227        context.tar_stream,
9228        context.max_tar_stream_size,
9229        Some(context.tar_stream_total_validator),
9230    )?;
9231    *context.next_envelope_index = (*context.next_envelope_index)
9232        .checked_add(1)
9233        .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
9234    *pending = PendingSequentialEnvelope::default();
9235    Ok(())
9236}
9237
9238fn decode_concatenated_zstd_frames_with_cap(
9239    plaintext: &[u8],
9240    dictionary: Option<&[u8]>,
9241    output: &mut Vec<u8>,
9242    max_output_len: usize,
9243    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9244) -> Result<(), FormatError> {
9245    let mut cursor = 0usize;
9246    while cursor < plaintext.len() {
9247        let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
9248            .map_err(|_| FormatError::InvalidZstdFrame)?;
9249        if frame_len == 0 {
9250            return Err(FormatError::InvalidZstdFrame);
9251        }
9252        let end = checked_add(cursor, frame_len, "zstd frame")?;
9253        validate_exact_zstd_frame(&plaintext[cursor..end])?;
9254        if let Some(dictionary) = dictionary {
9255            let mut decoder =
9256                zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
9257                    .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9258            read_zstd_frame_to_capped_output(
9259                &mut decoder,
9260                output,
9261                max_output_len,
9262                tar_stream_total_validator.as_deref_mut(),
9263            )?;
9264        } else {
9265            let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
9266                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9267            read_zstd_frame_to_capped_output(
9268                &mut decoder,
9269                output,
9270                max_output_len,
9271                tar_stream_total_validator.as_deref_mut(),
9272            )?;
9273        }
9274        cursor = end;
9275    }
9276    Ok(())
9277}
9278
9279fn read_zstd_frame_to_capped_output<R: Read>(
9280    decoder: &mut R,
9281    output: &mut Vec<u8>,
9282    max_output_len: usize,
9283    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9284) -> Result<(), FormatError> {
9285    let mut buf = [0u8; 64 * 1024];
9286    loop {
9287        let read = decoder
9288            .read(&mut buf)
9289            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9290        if read == 0 {
9291            return Ok(());
9292        }
9293        let next_len = output
9294            .len()
9295            .checked_add(read)
9296            .ok_or(FormatError::ReaderUnsupported(
9297                "sequential tar stream exceeds configured verification cap",
9298            ))?;
9299        if next_len > max_output_len {
9300            return Err(FormatError::ReaderUnsupported(
9301                "sequential tar stream exceeds configured verification cap",
9302            ));
9303        }
9304        output.extend_from_slice(&buf[..read]);
9305        if let Some(validator) = tar_stream_total_validator.as_mut() {
9306            validator.observe(output)?;
9307        }
9308    }
9309}
9310
9311fn load_archive_dictionary(
9312    blocks: &impl BlockProvider,
9313    subkeys: &Subkeys,
9314    volume_header: &VolumeHeader,
9315    crypto_header: &CryptoHeaderFixed,
9316    index_root: &IndexRoot,
9317) -> Result<Option<Vec<u8>>, FormatError> {
9318    if crypto_header.has_dictionary == 0 {
9319        return Ok(None);
9320    }
9321    let plaintext = load_metadata_object_from_parts(
9322        blocks,
9323        ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
9324        index_root.header.dictionary_decompressed_size,
9325    )?;
9326    Ok(Some(plaintext))
9327}
9328
9329#[derive(Clone, Copy)]
9330struct ObjectLoadContext<'a> {
9331    volume_header: &'a VolumeHeader,
9332    crypto_header: &'a CryptoHeaderFixed,
9333    extent: ObjectExtent,
9334    data_kind: BlockKind,
9335    parity_kind: BlockKind,
9336    key: &'a [u8; 32],
9337    nonce_seed: &'a [u8; 32],
9338    domain: &'a [u8],
9339    counter: u64,
9340    class_data_shard_max: u16,
9341    class_parity_shard_max: u16,
9342}
9343
9344impl<'a> ObjectLoadContext<'a> {
9345    fn index_root(
9346        volume_header: &'a VolumeHeader,
9347        crypto_header: &'a CryptoHeaderFixed,
9348        subkeys: &'a Subkeys,
9349        extent: ObjectExtent,
9350    ) -> Self {
9351        Self {
9352            volume_header,
9353            crypto_header,
9354            extent,
9355            data_kind: BlockKind::IndexRootData,
9356            parity_kind: BlockKind::IndexRootParity,
9357            key: &subkeys.index_root_key,
9358            nonce_seed: &subkeys.index_nonce_seed,
9359            domain: b"idxroot",
9360            counter: 0,
9361            class_data_shard_max: crypto_header.index_root_fec_data_shards,
9362            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9363        }
9364    }
9365
9366    fn index_shard(
9367        volume_header: &'a VolumeHeader,
9368        crypto_header: &'a CryptoHeaderFixed,
9369        subkeys: &'a Subkeys,
9370        entry: &ShardEntry,
9371    ) -> Self {
9372        Self {
9373            volume_header,
9374            crypto_header,
9375            extent: ObjectExtent {
9376                first_block_index: entry.first_block_index,
9377                data_block_count: entry.data_block_count,
9378                parity_block_count: entry.parity_block_count,
9379                encrypted_size: entry.encrypted_size,
9380            },
9381            data_kind: BlockKind::IndexShardData,
9382            parity_kind: BlockKind::IndexShardParity,
9383            key: &subkeys.index_shard_key,
9384            nonce_seed: &subkeys.index_nonce_seed,
9385            domain: b"idxshard",
9386            counter: entry.shard_index,
9387            class_data_shard_max: crypto_header.index_fec_data_shards,
9388            class_parity_shard_max: crypto_header.index_fec_parity_shards,
9389        }
9390    }
9391
9392    fn directory_hint(
9393        volume_header: &'a VolumeHeader,
9394        crypto_header: &'a CryptoHeaderFixed,
9395        subkeys: &'a Subkeys,
9396        entry: &DirectoryHintShardEntry,
9397    ) -> Self {
9398        Self {
9399            volume_header,
9400            crypto_header,
9401            extent: ObjectExtent {
9402                first_block_index: entry.first_block_index,
9403                data_block_count: entry.data_block_count,
9404                parity_block_count: entry.parity_block_count,
9405                encrypted_size: entry.encrypted_size,
9406            },
9407            data_kind: BlockKind::DirectoryHintData,
9408            parity_kind: BlockKind::DirectoryHintParity,
9409            key: &subkeys.dir_hint_key,
9410            nonce_seed: &subkeys.index_nonce_seed,
9411            domain: b"dirhint",
9412            counter: entry.hint_shard_index,
9413            class_data_shard_max: crypto_header.index_fec_data_shards,
9414            class_parity_shard_max: crypto_header.index_fec_parity_shards,
9415        }
9416    }
9417
9418    fn dictionary(
9419        volume_header: &'a VolumeHeader,
9420        crypto_header: &'a CryptoHeaderFixed,
9421        subkeys: &'a Subkeys,
9422        index_root: &IndexRoot,
9423    ) -> Result<Self, FormatError> {
9424        Ok(Self {
9425            volume_header,
9426            crypto_header,
9427            extent: dictionary_extent_from_index_root(index_root)?,
9428            data_kind: BlockKind::DictionaryData,
9429            parity_kind: BlockKind::DictionaryParity,
9430            key: &subkeys.dictionary_key,
9431            nonce_seed: &subkeys.index_nonce_seed,
9432            domain: b"dict",
9433            counter: 0,
9434            class_data_shard_max: crypto_header.index_root_fec_data_shards,
9435            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9436        })
9437    }
9438
9439    fn payload(
9440        volume_header: &'a VolumeHeader,
9441        crypto_header: &'a CryptoHeaderFixed,
9442        subkeys: &'a Subkeys,
9443        envelope: &EnvelopeEntry,
9444    ) -> Self {
9445        Self {
9446            volume_header,
9447            crypto_header,
9448            extent: ObjectExtent {
9449                first_block_index: envelope.first_block_index,
9450                data_block_count: envelope.data_block_count,
9451                parity_block_count: envelope.parity_block_count,
9452                encrypted_size: envelope.encrypted_size,
9453            },
9454            data_kind: BlockKind::PayloadData,
9455            parity_kind: BlockKind::PayloadParity,
9456            key: &subkeys.enc_key,
9457            nonce_seed: &subkeys.nonce_seed,
9458            domain: b"envelope",
9459            counter: envelope.envelope_index,
9460            class_data_shard_max: crypto_header.fec_data_shards,
9461            class_parity_shard_max: crypto_header.fec_parity_shards,
9462        }
9463    }
9464}
9465
9466fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
9467    if index_root.header.dictionary_data_block_count == 0
9468        || index_root.header.dictionary_encrypted_size == 0
9469        || index_root.header.dictionary_decompressed_size == 0
9470    {
9471        return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
9472    }
9473    Ok(ObjectExtent {
9474        first_block_index: index_root.header.dictionary_first_block,
9475        data_block_count: index_root.header.dictionary_data_block_count,
9476        parity_block_count: index_root.header.dictionary_parity_block_count,
9477        encrypted_size: index_root.header.dictionary_encrypted_size,
9478    })
9479}
9480
9481fn load_metadata_object_from_parts(
9482    blocks: &impl BlockProvider,
9483    context: ObjectLoadContext<'_>,
9484    decompressed_size: u32,
9485) -> Result<Vec<u8>, FormatError> {
9486    let compressed = load_decrypted_object_from_parts(blocks, context)?;
9487    decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
9488}
9489
9490fn load_decrypted_object_from_parts(
9491    blocks: &impl BlockProvider,
9492    context: ObjectLoadContext<'_>,
9493) -> Result<Vec<u8>, FormatError> {
9494    load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
9495}
9496
9497fn load_decrypted_object_from_parts_with_parity_policy(
9498    blocks: &impl BlockProvider,
9499    context: ObjectLoadContext<'_>,
9500    parity_policy: ParityReadPolicy,
9501) -> Result<Vec<u8>, FormatError> {
9502    let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
9503        blocks,
9504        context.crypto_header,
9505        context.extent,
9506        context.data_kind,
9507        context.parity_kind,
9508        context.class_data_shard_max,
9509        context.class_parity_shard_max,
9510        parity_policy,
9511    )?;
9512    let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
9513    for shard in repaired {
9514        encrypted.extend_from_slice(&shard);
9515    }
9516    if encrypted.len() != context.extent.encrypted_size as usize {
9517        return Err(FormatError::InvalidArchive(
9518            "object encrypted size does not match repaired shards",
9519        ));
9520    }
9521
9522    decrypt_padded_aead_object(
9523        AeadObjectContext {
9524            algo: context.crypto_header.aead_algo,
9525            key: context.key,
9526            nonce_seed: context.nonce_seed,
9527            domain: context.domain,
9528            archive_uuid: &context.volume_header.archive_uuid,
9529            session_id: &context.volume_header.session_id,
9530            counter: context.counter,
9531        },
9532        &encrypted,
9533    )
9534}
9535
9536fn load_repaired_object_data_shards_from_parts(
9537    blocks: &impl BlockProvider,
9538    crypto_header: &CryptoHeaderFixed,
9539    extent: ObjectExtent,
9540    data_kind: BlockKind,
9541    parity_kind: BlockKind,
9542    class_data_shard_max: u16,
9543    class_parity_shard_max: u16,
9544) -> Result<Vec<Vec<u8>>, FormatError> {
9545    load_repaired_object_data_shards_from_parts_with_parity_policy(
9546        blocks,
9547        crypto_header,
9548        extent,
9549        data_kind,
9550        parity_kind,
9551        class_data_shard_max,
9552        class_parity_shard_max,
9553        ParityReadPolicy::Always,
9554    )
9555}
9556
9557#[allow(clippy::too_many_arguments)]
9558fn load_repaired_object_data_shards_from_parts_with_parity_policy(
9559    blocks: &impl BlockProvider,
9560    crypto_header: &CryptoHeaderFixed,
9561    extent: ObjectExtent,
9562    data_kind: BlockKind,
9563    parity_kind: BlockKind,
9564    class_data_shard_max: u16,
9565    class_parity_shard_max: u16,
9566    parity_policy: ParityReadPolicy,
9567) -> Result<Vec<Vec<u8>>, FormatError> {
9568    validate_object_extent(
9569        extent,
9570        crypto_header,
9571        class_data_shard_max,
9572        class_parity_shard_max,
9573    )?;
9574    let block_size = crypto_header.block_size as usize;
9575    let data_count = extent.data_block_count as usize;
9576    let parity_count = extent.parity_block_count as usize;
9577    let mut data_shards = Vec::with_capacity(data_count);
9578    let mut parity_shards = Vec::with_capacity(parity_count);
9579
9580    for offset in 0..data_count {
9581        let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
9582        if let Some(record) = blocks.block(block_index)? {
9583            if record.kind != data_kind {
9584                return Err(FormatError::InvalidArchive(
9585                    "object data block has unexpected kind",
9586                ));
9587            }
9588            let should_be_last = offset + 1 == data_count;
9589            if record.is_last_data() != should_be_last {
9590                return Err(FormatError::InvalidArchive(
9591                    "object last-data flag is not on the final data block",
9592                ));
9593            }
9594            data_shards.push(Some(record.payload.clone()));
9595        } else {
9596            data_shards.push(None);
9597        }
9598    }
9599
9600    if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
9601        return repair_data_gf16(&data_shards, &[], block_size);
9602    }
9603
9604    for offset in 0..parity_count {
9605        let block_index = checked_u64_add(
9606            extent.first_block_index,
9607            data_count as u64 + offset as u64,
9608            "object",
9609        )?;
9610        if let Some(record) = blocks.block(block_index)? {
9611            if record.kind != parity_kind {
9612                return Err(FormatError::InvalidArchive(
9613                    "object parity block has unexpected kind",
9614                ));
9615            }
9616            if record.is_last_data() {
9617                return Err(FormatError::InvalidArchive(
9618                    "object parity block has last-data flag",
9619                ));
9620            }
9621            parity_shards.push(Some(record.payload.clone()));
9622        } else {
9623            parity_shards.push(None);
9624        }
9625    }
9626
9627    repair_data_gf16(&data_shards, &parity_shards, block_size)
9628}
9629
9630fn validate_object_extent(
9631    extent: ObjectExtent,
9632    crypto_header: &CryptoHeaderFixed,
9633    class_data_shard_max: u16,
9634    class_parity_shard_max: u16,
9635) -> Result<(), FormatError> {
9636    if extent.data_block_count == 0 || extent.encrypted_size == 0 {
9637        return Err(FormatError::InvalidArchive(
9638            "encrypted object has zero data blocks or size",
9639        ));
9640    }
9641    if extent.data_block_count > class_data_shard_max as u32 {
9642        return Err(FormatError::InvalidArchive(
9643            "encrypted object exceeds its class data-shard maximum",
9644        ));
9645    }
9646    if extent.parity_block_count > class_parity_shard_max as u32 {
9647        return Err(FormatError::InvalidArchive(
9648            "encrypted object exceeds its class parity-shard maximum",
9649        ));
9650    }
9651    let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
9652    if extent.parity_block_count != required_parity {
9653        return Err(FormatError::InvalidArchive(
9654            "encrypted object parity does not match v41 compute_parity",
9655        ));
9656    }
9657    let total = checked_u64_add(
9658        extent.data_block_count as u64,
9659        extent.parity_block_count as u64,
9660        "encrypted object shard count overflow",
9661    )?;
9662    if total > 65_535 {
9663        return Err(FormatError::FecTooManyShards(total as usize));
9664    }
9665    let expected = checked_u64_mul(
9666        extent.data_block_count as u64,
9667        crypto_header.block_size as u64,
9668        "encrypted object size overflow",
9669    )?;
9670    if expected != extent.encrypted_size as u64 {
9671        return Err(FormatError::InvalidArchive(
9672            "encrypted object size is not data_block_count * block_size",
9673        ));
9674    }
9675    if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
9676        return Err(FormatError::InvalidArchive(
9677            "encrypted object is too small for AEAD tag",
9678        ));
9679    }
9680    Ok(())
9681}
9682
9683pub(crate) fn required_object_parity(
9684    data_block_count: u64,
9685    crypto_header: &CryptoHeaderFixed,
9686) -> Result<u32, FormatError> {
9687    let min_parity =
9688        if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
9689            1
9690        } else {
9691            0
9692        };
9693    let mut parity = 0u64;
9694    for _ in 0..100 {
9695        let total = data_block_count
9696            .checked_add(parity)
9697            .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
9698        let by_volume = checked_u64_mul(
9699            crypto_header.volume_loss_tolerance as u64,
9700            ceil_div_u64(total, crypto_header.stripe_width as u64)?,
9701            "volume-loss parity overflow",
9702        )?;
9703        let by_bitrot = ceil_div_u64(
9704            checked_u64_mul(
9705                total,
9706                crypto_header.bit_rot_buffer_pct as u64,
9707                "bit-rot parity overflow",
9708            )?,
9709            100,
9710        )?;
9711        let next = by_volume
9712            .checked_add(by_bitrot)
9713            .ok_or(FormatError::InvalidArchive("parity overflow"))?
9714            .max(min_parity);
9715        if next == parity {
9716            return u32::try_from(next)
9717                .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
9718        }
9719        parity = next;
9720    }
9721    Err(FormatError::InvalidArchive(
9722        "parity calculation did not converge",
9723    ))
9724}
9725
9726fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
9727    if denominator == 0 {
9728        return Err(FormatError::InvalidArchive("division by zero"));
9729    }
9730    numerator
9731        .checked_add(denominator - 1)
9732        .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
9733        .map(|value| value / denominator)
9734}
9735
9736fn frame_range_for_file<'b>(
9737    shard: &'b IndexShard,
9738    file: &FileEntry,
9739) -> Result<Vec<&'b FrameEntry>, FormatError> {
9740    let mut frames = Vec::with_capacity(file.frame_count as usize);
9741    for offset in 0..file.frame_count as u64 {
9742        let frame_index =
9743            file.first_frame_index
9744                .checked_add(offset)
9745                .ok_or(FormatError::InvalidArchive(
9746                    "FileEntry frame range overflow",
9747                ))?;
9748        let frame = shard
9749            .frames
9750            .iter()
9751            .find(|entry| entry.frame_index == frame_index)
9752            .ok_or(FormatError::InvalidArchive(
9753                "FileEntry references missing FrameEntry",
9754            ))?;
9755        frames.push(frame);
9756    }
9757    Ok(frames)
9758}
9759
9760fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
9761    MetadataLimits {
9762        block_size: crypto_header.block_size,
9763        max_path_length: crypto_header.max_path_length,
9764        max_payload_data_shards: crypto_header.fec_data_shards,
9765        max_payload_parity_shards: crypto_header.fec_parity_shards,
9766        max_index_data_shards: crypto_header.index_fec_data_shards,
9767        max_index_parity_shards: crypto_header.index_fec_parity_shards,
9768        max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
9769        max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
9770        ..MetadataLimits::default()
9771    }
9772}
9773
9774fn verify_dense_keys<T>(
9775    entries: &BTreeMap<u64, T>,
9776    expected_count: u64,
9777    structure: &'static str,
9778) -> Result<(), FormatError> {
9779    if entries.len() as u64 != expected_count {
9780        return Err(FormatError::InvalidArchive(
9781            "decoded table count does not match IndexRoot",
9782        ));
9783    }
9784    for expected in 0..expected_count {
9785        if !entries.contains_key(&expected) {
9786            return Err(FormatError::InvalidMetadata {
9787                structure,
9788                reason: "global index coverage has a gap",
9789            });
9790        }
9791    }
9792    Ok(())
9793}
9794
9795fn validate_envelope_frame_coverage(
9796    frames: &BTreeMap<u64, FrameEntry>,
9797    envelopes: &BTreeMap<u64, EnvelopeEntry>,
9798) -> Result<(), FormatError> {
9799    let mut accounted_frames = BTreeSet::new();
9800    for envelope in envelopes.values() {
9801        let first = envelope.first_frame_index;
9802        let end =
9803            first
9804                .checked_add(envelope.frame_count as u64)
9805                .ok_or(FormatError::InvalidArchive(
9806                    "EnvelopeEntry frame range overflow",
9807                ))?;
9808        let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
9809        for frame_index in first..end {
9810            let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
9811                "EnvelopeEntry references missing FrameEntry",
9812            ))?;
9813            if frame.envelope_index != envelope.envelope_index {
9814                return Err(FormatError::InvalidArchive(
9815                    "FrameEntry envelope_index does not match containing EnvelopeEntry",
9816                ));
9817            }
9818            if !accounted_frames.insert(frame_index) {
9819                return Err(FormatError::InvalidArchive(
9820                    "FrameEntry is covered by multiple EnvelopeEntries",
9821                ));
9822            }
9823            let start = frame.offset_in_envelope as usize;
9824            let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
9825            if end > envelope.plaintext_size as usize {
9826                return Err(FormatError::InvalidArchive(
9827                    "FrameEntry exceeds EnvelopeEntry plaintext_size",
9828                ));
9829            }
9830            ranges.push((start, end));
9831        }
9832        validate_exact_coverage_ranges(
9833            &mut ranges,
9834            envelope.plaintext_size as usize,
9835            "EnvelopeEntry frame coverage has a gap or overlap",
9836        )?;
9837    }
9838
9839    for frame_index in frames.keys() {
9840        if !accounted_frames.contains(frame_index) {
9841            return Err(FormatError::InvalidArchive(
9842                "FrameEntry is not covered by any EnvelopeEntry",
9843            ));
9844        }
9845    }
9846    Ok(())
9847}
9848
9849fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
9850    let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
9851    for shard in shards {
9852        for (idx, file) in shard.files.iter().enumerate() {
9853            let path = shard
9854                .file_path(idx)
9855                .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
9856                .to_vec();
9857            let start = shard
9858                .tar_member_group_start(idx)
9859                .ok_or(FormatError::InvalidArchive(
9860                    "FileEntry tar member start is missing",
9861                ))?;
9862            let key = (file.path_hash, path, start);
9863            validate_global_file_table_key_step(previous.as_ref(), &key)?;
9864            previous = Some(key);
9865        }
9866    }
9867    Ok(())
9868}
9869
9870fn validate_global_file_table_key_step(
9871    previous: Option<&([u8; 8], Vec<u8>, u64)>,
9872    current: &([u8; 8], Vec<u8>, u64),
9873) -> Result<(), FormatError> {
9874    if let Some(previous) = previous {
9875        if previous >= current {
9876            return Err(FormatError::InvalidArchive(
9877                "global FileEntry rows are not sorted and unique",
9878            ));
9879        }
9880    }
9881    Ok(())
9882}
9883
9884fn validate_file_extent_coverage_ranges(
9885    extents: &[(u64, u64)],
9886    tar_len: u64,
9887) -> Result<(), FormatError> {
9888    let mut ranges = Vec::with_capacity(extents.len());
9889    for (start, len) in extents {
9890        let end = checked_u64_add(*start, *len, "FileEntry")?;
9891        if end > tar_len {
9892            return Err(FormatError::InvalidArchive(
9893                "FileEntry extent exceeds IndexRoot tar_total_size",
9894            ));
9895        }
9896        ranges.push((*start, end));
9897    }
9898    validate_exact_coverage_ranges_u64(
9899        &mut ranges,
9900        tar_len,
9901        "FileEntry extents do not cover tar stream exactly",
9902    )
9903}
9904
9905fn add_expected_directory_hint_rows(
9906    map: &mut DirectoryHintMap,
9907    shard_row_index: u32,
9908    path: &[u8],
9909    kind: TarEntryKind,
9910) {
9911    map.entry(Vec::new()).or_default().insert(shard_row_index);
9912    for (idx, byte) in path.iter().enumerate() {
9913        if *byte == b'/' {
9914            map.entry(path[..idx].to_vec())
9915                .or_default()
9916                .insert(shard_row_index);
9917        }
9918    }
9919    if kind == TarEntryKind::Directory {
9920        map.entry(path.to_vec())
9921            .or_default()
9922            .insert(shard_row_index);
9923    }
9924}
9925
9926fn validate_directory_hint_tables_against_expected(
9927    tables: &[DirectoryHintTable],
9928    expected: &DirectoryHintMap,
9929) -> Result<(), FormatError> {
9930    let mut actual = Vec::new();
9931    let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
9932
9933    for table in tables {
9934        for entry_index in 0..table.entries.len() {
9935            let path = table
9936                .entry_path(entry_index)
9937                .ok_or(FormatError::InvalidArchive(
9938                    "DirectoryHintEntry path is missing",
9939                ))?;
9940            let key = (hash_prefix(path), path.to_vec());
9941            if let Some(previous) = &previous_key {
9942                if previous >= &key {
9943                    return Err(FormatError::InvalidArchive(
9944                        "DirectoryHintEntry rows are not globally sorted",
9945                    ));
9946                }
9947            }
9948            previous_key = Some(key);
9949
9950            let rows =
9951                table
9952                    .shard_rows_for_entry(entry_index)
9953                    .ok_or(FormatError::InvalidArchive(
9954                        "DirectoryHintEntry shard rows are missing",
9955                    ))?;
9956            actual.push((path.to_vec(), rows.to_vec()));
9957        }
9958    }
9959
9960    if actual != sorted_directory_hint_rows(expected) {
9961        return Err(FormatError::InvalidArchive(
9962            "directory hint map does not match decoded files",
9963        ));
9964    }
9965    Ok(())
9966}
9967
9968fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
9969    let mut rows = map
9970        .iter()
9971        .map(|(path, shard_rows)| {
9972            (
9973                path.clone(),
9974                shard_rows.iter().copied().collect::<Vec<u32>>(),
9975            )
9976        })
9977        .collect::<Vec<_>>();
9978    rows.sort_by(|(left_path, _), (right_path, _)| {
9979        hash_prefix(left_path)
9980            .cmp(&hash_prefix(right_path))
9981            .then_with(|| left_path.cmp(right_path))
9982    });
9983    rows
9984}
9985
9986fn validate_exact_coverage_ranges(
9987    ranges: &mut [(usize, usize)],
9988    expected_end: usize,
9989    reason: &'static str,
9990) -> Result<(), FormatError> {
9991    ranges.sort_unstable();
9992    let mut cursor = 0usize;
9993    for (start, end) in ranges.iter().copied() {
9994        if start != cursor || end < start {
9995            return Err(FormatError::InvalidArchive(reason));
9996        }
9997        cursor = end;
9998    }
9999    if cursor != expected_end {
10000        return Err(FormatError::InvalidArchive(reason));
10001    }
10002    Ok(())
10003}
10004
10005fn validate_exact_coverage_ranges_u64(
10006    ranges: &mut [(u64, u64)],
10007    expected_end: u64,
10008    reason: &'static str,
10009) -> Result<(), FormatError> {
10010    ranges.sort_unstable();
10011    let mut cursor = 0u64;
10012    for (start, end) in ranges.iter().copied() {
10013        if start != cursor || end < start {
10014            return Err(FormatError::InvalidArchive(reason));
10015        }
10016        cursor = end;
10017    }
10018    if cursor != expected_end {
10019        return Err(FormatError::InvalidArchive(reason));
10020    }
10021    Ok(())
10022}
10023
10024fn object_block_range(
10025    first_block_index: u64,
10026    data_block_count: u32,
10027    parity_block_count: u32,
10028    structure: &'static str,
10029) -> Result<(u64, u64), FormatError> {
10030    let total = data_block_count as u64 + parity_block_count as u64;
10031    if total == 0 {
10032        return Err(FormatError::InvalidArchive(structure));
10033    }
10034    let end = checked_u64_add(first_block_index, total, structure)?;
10035    Ok((first_block_index, end))
10036}
10037
10038fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
10039    ranges.sort_unstable();
10040    for pair in ranges.windows(2) {
10041        if pair[0].1 > pair[1].0 {
10042            return Err(FormatError::InvalidArchive(
10043                "encrypted object block ranges overlap",
10044            ));
10045        }
10046    }
10047    Ok(())
10048}
10049
10050pub(crate) fn observed_archive_size(
10051    sizes: impl IntoIterator<Item = u64>,
10052) -> Result<u64, FormatError> {
10053    sizes.into_iter().try_fold(0u64, |sum, size| {
10054        sum.checked_add(size).ok_or(FormatError::InvalidArchive(
10055            "observed archive size overflow",
10056        ))
10057    })
10058}
10059
10060pub(crate) fn total_extraction_size_cap(
10061    options: ReaderOptions,
10062    observed_archive_bytes: u64,
10063) -> u64 {
10064    options
10065        .max_total_extraction_size
10066        .min(observed_archive_bytes.saturating_mul(10))
10067}
10068
10069fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
10070    std::str::from_utf8(bytes)
10071        .map(|path| path.to_owned())
10072        .map_err(|_| FormatError::UnsafeArchivePath)
10073}
10074
10075fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
10076    let mut bytes = [0u8; 104];
10077    bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
10078    bytes[36..40].fill(0);
10079    bytes
10080}
10081
10082fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
10083    let digest = Sha256::digest(bytes);
10084    let mut out = [0u8; 32];
10085    out.copy_from_slice(&digest);
10086    out
10087}
10088
10089fn slice<'b>(
10090    bytes: &'b [u8],
10091    offset: usize,
10092    len: usize,
10093    structure: &'static str,
10094) -> Result<&'b [u8], FormatError> {
10095    let end = checked_add(offset, len, structure)?;
10096    bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10097        structure,
10098        expected: end,
10099        actual: bytes.len(),
10100    })
10101}
10102
10103fn read_at_vec(
10104    reader: &dyn ArchiveReadAt,
10105    offset: u64,
10106    len: usize,
10107    structure: &'static str,
10108) -> Result<Vec<u8>, FormatError> {
10109    let expected_end = offset
10110        .checked_add(len as u64)
10111        .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
10112    let observed_len = reader.len()?;
10113    if expected_end > observed_len {
10114        return Err(FormatError::InvalidLength {
10115            structure,
10116            expected: to_usize(expected_end, structure)?,
10117            actual: to_usize(observed_len, structure)?,
10118        });
10119    }
10120    let mut out = vec![0u8; len];
10121    reader.read_exact_at(offset, &mut out)?;
10122    Ok(out)
10123}
10124
10125fn read_at_vec_unchecked(
10126    reader: &dyn ArchiveReadAt,
10127    offset: u64,
10128    len: usize,
10129) -> Result<Vec<u8>, FormatError> {
10130    let mut out = vec![0u8; len];
10131    reader.read_exact_at(offset, &mut out)?;
10132    Ok(out)
10133}
10134
10135fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
10136where
10137    T: Sync,
10138    U: Send,
10139    F: Fn(&T) -> Result<U, FormatError> + Sync,
10140{
10141    if jobs <= 1 || items.len() <= 1 {
10142        return items.iter().map(f).collect();
10143    }
10144    let worker_count = jobs.min(items.len());
10145    let chunk_size = items.len().div_ceil(worker_count);
10146    let mut out = Vec::with_capacity(items.len());
10147    thread::scope(|scope| {
10148        let handles = items
10149            .chunks(chunk_size)
10150            .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
10151            .collect::<Vec<_>>();
10152        for handle in handles {
10153            let mut chunk = handle
10154                .join()
10155                .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
10156            out.append(&mut chunk);
10157        }
10158        Ok(out)
10159    })
10160}
10161
10162fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
10163    lhs.checked_add(rhs)
10164        .ok_or(FormatError::InvalidArchive(structure))
10165}
10166
10167fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
10168    lhs.checked_add(rhs)
10169        .ok_or(FormatError::InvalidArchive(structure))
10170}
10171
10172fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
10173    usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
10174}
10175
10176#[cfg(test)]
10177mod tests {
10178    use std::fs;
10179
10180    use super::*;
10181    use crate::compression::compress_zstd_frame;
10182    use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
10183    use crate::fec::encode_parity_gf16;
10184    use crate::format::{
10185        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
10186        CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION, READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10187        VOLUME_FORMAT_REV, VOLUME_FORMAT_REV_43, VOLUME_FORMAT_REV_44,
10188    };
10189    use crate::metadata::{
10190        DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
10191        ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
10192    };
10193    use crate::non_seekable_reader::{
10194        extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
10195        verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
10196        verify_non_seekable_stream_with_recipient_wrap_resolver_options, NonSeekableReaderOptions,
10197        SequentialRootAuthStatus,
10198    };
10199    use crate::raw_stream_profile::{
10200        serialize_raw_stream_content_model_extension, RAW_STREAM_UNSUPPORTED_MESSAGE,
10201    };
10202    use crate::wire::RecipientRecordV1;
10203    use crate::writer::{
10204        write_archive, write_archive_unencrypted, write_archive_with_dictionary,
10205        write_archive_with_kdf, write_archive_with_recipient_wrap_records,
10206        write_archive_with_root_auth, write_archive_with_root_auth_and_recipient_wrap_records,
10207        RegularFile, RootAuthSigningRequest, RootAuthWriterConfig, WriterOptions,
10208    };
10209
10210    fn master_key() -> MasterKey {
10211        MasterKey::from_raw_key(&[0x42; 32]).unwrap()
10212    }
10213
10214    fn recipient_wrap_test_record() -> RecipientRecordV1 {
10215        RecipientRecordV1 {
10216            record_length: 0,
10217            profile_id: 1,
10218            recipient_identity_type: 2,
10219            flags: 0,
10220            recipient_identity_length: 0,
10221            profile_payload_length: 0,
10222            recipient_identity_digest: [0u8; 32],
10223            recipient_identity_bytes: b"recipient-a".to_vec(),
10224            profile_payload_bytes: b"profile-payload".to_vec(),
10225        }
10226    }
10227
10228    fn recipient_wrap_layout(volume: &[u8]) -> (usize, usize, usize, usize, u32) {
10229        let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10230        let crypto_start = header.crypto_header_offset as usize;
10231        let crypto_len = header.crypto_header_length as usize;
10232        let crypto_end = crypto_start + crypto_len;
10233        let crypto = CryptoHeader::parse(
10234            &volume[crypto_start..crypto_end],
10235            header.crypto_header_length,
10236        )
10237        .unwrap();
10238        let KdfParams::RecipientWrap {
10239            key_wrap_table_length,
10240            ..
10241        } = crypto.kdf_params
10242        else {
10243            panic!("expected RecipientWrap KdfParams");
10244        };
10245        (
10246            crypto_start,
10247            crypto_len,
10248            crypto_end,
10249            key_wrap_table_length as usize,
10250            key_wrap_table_length,
10251        )
10252    }
10253
10254    fn rewrite_recipient_wrap_kdf_digest(crypto_bytes: &mut [u8], digest: [u8; 32]) {
10255        let digest_start = CRYPTO_HEADER_FIXED_LEN + 14;
10256        crypto_bytes[digest_start..digest_start + 32].copy_from_slice(&digest);
10257    }
10258
10259    fn mutate_top_level_recipient_wrap_public_profile(volume: &mut [u8]) {
10260        let (crypto_start, crypto_len, table_start, table_len, table_len_u32) =
10261            recipient_wrap_layout(volume);
10262        let table_end = table_start + table_len;
10263        volume[table_end - 1] ^= 0x5a;
10264        let digest = compute_key_wrap_table_digest(table_len_u32, &volume[table_start..table_end]);
10265        rewrite_recipient_wrap_kdf_digest(
10266            &mut volume[crypto_start..crypto_start + crypto_len],
10267            digest,
10268        );
10269    }
10270
10271    fn mutate_cmra_recipient_wrap_public_profile(volume: &mut [u8]) {
10272        rewrite_public_cmra_image(volume, |image| {
10273            let table_region = image
10274                .regions
10275                .iter_mut()
10276                .find(|region| region.region_type == 6)
10277                .unwrap();
10278            *table_region.bytes.last_mut().unwrap() ^= 0x5a;
10279            let digest =
10280                compute_key_wrap_table_digest(table_region.bytes.len() as u32, &table_region.bytes);
10281            let crypto_region = image
10282                .regions
10283                .iter_mut()
10284                .find(|region| region.region_type == 2)
10285                .unwrap();
10286            rewrite_recipient_wrap_kdf_digest(&mut crypto_region.bytes, digest);
10287        });
10288    }
10289
10290    fn add_raw_stream_profile_to_physical_crypto_header(volume: &mut Vec<u8>) {
10291        let mut header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10292        let crypto_start = header.crypto_header_offset as usize;
10293        let crypto_len = header.crypto_header_length as usize;
10294        let hmac_start = crypto_start + crypto_len - CRYPTO_HEADER_HMAC_LEN;
10295        let terminator_start = hmac_start - CRYPTO_EXTENSION_HEADER_LEN;
10296        let extension = serialize_raw_stream_content_model_extension();
10297        let new_crypto_len = header.crypto_header_length + extension.len() as u32;
10298
10299        volume.splice(terminator_start..terminator_start, extension);
10300        header.crypto_header_length = new_crypto_len;
10301        volume[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10302        volume[crypto_start + 4..crypto_start + 8].copy_from_slice(&new_crypto_len.to_le_bytes());
10303    }
10304
10305    fn recompute_physical_crypto_header_hmac(volume: &mut [u8], master_key: &MasterKey) {
10306        let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10307        let crypto_start = header.crypto_header_offset as usize;
10308        let crypto_end = crypto_start + header.crypto_header_length as usize;
10309        let hmac_start = crypto_end - CRYPTO_HEADER_HMAC_LEN;
10310        let subkeys =
10311            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
10312        let hmac = compute_hmac(
10313            HmacDomain::CryptoHeader,
10314            &subkeys.mac_key,
10315            &header.archive_uuid,
10316            &header.session_id,
10317            &volume[crypto_start..hmac_start],
10318        );
10319        volume[hmac_start..crypto_end].copy_from_slice(&hmac);
10320    }
10321
10322    #[test]
10323    fn reader_defaults_use_available_parallelism_jobs() {
10324        let options = ReaderOptions::default();
10325
10326        assert_eq!(options.jobs, default_jobs());
10327        assert!(options.jobs >= 1);
10328    }
10329
10330    #[test]
10331    fn reader_options_reject_zero_jobs() {
10332        let err = OpenedArchive::open_with_options(
10333            &[],
10334            &master_key(),
10335            ReaderOptions {
10336                jobs: 0,
10337                ..ReaderOptions::default()
10338            },
10339        )
10340        .unwrap_err();
10341
10342        assert_eq!(
10343            err,
10344            FormatError::ReaderUnsupported("jobs must be at least 1")
10345        );
10346    }
10347
10348    const TEST_ROOT_AUTH_ID: u16 = 0xe001;
10349    const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
10350
10351    fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
10352        RootAuthWriterConfig {
10353            authenticator_id: TEST_ROOT_AUTH_ID,
10354            signer_identity_type: 0,
10355            signer_identity: &[],
10356            authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
10357        }
10358    }
10359
10360    fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
10361        request.archive_root.to_vec()
10362    }
10363
10364    fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
10365        footer.authenticator_id == TEST_ROOT_AUTH_ID
10366            && footer.signer_identity_type == 0
10367            && footer.signer_identity_bytes.is_empty()
10368            && footer.authenticator_value.as_slice() == archive_root
10369    }
10370
10371    fn dictionary() -> &'static [u8] {
10372        b"dir/dict.txt common words common words common words dictionary payload"
10373    }
10374
10375    #[derive(Clone)]
10376    struct CountingReadAt {
10377        bytes: std::sync::Arc<Vec<u8>>,
10378        reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
10379        denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
10380    }
10381
10382    impl CountingReadAt {
10383        fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
10384            Self {
10385                bytes: std::sync::Arc::new(bytes),
10386                reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
10387                denied_ranges: std::sync::Arc::new(denied_ranges),
10388            }
10389        }
10390
10391        fn reads(&self) -> Vec<(u64, u64)> {
10392            self.reads.lock().unwrap().clone()
10393        }
10394    }
10395
10396    impl ArchiveReadAt for CountingReadAt {
10397        fn len(&self) -> Result<u64, FormatError> {
10398            u64::try_from(self.bytes.as_ref().len())
10399                .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
10400        }
10401
10402        fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
10403            let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
10404            self.reads.lock().unwrap().push((offset, end));
10405            if self
10406                .denied_ranges
10407                .iter()
10408                .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
10409            {
10410                return Err(FormatError::InvalidArchive("denied test read"));
10411            }
10412            let start = to_usize(offset, "archive")?;
10413            let end_usize = checked_add(start, buf.len(), "archive")?;
10414            let source = self
10415                .bytes
10416                .get(start..end_usize)
10417                .ok_or(FormatError::InvalidLength {
10418                    structure: "archive",
10419                    expected: end_usize,
10420                    actual: self.bytes.as_ref().len(),
10421                })?;
10422            buf.copy_from_slice(source);
10423            Ok(())
10424        }
10425    }
10426
10427    fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
10428        left_start < right_end && right_start < left_end
10429    }
10430
10431    fn single_stream_options() -> WriterOptions {
10432        WriterOptions {
10433            stripe_width: 1,
10434            volume_loss_tolerance: 0,
10435            ..WriterOptions::default()
10436        }
10437    }
10438
10439    struct ChunkedReader {
10440        bytes: Vec<u8>,
10441        cursor: usize,
10442        max_chunk: usize,
10443    }
10444
10445    impl ChunkedReader {
10446        fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
10447            Self {
10448                bytes,
10449                cursor: 0,
10450                max_chunk,
10451            }
10452        }
10453    }
10454
10455    impl std::io::Read for ChunkedReader {
10456        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
10457            if self.cursor >= self.bytes.len() {
10458                return Ok(0);
10459            }
10460            let available = self.bytes.len() - self.cursor;
10461            let len = available.min(buf.len()).min(self.max_chunk);
10462            buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
10463            self.cursor += len;
10464            Ok(len)
10465        }
10466    }
10467
10468    #[test]
10469    fn global_file_table_key_step_rejects_distinct_path_regression() {
10470        let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
10471        let current = ([1u8; 8], b"a.txt".to_vec(), 0);
10472
10473        assert_eq!(
10474            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
10475            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10476        );
10477    }
10478
10479    #[test]
10480    fn global_file_table_key_step_rejects_duplicate_full_key() {
10481        let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
10482        let current = ([1u8; 8], b"a.txt".to_vec(), 7);
10483
10484        assert_eq!(
10485            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
10486            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10487        );
10488    }
10489
10490    fn small_block_recovery_options() -> WriterOptions {
10491        WriterOptions {
10492            block_size: 4096,
10493            chunk_size: 32 * 1024,
10494            envelope_target_size: 32 * 1024,
10495            stripe_width: 1,
10496            volume_loss_tolerance: 0,
10497            bit_rot_buffer_pct: 1,
10498            fec_data_shards: 16,
10499            fec_parity_shards: 1,
10500            index_fec_data_shards: 4,
10501            index_fec_parity_shards: 1,
10502            index_root_fec_data_shards: 16,
10503            index_root_fec_parity_shards: 1,
10504            ..WriterOptions::default()
10505        }
10506    }
10507
10508    fn parity_rich_recovery_options() -> WriterOptions {
10509        WriterOptions {
10510            block_size: 4096,
10511            chunk_size: 32 * 1024,
10512            envelope_target_size: 32 * 1024,
10513            stripe_width: 1,
10514            volume_loss_tolerance: 0,
10515            bit_rot_buffer_pct: 40,
10516            fec_data_shards: 16,
10517            fec_parity_shards: 16,
10518            index_fec_data_shards: 4,
10519            index_fec_parity_shards: 4,
10520            index_root_fec_data_shards: 16,
10521            index_root_fec_parity_shards: 16,
10522            ..WriterOptions::default()
10523        }
10524    }
10525
10526    fn pseudo_random_bytes(len: usize) -> Vec<u8> {
10527        let mut state = 0x1234_5678u32;
10528        (0..len)
10529            .map(|_| {
10530                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10531                (state >> 24) as u8
10532            })
10533            .collect()
10534    }
10535
10536    #[test]
10537    fn opens_lists_verifies_and_extracts_one_file_archive() {
10538        let archive = write_archive(
10539            &[RegularFile::new("dir/hello.txt", b"hello m7")],
10540            &master_key(),
10541            single_stream_options(),
10542        )
10543        .unwrap();
10544        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10545
10546        assert_eq!(
10547            opened.list_files().unwrap(),
10548            vec![ArchiveEntry {
10549                path: "dir/hello.txt".to_string(),
10550                file_data_size: 8,
10551                kind: TarEntryKind::Regular,
10552                mode: 0o644,
10553                mtime: 0,
10554                diagnostics: Vec::new(),
10555            }]
10556        );
10557        opened.verify().unwrap();
10558        assert_eq!(
10559            opened.extract_file("dir/hello.txt").unwrap(),
10560            Some(b"hello m7".to_vec())
10561        );
10562        assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
10563    }
10564
10565    #[test]
10566    fn root_auth_archive_round_trips_and_verifies_with_callback() {
10567        let archive = write_archive_with_root_auth(
10568            &[RegularFile::new("signed.txt", b"root-auth payload")],
10569            &master_key(),
10570            single_stream_options(),
10571            RootAuthWriterConfig {
10572                authenticator_id: 0x7777,
10573                signer_identity_type: 1,
10574                signer_identity: b"test signer",
10575                authenticator_value_length: 32,
10576            },
10577            |request| Ok(request.archive_root.to_vec()),
10578        )
10579        .unwrap();
10580
10581        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10582        opened.verify().unwrap();
10583        let verified = opened
10584            .verify_root_auth_with(|footer, archive_root| {
10585                Ok(footer.authenticator_value == archive_root.as_slice())
10586            })
10587            .unwrap();
10588
10589        assert_eq!(verified.authenticator_id, 0x7777);
10590        assert_eq!(verified.signer_identity_type, 1);
10591        assert_eq!(verified.signer_identity_bytes, b"test signer");
10592        assert_eq!(
10593            verified.archive_root,
10594            opened.root_auth_footer.as_ref().unwrap().archive_root
10595        );
10596        assert_eq!(
10597            verified.diagnostics,
10598            vec![
10599                RootAuthDiagnostic::RootAuthContentVerified,
10600                RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
10601                RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
10602                RootAuthDiagnostic::RecoveryMarginUnchecked,
10603            ]
10604        );
10605    }
10606
10607    #[test]
10608    fn root_auth_rejects_fast_content_verification_token() {
10609        let archive = write_archive_with_root_auth(
10610            &[RegularFile::new("signed.txt", b"root-auth payload")],
10611            &master_key(),
10612            single_stream_options(),
10613            RootAuthWriterConfig {
10614                authenticator_id: 0x7777,
10615                signer_identity_type: 1,
10616                signer_identity: b"test signer",
10617                authenticator_value_length: 32,
10618            },
10619            |request| Ok(request.archive_root.to_vec()),
10620        )
10621        .unwrap();
10622
10623        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10624        let content_verification = opened.verify_content_fast().unwrap();
10625        assert_eq!(
10626            opened
10627                .verify_root_auth_with_verified_content(&content_verification, |_, _| Ok(true))
10628                .unwrap_err(),
10629            FormatError::ReaderUnsupported(
10630                "RootAuth verification requires full archive content verification"
10631            )
10632        );
10633    }
10634
10635    #[test]
10636    fn root_auth_verification_requires_authenticator_success() {
10637        let archive = write_archive_with_root_auth(
10638            &[RegularFile::new("signed.txt", b"root-auth payload")],
10639            &master_key(),
10640            single_stream_options(),
10641            RootAuthWriterConfig {
10642                authenticator_id: 9,
10643                signer_identity_type: 1,
10644                signer_identity: b"test signer",
10645                authenticator_value_length: 32,
10646            },
10647            |request| Ok(request.archive_root.to_vec()),
10648        )
10649        .unwrap();
10650        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10651
10652        assert_eq!(
10653            opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
10654            FormatError::InvalidArchive("root-auth authenticator verification failed")
10655        );
10656    }
10657
10658    #[test]
10659    fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
10660        let archive = write_archive_with_root_auth(
10661            &[RegularFile::new("public.txt", b"public commitment")],
10662            &master_key(),
10663            single_stream_options(),
10664            RootAuthWriterConfig {
10665                authenticator_id: 0x2222,
10666                signer_identity_type: 1,
10667                signer_identity: b"public verifier",
10668                authenticator_value_length: 32,
10669            },
10670            |request| Ok(request.archive_root.to_vec()),
10671        )
10672        .unwrap();
10673
10674        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
10675            Ok(footer.authenticator_value == archive_root.as_slice())
10676        })
10677        .unwrap();
10678
10679        assert_eq!(verified.authenticator_id, 0x2222);
10680        assert_eq!(verified.signer_identity_bytes, b"public verifier");
10681        assert!(verified.total_data_block_count > 0);
10682    }
10683
10684    #[test]
10685    fn public_no_key_verifier_not_invoked_for_future_revision() {
10686        let archive = write_archive_with_root_auth(
10687            &[RegularFile::new("public.txt", b"public callback")],
10688            &master_key(),
10689            single_stream_options(),
10690            RootAuthWriterConfig {
10691                authenticator_id: 0x2222,
10692                signer_identity_type: 1,
10693                signer_identity: b"public verifier",
10694                authenticator_value_length: 32,
10695            },
10696            |request| Ok(request.archive_root.to_vec()),
10697        )
10698        .unwrap();
10699        let mut bytes = archive.bytes;
10700        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10701        header.volume_format_rev = 45;
10702        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10703
10704        let mut called = false;
10705        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10706            called = true;
10707            Ok(true)
10708        })
10709        .unwrap_err();
10710
10711        assert!(!called);
10712        assert_eq!(
10713            err,
10714            FormatError::UnsupportedVolumeFormatRevision {
10715                format_version: FORMAT_VERSION,
10716                volume_format_rev: 45,
10717                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10718            }
10719        );
10720    }
10721
10722    #[test]
10723    fn public_no_key_rejects_public_header_revision_mismatch() {
10724        let archive = write_archive_with_root_auth(
10725            &[RegularFile::new("public.txt", b"public v44 only")],
10726            &master_key(),
10727            single_stream_options(),
10728            RootAuthWriterConfig {
10729                authenticator_id: 0x2222,
10730                signer_identity_type: 1,
10731                signer_identity: b"public verifier",
10732                authenticator_value_length: 32,
10733            },
10734            |request| Ok(request.archive_root.to_vec()),
10735        )
10736        .unwrap();
10737        let mut bytes = archive.bytes;
10738        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10739        header.volume_format_rev = VOLUME_FORMAT_REV_43;
10740        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10741
10742        let mut called = false;
10743        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10744            called = true;
10745            Ok(true)
10746        })
10747        .unwrap_err();
10748
10749        assert!(!called);
10750        assert_eq!(
10751            err,
10752            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10753        );
10754    }
10755
10756    #[test]
10757    fn public_no_key_rejects_recovered_footer_revision_mismatch() {
10758        let archive = write_archive_with_root_auth(
10759            &[RegularFile::new("public.txt", b"public footer mismatch")],
10760            &master_key(),
10761            single_stream_options(),
10762            RootAuthWriterConfig {
10763                authenticator_id: 0x2222,
10764                signer_identity_type: 1,
10765                signer_identity: b"public verifier",
10766                authenticator_value_length: 32,
10767            },
10768            |request| Ok(request.archive_root.to_vec()),
10769        )
10770        .unwrap();
10771        let mut bytes = archive.bytes;
10772        rewrite_public_cmra_image(&mut bytes, |image| {
10773            let root_auth_region = image
10774                .regions
10775                .iter_mut()
10776                .find(|region| region.region_type == 4)
10777                .unwrap();
10778            let mut footer = RootAuthFooterV1::parse(&root_auth_region.bytes).unwrap();
10779            footer.volume_format_rev = VOLUME_FORMAT_REV_43;
10780            root_auth_region.bytes = footer.to_bytes().unwrap();
10781        });
10782
10783        let mut called = false;
10784        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10785            called = true;
10786            Ok(true)
10787        })
10788        .unwrap_err();
10789
10790        assert!(!called);
10791        assert_eq!(
10792            err,
10793            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10794        );
10795    }
10796
10797    #[test]
10798    fn public_no_key_rejects_recovered_image_revision_mismatch() {
10799        let archive = write_archive_with_root_auth(
10800            &[RegularFile::new("public.txt", b"public image mismatch")],
10801            &master_key(),
10802            single_stream_options(),
10803            RootAuthWriterConfig {
10804                authenticator_id: 0x2222,
10805                signer_identity_type: 1,
10806                signer_identity: b"public verifier",
10807                authenticator_value_length: 32,
10808            },
10809            |request| Ok(request.archive_root.to_vec()),
10810        )
10811        .unwrap();
10812        let bytes = rewrite_cmra_image_variable_len(
10813            &archive.bytes,
10814            CmraRecoveryMode::PublicNoKey,
10815            |image| {
10816                image.volume_format_rev = VOLUME_FORMAT_REV_43;
10817            },
10818        );
10819
10820        let mut called = false;
10821        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10822            called = true;
10823            Ok(true)
10824        })
10825        .unwrap_err();
10826
10827        assert!(!called);
10828        assert_eq!(
10829            err,
10830            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10831        );
10832    }
10833
10834    #[test]
10835    fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
10836        let archive = write_archive_with_root_auth(
10837            &[RegularFile::new(
10838                "public-fields.txt",
10839                b"public source authority",
10840            )],
10841            &master_key(),
10842            single_stream_options(),
10843            RootAuthWriterConfig {
10844                authenticator_id: 0x2222,
10845                signer_identity_type: 1,
10846                signer_identity: b"public verifier",
10847                authenticator_value_length: 32,
10848            },
10849            |request| Ok(request.archive_root.to_vec()),
10850        )
10851        .unwrap();
10852        let mut bytes = archive.bytes.clone();
10853
10854        rewrite_public_cmra_image(&mut bytes, |image| {
10855            let manifest_region = image
10856                .regions
10857                .iter_mut()
10858                .find(|region| region.region_type == 3)
10859                .unwrap();
10860            manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
10861
10862            let trailer_region = image
10863                .regions
10864                .iter_mut()
10865                .find(|region| region.region_type == 5)
10866                .unwrap();
10867            let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
10868            trailer.block_count += 7;
10869            trailer_region.bytes = trailer.to_bytes().to_vec();
10870        });
10871
10872        public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
10873            Ok(footer.authenticator_value == archive_root.as_slice())
10874        })
10875        .unwrap();
10876    }
10877
10878    #[test]
10879    fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
10880        let archive = write_archive_with_root_auth(
10881            &[RegularFile::new(
10882                "public-crypto.txt",
10883                b"cross-volume public profile",
10884            )],
10885            &master_key(),
10886            WriterOptions {
10887                stripe_width: 2,
10888                volume_loss_tolerance: 0,
10889                ..WriterOptions::default()
10890            },
10891            RootAuthWriterConfig {
10892                authenticator_id: 0x3333,
10893                signer_identity_type: 1,
10894                signer_identity: b"public verifier",
10895                authenticator_value_length: 32,
10896            },
10897            |request| Ok(request.archive_root.to_vec()),
10898        )
10899        .unwrap();
10900        let mut volumes = archive.volumes.clone();
10901        let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
10902        let crypto_offset = volume_header.crypto_header_offset as usize;
10903        let expected_volume_size = 123_456_789u64;
10904        volumes[1][crypto_offset + 52..crypto_offset + 60]
10905            .copy_from_slice(&expected_volume_size.to_le_bytes());
10906        rewrite_public_cmra_image(&mut volumes[1], |image| {
10907            let crypto_region = image
10908                .regions
10909                .iter_mut()
10910                .find(|region| region.region_type == 2)
10911                .unwrap();
10912            crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
10913        });
10914
10915        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
10916        public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
10917            Ok(footer.authenticator_value == archive_root.as_slice())
10918        })
10919        .unwrap();
10920    }
10921
10922    #[test]
10923    fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
10924        let archive = write_archive_with_root_auth(
10925            &[RegularFile::new("cmra-header.txt", b"header fallback")],
10926            &master_key(),
10927            single_stream_options(),
10928            RootAuthWriterConfig {
10929                authenticator_id: 0x4444,
10930                signer_identity_type: 1,
10931                signer_identity: b"public verifier",
10932                authenticator_value_length: 32,
10933            },
10934            |request| Ok(request.archive_root.to_vec()),
10935        )
10936        .unwrap();
10937        let final_locator = final_recovery_locator(&archive.bytes);
10938
10939        let mut bad_crc = archive.bytes.clone();
10940        let crc_offset =
10941            final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
10942        bad_crc[crc_offset] ^= 0x55;
10943        public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
10944            Ok(footer.authenticator_value == archive_root.as_slice())
10945        })
10946        .unwrap();
10947
10948        let mut bad_magic = archive.bytes.clone();
10949        bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
10950        public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
10951            Ok(footer.authenticator_value == archive_root.as_slice())
10952        })
10953        .unwrap();
10954
10955        let mut bad_hint = archive.bytes.clone();
10956        bad_hint[crc_offset] ^= 0xAA;
10957        for offset in [
10958            bad_hint.len() - LOCATOR_PAIR_LEN,
10959            bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
10960        ] {
10961            let mut locator = CriticalRecoveryLocator::parse(
10962                &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
10963            )
10964            .unwrap();
10965            locator.volume_index_hint += 1;
10966            bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
10967                .copy_from_slice(&locator.to_bytes());
10968        }
10969        assert_eq!(
10970            public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
10971            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10972        );
10973    }
10974
10975    #[test]
10976    fn recovers_physical_volume_header_magic_from_cmra_authority() {
10977        let payload = b"front header authority".to_vec();
10978        let archive = write_archive(
10979            &[RegularFile::new("volume-header.txt", &payload)],
10980            &master_key(),
10981            small_block_recovery_options(),
10982        )
10983        .unwrap();
10984
10985        let mut corrupted = archive.bytes;
10986        corrupted[0] ^= 0x55;
10987
10988        let opened = open_archive(&corrupted, &master_key()).unwrap();
10989        assert_eq!(
10990            opened.extract_file("volume-header.txt").unwrap(),
10991            Some(payload)
10992        );
10993        opened.verify().unwrap();
10994    }
10995
10996    #[test]
10997    fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
10998        let payload = b"crc-valid wrong volume index".to_vec();
10999        let mut options = small_block_recovery_options();
11000        options.stripe_width = 2;
11001        options.volume_loss_tolerance = 0;
11002        let archive = write_archive(
11003            &[RegularFile::new("volume-index.txt", &payload)],
11004            &master_key(),
11005            options,
11006        )
11007        .unwrap();
11008
11009        let mut corrupted = archive.volumes[0].clone();
11010        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11011        assert_eq!(header.volume_index, 0);
11012        header.volume_index = 1;
11013        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11014        assert_eq!(
11015            VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
11016                .unwrap()
11017                .volume_index,
11018            1
11019        );
11020
11021        let opened = open_archive_volumes(
11022            &[corrupted.as_slice(), archive.volumes[1].as_slice()],
11023            &master_key(),
11024        )
11025        .unwrap();
11026        assert_eq!(opened.volume_header.volume_index, 0);
11027        assert_eq!(
11028            opened.extract_file("volume-index.txt").unwrap(),
11029            Some(payload)
11030        );
11031        opened.verify().unwrap();
11032    }
11033
11034    #[test]
11035    fn recovers_physical_crypto_header_magic_from_cmra_authority() {
11036        let payload = b"crypto header authority".to_vec();
11037        let archive = write_archive(
11038            &[RegularFile::new("crypto-header.txt", &payload)],
11039            &master_key(),
11040            small_block_recovery_options(),
11041        )
11042        .unwrap();
11043        let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
11044            .unwrap()
11045            .crypto_header_offset;
11046
11047        let mut corrupted = archive.bytes;
11048        corrupted[crypto_offset as usize] ^= 0x55;
11049
11050        let opened = open_archive(&corrupted, &master_key()).unwrap();
11051        assert_eq!(
11052            opened.extract_file("crypto-header.txt").unwrap(),
11053            Some(payload)
11054        );
11055        opened.verify().unwrap();
11056    }
11057
11058    #[test]
11059    fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
11060        let payload = b"read-at header authority".to_vec();
11061        let archive = write_archive(
11062            &[RegularFile::new("read-at-header.txt", &payload)],
11063            &master_key(),
11064            small_block_recovery_options(),
11065        )
11066        .unwrap();
11067
11068        let mut corrupted = archive.bytes;
11069        corrupted[0] ^= 0x55;
11070
11071        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
11072        assert_eq!(
11073            opened.extract_file("read-at-header.txt").unwrap(),
11074            Some(payload)
11075        );
11076        opened.verify().unwrap();
11077    }
11078
11079    #[test]
11080    fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11081        let payload = b"read-at crc-valid wrong volume index".to_vec();
11082        let mut options = small_block_recovery_options();
11083        options.stripe_width = 2;
11084        options.volume_loss_tolerance = 0;
11085        let archive = write_archive(
11086            &[RegularFile::new("read-at-volume-index.txt", &payload)],
11087            &master_key(),
11088            options,
11089        )
11090        .unwrap();
11091
11092        let mut corrupted = archive.volumes[0].clone();
11093        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11094        assert_eq!(header.volume_index, 0);
11095        header.volume_index = 1;
11096        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11097
11098        let opened = open_seekable_archive_volumes(
11099            vec![corrupted, archive.volumes[1].clone()],
11100            &master_key(),
11101        )
11102        .unwrap();
11103        assert_eq!(opened.volume_header.volume_index, 0);
11104        assert_eq!(
11105            opened.extract_file("read-at-volume-index.txt").unwrap(),
11106            Some(payload)
11107        );
11108        opened.verify().unwrap();
11109    }
11110
11111    #[test]
11112    fn recovers_cmra_header_magic_from_locator_tuple() {
11113        let payload = b"cmra header authority".to_vec();
11114        let archive = write_archive(
11115            &[RegularFile::new("cmra-header-magic.txt", &payload)],
11116            &master_key(),
11117            small_block_recovery_options(),
11118        )
11119        .unwrap();
11120        let locator = final_recovery_locator(&archive.bytes);
11121
11122        let mut corrupted = archive.bytes;
11123        corrupted[locator.cmra_offset as usize] ^= 0x55;
11124
11125        let opened = open_archive(&corrupted, &master_key()).unwrap();
11126        assert_eq!(
11127            opened.extract_file("cmra-header-magic.txt").unwrap(),
11128            Some(payload)
11129        );
11130        opened.verify().unwrap();
11131    }
11132
11133    #[test]
11134    fn recovers_cmra_shard_magic_as_erasure() {
11135        let payload = b"cmra shard authority".to_vec();
11136        let archive = write_archive(
11137            &[RegularFile::new("cmra-shard-magic.txt", &payload)],
11138            &master_key(),
11139            small_block_recovery_options(),
11140        )
11141        .unwrap();
11142        let locator = final_recovery_locator(&archive.bytes);
11143        let first_shard_offset =
11144            locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11145
11146        let mut corrupted = archive.bytes;
11147        corrupted[first_shard_offset] ^= 0x55;
11148
11149        let opened = open_archive(&corrupted, &master_key()).unwrap();
11150        assert_eq!(
11151            opened.extract_file("cmra-shard-magic.txt").unwrap(),
11152            Some(payload)
11153        );
11154        opened.verify().unwrap();
11155    }
11156
11157    #[test]
11158    fn key_holding_rejects_recovered_image_revision_mismatch() {
11159        let archive = write_archive(
11160            &[RegularFile::new("cmra-image-revision.txt", b"payload")],
11161            &master_key(),
11162            small_block_recovery_options(),
11163        )
11164        .unwrap();
11165        let mut mutated = rewrite_cmra_image_variable_len(
11166            &archive.bytes,
11167            CmraRecoveryMode::KeyHolding,
11168            |image| {
11169                image.volume_format_rev = VOLUME_FORMAT_REV_43;
11170            },
11171        );
11172        mutated[0] ^= 0x55;
11173
11174        assert!(open_archive(&mutated, &master_key()).is_err());
11175    }
11176
11177    #[test]
11178    fn key_holding_rejects_recovered_footer_revision_mismatch() {
11179        let archive = write_archive_with_root_auth(
11180            &[RegularFile::new("cmra-footer-revision.txt", b"payload")],
11181            &master_key(),
11182            single_stream_options(),
11183            test_root_auth_config(),
11184            |request| Ok(test_root_auth_value(request)),
11185        )
11186        .unwrap();
11187        let mut mutated = archive.bytes;
11188        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11189            let root_auth_region = image
11190                .regions
11191                .iter_mut()
11192                .find(|region| region.region_type == 4)
11193                .unwrap();
11194            let mut footer = RootAuthFooterV1::parse(&root_auth_region.bytes).unwrap();
11195            footer.volume_format_rev = VOLUME_FORMAT_REV_43;
11196            root_auth_region.bytes = footer.to_bytes().unwrap();
11197        });
11198        mutated[0] ^= 0x55;
11199
11200        assert!(open_archive(&mutated, &master_key()).is_err());
11201    }
11202
11203    #[test]
11204    fn key_holding_rejects_locator_image_revision_mismatch() {
11205        let archive = write_archive(
11206            &[RegularFile::new("cmra-locator-revision.txt", b"payload")],
11207            &master_key(),
11208            small_block_recovery_options(),
11209        )
11210        .unwrap();
11211        let mut mutated = archive.bytes;
11212        let locator = final_recovery_locator(&mutated);
11213        let mirror_offset = mutated.len() - LOCATOR_PAIR_LEN;
11214        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11215        for offset in [mirror_offset, final_offset] {
11216            rewrite_recovery_locator(&mut mutated, offset, |locator| {
11217                locator.volume_format_rev = VOLUME_FORMAT_REV_43;
11218            });
11219        }
11220        mutated[0] ^= 0x55;
11221        mutated[locator.cmra_offset as usize] ^= 0x55;
11222
11223        assert!(open_archive(&mutated, &master_key()).is_err());
11224    }
11225
11226    #[test]
11227    fn image_identity_allows_matching_v43_revision() {
11228        let archive = write_archive(
11229            &[RegularFile::new("matching-v43.txt", b"payload")],
11230            &master_key(),
11231            single_stream_options(),
11232        )
11233        .unwrap();
11234        let locator = final_recovery_locator(&archive.bytes);
11235        let recovered = recover_cmra(
11236            &archive.bytes,
11237            locator.cmra_offset,
11238            Some(CmraDecoderTuple::from(locator)),
11239            CmraRecoveryMode::KeyHolding,
11240        )
11241        .unwrap();
11242        let mut image = recovered.image;
11243        let mut header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11244        let crypto_start = header.crypto_header_offset as usize;
11245        let crypto_end = crypto_start + header.crypto_header_length as usize;
11246        let crypto = CryptoHeader::parse(
11247            &archive.bytes[crypto_start..crypto_end],
11248            header.crypto_header_length,
11249        )
11250        .unwrap();
11251
11252        image.volume_format_rev = VOLUME_FORMAT_REV_43;
11253        header.volume_format_rev = VOLUME_FORMAT_REV_43;
11254
11255        validate_image_identity(&image, &header, &crypto.fixed).unwrap();
11256    }
11257
11258    #[test]
11259    fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
11260        let archive = write_archive(
11261            &[RegularFile::new(
11262                "cmra-floor.txt",
11263                b"authenticated CMRA floor",
11264            )],
11265            &master_key(),
11266            single_stream_options(),
11267        )
11268        .unwrap();
11269        let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
11270        let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11271        let locator = CriticalRecoveryLocator::parse(
11272            &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11273        )
11274        .unwrap();
11275        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11276        let crypto_start = volume_header.crypto_header_offset as usize;
11277        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11278        let crypto_header = CryptoHeader::parse(
11279            &malformed[crypto_start..crypto_end],
11280            volume_header.crypto_header_length,
11281        )
11282        .unwrap();
11283        let subkeys = Subkeys::derive(
11284            &master_key(),
11285            &volume_header.archive_uuid,
11286            &volume_header.session_id,
11287        )
11288        .unwrap();
11289
11290        assert_eq!(
11291            parse_locator_cmra_candidate(
11292                &malformed,
11293                final_offset,
11294                locator,
11295                KeyHoldingTerminalContext {
11296                    subkeys: &subkeys,
11297                    volume_header: &volume_header,
11298                    crypto_header: &crypto_header.fixed,
11299                    crypto_header_bytes: &malformed[crypto_start..crypto_end],
11300                },
11301            )
11302            .unwrap_err(),
11303            FormatError::InvalidArchive(
11304                "CMRA parity shard count is below authenticated bit-rot lower bound"
11305            )
11306        );
11307        assert!(open_archive(&malformed, &master_key()).is_err());
11308    }
11309
11310    #[test]
11311    fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
11312        let archive = write_archive(
11313            &[RegularFile::new(
11314                "locator-order.txt",
11315                b"locator tuple first",
11316            )],
11317            &master_key(),
11318            single_stream_options(),
11319        )
11320        .unwrap();
11321        let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11322        let mut locator = final_recovery_locator(&archive.bytes);
11323        locator.cmra_shard_size = 513;
11324        locator.body_bytes_before_cmra = locator.cmra_offset + 1;
11325        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11326        let crypto_start = volume_header.crypto_header_offset as usize;
11327        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11328        let crypto_header = CryptoHeader::parse(
11329            &archive.bytes[crypto_start..crypto_end],
11330            volume_header.crypto_header_length,
11331        )
11332        .unwrap();
11333        let subkeys = Subkeys::derive(
11334            &master_key(),
11335            &volume_header.archive_uuid,
11336            &volume_header.session_id,
11337        )
11338        .unwrap();
11339
11340        assert_eq!(
11341            parse_locator_cmra_candidate(
11342                &archive.bytes,
11343                final_offset,
11344                locator,
11345                KeyHoldingTerminalContext {
11346                    subkeys: &subkeys,
11347                    volume_header: &volume_header,
11348                    crypto_header: &crypto_header.fixed,
11349                    crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
11350                },
11351            )
11352            .unwrap_err(),
11353            FormatError::InvalidArchive("CMRA shard_size is invalid")
11354        );
11355    }
11356
11357    #[test]
11358    fn sequential_extract_rejects_bytes_after_terminal_locator() {
11359        let archive = write_archive(
11360            &[RegularFile::new("seq.txt", b"sequential EOF")],
11361            &master_key(),
11362            single_stream_options(),
11363        )
11364        .unwrap();
11365        let mut appended = archive.bytes.clone();
11366        appended.extend_from_slice(&[0xAA; 32]);
11367
11368        assert_eq!(
11369            sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
11370            FormatError::InvalidArchive("sequential terminal does not end at EOF")
11371        );
11372    }
11373
11374    #[test]
11375    fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
11376        let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
11377        let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
11378
11379        assert_eq!(
11380            validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
11381            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
11382        );
11383    }
11384
11385    #[test]
11386    fn root_auth_verifies_key_holding_and_public_no_key_modes() {
11387        let archive = write_archive_with_root_auth(
11388            &[RegularFile::new("signed.txt", b"ed25519 payload")],
11389            &master_key(),
11390            single_stream_options(),
11391            test_root_auth_config(),
11392            |request| Ok(test_root_auth_value(request)),
11393        )
11394        .unwrap();
11395
11396        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11397        let root_auth = opened
11398            .verify_root_auth_with(|footer, archive_root| {
11399                Ok(test_root_auth_verifies(footer, archive_root))
11400            })
11401            .unwrap();
11402        assert_eq!(
11403            root_auth.archive_root,
11404            opened.root_auth_footer.as_ref().unwrap().archive_root
11405        );
11406
11407        let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
11408            Ok(test_root_auth_verifies(footer, archive_root))
11409        })
11410        .unwrap();
11411        assert_eq!(public.archive_root, root_auth.archive_root);
11412        assert_eq!(
11413            public.diagnostics,
11414            vec![
11415                PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
11416                PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
11417                PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
11418            ]
11419        );
11420    }
11421
11422    #[test]
11423    fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
11424        let options = WriterOptions {
11425            block_size: 4096,
11426            chunk_size: 16 * 1024,
11427            envelope_target_size: 16 * 1024,
11428            stripe_width: 2,
11429            volume_loss_tolerance: 1,
11430            bit_rot_buffer_pct: 0,
11431            fec_data_shards: 16,
11432            fec_parity_shards: 1,
11433            index_fec_data_shards: 4,
11434            index_fec_parity_shards: 1,
11435            index_root_fec_data_shards: 16,
11436            index_root_fec_parity_shards: 1,
11437            ..WriterOptions::default()
11438        };
11439        let archive = write_archive_with_root_auth(
11440            &[RegularFile::new("missing-volume.txt", b"recover me")],
11441            &master_key(),
11442            options,
11443            test_root_auth_config(),
11444            |request| Ok(test_root_auth_value(request)),
11445        )
11446        .unwrap();
11447
11448        let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
11449        let root_auth = opened
11450            .verify_root_auth_with(|footer, archive_root| {
11451                Ok(test_root_auth_verifies(footer, archive_root))
11452            })
11453            .unwrap();
11454        assert!(root_auth
11455            .diagnostics
11456            .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
11457    }
11458
11459    #[test]
11460    fn public_no_key_rejects_unsigned_archives() {
11461        let archive = write_archive(
11462            &[RegularFile::new("plain.txt", b"unsigned")],
11463            &master_key(),
11464            single_stream_options(),
11465        )
11466        .unwrap();
11467
11468        assert_eq!(
11469            public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
11470            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11471        );
11472    }
11473
11474    #[test]
11475    fn unsigned_archive_reports_root_auth_absent() {
11476        let archive = write_archive(
11477            &[RegularFile::new("plain.txt", b"unsigned")],
11478            &master_key(),
11479            single_stream_options(),
11480        )
11481        .unwrap();
11482        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11483
11484        assert_eq!(
11485            opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
11486            FormatError::ReaderUnsupported("root-auth footer is absent")
11487        );
11488    }
11489
11490    #[test]
11491    fn safe_extract_writes_regular_file_under_root() {
11492        let archive = write_archive(
11493            &[RegularFile::new("dir/hello.txt", b"safe m8")],
11494            &master_key(),
11495            single_stream_options(),
11496        )
11497        .unwrap();
11498        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11499        let tmp = tempfile::tempdir().unwrap();
11500
11501        opened
11502            .extract_file_to(
11503                "dir/hello.txt",
11504                tmp.path(),
11505                SafeExtractionOptions::default(),
11506            )
11507            .unwrap()
11508            .unwrap();
11509
11510        assert_eq!(
11511            std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
11512            b"safe m8"
11513        );
11514    }
11515
11516    #[test]
11517    fn seekable_extract_all_to_streams_unique_archive() {
11518        let archive = write_archive(
11519            &[
11520                RegularFile::new("alpha.txt", b"alpha"),
11521                RegularFile::new("dir/beta.txt", b"beta"),
11522            ],
11523            &master_key(),
11524            single_stream_options(),
11525        )
11526        .unwrap();
11527        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11528        let tmp = tempfile::tempdir().unwrap();
11529
11530        let diagnostics = opened
11531            .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11532            .unwrap();
11533
11534        assert_eq!(diagnostics.len(), 2);
11535        assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
11536        assert_eq!(
11537            fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
11538            b"beta"
11539        );
11540    }
11541
11542    #[test]
11543    fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
11544        let archive = write_archive(
11545            &[
11546                RegularFile::new("same.txt", b"old"),
11547                RegularFile::new("same.txt", b"new"),
11548            ],
11549            &master_key(),
11550            single_stream_options(),
11551        )
11552        .unwrap();
11553        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11554        let tmp = tempfile::tempdir().unwrap();
11555
11556        assert_eq!(
11557            opened
11558                .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11559                .unwrap_err(),
11560            FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
11561        );
11562    }
11563
11564    #[test]
11565    fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
11566        let archive = write_archive(
11567            &[
11568                RegularFile::new("same.txt", b"old"),
11569                RegularFile::new("same.txt", b"new"),
11570            ],
11571            &master_key(),
11572            single_stream_options(),
11573        )
11574        .unwrap();
11575        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11576        let tmp = tempfile::tempdir().unwrap();
11577
11578        let diagnostics = opened
11579            .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
11580            .unwrap();
11581
11582        assert_eq!(diagnostics.len(), 1);
11583        assert_eq!(diagnostics[0].0, "same.txt");
11584        assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
11585    }
11586
11587    #[test]
11588    fn safe_extract_rejects_overwriting_existing_file_by_default() {
11589        let archive = write_archive(
11590            &[RegularFile::new("hello.txt", b"new")],
11591            &master_key(),
11592            single_stream_options(),
11593        )
11594        .unwrap();
11595        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11596        let tmp = tempfile::tempdir().unwrap();
11597        std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
11598
11599        assert_eq!(
11600            opened
11601                .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
11602                .unwrap_err(),
11603            FormatError::UnsafeOverwrite
11604        );
11605        assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
11606    }
11607
11608    #[test]
11609    fn opens_and_verifies_empty_archive() {
11610        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11611        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11612
11613        assert!(opened.list_files().unwrap().is_empty());
11614        opened.verify().unwrap();
11615    }
11616
11617    #[test]
11618    fn default_reader_options_allow_v36_trailing_garbage_scan() {
11619        let archive = write_archive(
11620            &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
11621            &master_key(),
11622            single_stream_options(),
11623        )
11624        .unwrap();
11625        let mut with_trailing_garbage = archive.bytes.clone();
11626        with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
11627
11628        let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
11629        assert_eq!(
11630            opened.extract_file("garbage-tolerant.txt").unwrap(),
11631            Some(b"still intact".to_vec())
11632        );
11633    }
11634
11635    #[test]
11636    fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
11637        assert_eq!(
11638            open_archive(
11639                &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
11640                &master_key()
11641            )
11642            .unwrap_err(),
11643            FormatError::InvalidLength {
11644                structure: "archive",
11645                expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11646                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
11647            }
11648        );
11649
11650        let mut header = test_volume_header();
11651        header.crypto_header_length = 512;
11652        let mut unavailable_crypto = header.to_bytes().to_vec();
11653        unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
11654
11655        assert_eq!(
11656            open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
11657            FormatError::InvalidLength {
11658                structure: "CryptoHeader",
11659                expected: VOLUME_HEADER_LEN + 512,
11660                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11661            }
11662        );
11663    }
11664
11665    #[test]
11666    fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
11667        let archive = write_archive(
11668            &[RegularFile::new("offset.txt", b"offset")],
11669            &master_key(),
11670            small_block_recovery_options(),
11671        )
11672        .unwrap();
11673        let mut mutated = archive.bytes;
11674        let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11675        header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
11676        mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11677
11678        let opened = open_archive(&mutated, &master_key()).unwrap();
11679        assert_eq!(
11680            opened.volume_header.crypto_header_offset,
11681            VOLUME_HEADER_LEN as u32
11682        );
11683        assert_eq!(
11684            opened.extract_file("offset.txt").unwrap(),
11685            Some(b"offset".to_vec())
11686        );
11687        opened.verify().unwrap();
11688    }
11689
11690    #[test]
11691    fn rejects_wrong_key_before_metadata_release() {
11692        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11693        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11694
11695        assert_eq!(
11696            open_archive(&archive.bytes, &wrong).unwrap_err(),
11697            FormatError::HmacMismatch {
11698                structure: "CryptoHeader"
11699            }
11700        );
11701    }
11702
11703    #[test]
11704    fn ordinary_encrypted_writers_emit_v44_archives() {
11705        let raw_key_archive = write_archive(
11706            &[RegularFile::new("raw.txt", b"raw key payload")],
11707            &master_key(),
11708            single_stream_options(),
11709        )
11710        .unwrap();
11711        let raw_header = VolumeHeader::parse(&raw_key_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11712        assert_eq!(raw_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11713        let raw_opened = open_archive(&raw_key_archive.bytes, &master_key()).unwrap();
11714        assert_eq!(
11715            raw_opened.volume_header.volume_format_rev,
11716            VOLUME_FORMAT_REV_44
11717        );
11718        assert_eq!(
11719            raw_opened.extract_file("raw.txt").unwrap(),
11720            Some(b"raw key payload".to_vec())
11721        );
11722
11723        let passphrase_kdf = KdfParams::Argon2id {
11724            t_cost: 1,
11725            m_cost_kib: 8,
11726            parallelism: 1,
11727            salt: b"0123456789abcdef".to_vec(),
11728        };
11729        let passphrase_archive = write_archive_with_kdf(
11730            &[RegularFile::new("pass.txt", b"passphrase payload")],
11731            &master_key(),
11732            single_stream_options(),
11733            &passphrase_kdf,
11734        )
11735        .unwrap();
11736        let passphrase_header =
11737            VolumeHeader::parse(&passphrase_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11738        assert_eq!(passphrase_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11739        let passphrase_opened = open_archive(&passphrase_archive.bytes, &master_key()).unwrap();
11740        assert_eq!(
11741            passphrase_opened.volume_header.volume_format_rev,
11742            VOLUME_FORMAT_REV_44
11743        );
11744        assert_eq!(
11745            passphrase_opened.extract_file("pass.txt").unwrap(),
11746            Some(b"passphrase payload".to_vec())
11747        );
11748    }
11749
11750    #[test]
11751    fn rejects_future_volume_format_revision_before_key_mismatch() {
11752        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11753        let mut bytes = archive.bytes;
11754        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
11755        header.volume_format_rev = 45;
11756        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11757        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11758
11759        assert_eq!(
11760            open_archive(&bytes, &wrong).unwrap_err(),
11761            FormatError::UnsupportedVolumeFormatRevision {
11762                format_version: FORMAT_VERSION,
11763                volume_format_rev: 45,
11764                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11765            }
11766        );
11767    }
11768
11769    #[test]
11770    fn open_archive_unencrypted_accepts_v44_profile() {
11771        let archive = write_archive_unencrypted(
11772            &[RegularFile::new("payload.txt", b"smoke-v44-unencrypted")],
11773            WriterOptions {
11774                aead_algo: AeadAlgo::None,
11775                ..single_stream_options()
11776            },
11777        )
11778        .unwrap();
11779
11780        let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11781        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11782
11783        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11784        assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11785        assert_eq!(
11786            opened.extract_file("payload.txt").unwrap(),
11787            Some(b"smoke-v44-unencrypted".to_vec())
11788        );
11789        opened.verify().unwrap();
11790    }
11791
11792    #[test]
11793    fn root_auth_unencrypted_v44_round_trips_with_recomputed_archive_root() {
11794        let archive = write_archive_with_root_auth(
11795            &[RegularFile::new(
11796                "signed-v44.txt",
11797                b"root-auth v44 plaintext",
11798            )],
11799            &master_key(),
11800            WriterOptions {
11801                aead_algo: AeadAlgo::None,
11802                ..single_stream_options()
11803            },
11804            test_root_auth_config(),
11805            |request| Ok(test_root_auth_value(request)),
11806        )
11807        .unwrap();
11808        let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11809        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11810
11811        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11812        assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11813        assert_eq!(
11814            opened.extract_file("signed-v44.txt").unwrap(),
11815            Some(b"root-auth v44 plaintext".to_vec())
11816        );
11817
11818        let verified = opened
11819            .verify_root_auth_with(|footer, archive_root| {
11820                Ok(test_root_auth_verifies(footer, archive_root))
11821            })
11822            .unwrap();
11823
11824        assert_eq!(verified.format_version, FORMAT_VERSION);
11825        assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
11826        assert_eq!(
11827            verified.archive_root,
11828            opened.root_auth_footer.as_ref().unwrap().archive_root
11829        );
11830    }
11831
11832    #[test]
11833    fn recipientwrap_open_accepts_candidate_after_header_hmac() {
11834        let master = master_key();
11835        let archive = write_archive_with_recipient_wrap_records(
11836            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11837            &master,
11838            single_stream_options(),
11839            vec![recipient_wrap_test_record()],
11840        )
11841        .unwrap();
11842
11843        let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
11844            assert_eq!(
11845                context.archive_identity.volume_format_rev,
11846                VOLUME_FORMAT_REV_44
11847            );
11848            assert_eq!(context.record.profile_id, 1);
11849            Ok(vec![master.0])
11850        })
11851        .unwrap();
11852
11853        assert_eq!(
11854            opened.extract_file("wrapped.txt").unwrap(),
11855            Some(b"recipient payload".to_vec())
11856        );
11857        opened.verify().unwrap();
11858    }
11859
11860    #[test]
11861    fn recipientwrap_seekable_open_uses_lazy_block_source() {
11862        let master = master_key();
11863        let archive = write_archive_with_recipient_wrap_records(
11864            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11865            &master,
11866            single_stream_options(),
11867            vec![recipient_wrap_test_record()],
11868        )
11869        .unwrap();
11870
11871        let opened = open_seekable_archive_with_recipient_wrap_resolver_options(
11872            CountingReadAt::new(archive.bytes, vec![]),
11873            |context| {
11874                assert_eq!(
11875                    context.archive_identity.volume_format_rev,
11876                    VOLUME_FORMAT_REV_44
11877                );
11878                assert_eq!(context.record.profile_id, 1);
11879                Ok(vec![master.0])
11880            },
11881            ReaderOptions::default(),
11882        )
11883        .unwrap();
11884
11885        assert!(opened.blocks.is_empty());
11886        assert!(opened.lazy_blocks.is_some());
11887        assert_eq!(
11888            opened.extract_file("wrapped.txt").unwrap(),
11889            Some(b"recipient payload".to_vec())
11890        );
11891        opened.verify().unwrap();
11892    }
11893
11894    #[test]
11895    fn recipientwrap_seekable_volume_set_opens_with_resolver() {
11896        let master = master_key();
11897        let archive = write_archive_with_recipient_wrap_records(
11898            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11899            &master,
11900            WriterOptions {
11901                stripe_width: 2,
11902                volume_loss_tolerance: 0,
11903                ..WriterOptions::default()
11904            },
11905            vec![recipient_wrap_test_record()],
11906        )
11907        .unwrap();
11908        assert_eq!(archive.volumes.len(), 2);
11909
11910        let opened = open_seekable_archive_volumes_with_recipient_wrap_resolver_options(
11911            archive.volumes,
11912            |context| {
11913                assert_eq!(
11914                    context.archive_identity.volume_format_rev,
11915                    VOLUME_FORMAT_REV_44
11916                );
11917                assert_eq!(context.record.profile_id, 1);
11918                Ok(vec![master.0])
11919            },
11920            ReaderOptions::default(),
11921        )
11922        .unwrap();
11923
11924        assert_eq!(opened.observed_volume_count, 2);
11925        assert!(opened.blocks.is_empty());
11926        assert!(opened.lazy_blocks.is_some());
11927        assert_eq!(
11928            opened.extract_file("wrapped.txt").unwrap(),
11929            Some(b"recipient payload".to_vec())
11930        );
11931        opened.verify().unwrap();
11932    }
11933
11934    #[test]
11935    fn recipientwrap_open_tries_subsequent_records_after_failed_candidate() {
11936        let master = master_key();
11937        let mut first_record = recipient_wrap_test_record();
11938        first_record.recipient_identity_bytes = b"first-candidate".to_vec();
11939        let mut second_record = recipient_wrap_test_record();
11940        second_record.recipient_identity_bytes = b"second-candidate".to_vec();
11941
11942        let archive = write_archive_with_recipient_wrap_records(
11943            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11944            &master,
11945            single_stream_options(),
11946            vec![first_record, second_record],
11947        )
11948        .unwrap();
11949
11950        let mut attempts = Vec::new();
11951        let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
11952            attempts.push(context.record.recipient_identity_bytes.clone());
11953            if context.record.recipient_identity_bytes.as_slice() == b"second-candidate" {
11954                Ok(vec![master.0])
11955            } else {
11956                Ok(vec![[0x99u8; 32]])
11957            }
11958        })
11959        .unwrap();
11960
11961        assert_eq!(
11962            attempts,
11963            vec![b"first-candidate".to_vec(), b"second-candidate".to_vec(),]
11964        );
11965        assert_eq!(
11966            opened.extract_file("wrapped.txt").unwrap(),
11967            Some(b"recipient payload".to_vec())
11968        );
11969        opened.verify().unwrap();
11970    }
11971
11972    #[test]
11973    fn recipientwrap_startup_rejects_malformed_record_length() {
11974        let master = master_key();
11975        let options = WriterOptions {
11976            bit_rot_buffer_pct: 0,
11977            ..single_stream_options()
11978        };
11979        let archive = write_archive_with_recipient_wrap_records(
11980            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11981            &master,
11982            options,
11983            vec![recipient_wrap_test_record()],
11984        )
11985        .unwrap();
11986        let mut bytes = archive.bytes;
11987        let (_, _, table_start, _table_len, _) = recipient_wrap_layout(&bytes);
11988        bytes[table_start + 96..table_start + 100].copy_from_slice(&1u32.to_le_bytes());
11989
11990        assert_eq!(
11991            open_archive_with_recipient_wrap_resolver(&bytes, |_| { Ok(vec![master.0]) })
11992                .unwrap_err(),
11993            FormatError::InvalidArchive("RecipientRecordV1 record_length is too small")
11994        );
11995    }
11996
11997    #[test]
11998    fn recipientwrap_future_revision_rejects_before_resolver_callback() {
11999        let archive = write_archive_with_recipient_wrap_records(
12000            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12001            &master_key(),
12002            single_stream_options(),
12003            vec![recipient_wrap_test_record()],
12004        )
12005        .unwrap();
12006        let mut bytes = archive.bytes;
12007        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12008        header.volume_format_rev = VOLUME_FORMAT_REV_44 + 1;
12009        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12010
12011        let mut called = false;
12012        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12013            called = true;
12014            Ok(vec![master_key().0])
12015        })
12016        .unwrap_err();
12017
12018        assert!(!called);
12019        assert_eq!(
12020            err,
12021            FormatError::UnsupportedVolumeFormatRevision {
12022                format_version: FORMAT_VERSION,
12023                volume_format_rev: VOLUME_FORMAT_REV_44 + 1,
12024                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12025            }
12026        );
12027    }
12028
12029    #[test]
12030    fn recipientwrap_stripe_width_mismatch_rejects_before_resolver_callback() {
12031        let archive = write_archive_with_recipient_wrap_records(
12032            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12033            &master_key(),
12034            single_stream_options(),
12035            vec![recipient_wrap_test_record()],
12036        )
12037        .unwrap();
12038        let mut bytes = archive.bytes;
12039        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12040        header.stripe_width += 1;
12041        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12042
12043        let mut called = false;
12044        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12045            called = true;
12046            Ok(vec![master_key().0])
12047        })
12048        .unwrap_err();
12049
12050        assert!(!called);
12051        assert_eq!(
12052            err,
12053            FormatError::InvalidArchive("VolumeHeader and CryptoHeader stripe_width differ")
12054        );
12055    }
12056
12057    #[test]
12058    fn recipientwrap_defers_raw_stream_profile_rejection_until_after_resolver_callback() {
12059        let master = master_key();
12060        let archive = write_archive_with_recipient_wrap_records(
12061            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12062            &master,
12063            single_stream_options(),
12064            vec![recipient_wrap_test_record()],
12065        )
12066        .unwrap();
12067        let mut bytes = archive.bytes;
12068        add_raw_stream_profile_to_physical_crypto_header(&mut bytes);
12069        recompute_physical_crypto_header_hmac(&mut bytes, &master);
12070
12071        let mut called = false;
12072        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12073            called = true;
12074            Ok(vec![master.0])
12075        })
12076        .unwrap_err();
12077
12078        assert!(called);
12079        assert_eq!(
12080            err,
12081            FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE)
12082        );
12083    }
12084
12085    #[test]
12086    fn recipientwrap_recovers_physical_key_wrap_table_from_cmra_authority() {
12087        let master = master_key();
12088        let archive = write_archive_with_recipient_wrap_records(
12089            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12090            &master,
12091            single_stream_options(),
12092            vec![recipient_wrap_test_record()],
12093        )
12094        .unwrap();
12095        let mut bytes = archive.bytes;
12096        let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12097        let crypto_start = header.crypto_header_offset as usize;
12098        let crypto_end = crypto_start + header.crypto_header_length as usize;
12099        let crypto_header = CryptoHeader::parse(
12100            &bytes[crypto_start..crypto_end],
12101            header.crypto_header_length,
12102        )
12103        .unwrap();
12104        let KdfParams::RecipientWrap {
12105            key_wrap_table_length,
12106            ..
12107        } = crypto_header.kdf_params
12108        else {
12109            panic!("expected RecipientWrap KdfParams");
12110        };
12111        let table_start = crypto_end;
12112        let table_end = table_start + key_wrap_table_length as usize;
12113        bytes[table_end - 1] ^= 0x01;
12114
12115        let mut called = false;
12116        let opened = open_archive_with_recipient_wrap_resolver(&bytes, |context| {
12117            called = true;
12118            assert_eq!(
12119                context.archive_identity.volume_format_rev,
12120                VOLUME_FORMAT_REV_44
12121            );
12122            assert_eq!(context.record.profile_id, 1);
12123            Ok(vec![master.0])
12124        })
12125        .unwrap();
12126
12127        assert!(called);
12128        assert_eq!(
12129            opened.extract_file("wrapped.txt").unwrap(),
12130            Some(b"recipient payload".to_vec())
12131        );
12132        opened.verify().unwrap();
12133    }
12134
12135    #[test]
12136    fn recipientwrap_open_rejects_wrong_candidate_header_hmac() {
12137        let archive = write_archive_with_recipient_wrap_records(
12138            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12139            &master_key(),
12140            single_stream_options(),
12141            vec![recipient_wrap_test_record()],
12142        )
12143        .unwrap();
12144        let wrong_candidate = [0x99u8; 32];
12145
12146        assert_eq!(
12147            open_archive_with_recipient_wrap_resolver(&archive.bytes, |_| {
12148                Ok(vec![wrong_candidate])
12149            })
12150            .unwrap_err(),
12151            FormatError::KeyMaterialMismatch
12152        );
12153    }
12154
12155    #[test]
12156    fn recipientwrap_open_recovers_tampered_physical_crypto_header_hmac_from_cmra() {
12157        let master = master_key();
12158        let archive = write_archive_with_recipient_wrap_records(
12159            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12160            &master,
12161            single_stream_options(),
12162            vec![recipient_wrap_test_record()],
12163        )
12164        .unwrap();
12165        let mut bytes = archive.bytes.clone();
12166        let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12167        let hmac_end = header.crypto_header_offset as usize + header.crypto_header_length as usize;
12168        bytes[hmac_end - 1] ^= 0x55;
12169
12170        let opened =
12171            open_archive_with_recipient_wrap_resolver(&bytes, |_| Ok(vec![master.0])).unwrap();
12172
12173        assert_eq!(
12174            opened.extract_file("wrapped.txt").unwrap(),
12175            Some(b"recipient payload".to_vec())
12176        );
12177        assert_eq!(
12178            opened.crypto_header_bytes,
12179            archive.bytes[header.crypto_header_offset as usize..hmac_end]
12180        );
12181        opened.verify().unwrap();
12182    }
12183
12184    #[test]
12185    fn recipientwrap_archive_does_not_fall_back_to_raw_master_key_open() {
12186        let master = master_key();
12187        let archive = write_archive_with_recipient_wrap_records(
12188            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12189            &master,
12190            single_stream_options(),
12191            vec![recipient_wrap_test_record()],
12192        )
12193        .unwrap();
12194
12195        assert_eq!(
12196            open_archive(&archive.bytes, &master).unwrap_err(),
12197            FormatError::KeyMaterialMismatch
12198        );
12199    }
12200
12201    #[test]
12202    fn recipientwrap_seekable_archive_does_not_fall_back_to_raw_master_key_open() {
12203        let master = master_key();
12204        let archive = write_archive_with_recipient_wrap_records(
12205            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12206            &master,
12207            single_stream_options(),
12208            vec![recipient_wrap_test_record()],
12209        )
12210        .unwrap();
12211
12212        assert_eq!(
12213            open_seekable_archive(archive.bytes, &master).unwrap_err(),
12214            FormatError::KeyMaterialMismatch
12215        );
12216    }
12217
12218    #[test]
12219    fn public_no_key_verifies_signed_recipientwrap_block_commitment() {
12220        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12221            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12222            &master_key(),
12223            single_stream_options(),
12224            vec![recipient_wrap_test_record()],
12225            test_root_auth_config(),
12226            |request| Ok(test_root_auth_value(request)),
12227        )
12228        .unwrap();
12229
12230        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
12231            Ok(test_root_auth_verifies(footer, archive_root))
12232        })
12233        .unwrap();
12234
12235        assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
12236        assert_eq!(verified.total_data_block_count, 3);
12237    }
12238
12239    #[test]
12240    fn public_no_key_rejects_recipientwrap_startup_and_cmra_kdf_mismatch() {
12241        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12242            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12243            &master_key(),
12244            single_stream_options(),
12245            vec![recipient_wrap_test_record()],
12246            test_root_auth_config(),
12247            |request| Ok(test_root_auth_value(request)),
12248        )
12249        .unwrap();
12250        let mut bytes = archive.bytes;
12251        mutate_top_level_recipient_wrap_public_profile(&mut bytes);
12252
12253        let err = public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
12254            Ok(test_root_auth_verifies(footer, archive_root))
12255        })
12256        .unwrap_err();
12257
12258        assert_eq!(
12259            err,
12260            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
12261        );
12262    }
12263
12264    #[test]
12265    fn public_no_key_rejects_recipientwrap_kdf_profile_mismatch_across_volumes() {
12266        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12267            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12268            &master_key(),
12269            WriterOptions {
12270                stripe_width: 2,
12271                volume_loss_tolerance: 0,
12272                ..WriterOptions::default()
12273            },
12274            vec![recipient_wrap_test_record()],
12275            test_root_auth_config(),
12276            |request| Ok(test_root_auth_value(request)),
12277        )
12278        .unwrap();
12279        let mut volumes = archive.volumes;
12280        mutate_top_level_recipient_wrap_public_profile(&mut volumes[1]);
12281        mutate_cmra_recipient_wrap_public_profile(&mut volumes[1]);
12282
12283        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12284        let err = public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
12285            Ok(test_root_auth_verifies(footer, archive_root))
12286        })
12287        .unwrap_err();
12288
12289        assert_eq!(
12290            err,
12291            FormatError::InvalidArchive("public no-key volume global metadata differs")
12292        );
12293    }
12294
12295    #[test]
12296    fn write_archive_defaults_to_current_revision() {
12297        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12298        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12299
12300        assert_eq!(header.format_version, FORMAT_VERSION);
12301        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV);
12302    }
12303
12304    #[test]
12305    fn non_seekable_stream_rejects_future_volume_format_revision() {
12306        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12307        let mut bytes = archive.bytes;
12308        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12309        header.volume_format_rev = 45;
12310        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12311
12312        assert_eq!(
12313            verify_non_seekable_stream(std::io::Cursor::new(bytes), &master_key()).unwrap_err(),
12314            FormatError::UnsupportedVolumeFormatRevision {
12315                format_version: FORMAT_VERSION,
12316                volume_format_rev: 45,
12317                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12318            }
12319        );
12320    }
12321
12322    #[test]
12323    fn open_seekable_archive_rejects_future_volume_format_revision() {
12324        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12325        let mut bytes = archive.bytes;
12326        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12327        header.volume_format_rev = 45;
12328        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12329
12330        assert_eq!(
12331            open_seekable_archive(CountingReadAt::new(bytes, vec![]), &master_key()).unwrap_err(),
12332            FormatError::UnsupportedVolumeFormatRevision {
12333                format_version: FORMAT_VERSION,
12334                volume_format_rev: 45,
12335                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12336            }
12337        );
12338    }
12339
12340    #[test]
12341    fn rejects_payload_tamper_even_with_recomputed_block_crc() {
12342        let mut archive = write_archive(
12343            &[RegularFile::new("file.txt", b"authenticated")],
12344            &master_key(),
12345            single_stream_options(),
12346        )
12347        .unwrap()
12348        .bytes;
12349        let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
12350        let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
12351        let crypto = CryptoHeader::parse(
12352            &archive[VOLUME_HEADER_LEN..crypto_end],
12353            volume.crypto_header_length,
12354        )
12355        .unwrap();
12356        let block_size = crypto.fixed.block_size as usize;
12357        archive[crypto_end + 16] ^= 1;
12358        let crc_offset = crypto_end + 16 + block_size;
12359        let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
12360        archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12361
12362        let opened = open_archive(&archive, &master_key()).unwrap();
12363        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12364    }
12365
12366    #[test]
12367    fn list_and_extract_use_final_view_for_duplicate_paths() {
12368        let archive = write_archive(
12369            &[
12370                RegularFile::new("same.txt", b"old"),
12371                RegularFile::new("same.txt", b"newer"),
12372            ],
12373            &master_key(),
12374            single_stream_options(),
12375        )
12376        .unwrap();
12377        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12378
12379        assert_eq!(
12380            opened.list_index_entries().unwrap(),
12381            vec![ArchiveIndexEntry {
12382                path: "same.txt".to_string(),
12383                file_data_size: 5,
12384            }]
12385        );
12386        assert_eq!(
12387            opened.lookup_index_entry("same.txt").unwrap(),
12388            Some(ArchiveIndexEntry {
12389                path: "same.txt".to_string(),
12390                file_data_size: 5,
12391            })
12392        );
12393        assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
12394        assert_eq!(
12395            opened.list_files().unwrap(),
12396            vec![ArchiveEntry {
12397                path: "same.txt".to_string(),
12398                file_data_size: 5,
12399                kind: TarEntryKind::Regular,
12400                mode: 0o644,
12401                mtime: 0,
12402                diagnostics: Vec::new(),
12403            }]
12404        );
12405        assert_eq!(
12406            opened.extract_file("same.txt").unwrap(),
12407            Some(b"newer".to_vec())
12408        );
12409        opened.verify().unwrap();
12410    }
12411
12412    #[test]
12413    fn index_entries_do_not_decrypt_payload_envelopes() {
12414        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12415        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12416
12417        assert_eq!(
12418            opened.list_index_entries().unwrap(),
12419            vec![
12420                ArchiveIndexEntry {
12421                    path: "broken.txt".to_string(),
12422                    file_data_size: b"broken payload\n".len() as u64,
12423                },
12424                ArchiveIndexEntry {
12425                    path: "healthy.txt".to_string(),
12426                    file_data_size: b"healthy payload\n".len() as u64,
12427                },
12428            ]
12429        );
12430        assert_eq!(
12431            opened.lookup_index_entry("broken.txt").unwrap(),
12432            Some(ArchiveIndexEntry {
12433                path: "broken.txt".to_string(),
12434                file_data_size: b"broken payload\n".len() as u64,
12435            })
12436        );
12437        assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
12438    }
12439
12440    #[test]
12441    fn extract_file_does_not_decrypt_unselected_payload_envelope() {
12442        // This fixture corrupts only the unselected envelope, proving selected
12443        // extraction does not decrypt unrelated payload envelopes.
12444        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12445        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12446
12447        assert_eq!(
12448            opened.extract_file("healthy.txt").unwrap(),
12449            Some(b"healthy payload\n".to_vec())
12450        );
12451        assert_eq!(
12452            opened.extract_file("broken.txt").unwrap_err(),
12453            FormatError::AeadFailure
12454        );
12455        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12456    }
12457
12458    #[test]
12459    fn seekable_extract_does_not_read_unselected_payload_envelope() {
12460        let healthy = pseudo_random_bytes(64 * 1024);
12461        let broken = pseudo_random_bytes(64 * 1024);
12462        let options = WriterOptions {
12463            block_size: 4096,
12464            chunk_size: 4096,
12465            envelope_target_size: 8192,
12466            stripe_width: 1,
12467            volume_loss_tolerance: 0,
12468            bit_rot_buffer_pct: 0,
12469            fec_data_shards: 4,
12470            fec_parity_shards: 0,
12471            index_fec_data_shards: 4,
12472            index_fec_parity_shards: 0,
12473            index_root_fec_data_shards: 4,
12474            index_root_fec_parity_shards: 0,
12475            ..WriterOptions::default()
12476        };
12477        let archive = write_archive(
12478            &[
12479                RegularFile::new("healthy.bin", &healthy),
12480                RegularFile::new("broken.bin", &broken),
12481            ],
12482            &master_key(),
12483            options,
12484        )
12485        .unwrap();
12486        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12487        let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
12488        let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
12489        let denied_block_indices = broken_envelopes
12490            .iter()
12491            .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
12492            .flat_map(|envelope| {
12493                let block_count =
12494                    envelope.data_block_count as u64 + envelope.parity_block_count as u64;
12495                envelope.first_block_index..envelope.first_block_index + block_count
12496            })
12497            .collect::<BTreeSet<_>>();
12498        assert!(
12499            !denied_block_indices.is_empty(),
12500            "fixture must place broken.bin in at least one unshared envelope"
12501        );
12502        let denied_ranges = block_record_slots(&archive.bytes)
12503            .into_iter()
12504            .filter_map(|(offset, len, record)| {
12505                denied_block_indices
12506                    .contains(&record.block_index)
12507                    .then_some((offset as u64, (offset + len) as u64))
12508            })
12509            .collect::<Vec<_>>();
12510        assert!(!denied_ranges.is_empty());
12511
12512        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12513        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12514
12515        assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
12516        for (read_start, read_end) in reader.reads() {
12517            assert!(
12518                denied_ranges
12519                    .iter()
12520                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12521                "targeted extract read an unrelated payload BlockRecord range"
12522            );
12523        }
12524        assert_eq!(
12525            opened.extract_file("broken.bin").unwrap_err(),
12526            FormatError::InvalidArchive("denied test read")
12527        );
12528    }
12529
12530    #[test]
12531    fn extract_file_to_writer_streams_before_reading_later_envelopes() {
12532        struct FailOnFirstWrite;
12533
12534        impl std::io::Write for FailOnFirstWrite {
12535            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
12536                Err(std::io::Error::other("sink stopped"))
12537            }
12538
12539            fn flush(&mut self) -> std::io::Result<()> {
12540                Ok(())
12541            }
12542        }
12543
12544        let payload = pseudo_random_bytes(128 * 1024);
12545        let options = WriterOptions {
12546            block_size: 4096,
12547            chunk_size: 4096,
12548            envelope_target_size: 8192,
12549            stripe_width: 1,
12550            volume_loss_tolerance: 0,
12551            bit_rot_buffer_pct: 0,
12552            fec_data_shards: 4,
12553            fec_parity_shards: 0,
12554            index_fec_data_shards: 4,
12555            index_fec_parity_shards: 0,
12556            index_root_fec_data_shards: 4,
12557            index_root_fec_parity_shards: 0,
12558            ..WriterOptions::default()
12559        };
12560        let archive = write_archive(
12561            &[RegularFile::new("large.bin", &payload)],
12562            &master_key(),
12563            options,
12564        )
12565        .unwrap();
12566        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12567        let envelopes = envelope_entries_for_path(&eager, "large.bin");
12568        let first_envelope = envelopes
12569            .first()
12570            .expect("large fixture should have at least one envelope")
12571            .envelope_index;
12572        let later_envelope_blocks = envelopes
12573            .iter()
12574            .filter(|entry| entry.envelope_index != first_envelope)
12575            .flat_map(|entry| {
12576                let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
12577                entry.first_block_index..entry.first_block_index + block_count
12578            })
12579            .collect::<BTreeSet<_>>();
12580        assert!(
12581            !later_envelope_blocks.is_empty(),
12582            "fixture must span more than one payload envelope"
12583        );
12584        let denied_ranges = block_record_slots(&archive.bytes)
12585            .into_iter()
12586            .filter_map(|(offset, len, record)| {
12587                later_envelope_blocks
12588                    .contains(&record.block_index)
12589                    .then_some((offset as u64, (offset + len) as u64))
12590            })
12591            .collect::<Vec<_>>();
12592        assert!(!denied_ranges.is_empty());
12593
12594        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12595        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12596        let mut writer = FailOnFirstWrite;
12597
12598        let err = opened
12599            .extract_file_to_writer("large.bin", &mut writer)
12600            .unwrap_err();
12601        assert_eq!(err.to_string(), "extraction output write failed");
12602        for (read_start, read_end) in reader.reads() {
12603            assert!(
12604                denied_ranges
12605                    .iter()
12606                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12607                "streaming writer read a later payload envelope before surfacing writer failure"
12608            );
12609        }
12610    }
12611
12612    #[test]
12613    fn extract_file_to_writer_writes_bounded_chunks() {
12614        struct ChunkRecorder {
12615            total: usize,
12616            max_write: usize,
12617            writes: usize,
12618        }
12619
12620        impl std::io::Write for ChunkRecorder {
12621            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12622                self.total += buf.len();
12623                self.max_write = self.max_write.max(buf.len());
12624                self.writes += 1;
12625                Ok(buf.len())
12626            }
12627
12628            fn flush(&mut self) -> std::io::Result<()> {
12629                Ok(())
12630            }
12631        }
12632
12633        let payload = pseudo_random_bytes(128 * 1024);
12634        let options = WriterOptions {
12635            block_size: 4096,
12636            chunk_size: 4096,
12637            envelope_target_size: 8192,
12638            stripe_width: 1,
12639            volume_loss_tolerance: 0,
12640            bit_rot_buffer_pct: 0,
12641            fec_data_shards: 4,
12642            fec_parity_shards: 0,
12643            index_fec_data_shards: 4,
12644            index_fec_parity_shards: 0,
12645            index_root_fec_data_shards: 4,
12646            index_root_fec_parity_shards: 0,
12647            ..WriterOptions::default()
12648        };
12649        let archive = write_archive(
12650            &[RegularFile::new("large.bin", &payload)],
12651            &master_key(),
12652            options,
12653        )
12654        .unwrap();
12655        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12656        let mut writer = ChunkRecorder {
12657            total: 0,
12658            max_write: 0,
12659            writes: 0,
12660        };
12661
12662        opened
12663            .extract_file_to_writer("large.bin", &mut writer)
12664            .unwrap()
12665            .unwrap();
12666
12667        assert_eq!(writer.total, payload.len());
12668        assert!(writer.writes > 1);
12669        assert!(
12670            writer.max_write <= options.chunk_size as usize,
12671            "writer saw a {} byte chunk, larger than the {} byte frame target",
12672            writer.max_write,
12673            options.chunk_size
12674        );
12675    }
12676
12677    #[test]
12678    fn extract_file_to_writer_with_progress_reports_payload_bytes() {
12679        struct ChunkRecorder {
12680            total: usize,
12681        }
12682
12683        impl std::io::Write for ChunkRecorder {
12684            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12685                self.total += buf.len();
12686                Ok(buf.len())
12687            }
12688
12689            fn flush(&mut self) -> std::io::Result<()> {
12690                Ok(())
12691            }
12692        }
12693
12694        let payload = pseudo_random_bytes(128 * 1024);
12695        let options = WriterOptions {
12696            block_size: 4096,
12697            chunk_size: 4096,
12698            envelope_target_size: 8192,
12699            stripe_width: 1,
12700            volume_loss_tolerance: 0,
12701            bit_rot_buffer_pct: 0,
12702            fec_data_shards: 4,
12703            fec_parity_shards: 0,
12704            index_fec_data_shards: 4,
12705            index_fec_parity_shards: 0,
12706            index_root_fec_data_shards: 4,
12707            index_root_fec_parity_shards: 0,
12708            ..WriterOptions::default()
12709        };
12710        let archive = write_archive(
12711            &[RegularFile::new("large.bin", &payload)],
12712            &master_key(),
12713            options,
12714        )
12715        .unwrap();
12716        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12717        let mut writer = ChunkRecorder { total: 0 };
12718        let mut progress_events = Vec::new();
12719        let mut progress = |archive_path: &str, bytes: u64| {
12720            progress_events.push((archive_path.to_owned(), bytes));
12721        };
12722
12723        opened
12724            .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
12725            .unwrap()
12726            .unwrap();
12727
12728        let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
12729        assert_eq!(writer.total, payload.len());
12730        assert_eq!(reported_bytes, payload.len() as u64);
12731        assert!(progress_events.len() > 1);
12732        assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
12733    }
12734
12735    #[test]
12736    fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
12737        let payload = pseudo_random_bytes(128 * 1024);
12738        let options = WriterOptions {
12739            block_size: 4096,
12740            chunk_size: 4096,
12741            envelope_target_size: 8192,
12742            stripe_width: 1,
12743            volume_loss_tolerance: 0,
12744            bit_rot_buffer_pct: 0,
12745            fec_data_shards: 4,
12746            fec_parity_shards: 0,
12747            index_fec_data_shards: 4,
12748            index_fec_parity_shards: 0,
12749            index_root_fec_data_shards: 4,
12750            index_root_fec_parity_shards: 0,
12751            ..WriterOptions::default()
12752        };
12753        let archive = write_archive(
12754            &[RegularFile::new("large.bin", &payload)],
12755            &master_key(),
12756            options,
12757        )
12758        .unwrap();
12759        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12760        let envelopes = envelope_entries_for_path(&eager, "large.bin");
12761        let last_envelope = envelopes
12762            .last()
12763            .expect("large fixture should have at least one envelope");
12764        assert_ne!(
12765            envelopes.first().unwrap().envelope_index,
12766            last_envelope.envelope_index,
12767            "fixture must span more than one payload envelope"
12768        );
12769        let corrupt_slot = block_record_slots(&archive.bytes)
12770            .into_iter()
12771            .enumerate()
12772            .find_map(|(slot, (_, _, record))| {
12773                (record.block_index == last_envelope.first_block_index).then_some(slot)
12774            })
12775            .unwrap();
12776        let mut corrupted = archive.bytes;
12777        corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
12778        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
12779        let tmp = tempfile::tempdir().unwrap();
12780
12781        assert!(matches!(
12782            opened
12783                .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
12784                .unwrap_err(),
12785            FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
12786        ));
12787        assert!(!tmp.path().join("large.bin").exists());
12788        assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
12789    }
12790
12791    #[test]
12792    fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
12793        let archive = write_archive(
12794            &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
12795            &master_key(),
12796            single_stream_options(),
12797        )
12798        .unwrap();
12799        let opened = open_archive_with_bootstrap_sidecar(
12800            &archive.bytes,
12801            &archive.bootstrap_sidecar,
12802            &master_key(),
12803        )
12804        .unwrap();
12805
12806        assert_eq!(
12807            opened.list_files().unwrap(),
12808            vec![ArchiveEntry {
12809                path: "dir/sidecar.txt".to_string(),
12810                file_data_size: 13,
12811                kind: TarEntryKind::Regular,
12812                mode: 0o644,
12813                mtime: 0,
12814                diagnostics: Vec::new(),
12815            }]
12816        );
12817        assert_eq!(
12818            opened.extract_file("dir/sidecar.txt").unwrap(),
12819            Some(b"hello sidecar".to_vec())
12820        );
12821        opened.verify().unwrap();
12822    }
12823
12824    #[test]
12825    fn fast_verify_plaintext_zero_recovery_defers_payload_semantics() {
12826        let options = WriterOptions {
12827            aead_algo: AeadAlgo::None,
12828            volume_loss_tolerance: 0,
12829            bit_rot_buffer_pct: 0,
12830            ..single_stream_options()
12831        };
12832        let archive = write_archive_unencrypted(
12833            &[RegularFile::new(
12834                "payload.txt",
12835                b"payload bytes large enough to produce a zstd frame",
12836            )],
12837            options,
12838        )
12839        .unwrap();
12840        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12841        let tables = opened.load_payload_index_tables().unwrap();
12842        let first_envelope = tables.envelopes.values().next().unwrap();
12843        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12844        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
12845        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12846        let payload_offset = crypto_end + first_envelope.first_block_index as usize * record_len;
12847
12848        let mut tampered = archive.bytes.clone();
12849        tampered[payload_offset + 16] ^= 0x01;
12850        let crc_offset = payload_offset + 16 + opened.crypto_header.block_size as usize;
12851        let crc = crc32c::crc32c(&tampered[payload_offset..crc_offset]);
12852        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12853
12854        let tampered_opened = open_archive(&tampered, &master_key()).unwrap();
12855        assert!(tampered_opened.fast_verify_defers_payload_semantics());
12856        tampered_opened.verify_content_fast().unwrap();
12857        assert!(tampered_opened.verify_content().is_err());
12858    }
12859
12860    #[test]
12861    fn fast_verify_root_auth_archive_requires_full_root_auth_scan() {
12862        let archive = write_archive_with_root_auth(
12863            &[RegularFile::new("signed.txt", b"root-auth payload")],
12864            &master_key(),
12865            single_stream_options(),
12866            RootAuthWriterConfig {
12867                authenticator_id: 0x7777,
12868                signer_identity_type: 1,
12869                signer_identity: b"test signer",
12870                authenticator_value_length: 32,
12871            },
12872            |request| Ok(request.archive_root.to_vec()),
12873        )
12874        .unwrap();
12875
12876        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12877        assert!(!opened.fast_verify_defers_payload_semantics());
12878        assert!(matches!(
12879            opened.verify_content_fast().unwrap().mode,
12880            ContentVerificationMode::Fast
12881        ));
12882    }
12883
12884    #[test]
12885    fn fast_verify_dictionary_archive_does_not_defer_payload_semantics() {
12886        let archive = write_archive_with_dictionary(
12887            &[RegularFile::new("dict.txt", b"dictionary payload")],
12888            &master_key(),
12889            single_stream_options(),
12890            dictionary(),
12891        )
12892        .unwrap();
12893
12894        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12895        assert!(!opened.fast_verify_defers_payload_semantics());
12896    }
12897
12898    #[test]
12899    fn fast_verify_encrypted_archive_does_not_defer_payload_semantics() {
12900        let options = WriterOptions {
12901            aead_algo: AeadAlgo::AesGcmSiv256,
12902            volume_loss_tolerance: 0,
12903            bit_rot_buffer_pct: 0,
12904            ..single_stream_options()
12905        };
12906        let archive = write_archive(
12907            &[RegularFile::new("payload.txt", b"encrypted payload")],
12908            &master_key(),
12909            options,
12910        )
12911        .unwrap();
12912
12913        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12914        assert!(!opened.fast_verify_defers_payload_semantics());
12915    }
12916
12917    #[test]
12918    fn fast_verify_repair_archive_does_not_defer_payload_semantics() {
12919        let options = WriterOptions {
12920            fec_parity_shards: 2,
12921            volume_loss_tolerance: 0,
12922            bit_rot_buffer_pct: 0,
12923            ..single_stream_options()
12924        };
12925        let archive = write_archive(
12926            &[RegularFile::new("payload.txt", b"payload for repair")],
12927            &master_key(),
12928            options,
12929        )
12930        .unwrap();
12931
12932        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12933        assert!(!opened.fast_verify_defers_payload_semantics());
12934    }
12935
12936    #[test]
12937    fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
12938        let archive = write_archive_with_dictionary(
12939            &[RegularFile::new(
12940                "dir/dict.txt",
12941                b"common words common words dictionary payload",
12942            )],
12943            &master_key(),
12944            single_stream_options(),
12945            dictionary(),
12946        )
12947        .unwrap();
12948        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12949
12950        assert_eq!(opened.crypto_header.has_dictionary, 1);
12951        assert!(opened.index_root.header.dictionary_data_block_count > 0);
12952        assert_eq!(
12953            opened.list_files().unwrap(),
12954            vec![ArchiveEntry {
12955                path: "dir/dict.txt".to_string(),
12956                file_data_size: 44,
12957                kind: TarEntryKind::Regular,
12958                mode: 0o644,
12959                mtime: 0,
12960                diagnostics: Vec::new(),
12961            }]
12962        );
12963        assert_eq!(
12964            opened.extract_file("dir/dict.txt").unwrap(),
12965            Some(b"common words common words dictionary payload".to_vec())
12966        );
12967        opened.verify().unwrap();
12968    }
12969
12970    #[test]
12971    fn dictionary_object_tamper_fails_before_payload_decompression() {
12972        let archive = write_archive_with_dictionary(
12973            &[RegularFile::new(
12974                "dir/dict.txt",
12975                b"common words common words dictionary payload",
12976            )],
12977            &master_key(),
12978            single_stream_options(),
12979            dictionary(),
12980        )
12981        .unwrap();
12982        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12983        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12984        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
12985        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
12986        let dictionary_offset =
12987            crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
12988
12989        let mut tampered = archive.bytes.clone();
12990        tampered[dictionary_offset + 16] ^= 0x01;
12991        let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
12992        let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
12993        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12994
12995        assert_eq!(
12996            open_archive(&tampered, &master_key()).unwrap_err(),
12997            FormatError::AeadFailure
12998        );
12999    }
13000
13001    #[test]
13002    fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
13003        let archive = write_archive_with_dictionary(
13004            &[RegularFile::new(
13005                "dict-sidecar.txt",
13006                b"common words common words sidecar payload",
13007            )],
13008            &master_key(),
13009            single_stream_options(),
13010            dictionary(),
13011        )
13012        .unwrap();
13013        let opened = open_non_seekable_archive(
13014            &archive.bytes,
13015            &master_key(),
13016            Some(&archive.bootstrap_sidecar),
13017        )
13018        .unwrap();
13019
13020        assert_eq!(
13021            opened.extract_file("dict-sidecar.txt").unwrap(),
13022            Some(b"common words common words sidecar payload".to_vec())
13023        );
13024        opened.verify().unwrap();
13025    }
13026
13027    #[test]
13028    fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
13029        let archive = write_archive(
13030            &[RegularFile::new(
13031                "sidecar-terminal.txt",
13032                b"sidecar authority",
13033            )],
13034            &master_key(),
13035            single_stream_options(),
13036        )
13037        .unwrap();
13038        let mut corrupted = archive.bytes.clone();
13039        corrupt_v41_terminal_recovery(&mut corrupted);
13040        assert!(open_archive(&corrupted, &master_key()).is_err());
13041
13042        let opened =
13043            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
13044                .unwrap();
13045
13046        assert!(opened.volume_trailer.is_none());
13047        assert_eq!(
13048            opened.extract_file("sidecar-terminal.txt").unwrap(),
13049            Some(b"sidecar authority".to_vec())
13050        );
13051        opened.verify().unwrap();
13052    }
13053
13054    #[test]
13055    fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
13056        let archive = write_archive_with_dictionary(
13057            &[RegularFile::new(
13058                "dict-no-terminal.txt",
13059                b"common words common words without terminal",
13060            )],
13061            &master_key(),
13062            single_stream_options(),
13063            dictionary(),
13064        )
13065        .unwrap();
13066        let terminal_offset = terminal_material_offset(&archive.bytes);
13067        let truncated = archive.bytes[..terminal_offset].to_vec();
13068        assert!(open_archive(&truncated, &master_key()).is_err());
13069
13070        let opened =
13071            open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
13072                .unwrap();
13073
13074        assert!(opened.volume_trailer.is_none());
13075        assert_eq!(
13076            opened.extract_file("dict-no-terminal.txt").unwrap(),
13077            Some(b"common words common words without terminal".to_vec())
13078        );
13079        opened.verify().unwrap();
13080    }
13081
13082    #[test]
13083    fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
13084        let archive = write_archive(
13085            &[RegularFile::new(
13086                "sidecar-erasure.txt",
13087                b"repair through sidecar",
13088            )],
13089            &master_key(),
13090            single_stream_options(),
13091        )
13092        .unwrap();
13093        let mut corrupted = archive.bytes.clone();
13094        corrupt_first_block_record_payload(&mut corrupted);
13095
13096        let opened = open_archive_with_bootstrap_sidecar(
13097            &corrupted,
13098            &archive.bootstrap_sidecar,
13099            &master_key(),
13100        )
13101        .unwrap();
13102        assert_eq!(
13103            opened.extract_file("sidecar-erasure.txt").unwrap(),
13104            Some(b"repair through sidecar".to_vec())
13105        );
13106    }
13107
13108    #[test]
13109    fn extraction_rejects_logical_payload_above_total_size_cap() {
13110        let archive = write_archive(
13111            &[RegularFile::new("cap.txt", b"payload")],
13112            &master_key(),
13113            single_stream_options(),
13114        )
13115        .unwrap();
13116        let options = ReaderOptions {
13117            max_total_extraction_size: 3,
13118            ..ReaderOptions::default()
13119        };
13120        let opened =
13121            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13122
13123        assert_eq!(
13124            opened.extract_file("cap.txt").unwrap_err(),
13125            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13126        );
13127    }
13128
13129    #[test]
13130    fn verify_does_not_apply_extraction_payload_cap() {
13131        let archive = write_archive(
13132            &[RegularFile::new("verify-cap.txt", b"payload")],
13133            &master_key(),
13134            single_stream_options(),
13135        )
13136        .unwrap();
13137        let options = ReaderOptions {
13138            max_total_extraction_size: 3,
13139            ..ReaderOptions::default()
13140        };
13141        let opened =
13142            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13143
13144        opened.verify().unwrap();
13145        assert_eq!(
13146            opened.extract_file("verify-cap.txt").unwrap_err(),
13147            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13148        );
13149    }
13150
13151    #[test]
13152    fn verify_streams_past_legacy_in_memory_tar_cap() {
13153        let data = vec![0x5a; 4096];
13154        let archive = write_archive(
13155            &[RegularFile::new("verify-large.txt", &data)],
13156            &master_key(),
13157            single_stream_options(),
13158        )
13159        .unwrap();
13160        let options = ReaderOptions {
13161            max_verify_tar_size: 1,
13162            ..ReaderOptions::default()
13163        };
13164        let opened =
13165            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13166
13167        opened.verify().unwrap();
13168    }
13169
13170    #[test]
13171    fn dictionary_sidecar_requires_dictionary_record_section() {
13172        let archive = write_archive_with_dictionary(
13173            &[RegularFile::new("dict-missing.txt", b"common words")],
13174            &master_key(),
13175            single_stream_options(),
13176            dictionary(),
13177        )
13178        .unwrap();
13179        let header = BootstrapSidecarHeader::parse(
13180            &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
13181        )
13182        .unwrap();
13183        let mut missing_dictionary =
13184            archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
13185        rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
13186            header.flags &= !0x04;
13187            header.dictionary_records_offset = 0;
13188            header.dictionary_records_length = 0;
13189        });
13190
13191        assert_eq!(
13192            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
13193                .unwrap_err(),
13194            FormatError::ReaderUnsupported("dictionary bootstrap required")
13195        );
13196    }
13197
13198    #[test]
13199    fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
13200        let archive = write_archive_with_dictionary(
13201            &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
13202            &master_key(),
13203            single_stream_options(),
13204            dictionary(),
13205        )
13206        .unwrap();
13207
13208        let mut wrong_kind = archive.bootstrap_sidecar.clone();
13209        mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
13210            record.kind = BlockKind::IndexRootData;
13211        });
13212        assert_eq!(
13213            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
13214                .unwrap_err(),
13215            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
13216        );
13217
13218        let mut wrong_last = archive.bootstrap_sidecar.clone();
13219        mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
13220            record.flags = 0;
13221        });
13222        assert_eq!(
13223            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
13224                .unwrap_err(),
13225            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
13226        );
13227    }
13228
13229    #[test]
13230    fn non_seekable_random_access_requires_sidecar() {
13231        let archive = write_archive(
13232            &[RegularFile::new("file.txt", b"payload")],
13233            &master_key(),
13234            single_stream_options(),
13235        )
13236        .unwrap();
13237
13238        assert_eq!(
13239            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13240            FormatError::ReaderUnsupported(
13241                "non-seekable random access requires a bootstrap sidecar"
13242            )
13243        );
13244        assert!(open_non_seekable_archive(
13245            &archive.bytes,
13246            &master_key(),
13247            Some(&archive.bootstrap_sidecar)
13248        )
13249        .is_ok());
13250    }
13251
13252    #[test]
13253    fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
13254        let archive = write_archive(
13255            &[RegularFile::new("sparse.txt", b"sparse sidecar")],
13256            &master_key(),
13257            single_stream_options(),
13258        )
13259        .unwrap();
13260        let index_root_only = sparse_bootstrap_sidecar(
13261            &archive.bootstrap_sidecar,
13262            &master_key(),
13263            false,
13264            true,
13265            false,
13266        );
13267
13268        assert_eq!(
13269            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
13270                .unwrap_err(),
13271            FormatError::ReaderUnsupported(
13272                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13273            )
13274        );
13275    }
13276
13277    #[test]
13278    fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
13279        let archive = write_archive(
13280            &[RegularFile::new("sparse-index.txt", b"recover index root")],
13281            &master_key(),
13282            single_stream_options(),
13283        )
13284        .unwrap();
13285        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13286        let mut corrupted = archive.bytes.clone();
13287        corrupt_object_extent_records(
13288            &mut corrupted,
13289            index_root_extent_from_manifest(&opened.manifest_footer),
13290        );
13291        assert!(open_archive(&corrupted, &master_key()).is_err());
13292
13293        let index_root_only = sparse_bootstrap_sidecar(
13294            &archive.bootstrap_sidecar,
13295            &master_key(),
13296            false,
13297            true,
13298            false,
13299        );
13300        let recovered =
13301            open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
13302                .unwrap();
13303
13304        assert_eq!(
13305            recovered.extract_file("sparse-index.txt").unwrap(),
13306            Some(b"recover index root".to_vec())
13307        );
13308        recovered.verify().unwrap();
13309    }
13310
13311    #[test]
13312    fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
13313        let archive = write_archive_with_dictionary(
13314            &[RegularFile::new(
13315                "sparse-dict.txt",
13316                b"common words common words sparse dictionary",
13317            )],
13318            &master_key(),
13319            single_stream_options(),
13320            dictionary(),
13321        )
13322        .unwrap();
13323        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13324        let mut corrupted = archive.bytes.clone();
13325        corrupt_object_extent_records(
13326            &mut corrupted,
13327            dictionary_extent_from_index_root(&opened.index_root).unwrap(),
13328        );
13329        assert!(open_archive(&corrupted, &master_key()).is_err());
13330
13331        let dictionary_only = sparse_bootstrap_sidecar(
13332            &archive.bootstrap_sidecar,
13333            &master_key(),
13334            false,
13335            false,
13336            true,
13337        );
13338        assert_eq!(
13339            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
13340                .unwrap_err(),
13341            FormatError::ReaderUnsupported(
13342                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13343            )
13344        );
13345
13346        let recovered =
13347            open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
13348                .unwrap();
13349        assert_eq!(
13350            recovered.extract_file("sparse-dict.txt").unwrap(),
13351            Some(b"common words common words sparse dictionary".to_vec())
13352        );
13353        recovered.verify().unwrap();
13354    }
13355
13356    #[test]
13357    fn sequential_extracts_dictionary_free_tar_stream() {
13358        let archive = write_archive(
13359            &[RegularFile::new("seq.txt", b"streaming")],
13360            &master_key(),
13361            single_stream_options(),
13362        )
13363        .unwrap();
13364
13365        let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
13366        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13367        assert_eq!(member.path, b"seq.txt");
13368        assert_eq!(member.data, b"streaming");
13369    }
13370
13371    #[test]
13372    fn sequential_rejects_logical_payload_above_total_size_cap() {
13373        let archive = write_archive(
13374            &[RegularFile::new("seq-cap.txt", b"payload")],
13375            &master_key(),
13376            single_stream_options(),
13377        )
13378        .unwrap();
13379        let options = ReaderOptions {
13380            max_total_extraction_size: 3,
13381            ..ReaderOptions::default()
13382        };
13383
13384        assert_eq!(
13385            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13386                .unwrap_err(),
13387            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13388        );
13389    }
13390
13391    #[test]
13392    fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
13393        let archive = write_archive(
13394            &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
13395            &master_key(),
13396            single_stream_options(),
13397        )
13398        .unwrap();
13399        let options = ReaderOptions {
13400            max_verify_tar_size: 512,
13401            ..ReaderOptions::default()
13402        };
13403
13404        assert_eq!(
13405            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13406                .unwrap_err(),
13407            FormatError::ReaderUnsupported(
13408                "sequential tar stream exceeds configured verification cap"
13409            )
13410        );
13411    }
13412
13413    #[test]
13414    fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
13415        let archive = write_archive(
13416            &[RegularFile::new("seq-erasure.txt", b"stream repair")],
13417            &master_key(),
13418            single_stream_options(),
13419        )
13420        .unwrap();
13421        let mut corrupted = archive.bytes;
13422        corrupt_first_block_record_payload(&mut corrupted);
13423
13424        let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
13425        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13426        assert_eq!(member.path, b"seq-erasure.txt");
13427        assert_eq!(member.data, b"stream repair");
13428    }
13429
13430    #[test]
13431    fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
13432        let archive = write_archive(
13433            &[RegularFile::new("seq-no-parity.txt", b"no repair")],
13434            &master_key(),
13435            WriterOptions {
13436                bit_rot_buffer_pct: 0,
13437                fec_parity_shards: 0,
13438                index_fec_parity_shards: 0,
13439                index_root_fec_parity_shards: 0,
13440                ..single_stream_options()
13441            },
13442        )
13443        .unwrap();
13444        let mut corrupted = archive.bytes;
13445        corrupt_first_block_record_payload(&mut corrupted);
13446
13447        assert_eq!(
13448            sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
13449            FormatError::BadCrc {
13450                structure: "BlockRecord"
13451            }
13452        );
13453    }
13454
13455    #[test]
13456    fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
13457        let archive = write_archive(
13458            &[RegularFile::new(
13459                "seq.txt",
13460                b"payload must not be returned after terminal auth failure",
13461            )],
13462            &master_key(),
13463            single_stream_options(),
13464        )
13465        .unwrap();
13466        let mut corrupted = archive.bytes;
13467        corrupt_v41_terminal_recovery(&mut corrupted);
13468
13469        match sequential_extract_tar_stream(&corrupted, &master_key()) {
13470            Ok(bytes) => panic!(
13471                "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
13472                bytes.len()
13473            ),
13474            Err(err) => assert_eq!(
13475                err,
13476                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13477            ),
13478        }
13479    }
13480
13481    #[test]
13482    fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
13483        let archive = write_archive_with_dictionary(
13484            &[RegularFile::new(
13485                "seq-dict.txt",
13486                b"common words common words dictionary payload",
13487            )],
13488            &master_key(),
13489            single_stream_options(),
13490            b"common words dictionary",
13491        )
13492        .unwrap();
13493
13494        match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
13495            Ok(bytes) => panic!(
13496                "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
13497                bytes.len()
13498            ),
13499            Err(err) => assert_eq!(
13500                err,
13501                FormatError::ReaderUnsupported(
13502                    "dictionary bootstrap required for non-seekable sequential extraction"
13503                )
13504            ),
13505        }
13506    }
13507
13508    #[test]
13509    fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
13510        let archive = write_archive_with_dictionary(
13511            &[RegularFile::new(
13512                "seq-dict-open.txt",
13513                b"common words common words bootstrap required",
13514            )],
13515            &master_key(),
13516            single_stream_options(),
13517            b"common words bootstrap",
13518        )
13519        .unwrap();
13520
13521        assert_eq!(
13522            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13523            FormatError::ReaderUnsupported(
13524                "non-seekable random access requires a bootstrap sidecar"
13525            )
13526        );
13527    }
13528
13529    #[test]
13530    fn sequential_zstd_stream_rejects_skippable_frame_segments() {
13531        let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
13532        let mut output = Vec::new();
13533
13534        assert_eq!(
13535            decode_concatenated_zstd_frames_with_cap(
13536                &skippable,
13537                None,
13538                &mut output,
13539                usize::MAX,
13540                None,
13541            )
13542            .unwrap_err(),
13543            FormatError::NotStandardZstdFrame
13544        );
13545        assert!(output.is_empty());
13546    }
13547
13548    #[test]
13549    fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
13550        let archive = write_archive(
13551            &[RegularFile::new("live.txt", b"stream verify")],
13552            &master_key(),
13553            single_stream_options(),
13554        )
13555        .unwrap();
13556
13557        let report =
13558            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13559
13560        assert_eq!(report.file_count, 1);
13561        assert_eq!(report.total_volumes, 1);
13562        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13563        assert!(report.payload_block_count > 0);
13564    }
13565
13566    #[test]
13567    fn live_non_seekable_verify_stream_accepts_recipientwrap_archive() {
13568        let master = master_key();
13569        let archive = write_archive_with_recipient_wrap_records(
13570            &[RegularFile::new(
13571                "wrapped-live.txt",
13572                b"stream recipient verify",
13573            )],
13574            &master,
13575            single_stream_options(),
13576            vec![recipient_wrap_test_record()],
13577        )
13578        .unwrap();
13579
13580        let mut called = false;
13581        let report = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13582            std::io::Cursor::new(archive.bytes),
13583            |context| {
13584                called = true;
13585                assert_eq!(
13586                    context.archive_identity.volume_format_rev,
13587                    VOLUME_FORMAT_REV_44
13588                );
13589                assert_eq!(context.record.profile_id, 1);
13590                Ok(vec![master.0])
13591            },
13592            NonSeekableReaderOptions::default(),
13593        )
13594        .unwrap();
13595
13596        assert!(called);
13597        assert_eq!(report.file_count, 1);
13598        assert_eq!(report.total_volumes, 1);
13599        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13600    }
13601
13602    #[test]
13603    fn live_non_seekable_recipientwrap_resolver_rejects_unencrypted_archive() {
13604        let archive = write_archive_unencrypted(
13605            &[RegularFile::new("plain-live.txt", b"plaintext payload")],
13606            single_stream_options(),
13607        )
13608        .unwrap();
13609
13610        let mut called = false;
13611        let err = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13612            std::io::Cursor::new(archive.bytes),
13613            |_| {
13614                called = true;
13615                Ok(vec![master_key().0])
13616            },
13617            NonSeekableReaderOptions::default(),
13618        )
13619        .unwrap_err();
13620
13621        assert!(!called);
13622        assert_eq!(err, FormatError::KeyMaterialMismatch);
13623    }
13624
13625    #[test]
13626    fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
13627        let archive = write_archive(
13628            &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
13629            &master_key(),
13630            single_stream_options(),
13631        )
13632        .unwrap();
13633
13634        let report =
13635            verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
13636                .unwrap();
13637
13638        assert_eq!(report.file_count, 1);
13639        assert_eq!(report.tar_total_size % 512, 0);
13640    }
13641
13642    #[test]
13643    fn live_non_seekable_verify_stream_accepts_empty_archive() {
13644        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13645
13646        let report =
13647            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13648
13649        assert_eq!(report.file_count, 0);
13650        assert_eq!(report.payload_block_count, 0);
13651        assert_eq!(report.tar_total_size, 0);
13652    }
13653
13654    #[test]
13655    fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
13656        let archive = write_archive_with_dictionary(
13657            &[RegularFile::new(
13658                "live-dict.txt",
13659                b"common words common words dictionary payload",
13660            )],
13661            &master_key(),
13662            single_stream_options(),
13663            b"common words dictionary",
13664        )
13665        .unwrap();
13666
13667        assert_eq!(
13668            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
13669                .unwrap_err(),
13670            FormatError::ReaderUnsupported(
13671                "dictionary bootstrap required for non-seekable sequential verification"
13672            )
13673        );
13674    }
13675
13676    #[test]
13677    fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
13678        let archive = write_archive_with_dictionary(
13679            &[RegularFile::new(
13680                "live-dict-sidecar.txt",
13681                b"common words common words dictionary payload",
13682            )],
13683            &master_key(),
13684            single_stream_options(),
13685            b"common words dictionary",
13686        )
13687        .unwrap();
13688
13689        let report = verify_non_seekable_stream_with_bootstrap_sidecar(
13690            std::io::Cursor::new(archive.bytes),
13691            &archive.bootstrap_sidecar,
13692            &master_key(),
13693            NonSeekableReaderOptions::default(),
13694        )
13695        .unwrap();
13696
13697        assert_eq!(report.file_count, 1);
13698        assert_eq!(report.total_volumes, 1);
13699    }
13700
13701    #[test]
13702    fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
13703        let archive = write_archive(
13704            &[RegularFile::new("tail-cap.txt", b"payload")],
13705            &master_key(),
13706            single_stream_options(),
13707        )
13708        .unwrap();
13709        let options = NonSeekableReaderOptions {
13710            max_terminal_tail_size: 8,
13711            ..NonSeekableReaderOptions::default()
13712        };
13713
13714        assert_eq!(
13715            verify_non_seekable_stream_with_options(
13716                std::io::Cursor::new(archive.bytes),
13717                &master_key(),
13718                options
13719            )
13720            .unwrap_err(),
13721            FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
13722        );
13723    }
13724
13725    #[test]
13726    fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
13727        let archive = write_archive(
13728            &[RegularFile::new("metadata-cap.txt", b"payload")],
13729            &master_key(),
13730            single_stream_options(),
13731        )
13732        .unwrap();
13733        let options = NonSeekableReaderOptions {
13734            max_retained_metadata_bytes: 1,
13735            ..NonSeekableReaderOptions::default()
13736        };
13737
13738        assert_eq!(
13739            verify_non_seekable_stream_with_options(
13740                std::io::Cursor::new(archive.bytes),
13741                &master_key(),
13742                options
13743            )
13744            .unwrap_err(),
13745            FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
13746        );
13747    }
13748
13749    #[test]
13750    fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
13751        let archive = write_archive(
13752            &[RegularFile::new("metadata-erasure.txt", b"payload")],
13753            &master_key(),
13754            single_stream_options(),
13755        )
13756        .unwrap();
13757        let mut corrupted = archive.bytes;
13758        let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
13759        corrupt_block_record_payload_at_slot(&mut corrupted, slot);
13760
13761        let report =
13762            verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
13763
13764        assert_eq!(report.file_count, 1);
13765    }
13766
13767    #[test]
13768    fn live_non_seekable_verify_rejects_member_count_above_cap() {
13769        let archive = write_archive(
13770            &[RegularFile::new("member-cap.txt", b"payload")],
13771            &master_key(),
13772            single_stream_options(),
13773        )
13774        .unwrap();
13775        let options = NonSeekableReaderOptions {
13776            max_streamed_member_count: 0,
13777            ..NonSeekableReaderOptions::default()
13778        };
13779
13780        assert_eq!(
13781            verify_non_seekable_stream_with_options(
13782                std::io::Cursor::new(archive.bytes),
13783                &master_key(),
13784                options
13785            )
13786            .unwrap_err(),
13787            FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
13788        );
13789    }
13790
13791    #[test]
13792    fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
13793        let archive = write_archive(
13794            &[RegularFile::new("live-total-cap.txt", b"payload")],
13795            &master_key(),
13796            single_stream_options(),
13797        )
13798        .unwrap();
13799        let mut options = NonSeekableReaderOptions::default();
13800        options.reader.max_total_extraction_size = 3;
13801
13802        assert_eq!(
13803            verify_non_seekable_stream_with_options(
13804                std::io::Cursor::new(archive.bytes),
13805                &master_key(),
13806                options
13807            )
13808            .unwrap_err(),
13809            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13810        );
13811    }
13812
13813    #[test]
13814    fn live_non_seekable_verify_reports_root_auth_wire_only() {
13815        let archive = write_archive_with_root_auth(
13816            &[RegularFile::new("signed-live.txt", b"root-auth stream")],
13817            &master_key(),
13818            single_stream_options(),
13819            test_root_auth_config(),
13820            |request| Ok(test_root_auth_value(request)),
13821        )
13822        .unwrap();
13823
13824        let report =
13825            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13826
13827        assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
13828    }
13829
13830    #[test]
13831    fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
13832        let archive = write_archive(
13833            &[
13834                RegularFile::new("alpha.txt", b"alpha"),
13835                RegularFile::new("nested/beta.txt", b"beta"),
13836            ],
13837            &master_key(),
13838            single_stream_options(),
13839        )
13840        .unwrap();
13841        let tmp = tempfile::tempdir().unwrap();
13842        let out = tmp.path().join("out");
13843
13844        let report = extract_non_seekable_stream_to_dir(
13845            std::io::Cursor::new(archive.bytes),
13846            &master_key(),
13847            &out,
13848            NonSeekableReaderOptions::default(),
13849            SafeExtractionOptions::default(),
13850        )
13851        .unwrap();
13852
13853        assert_eq!(report.verification.file_count, 2);
13854        assert_eq!(report.extracted_member_count, 2);
13855        assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
13856        assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
13857    }
13858
13859    #[test]
13860    fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
13861        let archive = write_archive(
13862            &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
13863            &master_key(),
13864            single_stream_options(),
13865        )
13866        .unwrap();
13867        let tmp = tempfile::tempdir().unwrap();
13868        let out = tmp.path().join("out");
13869
13870        extract_non_seekable_stream_to_dir(
13871            ChunkedReader::new(archive.bytes, 1),
13872            &master_key(),
13873            &out,
13874            NonSeekableReaderOptions::default(),
13875            SafeExtractionOptions::default(),
13876        )
13877        .unwrap();
13878
13879        assert_eq!(
13880            fs::read(out.join("tiny-extract.txt")).unwrap(),
13881            b"chunked extraction"
13882        );
13883    }
13884
13885    #[test]
13886    fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
13887        let archive = write_archive(
13888            &[RegularFile::new("late-fail.txt", b"must remain staged")],
13889            &master_key(),
13890            single_stream_options(),
13891        )
13892        .unwrap();
13893        let mut corrupted = archive.bytes;
13894        corrupt_v41_terminal_recovery(&mut corrupted);
13895        let tmp = tempfile::tempdir().unwrap();
13896        let out = tmp.path().join("out");
13897
13898        match extract_non_seekable_stream_to_dir(
13899            std::io::Cursor::new(corrupted),
13900            &master_key(),
13901            &out,
13902            NonSeekableReaderOptions::default(),
13903            SafeExtractionOptions::default(),
13904        )
13905        .unwrap_err()
13906        {
13907            ExtractError::Format(err) => assert_eq!(
13908                err,
13909                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13910            ),
13911            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
13912        }
13913        assert!(!out.exists());
13914    }
13915
13916    #[test]
13917    fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
13918        let archive = write_archive(
13919            &[RegularFile::new("same.txt", b"new")],
13920            &master_key(),
13921            single_stream_options(),
13922        )
13923        .unwrap();
13924        let tmp = tempfile::tempdir().unwrap();
13925        let out = tmp.path().join("out");
13926        fs::create_dir(&out).unwrap();
13927        fs::write(out.join("same.txt"), b"old").unwrap();
13928
13929        match extract_non_seekable_stream_to_dir(
13930            std::io::Cursor::new(archive.bytes.clone()),
13931            &master_key(),
13932            &out,
13933            NonSeekableReaderOptions::default(),
13934            SafeExtractionOptions::default(),
13935        )
13936        .unwrap_err()
13937        {
13938            ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
13939            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
13940        }
13941        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
13942
13943        extract_non_seekable_stream_to_dir(
13944            std::io::Cursor::new(archive.bytes),
13945            &master_key(),
13946            &out,
13947            NonSeekableReaderOptions::default(),
13948            SafeExtractionOptions {
13949                overwrite_existing: true,
13950            },
13951        )
13952        .unwrap();
13953        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
13954    }
13955
13956    #[test]
13957    fn live_non_seekable_list_stream_matches_seekable_final_view() {
13958        let archive = write_archive(
13959            &[
13960                RegularFile::new("a.txt", b"a"),
13961                RegularFile::new("b.txt", b"bb"),
13962            ],
13963            &master_key(),
13964            single_stream_options(),
13965        )
13966        .unwrap();
13967        let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
13968        let expected = seekable.list_files().unwrap();
13969
13970        let report = list_non_seekable_stream(
13971            std::io::Cursor::new(archive.bytes),
13972            &master_key(),
13973            NonSeekableReaderOptions::default(),
13974        )
13975        .unwrap();
13976
13977        assert_eq!(report.verification.file_count, 2);
13978        assert_eq!(report.entries, expected);
13979    }
13980
13981    #[test]
13982    fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
13983        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13984        let mut bad_flags = archive.bootstrap_sidecar.clone();
13985        rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
13986            header.flags |= 0x08;
13987        });
13988        assert_eq!(
13989            open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
13990                .unwrap_err(),
13991            FormatError::UnknownBootstrapSidecarFlags(0x0b)
13992        );
13993
13994        let mut trailing = archive.bootstrap_sidecar.clone();
13995        trailing.push(0);
13996        assert_eq!(
13997            open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
13998                .unwrap_err(),
13999            FormatError::NonCanonicalBootstrapSidecarLayout
14000        );
14001    }
14002
14003    #[test]
14004    fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
14005        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14006        let mut wrong_volume = archive.bootstrap_sidecar.clone();
14007        mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
14008            footer.volume_index = 1;
14009        });
14010        assert_eq!(
14011            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
14012                .unwrap_err(),
14013            FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
14014        );
14015
14016        let mut non_authoritative = archive.bootstrap_sidecar.clone();
14017        mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
14018            footer.is_authoritative = 0;
14019        });
14020        assert_eq!(
14021            open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
14022                .unwrap_err(),
14023            FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
14024        );
14025    }
14026
14027    #[test]
14028    fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
14029        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14030        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14031        let crypto_start = volume_header.crypto_header_offset as usize;
14032        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14033        let crypto_header = CryptoHeader::parse(
14034            &archive.bytes[crypto_start..crypto_end],
14035            volume_header.crypto_header_length,
14036        )
14037        .unwrap();
14038        let subkeys = Subkeys::derive(
14039            &master_key(),
14040            &volume_header.archive_uuid,
14041            &volume_header.session_id,
14042        )
14043        .unwrap();
14044        let mut opened_header = volume_header;
14045        opened_header.volume_index = 1;
14046
14047        let parsed = parse_bootstrap_sidecar(
14048            &archive.bootstrap_sidecar,
14049            &opened_header,
14050            &crypto_header.fixed,
14051            &subkeys,
14052        )
14053        .unwrap();
14054
14055        assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
14056    }
14057
14058    #[test]
14059    fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
14060        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14061        let mut conflicting = archive.bootstrap_sidecar.clone();
14062        mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
14063            footer.index_root_first_block += 1;
14064        });
14065
14066        assert_eq!(
14067            open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
14068                .unwrap_err(),
14069            FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
14070        );
14071    }
14072
14073    #[test]
14074    fn sidecar_size_cap_counts_only_present_sparse_sections() {
14075        let mut crypto_header = test_crypto_header();
14076        crypto_header.has_dictionary = 1;
14077        crypto_header.index_root_fec_data_shards = 1;
14078        crypto_header.index_root_fec_parity_shards = 0;
14079        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14080        let header = BootstrapSidecarHeader {
14081            archive_uuid: [0x31; 16],
14082            session_id: [0x42; 16],
14083            flags: 0x04,
14084            manifest_footer_offset: 0,
14085            manifest_footer_length: 0,
14086            index_root_records_offset: 0,
14087            index_root_records_length: 0,
14088            dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14089            dictionary_records_length: record_len,
14090            sidecar_hmac: [0u8; 32],
14091            header_crc32c: 0,
14092        };
14093
14094        validate_sidecar_size_cap(
14095            &header,
14096            &crypto_header,
14097            BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
14098        )
14099        .unwrap();
14100        assert_eq!(
14101            validate_sidecar_size_cap(
14102                &header,
14103                &crypto_header,
14104                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
14105            )
14106            .unwrap_err(),
14107            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14108        );
14109    }
14110
14111    #[test]
14112    fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
14113        let mut crypto_header = test_crypto_header();
14114        crypto_header.index_root_fec_data_shards = 1;
14115        crypto_header.index_root_fec_parity_shards = 0;
14116        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14117        let header = BootstrapSidecarHeader {
14118            archive_uuid: [0x31; 16],
14119            session_id: [0x42; 16],
14120            flags: 0x02,
14121            manifest_footer_offset: 0,
14122            manifest_footer_length: 0,
14123            index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14124            index_root_records_length: record_len * 2,
14125            dictionary_records_offset: 0,
14126            dictionary_records_length: 0,
14127            sidecar_hmac: [0u8; 32],
14128            header_crc32c: 0,
14129        };
14130
14131        assert_eq!(
14132            validate_sidecar_size_cap(
14133                &header,
14134                &crypto_header,
14135                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
14136            )
14137            .unwrap_err(),
14138            FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
14139        );
14140    }
14141
14142    #[test]
14143    fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
14144        let mut crypto_header = test_crypto_header();
14145        crypto_header.block_size = u32::MAX;
14146        crypto_header.index_root_fec_data_shards = u16::MAX;
14147        crypto_header.index_root_fec_parity_shards = u16::MAX;
14148        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14149        let max_records = crypto_header.index_root_fec_data_shards as u64
14150            + crypto_header.index_root_fec_parity_shards as u64;
14151        let max_section_len = max_records * record_len;
14152        let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
14153            + MANIFEST_FOOTER_LEN as u64
14154            + max_section_len
14155            + max_section_len;
14156        let header = BootstrapSidecarHeader {
14157            archive_uuid: [0x31; 16],
14158            session_id: [0x42; 16],
14159            flags: 0x01 | 0x02 | 0x04,
14160            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14161            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14162            index_root_records_offset: 0,
14163            index_root_records_length: max_section_len,
14164            dictionary_records_offset: 0,
14165            dictionary_records_length: max_section_len,
14166            sidecar_hmac: [0u8; 32],
14167            header_crc32c: 0,
14168        };
14169
14170        validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
14171        assert_eq!(
14172            validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
14173            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14174        );
14175    }
14176
14177    #[test]
14178    fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
14179        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14180        let mut with_dictionary = archive.bootstrap_sidecar.clone();
14181        let header =
14182            BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
14183                .unwrap();
14184        let record_len = sidecar_record_len(&with_dictionary);
14185        let first_record = header.index_root_records_offset as usize;
14186        let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
14187        let dictionary_offset = with_dictionary.len() as u64;
14188        with_dictionary.extend_from_slice(&copied_record);
14189        rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
14190            header.flags |= 0x04;
14191            header.dictionary_records_offset = dictionary_offset;
14192            header.dictionary_records_length = record_len as u64;
14193        });
14194
14195        assert_eq!(
14196            open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
14197                .unwrap_err(),
14198            FormatError::InvalidArchive(
14199                "bootstrap sidecar has dictionary records while has_dictionary is false"
14200            )
14201        );
14202    }
14203
14204    #[test]
14205    fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
14206        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14207        let mut missing = archive.bootstrap_sidecar.clone();
14208        let record_len = sidecar_record_len(&missing);
14209        let new_len = missing.len() - record_len;
14210        missing.truncate(new_len);
14211        rewrite_sidecar_header(&mut missing, &master_key(), |header| {
14212            header.index_root_records_length -= record_len as u64;
14213        });
14214        assert_eq!(
14215            open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
14216                .unwrap_err(),
14217            FormatError::InvalidArchive(
14218                "sidecar BlockRecord section does not match declared extent"
14219            )
14220        );
14221
14222        let mut duplicate = archive.bootstrap_sidecar.clone();
14223        mutate_sidecar_index_record(&mut duplicate, 1, |record| {
14224            record.block_index -= 1;
14225        });
14226        assert_eq!(
14227            open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
14228                .unwrap_err(),
14229            FormatError::InvalidArchive(
14230                "sidecar BlockRecord section has missing or duplicate blocks"
14231            )
14232        );
14233
14234        let mut misordered = archive.bootstrap_sidecar.clone();
14235        swap_sidecar_index_records(&mut misordered, 0, 1);
14236        assert_eq!(
14237            open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
14238                .unwrap_err(),
14239            FormatError::InvalidArchive(
14240                "sidecar BlockRecord section has missing or duplicate blocks"
14241            )
14242        );
14243
14244        let mut wrong_kind = archive.bootstrap_sidecar.clone();
14245        mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
14246            record.kind = BlockKind::PayloadData;
14247        });
14248        assert_eq!(
14249            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
14250                .unwrap_err(),
14251            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
14252        );
14253
14254        let mut wrong_last = archive.bootstrap_sidecar.clone();
14255        mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
14256            record.flags = 0;
14257        });
14258        assert_eq!(
14259            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
14260                .unwrap_err(),
14261            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
14262        );
14263    }
14264
14265    #[test]
14266    fn verify_helper_rejects_envelope_frame_coverage_gap() {
14267        let frames = BTreeMap::from([(
14268            0,
14269            FrameEntry {
14270                frame_index: 0,
14271                envelope_index: 0,
14272                offset_in_envelope: 0,
14273                compressed_size: 10,
14274                decompressed_size: 512,
14275                flags: 0,
14276                tar_stream_offset: 0,
14277            },
14278        )]);
14279        let envelopes = BTreeMap::from([(
14280            0,
14281            EnvelopeEntry {
14282                envelope_index: 0,
14283                first_block_index: 0,
14284                data_block_count: 1,
14285                parity_block_count: 1,
14286                encrypted_size: 4096,
14287                plaintext_size: 11,
14288                first_frame_index: 0,
14289                frame_count: 1,
14290            },
14291        )]);
14292
14293        assert_eq!(
14294            validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
14295            FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
14296        );
14297    }
14298
14299    #[test]
14300    fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
14301        assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
14302        assert_eq!(
14303            validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
14304            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14305        );
14306        assert_eq!(
14307            validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
14308            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14309        );
14310    }
14311
14312    #[test]
14313    fn verify_rejects_authenticated_content_hash_mismatch() {
14314        let options = WriterOptions {
14315            index_root_fec_parity_shards: 0,
14316            ..single_stream_options()
14317        };
14318        let archive = write_archive(
14319            &[RegularFile::new("content-hash.txt", b"hash covered")],
14320            &master_key(),
14321            options,
14322        )
14323        .unwrap();
14324        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14325
14326        let mut root = opened.index_root.clone();
14327        root.header.content_sha256 = [0xa5; 32];
14328        let root_plaintext = root.to_bytes();
14329        IndexRoot::parse(
14330            &root_plaintext,
14331            false,
14332            metadata_limits(&opened.crypto_header),
14333        )
14334        .unwrap();
14335        assert_eq!(
14336            root_plaintext.len() as u32,
14337            opened.manifest_footer.index_root_decompressed_size
14338        );
14339
14340        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14341        let mut next_block_index = opened.manifest_footer.index_root_first_block;
14342        let replacement = encrypt_test_object(
14343            &compressed_root,
14344            &opened.subkeys.index_root_key,
14345            &opened.subkeys.index_nonce_seed,
14346            b"idxroot",
14347            0,
14348            BlockKind::IndexRootData,
14349            &mut next_block_index,
14350            &opened.crypto_header,
14351            &opened.volume_header,
14352        );
14353        assert_eq!(
14354            replacement.extent.data_block_count,
14355            opened.manifest_footer.index_root_data_block_count
14356        );
14357        assert_eq!(
14358            replacement.extent.encrypted_size,
14359            opened.manifest_footer.index_root_encrypted_size
14360        );
14361
14362        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14363        let crypto_end = volume_header.crypto_header_offset as usize
14364            + volume_header.crypto_header_length as usize;
14365        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14366        let mut malformed = archive.bytes.clone();
14367        for record in replacement.records {
14368            let offset = crypto_end + record.block_index as usize * record_len;
14369            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14370        }
14371
14372        let reopened = open_archive(&malformed, &master_key()).unwrap();
14373        assert_eq!(
14374            reopened.verify().unwrap_err(),
14375            FormatError::InvalidArchive(
14376                "IndexRoot content_sha256 does not match decoded tar stream"
14377            )
14378        );
14379    }
14380
14381    #[test]
14382    fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
14383        let (mut path_mismatch, _) = multi_envelope_reader_fixture();
14384        rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
14385            path[0] = b'x';
14386        });
14387        assert_eq!(
14388            path_mismatch.verify().unwrap_err(),
14389            FormatError::InvalidArchive("tar member path does not match FileEntry path")
14390        );
14391
14392        let (mut size_mismatch, _) = multi_envelope_reader_fixture();
14393        rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
14394            file.file_data_size += 1;
14395        });
14396        assert_eq!(
14397            size_mismatch.verify().unwrap_err(),
14398            FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
14399        );
14400    }
14401
14402    #[test]
14403    fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
14404        let (mut opened, _) = multi_envelope_reader_fixture();
14405        let locating = opened.index_root.shards[0].clone();
14406        let mut duplicate = opened.load_index_shard(&locating).unwrap();
14407        duplicate.header.shard_index = 1;
14408        duplicate.frames[0].flags ^= 0x0000_0001;
14409        let duplicate_plaintext = duplicate.to_bytes();
14410        let mut next_block_index = opened
14411            .blocks
14412            .keys()
14413            .last()
14414            .copied()
14415            .map(|index| index + 1)
14416            .unwrap_or(0);
14417        let duplicate_object = encrypt_test_object(
14418            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14419            &opened.subkeys.index_shard_key,
14420            &opened.subkeys.index_nonce_seed,
14421            b"idxshard",
14422            1,
14423            BlockKind::IndexShardData,
14424            &mut next_block_index,
14425            &opened.crypto_header,
14426            &opened.volume_header,
14427        );
14428        insert_records(&mut opened.blocks, &duplicate_object.records);
14429        opened.index_root.shards.push(ShardEntry {
14430            shard_index: 1,
14431            first_block_index: duplicate_object.extent.first_block_index,
14432            data_block_count: duplicate_object.extent.data_block_count,
14433            parity_block_count: 0,
14434            encrypted_size: duplicate_object.extent.encrypted_size,
14435            decompressed_size: duplicate_plaintext.len() as u32,
14436            file_count: locating.file_count,
14437            first_path_hash: locating.first_path_hash,
14438            last_path_hash: locating.last_path_hash,
14439        });
14440        opened.index_root.header.file_count += locating.file_count as u64;
14441
14442        assert_eq!(
14443            opened.verify().unwrap_err(),
14444            FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
14445        );
14446    }
14447
14448    #[test]
14449    fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
14450        let (mut opened, _) = multi_envelope_reader_fixture();
14451        let locating = opened.index_root.shards[0].clone();
14452        let mut duplicate = opened.load_index_shard(&locating).unwrap();
14453        duplicate.header.shard_index = 1;
14454        duplicate.envelopes[0].first_block_index += 1;
14455        let duplicate_plaintext = duplicate.to_bytes();
14456        let mut next_block_index = opened
14457            .blocks
14458            .keys()
14459            .last()
14460            .copied()
14461            .map(|index| index + 1)
14462            .unwrap_or(0);
14463        let duplicate_object = encrypt_test_object(
14464            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14465            &opened.subkeys.index_shard_key,
14466            &opened.subkeys.index_nonce_seed,
14467            b"idxshard",
14468            1,
14469            BlockKind::IndexShardData,
14470            &mut next_block_index,
14471            &opened.crypto_header,
14472            &opened.volume_header,
14473        );
14474        insert_records(&mut opened.blocks, &duplicate_object.records);
14475        opened.index_root.shards.push(ShardEntry {
14476            shard_index: 1,
14477            first_block_index: duplicate_object.extent.first_block_index,
14478            data_block_count: duplicate_object.extent.data_block_count,
14479            parity_block_count: 0,
14480            encrypted_size: duplicate_object.extent.encrypted_size,
14481            decompressed_size: duplicate_plaintext.len() as u32,
14482            file_count: locating.file_count,
14483            first_path_hash: locating.first_path_hash,
14484            last_path_hash: locating.last_path_hash,
14485        });
14486        opened.index_root.header.file_count += locating.file_count as u64;
14487
14488        assert_eq!(
14489            opened.verify().unwrap_err(),
14490            FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
14491        );
14492    }
14493
14494    #[test]
14495    fn verify_rejects_non_contiguous_global_envelope_indexes() {
14496        let (mut opened, _) = multi_envelope_reader_fixture();
14497        replace_first_index_shard(&mut opened, |shard| {
14498            let frame = shard
14499                .frames
14500                .iter_mut()
14501                .find(|entry| entry.frame_index == 1)
14502                .unwrap();
14503            frame.envelope_index = 2;
14504
14505            let envelope = shard
14506                .envelopes
14507                .iter_mut()
14508                .find(|entry| entry.envelope_index == 1)
14509                .unwrap();
14510            envelope.envelope_index = 2;
14511        });
14512
14513        assert_eq!(
14514            opened.verify().unwrap_err(),
14515            FormatError::InvalidMetadata {
14516                structure: "EnvelopeEntry",
14517                reason: "global index coverage has a gap",
14518            }
14519        );
14520    }
14521
14522    #[test]
14523    fn verify_rejects_payload_object_extent_overlap() {
14524        let (mut opened, _) = multi_envelope_reader_fixture();
14525        replace_first_index_shard(&mut opened, |shard| {
14526            let first_block_index = shard.envelopes[0].first_block_index;
14527            shard.envelopes[1].first_block_index = first_block_index;
14528        });
14529
14530        assert_eq!(
14531            opened.verify().unwrap_err(),
14532            FormatError::InvalidArchive("encrypted object block ranges overlap")
14533        );
14534    }
14535
14536    #[test]
14537    fn verify_accepts_cross_shard_shared_envelope_frame_union() {
14538        let volume_header = test_volume_header();
14539        let crypto_header = test_crypto_header();
14540        let subkeys = Subkeys::derive(
14541            &master_key(),
14542            &volume_header.archive_uuid,
14543            &volume_header.session_id,
14544        )
14545        .unwrap();
14546        let mut next_block_index = 0u64;
14547        let mut blocks = BTreeMap::new();
14548
14549        let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
14550        let beta = test_member(b"beta.txt", b"beta cross shard\n");
14551        let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
14552        let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
14553        let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
14554        let envelope_plaintext =
14555            [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
14556        let payload = encrypt_test_object(
14557            &envelope_plaintext,
14558            &subkeys.enc_key,
14559            &subkeys.nonce_seed,
14560            b"envelope",
14561            0,
14562            BlockKind::PayloadData,
14563            &mut next_block_index,
14564            &crypto_header,
14565            &volume_header,
14566        );
14567        insert_records(&mut blocks, &payload.records);
14568
14569        let envelope = EnvelopeEntry {
14570            envelope_index: 0,
14571            first_block_index: payload.extent.first_block_index,
14572            data_block_count: payload.extent.data_block_count,
14573            parity_block_count: 0,
14574            encrypted_size: payload.extent.encrypted_size,
14575            plaintext_size: envelope_plaintext.len() as u32,
14576            first_frame_index: 0,
14577            frame_count: 2,
14578        };
14579        let frame0 = FrameEntry {
14580            frame_index: 0,
14581            envelope_index: 0,
14582            offset_in_envelope: 0,
14583            compressed_size: frame0_plaintext.len() as u32,
14584            decompressed_size: alpha.len() as u32,
14585            flags: 0x0000_0003,
14586            tar_stream_offset: 0,
14587        };
14588        let frame1 = FrameEntry {
14589            frame_index: 1,
14590            envelope_index: 0,
14591            offset_in_envelope: frame0_plaintext.len() as u32,
14592            compressed_size: frame1_plaintext.len() as u32,
14593            decompressed_size: beta.len() as u32,
14594            flags: 0x0000_0003,
14595            tar_stream_offset: alpha.len() as u64,
14596        };
14597
14598        let (shard0_plaintext, first0, last0) = build_test_index_shard(
14599            &[TestFileMeta {
14600                path: b"alpha.txt".to_vec(),
14601                frame_index: 0,
14602                tar_stream_offset: 0,
14603                member_group_size: alpha.len() as u64,
14604                file_data_size: b"alpha cross shard\n".len() as u64,
14605            }],
14606            &[frame0],
14607            std::slice::from_ref(&envelope),
14608        );
14609        let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
14610            &[TestFileMeta {
14611                path: b"beta.txt".to_vec(),
14612                frame_index: 1,
14613                tar_stream_offset: alpha.len() as u64,
14614                member_group_size: beta.len() as u64,
14615                file_data_size: b"beta cross shard\n".len() as u64,
14616            }],
14617            &[frame1],
14618            std::slice::from_ref(&envelope),
14619        );
14620        shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
14621
14622        let shard0 = encrypt_test_object(
14623            &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
14624            &subkeys.index_shard_key,
14625            &subkeys.index_nonce_seed,
14626            b"idxshard",
14627            0,
14628            BlockKind::IndexShardData,
14629            &mut next_block_index,
14630            &crypto_header,
14631            &volume_header,
14632        );
14633        let shard1 = encrypt_test_object(
14634            &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
14635            &subkeys.index_shard_key,
14636            &subkeys.index_nonce_seed,
14637            b"idxshard",
14638            1,
14639            BlockKind::IndexShardData,
14640            &mut next_block_index,
14641            &crypto_header,
14642            &volume_header,
14643        );
14644        insert_records(&mut blocks, &shard0.records);
14645        insert_records(&mut blocks, &shard1.records);
14646
14647        let index_root = IndexRoot {
14648            header: IndexRootHeader {
14649                frame_count: 2,
14650                envelope_count: 1,
14651                file_count: 2,
14652                payload_block_count: payload.extent.data_block_count as u64,
14653                tar_total_size: tar_stream.len() as u64,
14654                content_sha256: sha256_bytes(&tar_stream),
14655                ..IndexRootHeader::empty()
14656            },
14657            shards: vec![
14658                ShardEntry {
14659                    shard_index: 0,
14660                    first_block_index: shard0.extent.first_block_index,
14661                    data_block_count: shard0.extent.data_block_count,
14662                    parity_block_count: 0,
14663                    encrypted_size: shard0.extent.encrypted_size,
14664                    decompressed_size: shard0_plaintext.len() as u32,
14665                    file_count: 1,
14666                    first_path_hash: first0,
14667                    last_path_hash: last0,
14668                },
14669                ShardEntry {
14670                    shard_index: 1,
14671                    first_block_index: shard1.extent.first_block_index,
14672                    data_block_count: shard1.extent.data_block_count,
14673                    parity_block_count: 0,
14674                    encrypted_size: shard1.extent.encrypted_size,
14675                    decompressed_size: shard1_plaintext.len() as u32,
14676                    file_count: 1,
14677                    first_path_hash: first1,
14678                    last_path_hash: last1,
14679                },
14680            ],
14681            directory_hint_shards: Vec::new(),
14682        };
14683
14684        let index_root_plaintext = index_root.to_bytes();
14685        let index_root_object = encrypt_test_object(
14686            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14687            &subkeys.index_root_key,
14688            &subkeys.index_nonce_seed,
14689            b"idxroot",
14690            0,
14691            BlockKind::IndexRootData,
14692            &mut next_block_index,
14693            &crypto_header,
14694            &volume_header,
14695        );
14696        insert_records(&mut blocks, &index_root_object.records);
14697
14698        let archive_uuid = volume_header.archive_uuid;
14699        let session_id = volume_header.session_id;
14700        let opened = OpenedArchive {
14701            options: ReaderOptions::default(),
14702            observed_archive_bytes: 1_000_000,
14703            observed_volume_count: 1,
14704            subkeys,
14705            blocks,
14706            lazy_blocks: None,
14707            crypto_header_bytes: Vec::new(),
14708            volume_header,
14709            crypto_header,
14710            manifest_footer: ManifestFooter {
14711                archive_uuid,
14712                session_id,
14713                volume_index: 0,
14714                is_authoritative: 1,
14715                total_volumes: 1,
14716                index_root_first_block: index_root_object.extent.first_block_index,
14717                index_root_data_block_count: index_root_object.extent.data_block_count,
14718                index_root_parity_block_count: 0,
14719                index_root_encrypted_size: index_root_object.extent.encrypted_size,
14720                index_root_decompressed_size: index_root_plaintext.len() as u32,
14721                manifest_hmac: [0u8; 32],
14722            },
14723            volume_trailer: Some(VolumeTrailer {
14724                archive_uuid,
14725                session_id,
14726                volume_index: 0,
14727                block_count: next_block_index,
14728                bytes_written: 0,
14729                manifest_footer_offset: 0,
14730                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14731                closed_at_ns: 0,
14732                root_auth_footer_offset: 0,
14733                root_auth_footer_length: 0,
14734                root_auth_flags: 0,
14735                trailer_hmac: [0u8; 32],
14736            }),
14737            root_auth_footer: None,
14738            index_root,
14739            payload_dictionary: None,
14740        };
14741
14742        opened.verify().unwrap();
14743    }
14744
14745    #[test]
14746    fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
14747        let options = WriterOptions {
14748            index_root_fec_parity_shards: 0,
14749            ..single_stream_options()
14750        };
14751        let archive = write_archive(
14752            &[RegularFile::new("only.txt", b"only payload")],
14753            &master_key(),
14754            options,
14755        )
14756        .unwrap();
14757        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14758        assert!(opened.index_root.directory_hint_shards.is_empty());
14759
14760        let mut root = opened.index_root.clone();
14761        root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
14762        root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
14763        let root_plaintext = root.to_bytes();
14764        IndexRoot::parse(
14765            &root_plaintext,
14766            false,
14767            metadata_limits(&opened.crypto_header),
14768        )
14769        .unwrap();
14770        assert_eq!(
14771            root_plaintext.len() as u32,
14772            opened.manifest_footer.index_root_decompressed_size
14773        );
14774
14775        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14776        let mut next_block_index = opened.manifest_footer.index_root_first_block;
14777        let replacement = encrypt_test_object(
14778            &compressed_root,
14779            &opened.subkeys.index_root_key,
14780            &opened.subkeys.index_nonce_seed,
14781            b"idxroot",
14782            0,
14783            BlockKind::IndexRootData,
14784            &mut next_block_index,
14785            &opened.crypto_header,
14786            &opened.volume_header,
14787        );
14788        assert_eq!(
14789            replacement.extent.first_block_index,
14790            opened.manifest_footer.index_root_first_block
14791        );
14792        assert_eq!(
14793            replacement.extent.data_block_count,
14794            opened.manifest_footer.index_root_data_block_count
14795        );
14796        assert_eq!(
14797            replacement.extent.encrypted_size,
14798            opened.manifest_footer.index_root_encrypted_size
14799        );
14800
14801        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14802        let crypto_end = volume_header.crypto_header_offset as usize
14803            + volume_header.crypto_header_length as usize;
14804        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14805        let mut malformed = archive.bytes.clone();
14806        for record in replacement.records {
14807            let offset = crypto_end + record.block_index as usize * record_len;
14808            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14809        }
14810
14811        let reopened = open_archive(&malformed, &master_key()).unwrap();
14812        assert_eq!(
14813            reopened.index_root.header.file_count,
14814            DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
14815        );
14816        assert!(reopened.index_root.directory_hint_shards.is_empty());
14817
14818        assert_eq!(
14819            reopened.verify().unwrap_err(),
14820            FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
14821        );
14822    }
14823
14824    #[test]
14825    fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
14826        let mut map = DirectoryHintMap::new();
14827        add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
14828        add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
14829
14830        assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
14831        assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
14832        assert_eq!(
14833            map.get(b"foo/bar".as_slice()),
14834            Some(&BTreeSet::from([2, 4]))
14835        );
14836        assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
14837        assert!(!map.contains_key(b"foobar".as_slice()));
14838    }
14839
14840    #[test]
14841    fn directory_hint_validation_requires_exact_global_map() {
14842        let mut expected = DirectoryHintMap::new();
14843        add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
14844        add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
14845        let rows = sorted_directory_hint_rows(&expected);
14846        let table = directory_hint_table_from_rows(7, &rows, 2);
14847
14848        validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
14849            .unwrap();
14850
14851        let mut missing_root = expected.clone();
14852        missing_root.remove(&Vec::new());
14853        let missing_root_rows = sorted_directory_hint_rows(&missing_root);
14854        let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
14855        assert_eq!(
14856            validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
14857                .unwrap_err(),
14858            FormatError::InvalidArchive("directory hint map does not match decoded files")
14859        );
14860
14861        let mut expected_missing_directory_entry = expected.clone();
14862        expected_missing_directory_entry
14863            .get_mut(b"foo".as_slice())
14864            .unwrap()
14865            .remove(&1);
14866        assert_eq!(
14867            validate_directory_hint_tables_against_expected(
14868                std::slice::from_ref(&table),
14869                &expected_missing_directory_entry,
14870            )
14871            .unwrap_err(),
14872            FormatError::InvalidArchive("directory hint map does not match decoded files")
14873        );
14874
14875        let mut extra = expected.clone();
14876        extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
14877        let extra_rows = sorted_directory_hint_rows(&extra);
14878        let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
14879        assert_eq!(
14880            validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
14881            FormatError::InvalidArchive("directory hint map does not match decoded files")
14882        );
14883    }
14884
14885    #[test]
14886    fn directory_hint_validation_rejects_global_order_mismatch() {
14887        let mut expected = DirectoryHintMap::new();
14888        expected.insert(Vec::new(), BTreeSet::from([0]));
14889        expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
14890        let rows = sorted_directory_hint_rows(&expected);
14891        let first = directory_hint_table_from_rows(8, &rows[..1], 1);
14892        let second = directory_hint_table_from_rows(9, &rows[1..], 1);
14893
14894        assert_eq!(
14895            validate_directory_hint_tables_against_expected(&[second, first], &expected)
14896                .unwrap_err(),
14897            FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
14898        );
14899    }
14900
14901    #[test]
14902    fn object_extent_rejects_parity_above_class_cap() {
14903        let crypto_header = CryptoHeaderFixed {
14904            length: 0,
14905            compression_algo: CompressionAlgo::ZstdFramed,
14906            aead_algo: AeadAlgo::AesGcmSiv256,
14907            fec_algo: FecAlgo::ReedSolomonGF16,
14908            kdf_algo: KdfAlgo::Raw,
14909            chunk_size: 1024,
14910            envelope_target_size: 4096,
14911            block_size: 4096,
14912            fec_data_shards: 1,
14913            fec_parity_shards: 1,
14914            index_fec_data_shards: 1,
14915            index_fec_parity_shards: 1,
14916            index_root_fec_data_shards: 1,
14917            index_root_fec_parity_shards: 1,
14918            stripe_width: 1,
14919            volume_loss_tolerance: 0,
14920            bit_rot_buffer_pct: 0,
14921            has_dictionary: 0,
14922            max_path_length: 4096,
14923            expected_volume_size: 0,
14924        };
14925        let extent = ObjectExtent {
14926            first_block_index: 0,
14927            data_block_count: 1,
14928            parity_block_count: 2,
14929            encrypted_size: 4096,
14930        };
14931
14932        assert_eq!(
14933            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
14934            FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
14935        );
14936    }
14937
14938    #[test]
14939    fn object_extent_rejects_parity_below_recoverability_requirement() {
14940        let crypto_header = CryptoHeaderFixed {
14941            length: 0,
14942            compression_algo: CompressionAlgo::ZstdFramed,
14943            aead_algo: AeadAlgo::AesGcmSiv256,
14944            fec_algo: FecAlgo::ReedSolomonGF16,
14945            kdf_algo: KdfAlgo::Raw,
14946            chunk_size: 1024,
14947            envelope_target_size: 4096,
14948            block_size: 4096,
14949            fec_data_shards: 1,
14950            fec_parity_shards: 1,
14951            index_fec_data_shards: 1,
14952            index_fec_parity_shards: 1,
14953            index_root_fec_data_shards: 1,
14954            index_root_fec_parity_shards: 1,
14955            stripe_width: 2,
14956            volume_loss_tolerance: 1,
14957            bit_rot_buffer_pct: 0,
14958            has_dictionary: 0,
14959            max_path_length: 4096,
14960            expected_volume_size: 0,
14961        };
14962        let extent = ObjectExtent {
14963            first_block_index: 0,
14964            data_block_count: 1,
14965            parity_block_count: 0,
14966            encrypted_size: 4096,
14967        };
14968
14969        assert_eq!(
14970            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
14971            FormatError::InvalidArchive(
14972                "encrypted object parity does not match v41 compute_parity"
14973            )
14974        );
14975    }
14976
14977    #[test]
14978    fn encrypted_object_extent_matrix_rejects_overlaps() {
14979        let (opened, _) = multi_envelope_reader_fixture();
14980        let loaded_shard = opened
14981            .load_index_shard(&opened.index_root.shards[0])
14982            .unwrap();
14983        let base_envelopes = loaded_shard
14984            .envelopes
14985            .iter()
14986            .map(|entry| (entry.envelope_index, entry.clone()))
14987            .collect::<BTreeMap<_, _>>();
14988        let payload_start = loaded_shard.envelopes[0].first_block_index;
14989        let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
14990
14991        let mut payload_overlap = base_envelopes.clone();
14992        payload_overlap
14993            .get_mut(&loaded_shard.envelopes[1].envelope_index)
14994            .unwrap()
14995            .first_block_index = payload_start;
14996        assert_eq!(
14997            opened
14998                .validate_encrypted_object_block_ranges(&payload_overlap)
14999                .unwrap_err(),
15000            overlap
15001        );
15002
15003        let mut shard_overlap = opened.clone();
15004        let shard = shard_overlap.index_root.shards[0].clone();
15005        shard_overlap.index_root.shards.push(ShardEntry {
15006            shard_index: 1,
15007            ..shard
15008        });
15009        assert_eq!(
15010            shard_overlap
15011                .validate_encrypted_object_block_ranges(&base_envelopes)
15012                .unwrap_err(),
15013            overlap
15014        );
15015
15016        let mut dictionary_overlap = opened.clone();
15017        dictionary_overlap.crypto_header.has_dictionary = 1;
15018        dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
15019        dictionary_overlap
15020            .index_root
15021            .header
15022            .dictionary_data_block_count = 1;
15023        dictionary_overlap
15024            .index_root
15025            .header
15026            .dictionary_parity_block_count = 0;
15027        dictionary_overlap
15028            .index_root
15029            .header
15030            .dictionary_encrypted_size = 4096;
15031        dictionary_overlap
15032            .index_root
15033            .header
15034            .dictionary_decompressed_size = 128;
15035        assert_eq!(
15036            dictionary_overlap
15037                .validate_encrypted_object_block_ranges(&base_envelopes)
15038                .unwrap_err(),
15039            overlap
15040        );
15041
15042        let mut hint_overlap = opened.clone();
15043        hint_overlap
15044            .index_root
15045            .directory_hint_shards
15046            .push(DirectoryHintShardEntry {
15047                hint_shard_index: 0,
15048                first_dir_hash: [0; 8],
15049                last_dir_hash: [0; 8],
15050                first_block_index: payload_start,
15051                data_block_count: 1,
15052                parity_block_count: 0,
15053                encrypted_size: 4096,
15054                decompressed_size: 128,
15055                entry_count: 1,
15056            });
15057        assert_eq!(
15058            hint_overlap
15059                .validate_encrypted_object_block_ranges(&base_envelopes)
15060                .unwrap_err(),
15061            overlap
15062        );
15063    }
15064
15065    #[test]
15066    fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
15067        let volume_header = test_volume_header();
15068        let crypto_header = test_crypto_header();
15069        let subkeys = Subkeys::derive(
15070            &master_key(),
15071            &volume_header.archive_uuid,
15072            &volume_header.session_id,
15073        )
15074        .unwrap();
15075        let mut next_block_index = 0u64;
15076
15077        let index_root_payload = b"index root metadata object";
15078        let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
15079        assert_metadata_object_from_compressed(
15080            &{
15081                let mut bytes = index_root_compressed.clone();
15082                bytes.push(0);
15083                bytes
15084            },
15085            index_root_payload.len(),
15086            &subkeys,
15087            &volume_header,
15088            &crypto_header,
15089            &subkeys.index_root_key,
15090            &subkeys.index_nonce_seed,
15091            b"idxroot",
15092            0,
15093            BlockKind::IndexRootData,
15094            BlockKind::IndexRootParity,
15095            crypto_header.index_root_fec_data_shards,
15096            crypto_header.index_root_fec_parity_shards,
15097            &mut next_block_index,
15098            FormatError::TrailingBytesAfterZstdFrame,
15099        );
15100        assert_metadata_object_from_compressed(
15101            &index_root_compressed,
15102            index_root_payload.len() + 1,
15103            &subkeys,
15104            &volume_header,
15105            &crypto_header,
15106            &subkeys.index_root_key,
15107            &subkeys.index_nonce_seed,
15108            b"idxroot",
15109            0,
15110            BlockKind::IndexRootData,
15111            BlockKind::IndexRootParity,
15112            crypto_header.index_root_fec_data_shards,
15113            crypto_header.index_root_fec_parity_shards,
15114            &mut next_block_index,
15115            FormatError::ZstdDecompressedSizeMismatch {
15116                expected: index_root_payload.len() + 1,
15117                actual: index_root_payload.len(),
15118            },
15119        );
15120
15121        let index_shard_payload = b"index shard metadata object";
15122        let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
15123        assert_metadata_object_from_compressed(
15124            &{
15125                let mut bytes = index_shard_compressed.clone();
15126                bytes.push(0);
15127                bytes
15128            },
15129            index_shard_payload.len(),
15130            &subkeys,
15131            &volume_header,
15132            &crypto_header,
15133            &subkeys.index_shard_key,
15134            &subkeys.index_nonce_seed,
15135            b"idxshard",
15136            1,
15137            BlockKind::IndexShardData,
15138            BlockKind::IndexShardParity,
15139            crypto_header.index_fec_data_shards,
15140            crypto_header.index_fec_parity_shards,
15141            &mut next_block_index,
15142            FormatError::TrailingBytesAfterZstdFrame,
15143        );
15144        assert_metadata_object_from_compressed(
15145            &index_shard_compressed,
15146            index_shard_payload.len() + 1,
15147            &subkeys,
15148            &volume_header,
15149            &crypto_header,
15150            &subkeys.index_shard_key,
15151            &subkeys.index_nonce_seed,
15152            b"idxshard",
15153            1,
15154            BlockKind::IndexShardData,
15155            BlockKind::IndexShardParity,
15156            crypto_header.index_fec_data_shards,
15157            crypto_header.index_fec_parity_shards,
15158            &mut next_block_index,
15159            FormatError::ZstdDecompressedSizeMismatch {
15160                expected: index_shard_payload.len() + 1,
15161                actual: index_shard_payload.len(),
15162            },
15163        );
15164
15165        let directory_hint_payload = b"directory hint metadata object";
15166        let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
15167        assert_metadata_object_from_compressed(
15168            &{
15169                let mut bytes = directory_hint_compressed.clone();
15170                bytes.push(0);
15171                bytes
15172            },
15173            directory_hint_payload.len(),
15174            &subkeys,
15175            &volume_header,
15176            &crypto_header,
15177            &subkeys.dir_hint_key,
15178            &subkeys.index_nonce_seed,
15179            b"dirhint",
15180            0,
15181            BlockKind::DirectoryHintData,
15182            BlockKind::DirectoryHintParity,
15183            crypto_header.index_fec_data_shards,
15184            crypto_header.index_fec_parity_shards,
15185            &mut next_block_index,
15186            FormatError::TrailingBytesAfterZstdFrame,
15187        );
15188        assert_metadata_object_from_compressed(
15189            &directory_hint_compressed,
15190            directory_hint_payload.len() + 1,
15191            &subkeys,
15192            &volume_header,
15193            &crypto_header,
15194            &subkeys.dir_hint_key,
15195            &subkeys.index_nonce_seed,
15196            b"dirhint",
15197            0,
15198            BlockKind::DirectoryHintData,
15199            BlockKind::DirectoryHintParity,
15200            crypto_header.index_fec_data_shards,
15201            crypto_header.index_fec_parity_shards,
15202            &mut next_block_index,
15203            FormatError::ZstdDecompressedSizeMismatch {
15204                expected: directory_hint_payload.len() + 1,
15205                actual: directory_hint_payload.len(),
15206            },
15207        );
15208
15209        let dictionary_payload = b"dictionary metadata object";
15210        let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
15211        assert_metadata_object_from_compressed(
15212            &{
15213                let mut bytes = dictionary_compressed.clone();
15214                bytes.push(0);
15215                bytes
15216            },
15217            dictionary_payload.len(),
15218            &subkeys,
15219            &volume_header,
15220            &crypto_header,
15221            &subkeys.dictionary_key,
15222            &subkeys.index_nonce_seed,
15223            b"dict",
15224            0,
15225            BlockKind::DictionaryData,
15226            BlockKind::DictionaryParity,
15227            crypto_header.index_root_fec_data_shards,
15228            crypto_header.index_root_fec_parity_shards,
15229            &mut next_block_index,
15230            FormatError::TrailingBytesAfterZstdFrame,
15231        );
15232        assert_metadata_object_from_compressed(
15233            &dictionary_compressed,
15234            dictionary_payload.len() + 1,
15235            &subkeys,
15236            &volume_header,
15237            &crypto_header,
15238            &subkeys.dictionary_key,
15239            &subkeys.index_nonce_seed,
15240            b"dict",
15241            0,
15242            BlockKind::DictionaryData,
15243            BlockKind::DictionaryParity,
15244            crypto_header.index_root_fec_data_shards,
15245            crypto_header.index_root_fec_parity_shards,
15246            &mut next_block_index,
15247            FormatError::ZstdDecompressedSizeMismatch {
15248                expected: dictionary_payload.len() + 1,
15249                actual: dictionary_payload.len(),
15250            },
15251        );
15252    }
15253
15254    #[test]
15255    fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
15256        let volume_header = test_volume_header();
15257        let crypto_header = test_crypto_header();
15258        let subkeys = Subkeys::derive(
15259            &master_key(),
15260            &volume_header.archive_uuid,
15261            &volume_header.session_id,
15262        )
15263        .unwrap();
15264        let mut next_block_index = 0u64;
15265
15266        let index_root_payload = b"index root metadata object";
15267        let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
15268            index_root_payload,
15269            &subkeys,
15270            &volume_header,
15271            &crypto_header,
15272            &subkeys.index_root_key,
15273            &subkeys.index_nonce_seed,
15274            b"idxroot",
15275            0,
15276            BlockKind::IndexRootData,
15277            &mut next_block_index,
15278        );
15279        let mut index_root_extent = index_root_extent;
15280        index_root_extent.encrypted_size = index_root_extent
15281            .encrypted_size
15282            .saturating_add(crypto_header.block_size);
15283        assert_eq!(
15284            load_metadata_object_from_parts(
15285                &index_root_records,
15286                ObjectLoadContext::index_root(
15287                    &volume_header,
15288                    &crypto_header,
15289                    &subkeys,
15290                    index_root_extent,
15291                ),
15292                index_root_payload.len() as u32,
15293            )
15294            .unwrap_err(),
15295            FormatError::InvalidArchive(
15296                "encrypted object size is not data_block_count * block_size"
15297            )
15298        );
15299
15300        let index_shard_payload = b"index shard metadata object";
15301        let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
15302            index_shard_payload,
15303            &subkeys,
15304            &volume_header,
15305            &crypto_header,
15306            &subkeys.index_shard_key,
15307            &subkeys.index_nonce_seed,
15308            b"idxshard",
15309            1,
15310            BlockKind::IndexShardData,
15311            &mut next_block_index,
15312        );
15313        let mut index_shard_extent = index_shard_extent;
15314        index_shard_extent.encrypted_size = index_shard_extent
15315            .encrypted_size
15316            .saturating_add(crypto_header.block_size);
15317        assert_eq!(
15318            load_metadata_object_from_parts(
15319                &index_shard_records,
15320                ObjectLoadContext {
15321                    volume_header: &volume_header,
15322                    crypto_header: &crypto_header,
15323                    extent: index_shard_extent,
15324                    data_kind: BlockKind::IndexShardData,
15325                    parity_kind: BlockKind::IndexShardParity,
15326                    key: &subkeys.index_shard_key,
15327                    nonce_seed: &subkeys.index_nonce_seed,
15328                    domain: b"idxshard",
15329                    counter: 1,
15330                    class_data_shard_max: crypto_header.index_fec_data_shards,
15331                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
15332                },
15333                index_shard_payload.len() as u32,
15334            )
15335            .unwrap_err(),
15336            FormatError::InvalidArchive(
15337                "encrypted object size is not data_block_count * block_size"
15338            )
15339        );
15340
15341        let directory_hint_payload = b"directory hint metadata object";
15342        let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
15343            directory_hint_payload,
15344            &subkeys,
15345            &volume_header,
15346            &crypto_header,
15347            &subkeys.dir_hint_key,
15348            &subkeys.index_nonce_seed,
15349            b"dirhint",
15350            0,
15351            BlockKind::DirectoryHintData,
15352            &mut next_block_index,
15353        );
15354        let mut directory_hint_extent = directory_hint_extent;
15355        directory_hint_extent.encrypted_size = directory_hint_extent
15356            .encrypted_size
15357            .saturating_add(crypto_header.block_size);
15358        assert_eq!(
15359            load_metadata_object_from_parts(
15360                &directory_hint_records,
15361                ObjectLoadContext {
15362                    volume_header: &volume_header,
15363                    crypto_header: &crypto_header,
15364                    extent: directory_hint_extent,
15365                    data_kind: BlockKind::DirectoryHintData,
15366                    parity_kind: BlockKind::DirectoryHintParity,
15367                    key: &subkeys.dir_hint_key,
15368                    nonce_seed: &subkeys.index_nonce_seed,
15369                    domain: b"dirhint",
15370                    counter: 0,
15371                    class_data_shard_max: crypto_header.index_fec_data_shards,
15372                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
15373                },
15374                directory_hint_payload.len() as u32,
15375            )
15376            .unwrap_err(),
15377            FormatError::InvalidArchive(
15378                "encrypted object size is not data_block_count * block_size"
15379            )
15380        );
15381
15382        let dictionary_payload = b"dictionary metadata object";
15383        let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
15384            dictionary_payload,
15385            &subkeys,
15386            &volume_header,
15387            &crypto_header,
15388            &subkeys.dictionary_key,
15389            &subkeys.index_nonce_seed,
15390            b"dict",
15391            0,
15392            BlockKind::DictionaryData,
15393            &mut next_block_index,
15394        );
15395        let mut dictionary_extent = dictionary_extent;
15396        dictionary_extent.encrypted_size = dictionary_extent
15397            .encrypted_size
15398            .saturating_add(crypto_header.block_size);
15399        assert_eq!(
15400            load_metadata_object_from_parts(
15401                &dictionary_records,
15402                ObjectLoadContext {
15403                    volume_header: &volume_header,
15404                    crypto_header: &crypto_header,
15405                    extent: dictionary_extent,
15406                    data_kind: BlockKind::DictionaryData,
15407                    parity_kind: BlockKind::DictionaryParity,
15408                    key: &subkeys.dictionary_key,
15409                    nonce_seed: &subkeys.index_nonce_seed,
15410                    domain: b"dict",
15411                    counter: 0,
15412                    class_data_shard_max: crypto_header.index_root_fec_data_shards,
15413                    class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
15414                },
15415                dictionary_payload.len() as u32,
15416            )
15417            .unwrap_err(),
15418            FormatError::InvalidArchive(
15419                "encrypted object size is not data_block_count * block_size"
15420            )
15421        );
15422    }
15423
15424    #[test]
15425    fn opens_complete_multi_volume_archive() {
15426        let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
15427        let archive = write_archive(
15428            &files,
15429            &master_key(),
15430            WriterOptions {
15431                stripe_width: 2,
15432                volume_loss_tolerance: 1,
15433                ..single_stream_options()
15434            },
15435        )
15436        .unwrap();
15437        assert_eq!(archive.volumes.len(), 2);
15438
15439        let volume_refs = archive
15440            .volumes
15441            .iter()
15442            .map(Vec::as_slice)
15443            .collect::<Vec<_>>();
15444        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15445
15446        assert_eq!(opened.volume_header.stripe_width, 2);
15447        assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
15448        assert_eq!(
15449            opened.extract_file("alpha.txt").unwrap(),
15450            Some(b"hello from volume stripes".to_vec())
15451        );
15452        opened.verify().unwrap();
15453    }
15454
15455    #[test]
15456    fn recovers_from_one_missing_volume_when_parity_allows() {
15457        let files = [RegularFile::new("alpha.txt", b"recover me")];
15458        let archive = write_archive(
15459            &files,
15460            &master_key(),
15461            WriterOptions {
15462                stripe_width: 2,
15463                volume_loss_tolerance: 1,
15464                ..single_stream_options()
15465            },
15466        )
15467        .unwrap();
15468
15469        let recovered =
15470            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
15471        assert_eq!(
15472            recovered.extract_file("alpha.txt").unwrap(),
15473            Some(b"recover me".to_vec())
15474        );
15475        recovered.verify().unwrap();
15476    }
15477
15478    #[test]
15479    fn recovers_from_crc_corrupted_block_when_parity_allows() {
15480        let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
15481        let archive = write_archive(
15482            &files,
15483            &master_key(),
15484            WriterOptions {
15485                stripe_width: 2,
15486                volume_loss_tolerance: 1,
15487                ..single_stream_options()
15488            },
15489        )
15490        .unwrap();
15491        let mut volumes = archive.volumes.clone();
15492        corrupt_first_block_record_payload(&mut volumes[0]);
15493
15494        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15495        let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15496
15497        assert_eq!(
15498            recovered.extract_file("alpha.txt").unwrap(),
15499            Some(b"repair corrupt block".to_vec())
15500        );
15501        recovered.verify().unwrap();
15502    }
15503
15504    #[test]
15505    fn rejects_multi_volume_count_mismatch_without_tolerance() {
15506        let files = [RegularFile::new("alpha.txt", b"count check")];
15507        let archive = write_archive(
15508            &files,
15509            &master_key(),
15510            WriterOptions {
15511                stripe_width: 3,
15512                volume_loss_tolerance: 0,
15513                ..single_stream_options()
15514            },
15515        )
15516        .unwrap();
15517
15518        assert_eq!(
15519            open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
15520            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15521        );
15522    }
15523
15524    #[test]
15525    fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
15526        let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
15527        let archive = write_archive(
15528            &files,
15529            &master_key(),
15530            WriterOptions {
15531                stripe_width: 2,
15532                volume_loss_tolerance: 1,
15533                ..single_stream_options()
15534            },
15535        )
15536        .unwrap();
15537
15538        let mut bad_first = archive.volumes[0].clone();
15539        rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
15540            footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
15541        });
15542
15543        open_archive_volumes(
15544            &[bad_first.as_slice(), archive.volumes[1].as_slice()],
15545            &master_key(),
15546        )
15547        .unwrap();
15548    }
15549
15550    #[test]
15551    fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
15552        let files = [RegularFile::new("alpha.txt", b"repair meta root")];
15553        let archive = write_archive(
15554            &files,
15555            &master_key(),
15556            WriterOptions {
15557                stripe_width: 2,
15558                volume_loss_tolerance: 1,
15559                ..single_stream_options()
15560            },
15561        )
15562        .unwrap();
15563        let mut volumes = archive.volumes.clone();
15564
15565        let mut corrupted = false;
15566        for volume in &mut volumes {
15567            if let Some(slot) =
15568                block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
15569            {
15570                corrupt_block_record_payload_at_slot(volume, *slot);
15571                corrupted = true;
15572                break;
15573            }
15574        }
15575        assert!(corrupted, "expected an IndexRootData record");
15576
15577        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15578        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15579        assert_eq!(
15580            opened.extract_file("alpha.txt").unwrap(),
15581            Some(b"repair meta root".to_vec())
15582        );
15583        opened.verify().unwrap();
15584    }
15585
15586    #[test]
15587    fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
15588        let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
15589        let archive = write_archive(
15590            &files,
15591            &master_key(),
15592            WriterOptions {
15593                stripe_width: 2,
15594                volume_loss_tolerance: 1,
15595                ..single_stream_options()
15596            },
15597        )
15598        .unwrap();
15599        let mut volumes = archive.volumes.clone();
15600
15601        let mut corrupted = false;
15602        for volume in &mut volumes {
15603            if let Some(slot) =
15604                block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
15605            {
15606                corrupt_block_record_payload_at_slot(volume, *slot);
15607                corrupted = true;
15608                break;
15609            }
15610        }
15611        assert!(corrupted, "expected an IndexShardData record");
15612
15613        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15614        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15615        assert_eq!(
15616            opened.extract_file("alpha.txt").unwrap(),
15617            Some(b"repair meta shard".to_vec())
15618        );
15619        opened.verify().unwrap();
15620    }
15621
15622    #[test]
15623    fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
15624        let files = [RegularFile::new(
15625            "alpha.txt",
15626            b"bitrot parity is not volume loss",
15627        )];
15628        let archive = write_archive(
15629            &files,
15630            &master_key(),
15631            WriterOptions {
15632                stripe_width: 2,
15633                volume_loss_tolerance: 0,
15634                bit_rot_buffer_pct: 1,
15635                ..single_stream_options()
15636            },
15637        )
15638        .unwrap();
15639
15640        assert_eq!(
15641            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
15642            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15643        );
15644    }
15645
15646    #[test]
15647    fn repairs_crc_erasure_only_within_parity_budget() {
15648        let payload = pseudo_random_bytes(12_000);
15649        let archive = write_archive(
15650            &[RegularFile::new("rot.bin", &payload)],
15651            &master_key(),
15652            small_block_recovery_options(),
15653        )
15654        .unwrap();
15655        let payload_slots = first_payload_data_run_slots(&archive.bytes);
15656        assert!(
15657            payload_slots.len() >= 2,
15658            "fixture must contain a multi-block payload object"
15659        );
15660
15661        let mut one_erasure = archive.bytes.clone();
15662        corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
15663        let repaired = open_archive(&one_erasure, &master_key()).unwrap();
15664        assert_eq!(
15665            repaired.extract_file("rot.bin").unwrap(),
15666            Some(payload.clone())
15667        );
15668
15669        let mut two_erasures = archive.bytes.clone();
15670        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
15671        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
15672        let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
15673        assert_eq!(
15674            unrepaired.extract_file("rot.bin").unwrap_err(),
15675            FormatError::FecTooFewAvailableShards
15676        );
15677    }
15678
15679    #[test]
15680    fn verify_rejects_missing_required_object_block_extent() {
15681        let (mut opened, missing_block) = multi_envelope_reader_fixture();
15682        assert!(opened.blocks.remove(&missing_block).is_some());
15683
15684        assert_eq!(
15685            opened.verify().unwrap_err(),
15686            FormatError::FecTooFewAvailableShards
15687        );
15688    }
15689
15690    #[test]
15691    fn parity_crc_erasure_does_not_hide_authenticated_data() {
15692        let payload = pseudo_random_bytes(12_000);
15693        let archive = write_archive(
15694            &[RegularFile::new("parity-erasure.bin", &payload)],
15695            &master_key(),
15696            parity_rich_recovery_options(),
15697        )
15698        .unwrap();
15699        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15700        let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
15701        assert!(
15702            parity_slots.len() >= 2,
15703            "fixture must contain redundant parity shards"
15704        );
15705        let mut corrupted = archive.bytes;
15706        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15707        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
15708
15709        let opened = open_archive(&corrupted, &master_key()).unwrap();
15710        assert_eq!(
15711            opened.extract_file("parity-erasure.bin").unwrap(),
15712            Some(payload)
15713        );
15714        opened.verify().unwrap();
15715    }
15716
15717    #[test]
15718    fn repair_patches_restore_crc_erased_payload_block() {
15719        let payload = pseudo_random_bytes(12_000);
15720        let archive = write_archive(
15721            &[RegularFile::new("rot.bin", &payload)],
15722            &master_key(),
15723            small_block_recovery_options(),
15724        )
15725        .unwrap();
15726        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15727        let mut corrupted = archive.bytes.clone();
15728        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15729
15730        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15731        opened.verify().unwrap();
15732        let patches = opened.repair_patches().unwrap();
15733        assert_eq!(patches.len(), 1);
15734        apply_repair_patches(&mut corrupted, &patches);
15735
15736        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15737        repaired.verify().unwrap();
15738        assert!(repaired.repair_patches().unwrap().is_empty());
15739    }
15740
15741    #[test]
15742    fn repair_patches_restore_crc_erased_payload_parity_block() {
15743        let payload = pseudo_random_bytes(12_000);
15744        let archive = write_archive(
15745            &[RegularFile::new("parity-erasure.bin", &payload)],
15746            &master_key(),
15747            parity_rich_recovery_options(),
15748        )
15749        .unwrap();
15750        let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
15751        let mut corrupted = archive.bytes.clone();
15752        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
15753
15754        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15755        opened.verify().unwrap();
15756        let patches = opened.repair_patches().unwrap();
15757        assert_eq!(patches.len(), 1);
15758        apply_repair_patches(&mut corrupted, &patches);
15759
15760        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15761        repaired.verify().unwrap();
15762        assert!(repaired.repair_patches().unwrap().is_empty());
15763    }
15764
15765    #[test]
15766    fn recovers_physical_odd_block_size_from_cmra_authority() {
15767        let archive = write_archive(
15768            &[RegularFile::new("odd-block.txt", b"payload")],
15769            &master_key(),
15770            small_block_recovery_options(),
15771        )
15772        .unwrap();
15773        let mut malformed = archive.bytes;
15774        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
15775        let block_size_offset = volume_header.crypto_header_offset as usize + 24;
15776        malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
15777
15778        let opened = open_archive(&malformed, &master_key()).unwrap();
15779        assert_ne!(opened.crypto_header.block_size, 4097);
15780        assert_eq!(
15781            opened.extract_file("odd-block.txt").unwrap(),
15782            Some(b"payload".to_vec())
15783        );
15784        opened.verify().unwrap();
15785    }
15786
15787    #[test]
15788    fn repairs_structurally_malformed_payload_block_slots() {
15789        let payload = pseudo_random_bytes(12_000);
15790        let archive = write_archive(
15791            &[RegularFile::new("structural-block.bin", &payload)],
15792            &master_key(),
15793            small_block_recovery_options(),
15794        )
15795        .unwrap();
15796        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15797
15798        let mut bad_magic = archive.bytes.clone();
15799        corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
15800        assert_eq!(
15801            open_archive(&bad_magic, &master_key())
15802                .unwrap()
15803                .extract_file("structural-block.bin")
15804                .unwrap(),
15805            Some(payload.clone())
15806        );
15807
15808        let mut bad_reserved = archive.bytes;
15809        corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
15810        assert_eq!(
15811            open_archive(&bad_reserved, &master_key())
15812                .unwrap()
15813                .extract_file("structural-block.bin")
15814                .unwrap(),
15815            Some(payload)
15816        );
15817    }
15818
15819    #[test]
15820    fn repair_patches_restore_structurally_malformed_payload_block_slot() {
15821        let payload = pseudo_random_bytes(12_000);
15822        let archive = write_archive(
15823            &[RegularFile::new("structural-patch.bin", &payload)],
15824            &master_key(),
15825            small_block_recovery_options(),
15826        )
15827        .unwrap();
15828        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15829        let mut corrupted = archive.bytes.clone();
15830        corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
15831
15832        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15833        opened.verify().unwrap();
15834        assert_eq!(
15835            opened.extract_file("structural-patch.bin").unwrap(),
15836            Some(payload)
15837        );
15838        let patches = opened.repair_patches().unwrap();
15839        assert_eq!(patches.len(), 1);
15840        apply_repair_patches(&mut corrupted, &patches);
15841
15842        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15843        repaired.verify().unwrap();
15844        assert!(repaired.repair_patches().unwrap().is_empty());
15845    }
15846
15847    #[test]
15848    fn repairs_structurally_malformed_index_root_block_slot() {
15849        let archive = write_archive(
15850            &[RegularFile::new(
15851                "structural-index-root.txt",
15852                b"metadata repair",
15853            )],
15854            &master_key(),
15855            small_block_recovery_options(),
15856        )
15857        .unwrap();
15858        let index_root_slot =
15859            first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
15860        let mut corrupted = archive.bytes;
15861        corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
15862
15863        let opened = open_archive(&corrupted, &master_key()).unwrap();
15864        assert_eq!(
15865            opened.extract_file("structural-index-root.txt").unwrap(),
15866            Some(b"metadata repair".to_vec())
15867        );
15868        opened.verify().unwrap();
15869    }
15870
15871    #[test]
15872    fn rejects_parity_block_with_last_data_flag() {
15873        let archive = write_archive(
15874            &[RegularFile::new("parity-flag.txt", b"payload")],
15875            &master_key(),
15876            small_block_recovery_options(),
15877        )
15878        .unwrap();
15879        let parity_slot =
15880            first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
15881        let mut malformed = archive.bytes;
15882        mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
15883            record.flags = 0x01;
15884        });
15885
15886        assert_eq!(
15887            open_archive(&malformed, &master_key()).unwrap_err(),
15888            FormatError::ParityBlockHasLastDataFlag
15889        );
15890    }
15891
15892    #[test]
15893    fn rejects_missing_and_duplicate_payload_last_data_flags() {
15894        let payload = pseudo_random_bytes(12_000);
15895        let archive = write_archive(
15896            &[RegularFile::new("flags.bin", &payload)],
15897            &master_key(),
15898            small_block_recovery_options(),
15899        )
15900        .unwrap();
15901        let payload_slots = first_payload_data_run_slots(&archive.bytes);
15902        assert!(
15903            payload_slots.len() >= 2,
15904            "fixture must contain a multi-block payload object"
15905        );
15906
15907        let mut duplicate_last = archive.bytes.clone();
15908        mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
15909            record.flags = 0x01;
15910        });
15911        let opened = open_archive(&duplicate_last, &master_key()).unwrap();
15912        assert_eq!(
15913            opened.extract_file("flags.bin").unwrap_err(),
15914            FormatError::InvalidArchive("object last-data flag is not on the final data block")
15915        );
15916
15917        let mut missing_last = archive.bytes;
15918        mutate_block_record_at_slot(
15919            &mut missing_last,
15920            *payload_slots.last().unwrap(),
15921            |record| {
15922                record.flags = 0;
15923            },
15924        );
15925        let opened = open_archive(&missing_last, &master_key()).unwrap();
15926        assert_eq!(
15927            opened.extract_file("flags.bin").unwrap_err(),
15928            FormatError::InvalidArchive("object last-data flag is not on the final data block")
15929        );
15930    }
15931
15932    #[test]
15933    fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
15934        let files = [RegularFile::new(
15935            "footer-copy.txt",
15936            b"survives one bad footer",
15937        )];
15938        let archive = write_archive(
15939            &files,
15940            &master_key(),
15941            WriterOptions {
15942                stripe_width: 2,
15943                volume_loss_tolerance: 1,
15944                ..single_stream_options()
15945            },
15946        )
15947        .unwrap();
15948        let mut volumes = archive.volumes.clone();
15949        corrupt_manifest_footer_hmac(&mut volumes[0]);
15950
15951        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15952        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15953        assert_eq!(opened.manifest_footer.volume_index, 0);
15954        assert_eq!(opened.volume_header.volume_index, 0);
15955        assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
15956        assert_eq!(
15957            opened.extract_file("footer-copy.txt").unwrap(),
15958            Some(b"survives one bad footer".to_vec())
15959        );
15960        opened.verify().unwrap();
15961    }
15962
15963    #[test]
15964    fn manifest_footer_corruption_requires_trusted_sidecar() {
15965        let archive = write_archive(
15966            &[RegularFile::new("footer.txt", b"sidecar authority")],
15967            &master_key(),
15968            single_stream_options(),
15969        )
15970        .unwrap();
15971        let manifest_offset = terminal_material_offset(&archive.bytes);
15972        let mut corrupted = archive.bytes.clone();
15973        corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
15974        corrupt_v41_terminal_recovery(&mut corrupted);
15975
15976        assert!(open_archive(&corrupted, &master_key()).is_err());
15977
15978        let opened =
15979            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
15980                .unwrap();
15981        assert!(opened.volume_trailer.is_none());
15982        assert_eq!(
15983            opened.extract_file("footer.txt").unwrap(),
15984            Some(b"sidecar authority".to_vec())
15985        );
15986        opened.verify().unwrap();
15987    }
15988
15989    #[test]
15990    fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
15991        let archive = write_archive(
15992            &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
15993            &master_key(),
15994            single_stream_options(),
15995        )
15996        .unwrap();
15997        let strict_options = ReaderOptions {
15998            max_trailing_garbage_scan: 0,
15999            ..ReaderOptions::default()
16000        };
16001
16002        let manifest_offset = terminal_material_offset(&archive.bytes);
16003        for offset in [
16004            manifest_offset + 71,
16005            manifest_offset + MANIFEST_HMAC_COVERED_LEN,
16006        ] {
16007            let mut corrupted = archive.bytes.clone();
16008            corrupted[offset] ^= 0x01;
16009            open_archive(&corrupted, &master_key()).unwrap();
16010        }
16011
16012        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16013        for offset in [
16014            trailer_offset + 75,
16015            trailer_offset + TRAILER_HMAC_COVERED_LEN,
16016        ] {
16017            let mut corrupted = archive.bytes.clone();
16018            corrupted[offset] ^= 0x01;
16019            OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
16020        }
16021
16022        let mut covered_sidecar = archive.bootstrap_sidecar.clone();
16023        let mut header =
16024            BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16025                .unwrap();
16026        header.manifest_footer_offset += 1;
16027        covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16028        assert_eq!(
16029            open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
16030                .unwrap_err(),
16031            FormatError::HmacMismatch {
16032                structure: "BootstrapSidecarHeader"
16033            }
16034        );
16035
16036        let mut tag_sidecar = archive.bootstrap_sidecar.clone();
16037        let mut header =
16038            BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
16039        header.sidecar_hmac[0] ^= 1;
16040        tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16041        assert_eq!(
16042            open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
16043                .unwrap_err(),
16044            FormatError::HmacMismatch {
16045                structure: "BootstrapSidecarHeader"
16046            }
16047        );
16048
16049        let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
16050        let header =
16051            BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16052                .unwrap();
16053        let mut header_bytes = header.to_bytes();
16054        header_bytes[124] ^= 0x01;
16055        let crc = crc32c::crc32c(&header_bytes[..124]);
16056        header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
16057        non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
16058        let opened = open_archive_with_bootstrap_sidecar(
16059            &archive.bytes,
16060            &non_covered_sidecar,
16061            &master_key(),
16062        )
16063        .unwrap();
16064        assert_eq!(
16065            opened.extract_file("hmac-boundary.txt").unwrap(),
16066            Some(b"boundary bytes".to_vec())
16067        );
16068    }
16069
16070    #[test]
16071    fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
16072        let archive = write_archive(
16073            &[RegularFile::new("volume-index.txt", b"identity")],
16074            &master_key(),
16075            single_stream_options(),
16076        )
16077        .unwrap();
16078
16079        let mut bad_trailer = archive.bytes.clone();
16080        rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
16081            trailer.volume_index = 1;
16082        });
16083        open_archive(&bad_trailer, &master_key()).unwrap();
16084
16085        let mut bad_manifest = archive.bytes;
16086        rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
16087            footer.volume_index = 1;
16088        });
16089        open_archive(&bad_manifest, &master_key()).unwrap();
16090    }
16091
16092    #[test]
16093    fn rejects_same_key_header_terminal_material_splice() {
16094        let first = write_archive(
16095            &[RegularFile::new("splice.txt", b"same shape")],
16096            &master_key(),
16097            single_stream_options(),
16098        )
16099        .unwrap();
16100        let second = write_archive(
16101            &[RegularFile::new("splice.txt", b"same shape")],
16102            &master_key(),
16103            single_stream_options(),
16104        )
16105        .unwrap();
16106        assert_ne!(first.archive_uuid, second.archive_uuid);
16107        assert_eq!(
16108            terminal_material_offset(&first.bytes),
16109            terminal_material_offset(&second.bytes)
16110        );
16111        assert_eq!(first.bytes.len(), second.bytes.len());
16112
16113        let terminal_offset = terminal_material_offset(&first.bytes);
16114        let mut spliced = first.bytes.clone();
16115        spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
16116
16117        assert_eq!(
16118            open_archive(&spliced, &master_key()).unwrap_err(),
16119            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16120        );
16121    }
16122
16123    #[test]
16124    fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
16125        let kdf_params = crate::crypto::KdfParams::Argon2id {
16126            t_cost: 1,
16127            m_cost_kib: 8,
16128            parallelism: 1,
16129            salt: b"0123456789abcdef".to_vec(),
16130        };
16131        let archive = write_archive_with_kdf(
16132            &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
16133            &master_key(),
16134            single_stream_options(),
16135            &kdf_params,
16136        )
16137        .unwrap();
16138        let mut mutated = archive.bytes.clone();
16139        let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
16140        let subkeys = Subkeys::derive(
16141            &master_key(),
16142            &volume_header.archive_uuid,
16143            &volume_header.session_id,
16144        )
16145        .unwrap();
16146
16147        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
16148            let crypto_region = image
16149                .regions
16150                .iter_mut()
16151                .find(|region| region.region_type == 2)
16152                .unwrap();
16153            let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
16154            let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
16155            crypto_region.bytes[salt_start] ^= 0x01;
16156            let hmac = compute_hmac(
16157                HmacDomain::CryptoHeader,
16158                &subkeys.mac_key,
16159                &volume_header.archive_uuid,
16160                &volume_header.session_id,
16161                &crypto_region.bytes[..hmac_offset],
16162            );
16163            crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
16164        });
16165
16166        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16167        let locator = final_recovery_locator(&mutated);
16168        let crypto_start = volume_header.crypto_header_offset as usize;
16169        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16170        let parsed_crypto = CryptoHeader::parse(
16171            &mutated[crypto_start..crypto_end],
16172            volume_header.crypto_header_length,
16173        )
16174        .unwrap();
16175        assert_eq!(
16176            parse_locator_cmra_candidate(
16177                &mutated,
16178                final_offset,
16179                locator,
16180                KeyHoldingTerminalContext {
16181                    subkeys: &subkeys,
16182                    volume_header: &volume_header,
16183                    crypto_header: &parsed_crypto.fixed,
16184                    crypto_header_bytes: &mutated[crypto_start..crypto_end],
16185                },
16186            )
16187            .unwrap_err(),
16188            FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
16189        );
16190        assert!(open_archive(&mutated, &master_key()).is_err());
16191    }
16192
16193    #[test]
16194    fn recovers_physical_crypto_header_splice_from_cmra_authority() {
16195        let base = WriterOptions {
16196            archive_uuid: Some([0x11; 16]),
16197            session_id: Some([0x22; 16]),
16198            ..small_block_recovery_options()
16199        };
16200        let same_archive = WriterOptions {
16201            archive_uuid: Some([0x11; 16]),
16202            session_id: Some([0x33; 16]),
16203            ..small_block_recovery_options()
16204        };
16205
16206        let first = write_archive(
16207            &[RegularFile::new("splice.txt", b"same shape")],
16208            &master_key(),
16209            base,
16210        )
16211        .unwrap();
16212        let second = write_archive(
16213            &[RegularFile::new("splice.txt", b"same shape")],
16214            &master_key(),
16215            same_archive,
16216        )
16217        .unwrap();
16218
16219        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16220        let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
16221        let crypto_start = volume_header.crypto_header_offset as usize;
16222        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16223        let second_crypto_end = second_volume_header.crypto_header_offset as usize
16224            + second_volume_header.crypto_header_length as usize;
16225        assert_eq!(crypto_end, second_crypto_end);
16226
16227        let mut spliced = first.bytes.clone();
16228        spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
16229
16230        let opened = open_archive(&spliced, &master_key()).unwrap();
16231        assert_eq!(
16232            opened.crypto_header_bytes,
16233            first.bytes[crypto_start..crypto_end].to_vec()
16234        );
16235        assert_eq!(
16236            opened.extract_file("splice.txt").unwrap(),
16237            Some(b"same shape".to_vec())
16238        );
16239        opened.verify().unwrap();
16240    }
16241
16242    #[test]
16243    fn rejects_same_key_object_splice_with_session_mismatch() {
16244        let first = write_archive(
16245            &[RegularFile::new("splice.txt", b"same shape")],
16246            &master_key(),
16247            WriterOptions {
16248                archive_uuid: Some([0x11; 16]),
16249                session_id: Some([0x22; 16]),
16250                ..single_stream_options()
16251            },
16252        )
16253        .unwrap();
16254        let second = write_archive(
16255            &[RegularFile::new("splice.txt", b"same shape")],
16256            &master_key(),
16257            WriterOptions {
16258                archive_uuid: Some([0x11; 16]),
16259                session_id: Some([0x33; 16]),
16260                ..single_stream_options()
16261            },
16262        )
16263        .unwrap();
16264
16265        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16266        let crypto_end = volume_header.crypto_header_offset as usize
16267            + volume_header.crypto_header_length as usize;
16268        let terminal_offset = terminal_material_offset(&first.bytes);
16269        let second_terminal_offset = terminal_material_offset(&second.bytes);
16270        assert_eq!(terminal_offset, second_terminal_offset);
16271
16272        let mut spliced = first.bytes.clone();
16273        spliced[crypto_end..terminal_offset]
16274            .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
16275
16276        assert_eq!(
16277            open_archive(&spliced, &master_key()).unwrap_err(),
16278            FormatError::AeadFailure
16279        );
16280    }
16281
16282    #[test]
16283    fn rejects_authenticated_trailer_pointer_and_count_mutations() {
16284        let archive = write_archive(
16285            &[RegularFile::new(
16286                "trailer-range.txt",
16287                b"authenticated ranges",
16288            )],
16289            &master_key(),
16290            single_stream_options(),
16291        )
16292        .unwrap();
16293        let strict_options = ReaderOptions {
16294            max_trailing_garbage_scan: 0,
16295            ..ReaderOptions::default()
16296        };
16297        let bytes = archive.bytes;
16298        let manifest_offset = terminal_material_offset(&bytes);
16299        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16300
16301        let mut wrong_footer_length = bytes.clone();
16302        rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
16303            trailer.manifest_footer_length = 42;
16304        });
16305        OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
16306            .unwrap();
16307
16308        for (label, offset) in [
16309            (
16310                "offset before trailer by 1",
16311                manifest_offset.saturating_sub(1),
16312            ),
16313            ("offset after trailer", manifest_offset + 1),
16314            ("offset at stream start", 0),
16315            ("offset at trailer", trailer_offset),
16316            ("offset beyond trailer", trailer_offset + 4),
16317        ] {
16318            let mut wrong_footer_offset = bytes.clone();
16319            rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16320                trailer.manifest_footer_offset = offset as u64;
16321            });
16322            open_archive(&wrong_footer_offset, &master_key())
16323                .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
16324        }
16325
16326        let mut wrong_bytes_written = bytes.clone();
16327        rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
16328            trailer.bytes_written += 1;
16329        });
16330        open_archive(&wrong_bytes_written, &master_key()).unwrap();
16331
16332        let mut wrong_block_count = bytes.clone();
16333        rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
16334            trailer.block_count += 1;
16335        });
16336        open_archive(&wrong_block_count, &master_key()).unwrap();
16337
16338        let mut wrong_footer_offset = bytes.clone();
16339        rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16340            trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
16341        });
16342        open_archive(&wrong_footer_offset, &master_key()).unwrap();
16343    }
16344
16345    #[test]
16346    fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
16347        let archive = write_archive(
16348            &[RegularFile::new(
16349                "trailer-trailing-scan.txt",
16350                b"trailer scan boundaries",
16351            )],
16352            &master_key(),
16353            single_stream_options(),
16354        )
16355        .unwrap();
16356        let options = ReaderOptions {
16357            max_trailing_garbage_scan: 8,
16358            ..ReaderOptions::default()
16359        };
16360
16361        let mut within_scan = archive.bytes.clone();
16362        within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
16363        let opened =
16364            OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
16365        assert_eq!(
16366            opened.extract_file("trailer-trailing-scan.txt").unwrap(),
16367            Some(b"trailer scan boundaries".to_vec())
16368        );
16369
16370        let mut beyond_scan = archive.bytes.clone();
16371        beyond_scan.resize(
16372            beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
16373            0xAA,
16374        );
16375        assert_eq!(
16376            OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
16377            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16378        );
16379    }
16380
16381    #[test]
16382    fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
16383        let archive = write_archive(
16384            &[RegularFile::new("index-root-size.txt", b"extent size")],
16385            &master_key(),
16386            single_stream_options(),
16387        )
16388        .unwrap();
16389        let mut malformed = archive.bytes;
16390        let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
16391            .expect("archive should contain IndexRootData");
16392        mutate_block_record_at_slot(&mut malformed, slot, |record| {
16393            record.payload[0] ^= 0x55;
16394        });
16395
16396        assert_eq!(
16397            open_archive(&malformed, &master_key()).unwrap_err(),
16398            FormatError::AeadFailure
16399        );
16400    }
16401
16402    #[test]
16403    fn rejects_block_record_at_wrong_stripe_position() {
16404        let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
16405        let archive = write_archive(
16406            &files,
16407            &master_key(),
16408            WriterOptions {
16409                stripe_width: 2,
16410                volume_loss_tolerance: 1,
16411                ..single_stream_options()
16412            },
16413        )
16414        .unwrap();
16415        let mut volumes = archive.volumes.clone();
16416        mutate_first_block_record(&mut volumes[0], |record| {
16417            record.block_index += 2;
16418        });
16419
16420        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16421        assert_eq!(
16422            open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
16423            FormatError::InvalidArchive("BlockRecord index does not match volume position")
16424        );
16425    }
16426
16427    #[test]
16428    fn rejects_decreasing_block_record_index_in_required_region() {
16429        let archive = write_archive(
16430            &[RegularFile::new("alpha.txt", b"decreasing block index")],
16431            &master_key(),
16432            single_stream_options(),
16433        )
16434        .unwrap();
16435        assert!(block_record_slots(&archive.bytes).len() >= 2);
16436
16437        let mut malformed = archive.bytes;
16438        mutate_block_record_at_slot(&mut malformed, 1, |record| {
16439            record.block_index = 0;
16440        });
16441
16442        assert_eq!(
16443            open_archive(&malformed, &master_key()).unwrap_err(),
16444            FormatError::InvalidArchive("BlockRecord index does not match volume position")
16445        );
16446    }
16447
16448    #[test]
16449    fn rejects_duplicate_authenticated_volume_indexes() {
16450        let files = [RegularFile::new("alpha.txt", b"duplicates")];
16451        let archive = write_archive(
16452            &files,
16453            &master_key(),
16454            WriterOptions {
16455                stripe_width: 2,
16456                volume_loss_tolerance: 1,
16457                ..single_stream_options()
16458            },
16459        )
16460        .unwrap();
16461
16462        assert_eq!(
16463            open_archive_volumes(
16464                &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
16465                &master_key()
16466            )
16467            .unwrap_err(),
16468            FormatError::InvalidArchive("duplicate authenticated volume index")
16469        );
16470    }
16471
16472    #[test]
16473    fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
16474        let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
16475        let archive = write_archive(
16476            &files,
16477            &master_key(),
16478            WriterOptions {
16479                stripe_width: 2,
16480                volume_loss_tolerance: 1,
16481                ..single_stream_options()
16482            },
16483        )
16484        .unwrap();
16485        let mut conflicting = archive.volumes[0].clone();
16486        corrupt_first_block_record_payload(&mut conflicting);
16487
16488        assert_eq!(
16489            open_archive_volumes(
16490                &[archive.volumes[0].as_slice(), conflicting.as_slice()],
16491                &master_key()
16492            )
16493            .unwrap_err(),
16494            FormatError::InvalidArchive("duplicate authenticated volume index")
16495        );
16496    }
16497
16498    fn directory_hint_table_from_rows(
16499        hint_shard_index: u64,
16500        rows: &[(Vec<u8>, Vec<u32>)],
16501        shard_count: u32,
16502    ) -> DirectoryHintTable {
16503        let mut entries = Vec::new();
16504        let mut shard_row_indexes = Vec::new();
16505        let mut string_pool = Vec::new();
16506
16507        for (path, rows) in rows {
16508            let path_offset = if path.is_empty() {
16509                0
16510            } else {
16511                let offset = string_pool.len() as u64;
16512                string_pool.extend_from_slice(path);
16513                offset
16514            };
16515            let shard_list_start_index = shard_row_indexes.len() as u32;
16516            shard_row_indexes.extend_from_slice(rows);
16517            entries.push(DirectoryHintEntry {
16518                dir_hash: hash_prefix(path),
16519                path_offset,
16520                path_length: path.len() as u32,
16521                shard_list_start_index,
16522                shard_count: rows.len() as u32,
16523            });
16524        }
16525
16526        let table_bytes =
16527            directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
16528        let locating = DirectoryHintShardEntry {
16529            hint_shard_index,
16530            first_dir_hash: hash_prefix(&rows.first().unwrap().0),
16531            last_dir_hash: hash_prefix(&rows.last().unwrap().0),
16532            first_block_index: 0,
16533            data_block_count: 1,
16534            parity_block_count: 0,
16535            encrypted_size: 4096,
16536            decompressed_size: table_bytes.len() as u32,
16537            entry_count: rows.len() as u64,
16538        };
16539        DirectoryHintTable::parse(
16540            &table_bytes,
16541            &locating,
16542            shard_count,
16543            MetadataLimits::default(),
16544        )
16545        .unwrap()
16546    }
16547
16548    fn directory_hint_table_bytes(
16549        hint_shard_index: u64,
16550        entries: Vec<DirectoryHintEntry>,
16551        shard_row_indexes: Vec<u32>,
16552        string_pool: Vec<u8>,
16553    ) -> Vec<u8> {
16554        let header_len = DirectoryHintTableHeader {
16555            version: 1,
16556            hint_shard_index,
16557            entry_count: 0,
16558            entry_table_offset: 0,
16559            shard_list_offset: 0,
16560            string_pool_offset: 0,
16561            string_pool_size: 0,
16562        }
16563        .to_bytes()
16564        .len();
16565        let entry_len = entries
16566            .first()
16567            .map(|entry| entry.to_bytes().len())
16568            .unwrap_or(0);
16569        let shard_list_offset = if entries.is_empty() {
16570            0
16571        } else {
16572            header_len + entries.len() * entry_len
16573        };
16574        let string_pool_offset = if string_pool.is_empty() {
16575            0
16576        } else {
16577            shard_list_offset + shard_row_indexes.len() * 4
16578        };
16579
16580        let header = DirectoryHintTableHeader {
16581            version: 1,
16582            hint_shard_index,
16583            entry_count: entries.len() as u64,
16584            entry_table_offset: if entries.is_empty() {
16585                0
16586            } else {
16587                header_len as u64
16588            },
16589            shard_list_offset: shard_list_offset as u64,
16590            string_pool_offset: string_pool_offset as u64,
16591            string_pool_size: string_pool.len() as u64,
16592        };
16593
16594        let mut out = Vec::new();
16595        out.extend_from_slice(&header.to_bytes());
16596        for entry in entries {
16597            out.extend_from_slice(&entry.to_bytes());
16598        }
16599        for row in shard_row_indexes {
16600            out.extend_from_slice(&row.to_le_bytes());
16601        }
16602        out.extend_from_slice(&string_pool);
16603        out
16604    }
16605
16606    fn corrupt_first_block_record_payload(volume: &mut [u8]) {
16607        let (record_offset, _) = first_block_record(volume);
16608        volume[record_offset + 16] ^= 0x55;
16609    }
16610
16611    fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
16612        let (record_offset, _) = block_record_at_slot(volume, slot);
16613        volume[record_offset + 16] ^= 0x55;
16614    }
16615
16616    fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
16617        for patch in patches {
16618            let offset = patch.record_offset as usize;
16619            let end = offset + patch.record_bytes.len();
16620            volume[offset..end].copy_from_slice(&patch.record_bytes);
16621        }
16622    }
16623
16624    fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
16625        let (record_offset, _) = block_record_at_slot(volume, slot);
16626        volume[record_offset] ^= 0x55;
16627    }
16628
16629    fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
16630        let (record_offset, _) = block_record_at_slot(volume, slot);
16631        volume[record_offset + 14] = 0x01;
16632    }
16633
16634    fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
16635        let manifest_offset = terminal_material_offset(volume);
16636        volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
16637    }
16638
16639    fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
16640        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16641        CriticalRecoveryLocator::parse(
16642            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
16643        )
16644        .unwrap()
16645    }
16646
16647    fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
16648        let locator = final_recovery_locator(volume);
16649        let tuple = CmraDecoderTuple::from(locator);
16650        assert!(parity_shard_count < tuple.parity_shard_count);
16651        let cmra_offset = locator.cmra_offset as usize;
16652        let shard_size = tuple.shard_size as usize;
16653        let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
16654        let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
16655        let mut header = CriticalMetadataRecoveryHeader::parse(
16656            &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
16657        )
16658        .unwrap();
16659        header.parity_shard_count = parity_shard_count;
16660
16661        let mut cmra =
16662            Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
16663        cmra.extend_from_slice(&header.to_bytes());
16664        let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
16665        for row in 0..kept_rows {
16666            let start = rows_start + row * row_len;
16667            cmra.extend_from_slice(&volume[start..start + row_len]);
16668        }
16669
16670        let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
16671        out.extend_from_slice(&volume[..cmra_offset]);
16672        out.extend_from_slice(&cmra);
16673        let mut mirror = locator;
16674        mirror.locator_sequence = 1;
16675        mirror.cmra_length = cmra.len() as u32;
16676        mirror.cmra_parity_shard_count = parity_shard_count;
16677        out.extend_from_slice(&mirror.to_bytes());
16678        let final_locator = CriticalRecoveryLocator {
16679            volume_format_rev: locator.volume_format_rev,
16680            locator_sequence: 0,
16681            ..mirror
16682        };
16683        out.extend_from_slice(&final_locator.to_bytes());
16684        out
16685    }
16686
16687    fn rewrite_public_cmra_image(
16688        volume: &mut [u8],
16689        mutate: impl FnOnce(&mut CriticalMetadataImage),
16690    ) {
16691        rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
16692    }
16693
16694    fn rewrite_cmra_image(
16695        volume: &mut [u8],
16696        mode: CmraRecoveryMode,
16697        mutate: impl FnOnce(&mut CriticalMetadataImage),
16698    ) {
16699        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16700        let locator = final_recovery_locator(volume);
16701        let tuple = CmraDecoderTuple::from(locator);
16702        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16703        let mut image = recovered.image;
16704        mutate(&mut image);
16705        refresh_critical_image_region_digests(&mut image);
16706        let image_bytes = image.to_bytes().unwrap();
16707        assert_eq!(image_bytes.len(), tuple.image_length as usize);
16708
16709        let shard_size = tuple.shard_size as usize;
16710        let data_shard_count = tuple.data_shard_count as usize;
16711        let parity_shard_count = tuple.parity_shard_count as usize;
16712        assert!(image_bytes.len() <= data_shard_count * shard_size);
16713
16714        let mut data_shards = Vec::with_capacity(data_shard_count);
16715        for idx in 0..data_shard_count {
16716            let start = idx * shard_size;
16717            let end = (start + shard_size).min(image_bytes.len());
16718            let mut payload = vec![0u8; shard_size];
16719            if start < image_bytes.len() {
16720                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
16721            }
16722            data_shards.push(payload);
16723        }
16724        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
16725        let image_sha256 = sha256_bytes(&image_bytes);
16726
16727        let header = CriticalMetadataRecoveryHeader {
16728            shard_size: tuple.shard_size,
16729            data_shard_count: tuple.data_shard_count,
16730            parity_shard_count: tuple.parity_shard_count,
16731            image_length: tuple.image_length,
16732            archive_uuid_hint: locator.archive_uuid_hint,
16733            session_id_hint: locator.session_id_hint,
16734            volume_index_hint: locator.volume_index_hint,
16735            image_sha256,
16736            header_crc32c: 0,
16737        };
16738        let mut cmra = Vec::new();
16739        cmra.extend_from_slice(&header.to_bytes());
16740        for (idx, payload) in data_shards.into_iter().enumerate() {
16741            let payload_len = if idx + 1 == data_shard_count {
16742                image_bytes.len() - idx * shard_size
16743            } else {
16744                shard_size
16745            };
16746            cmra.extend_from_slice(
16747                &CriticalMetadataRecoveryShard {
16748                    shard_index: idx as u16,
16749                    shard_role: 0,
16750                    shard_payload_length: payload_len as u32,
16751                    payload,
16752                    shard_crc32c: 0,
16753                }
16754                .to_bytes(shard_size)
16755                .unwrap(),
16756            );
16757        }
16758        for (idx, payload) in parity_shards.into_iter().enumerate() {
16759            cmra.extend_from_slice(
16760                &CriticalMetadataRecoveryShard {
16761                    shard_index: (data_shard_count + idx) as u16,
16762                    shard_role: 1,
16763                    shard_payload_length: shard_size as u32,
16764                    payload,
16765                    shard_crc32c: 0,
16766                }
16767                .to_bytes(shard_size)
16768                .unwrap(),
16769            );
16770        }
16771        assert_eq!(cmra.len() as u64, recovered.cmra_length);
16772        let cmra_offset = locator.cmra_offset as usize;
16773        volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
16774
16775        rewrite_locator_image_sha(volume, final_offset, image_sha256);
16776        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
16777        rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
16778    }
16779
16780    fn rewrite_cmra_image_variable_len(
16781        volume: &[u8],
16782        mode: CmraRecoveryMode,
16783        mutate: impl FnOnce(&mut CriticalMetadataImage),
16784    ) -> Vec<u8> {
16785        let locator = final_recovery_locator(volume);
16786        let tuple = CmraDecoderTuple::from(locator);
16787        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16788        let mut image = recovered.image;
16789        mutate(&mut image);
16790        refresh_critical_image_region_digests(&mut image);
16791        let image_bytes = image.to_bytes().unwrap();
16792
16793        let shard_size = tuple.shard_size as usize;
16794        let data_shard_count = image_bytes.len().div_ceil(shard_size);
16795        let parity_shard_count = tuple.parity_shard_count as usize;
16796        assert!(data_shard_count > 0);
16797        assert!(image_bytes.len() <= data_shard_count * shard_size);
16798
16799        let mut data_shards = Vec::with_capacity(data_shard_count);
16800        for idx in 0..data_shard_count {
16801            let start = idx * shard_size;
16802            let end = (start + shard_size).min(image_bytes.len());
16803            let mut payload = vec![0u8; shard_size];
16804            if start < image_bytes.len() {
16805                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
16806            }
16807            data_shards.push(payload);
16808        }
16809        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
16810        let image_sha256 = sha256_bytes(&image_bytes);
16811        let data_shard_count_u16 = u16::try_from(data_shard_count).unwrap();
16812        let image_length_u32 = u32::try_from(image_bytes.len()).unwrap();
16813
16814        let header = CriticalMetadataRecoveryHeader {
16815            shard_size: tuple.shard_size,
16816            data_shard_count: data_shard_count_u16,
16817            parity_shard_count: tuple.parity_shard_count,
16818            image_length: image_length_u32,
16819            archive_uuid_hint: locator.archive_uuid_hint,
16820            session_id_hint: locator.session_id_hint,
16821            volume_index_hint: locator.volume_index_hint,
16822            image_sha256,
16823            header_crc32c: 0,
16824        };
16825        let mut cmra = Vec::new();
16826        cmra.extend_from_slice(&header.to_bytes());
16827        for (idx, payload) in data_shards.into_iter().enumerate() {
16828            let payload_len = if idx + 1 == data_shard_count {
16829                image_bytes.len() - idx * shard_size
16830            } else {
16831                shard_size
16832            };
16833            cmra.extend_from_slice(
16834                &CriticalMetadataRecoveryShard {
16835                    shard_index: idx as u16,
16836                    shard_role: 0,
16837                    shard_payload_length: payload_len as u32,
16838                    payload,
16839                    shard_crc32c: 0,
16840                }
16841                .to_bytes(shard_size)
16842                .unwrap(),
16843            );
16844        }
16845        for (idx, payload) in parity_shards.into_iter().enumerate() {
16846            cmra.extend_from_slice(
16847                &CriticalMetadataRecoveryShard {
16848                    shard_index: (data_shard_count + idx) as u16,
16849                    shard_role: 1,
16850                    shard_payload_length: shard_size as u32,
16851                    payload,
16852                    shard_crc32c: 0,
16853                }
16854                .to_bytes(shard_size)
16855                .unwrap(),
16856            );
16857        }
16858
16859        let locator_base = CriticalRecoveryLocator {
16860            volume_format_rev: image.volume_format_rev,
16861            cmra_offset: locator.cmra_offset,
16862            cmra_length: cmra.len() as u32,
16863            volume_trailer_offset: locator.volume_trailer_offset,
16864            body_bytes_before_cmra: locator.body_bytes_before_cmra,
16865            archive_uuid_hint: locator.archive_uuid_hint,
16866            session_id_hint: locator.session_id_hint,
16867            volume_index_hint: locator.volume_index_hint,
16868            locator_sequence: 1,
16869            cmra_shard_size: tuple.shard_size,
16870            cmra_data_shard_count: data_shard_count_u16,
16871            cmra_parity_shard_count: tuple.parity_shard_count,
16872            cmra_image_length: image_length_u32,
16873            cmra_image_sha256: image_sha256,
16874            locator_crc32c: 0,
16875        };
16876
16877        let cmra_offset = locator.cmra_offset as usize;
16878        let mut out = Vec::new();
16879        out.extend_from_slice(&volume[..cmra_offset]);
16880        out.extend_from_slice(&cmra);
16881        out.extend_from_slice(&locator_base.to_bytes());
16882        out.extend_from_slice(
16883            &CriticalRecoveryLocator {
16884                locator_sequence: 0,
16885                ..locator_base
16886            }
16887            .to_bytes(),
16888        );
16889        out
16890    }
16891
16892    fn rewrite_recovery_locator(
16893        volume: &mut [u8],
16894        offset: usize,
16895        mutate: impl FnOnce(&mut CriticalRecoveryLocator),
16896    ) {
16897        let mut locator =
16898            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
16899                .unwrap();
16900        mutate(&mut locator);
16901        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
16902    }
16903
16904    fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
16905        image.volume_header_sha256 = sha256_bytes(
16906            &image
16907                .regions
16908                .iter()
16909                .find(|region| region.region_type == 1)
16910                .unwrap()
16911                .bytes,
16912        );
16913        image.crypto_header_sha256 = sha256_bytes(
16914            &image
16915                .regions
16916                .iter()
16917                .find(|region| region.region_type == 2)
16918                .unwrap()
16919                .bytes,
16920        );
16921        image.key_wrap_table_sha256 = image
16922            .regions
16923            .iter()
16924            .find(|region| region.region_type == 6)
16925            .map(|region| sha256_bytes(&region.bytes))
16926            .unwrap_or([0u8; 32]);
16927        image.manifest_footer_sha256 = sha256_bytes(
16928            &image
16929                .regions
16930                .iter()
16931                .find(|region| region.region_type == 3)
16932                .unwrap()
16933                .bytes,
16934        );
16935        image.root_auth_footer_sha256 = image
16936            .regions
16937            .iter()
16938            .find(|region| region.region_type == 4)
16939            .map(|region| sha256_bytes(&region.bytes))
16940            .unwrap_or([0u8; 32]);
16941        image.volume_trailer_sha256 = sha256_bytes(
16942            &image
16943                .regions
16944                .iter()
16945                .find(|region| region.region_type == 5)
16946                .unwrap()
16947                .bytes,
16948        );
16949    }
16950
16951    fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
16952        let mut locator =
16953            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
16954                .unwrap();
16955        locator.cmra_image_sha256 = image_sha256;
16956        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
16957    }
16958
16959    fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
16960        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16961        let final_locator = CriticalRecoveryLocator::parse(
16962            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
16963        )
16964        .unwrap();
16965        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
16966        volume[final_locator.cmra_offset as usize] ^= 0x55;
16967        volume[mirror_offset] ^= 0x55;
16968        volume[final_offset] ^= 0x55;
16969    }
16970
16971    fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
16972        let (record_offset, record_len) = first_block_record(volume);
16973        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
16974        let mut record = BlockRecord::parse(
16975            &volume[record_offset..record_offset + record_len],
16976            block_size,
16977        )
16978        .unwrap();
16979        mutate(&mut record);
16980        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
16981    }
16982
16983    fn mutate_block_record_at_slot(
16984        volume: &mut [u8],
16985        slot: usize,
16986        mutate: impl FnOnce(&mut BlockRecord),
16987    ) {
16988        let (record_offset, record_len) = block_record_at_slot(volume, slot);
16989        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
16990        let mut record = BlockRecord::parse(
16991            &volume[record_offset..record_offset + record_len],
16992            block_size,
16993        )
16994        .unwrap();
16995        mutate(&mut record);
16996        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
16997    }
16998
16999    fn first_block_record(volume: &[u8]) -> (usize, usize) {
17000        block_record_at_slot(volume, 0)
17001    }
17002
17003    fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
17004        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17005        let crypto_start = volume_header.crypto_header_offset as usize;
17006        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17007        let crypto_header = CryptoHeader::parse(
17008            &volume[crypto_start..crypto_end],
17009            volume_header.crypto_header_length,
17010        )
17011        .unwrap();
17012        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17013        let record_offset = crypto_end + slot * record_len;
17014        assert!(volume.len() >= record_offset + record_len);
17015        (record_offset, record_len)
17016    }
17017
17018    fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
17019        block_record_slots(volume)
17020            .into_iter()
17021            .enumerate()
17022            .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17023    }
17024
17025    fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
17026        block_record_slots(volume)
17027            .into_iter()
17028            .enumerate()
17029            .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17030            .collect()
17031    }
17032
17033    fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
17034        let mut slots = Vec::new();
17035        for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
17036            if record.kind == BlockKind::PayloadData {
17037                slots.push(slot);
17038            } else if !slots.is_empty() {
17039                break;
17040            }
17041        }
17042        slots
17043    }
17044
17045    fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
17046        envelope_entries_for_path(opened, path)
17047            .into_iter()
17048            .map(|entry| entry.envelope_index)
17049            .collect()
17050    }
17051
17052    fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
17053        let normalized =
17054            normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
17055        let located = opened.locate_index_file(&normalized).unwrap().unwrap();
17056        let file = &located.shard.files[located.file_index];
17057        frame_range_for_file(&located.shard, file)
17058            .unwrap()
17059            .into_iter()
17060            .map(|frame| {
17061                located
17062                    .shard
17063                    .envelopes
17064                    .iter()
17065                    .find(|entry| entry.envelope_index == frame.envelope_index)
17066                    .unwrap()
17067                    .clone()
17068            })
17069            .collect()
17070    }
17071
17072    fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
17073        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17074        let crypto_start = volume_header.crypto_header_offset as usize;
17075        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17076        let crypto_header = CryptoHeader::parse(
17077            &volume[crypto_start..crypto_end],
17078            volume_header.crypto_header_length,
17079        )
17080        .unwrap();
17081        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17082        let manifest_offset = terminal_material_offset(volume);
17083        assert_eq!((manifest_offset - crypto_end) % record_len, 0);
17084        let record_count = (manifest_offset - crypto_end) / record_len;
17085        (0..record_count)
17086            .map(|slot| {
17087                let offset = crypto_end + slot * record_len;
17088                let record = BlockRecord::parse(
17089                    &volume[offset..offset + record_len],
17090                    record_len - BLOCK_RECORD_FRAMING_LEN,
17091                )
17092                .unwrap();
17093                (offset, record_len, record)
17094            })
17095            .collect()
17096    }
17097
17098    fn rewrite_manifest_footer(
17099        volume: &mut [u8],
17100        master_key: &MasterKey,
17101        mutate: impl FnOnce(&mut ManifestFooter),
17102    ) {
17103        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17104        let offset = terminal_material_offset(volume);
17105        let mut footer =
17106            ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17107        mutate(&mut footer);
17108        footer.manifest_hmac = [0u8; 32];
17109        let mut footer_bytes = footer.to_bytes();
17110        let subkeys = Subkeys::derive(
17111            master_key,
17112            &volume_header.archive_uuid,
17113            &volume_header.session_id,
17114        )
17115        .unwrap();
17116        footer.manifest_hmac = compute_hmac(
17117            HmacDomain::ManifestFooter,
17118            &subkeys.mac_key,
17119            &volume_header.archive_uuid,
17120            &volume_header.session_id,
17121            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17122        );
17123        footer_bytes = footer.to_bytes();
17124        volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17125    }
17126
17127    fn rewrite_volume_trailer(
17128        volume: &mut [u8],
17129        master_key: &MasterKey,
17130        mutate: impl FnOnce(&mut VolumeTrailer),
17131    ) {
17132        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17133        let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
17134        let mut trailer =
17135            VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
17136        mutate(&mut trailer);
17137        trailer.trailer_hmac = [0u8; 32];
17138        let mut trailer_bytes = trailer.to_bytes();
17139        let subkeys = Subkeys::derive(
17140            master_key,
17141            &volume_header.archive_uuid,
17142            &volume_header.session_id,
17143        )
17144        .unwrap();
17145        trailer.trailer_hmac = compute_hmac(
17146            HmacDomain::VolumeTrailer,
17147            &subkeys.mac_key,
17148            &volume_header.archive_uuid,
17149            &volume_header.session_id,
17150            &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
17151        );
17152        trailer_bytes = trailer.to_bytes();
17153        volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
17154    }
17155
17156    fn rewrite_sidecar_header(
17157        sidecar: &mut [u8],
17158        master_key: &MasterKey,
17159        mutate: impl FnOnce(&mut BootstrapSidecarHeader),
17160    ) {
17161        let mut header =
17162            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17163        mutate(&mut header);
17164        write_signed_sidecar_header(sidecar, master_key, &mut header);
17165    }
17166
17167    fn write_signed_sidecar_header(
17168        sidecar: &mut [u8],
17169        master_key: &MasterKey,
17170        header: &mut BootstrapSidecarHeader,
17171    ) {
17172        header.sidecar_hmac = [0u8; 32];
17173        let mut header_bytes = header.to_bytes();
17174        let subkeys =
17175            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
17176        header.sidecar_hmac = compute_hmac(
17177            HmacDomain::BootstrapSidecar,
17178            &subkeys.mac_key,
17179            &header.archive_uuid,
17180            &header.session_id,
17181            &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
17182        );
17183        header_bytes = header.to_bytes();
17184        sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
17185    }
17186
17187    fn sparse_bootstrap_sidecar(
17188        source: &[u8],
17189        master_key: &MasterKey,
17190        include_manifest: bool,
17191        include_index_root: bool,
17192        include_dictionary: bool,
17193    ) -> Vec<u8> {
17194        let source_header =
17195            BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17196        let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
17197        let mut header = BootstrapSidecarHeader {
17198            archive_uuid: source_header.archive_uuid,
17199            session_id: source_header.session_id,
17200            flags: 0,
17201            manifest_footer_offset: 0,
17202            manifest_footer_length: 0,
17203            index_root_records_offset: 0,
17204            index_root_records_length: 0,
17205            dictionary_records_offset: 0,
17206            dictionary_records_length: 0,
17207            sidecar_hmac: [0u8; 32],
17208            header_crc32c: 0,
17209        };
17210
17211        if include_manifest {
17212            assert!(source_header.has_manifest_footer());
17213            let (offset, length) = append_sidecar_section(
17214                source,
17215                &mut sidecar,
17216                source_header.manifest_footer_offset,
17217                source_header.manifest_footer_length as u64,
17218            );
17219            header.flags |= 0x01;
17220            header.manifest_footer_offset = offset;
17221            header.manifest_footer_length = length as u32;
17222        }
17223        if include_index_root {
17224            assert!(source_header.has_index_root_records());
17225            let (offset, length) = append_sidecar_section(
17226                source,
17227                &mut sidecar,
17228                source_header.index_root_records_offset,
17229                source_header.index_root_records_length,
17230            );
17231            header.flags |= 0x02;
17232            header.index_root_records_offset = offset;
17233            header.index_root_records_length = length;
17234        }
17235        if include_dictionary {
17236            assert!(source_header.has_dictionary_records());
17237            let (offset, length) = append_sidecar_section(
17238                source,
17239                &mut sidecar,
17240                source_header.dictionary_records_offset,
17241                source_header.dictionary_records_length,
17242            );
17243            header.flags |= 0x04;
17244            header.dictionary_records_offset = offset;
17245            header.dictionary_records_length = length;
17246        }
17247
17248        write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
17249        sidecar
17250    }
17251
17252    fn append_sidecar_section(
17253        source: &[u8],
17254        sidecar: &mut Vec<u8>,
17255        source_offset: u64,
17256        length: u64,
17257    ) -> (u64, u64) {
17258        let source_offset = source_offset as usize;
17259        let length = length as usize;
17260        let offset = sidecar.len() as u64;
17261        sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
17262        (offset, length as u64)
17263    }
17264
17265    fn mutate_sidecar_manifest(
17266        sidecar: &mut [u8],
17267        master_key: &MasterKey,
17268        mutate: impl FnOnce(&mut ManifestFooter),
17269    ) {
17270        let header =
17271            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17272        let offset = header.manifest_footer_offset as usize;
17273        let mut footer =
17274            ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17275        mutate(&mut footer);
17276        footer.manifest_hmac = [0u8; 32];
17277        let mut footer_bytes = footer.to_bytes();
17278        let subkeys =
17279            Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
17280        footer.manifest_hmac = compute_hmac(
17281            HmacDomain::ManifestFooter,
17282            &subkeys.mac_key,
17283            &footer.archive_uuid,
17284            &footer.session_id,
17285            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17286        );
17287        footer_bytes = footer.to_bytes();
17288        sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17289    }
17290
17291    fn mutate_sidecar_index_record(
17292        sidecar: &mut [u8],
17293        record_index: usize,
17294        mutate: impl FnOnce(&mut BlockRecord),
17295    ) {
17296        let header =
17297            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17298        let record_len = sidecar_record_len(sidecar);
17299        let offset = header.index_root_records_offset as usize + record_index * record_len;
17300        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17301        let mut record =
17302            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17303        mutate(&mut record);
17304        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17305    }
17306
17307    fn mutate_sidecar_dictionary_record(
17308        sidecar: &mut [u8],
17309        record_index: usize,
17310        mutate: impl FnOnce(&mut BlockRecord),
17311    ) {
17312        let header =
17313            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17314        let record_len = sidecar_record_len(sidecar);
17315        let offset = header.dictionary_records_offset as usize + record_index * record_len;
17316        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17317        let mut record =
17318            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17319        mutate(&mut record);
17320        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17321    }
17322
17323    fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
17324        let header =
17325            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17326        let record_len = sidecar_record_len(sidecar);
17327        let left_offset = header.index_root_records_offset as usize + left * record_len;
17328        let right_offset = header.index_root_records_offset as usize + right * record_len;
17329        for idx in 0..record_len {
17330            sidecar.swap(left_offset + idx, right_offset + idx);
17331        }
17332    }
17333
17334    fn sidecar_record_len(sidecar: &[u8]) -> usize {
17335        let header =
17336            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17337        let footer_offset = header.manifest_footer_offset as usize;
17338        let footer =
17339            ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
17340                .unwrap();
17341        let index_record_count = footer.index_root_data_block_count as usize
17342            + footer.index_root_parity_block_count as usize;
17343        header.index_root_records_length as usize / index_record_count
17344    }
17345
17346    fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
17347        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17348        assert_eq!(volume_header.volume_index, 0);
17349        assert_eq!(volume_header.stripe_width, 1);
17350        let crypto_start = volume_header.crypto_header_offset as usize;
17351        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17352        let crypto_header = CryptoHeader::parse(
17353            &volume[crypto_start..crypto_end],
17354            volume_header.crypto_header_length,
17355        )
17356        .unwrap();
17357        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17358        let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
17359        for offset in 0..record_count {
17360            let block_index = extent.first_block_index + offset;
17361            let record_offset = crypto_end + block_index as usize * record_len;
17362            volume[record_offset + 16] ^= 0x55;
17363        }
17364    }
17365
17366    fn terminal_material_offset(volume: &[u8]) -> usize {
17367        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17368        let crypto_start = volume_header.crypto_header_offset as usize;
17369        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17370        let crypto_header = CryptoHeader::parse(
17371            &volume[crypto_start..crypto_end],
17372            volume_header.crypto_header_length,
17373        )
17374        .unwrap();
17375        let (_, offset, _) = parse_stream_block_prefix(
17376            volume,
17377            crypto_end,
17378            crypto_header.fixed.block_size as usize,
17379            &volume_header,
17380        )
17381        .unwrap();
17382        offset
17383    }
17384
17385    #[derive(Debug)]
17386    struct TestObject {
17387        extent: ObjectExtent,
17388        records: Vec<BlockRecord>,
17389    }
17390
17391    #[derive(Debug)]
17392    struct TestFileMeta {
17393        path: Vec<u8>,
17394        frame_index: u64,
17395        tar_stream_offset: u64,
17396        member_group_size: u64,
17397        file_data_size: u64,
17398    }
17399
17400    fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
17401        let volume_header = test_volume_header();
17402        let crypto_header = test_crypto_header();
17403        let subkeys = Subkeys::derive(
17404            &master_key(),
17405            &volume_header.archive_uuid,
17406            &volume_header.session_id,
17407        )
17408        .unwrap();
17409        let mut next_block_index = 0u64;
17410        let mut blocks = BTreeMap::new();
17411
17412        let healthy = test_member(b"healthy.txt", b"healthy payload\n");
17413        let broken = test_member(b"broken.txt", b"broken payload\n");
17414        let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
17415
17416        let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
17417        let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
17418
17419        let healthy_payload = encrypt_test_object(
17420            &healthy_frame,
17421            &subkeys.enc_key,
17422            &subkeys.nonce_seed,
17423            b"envelope",
17424            0,
17425            BlockKind::PayloadData,
17426            &mut next_block_index,
17427            &crypto_header,
17428            &volume_header,
17429        );
17430        let broken_payload = encrypt_test_object(
17431            &broken_frame,
17432            &subkeys.enc_key,
17433            &subkeys.nonce_seed,
17434            b"envelope",
17435            1,
17436            BlockKind::PayloadData,
17437            &mut next_block_index,
17438            &crypto_header,
17439            &volume_header,
17440        );
17441        let broken_payload_block = broken_payload.extent.first_block_index;
17442        insert_records(&mut blocks, &healthy_payload.records);
17443        insert_records(&mut blocks, &broken_payload.records);
17444
17445        let frames = vec![
17446            FrameEntry {
17447                frame_index: 0,
17448                envelope_index: 0,
17449                offset_in_envelope: 0,
17450                compressed_size: healthy_frame.len() as u32,
17451                decompressed_size: healthy.len() as u32,
17452                flags: 0x0000_0003,
17453                tar_stream_offset: 0,
17454            },
17455            FrameEntry {
17456                frame_index: 1,
17457                envelope_index: 1,
17458                offset_in_envelope: 0,
17459                compressed_size: broken_frame.len() as u32,
17460                decompressed_size: broken.len() as u32,
17461                flags: 0x0000_0003,
17462                tar_stream_offset: healthy.len() as u64,
17463            },
17464        ];
17465        let envelopes = vec![
17466            EnvelopeEntry {
17467                envelope_index: 0,
17468                first_block_index: healthy_payload.extent.first_block_index,
17469                data_block_count: healthy_payload.extent.data_block_count,
17470                parity_block_count: 0,
17471                encrypted_size: healthy_payload.extent.encrypted_size,
17472                plaintext_size: healthy_frame.len() as u32,
17473                first_frame_index: 0,
17474                frame_count: 1,
17475            },
17476            EnvelopeEntry {
17477                envelope_index: 1,
17478                first_block_index: broken_payload.extent.first_block_index,
17479                data_block_count: broken_payload.extent.data_block_count,
17480                parity_block_count: 0,
17481                encrypted_size: broken_payload.extent.encrypted_size,
17482                plaintext_size: broken_frame.len() as u32,
17483                first_frame_index: 1,
17484                frame_count: 1,
17485            },
17486        ];
17487        let files = vec![
17488            TestFileMeta {
17489                path: b"healthy.txt".to_vec(),
17490                frame_index: 0,
17491                tar_stream_offset: 0,
17492                member_group_size: healthy.len() as u64,
17493                file_data_size: b"healthy payload\n".len() as u64,
17494            },
17495            TestFileMeta {
17496                path: b"broken.txt".to_vec(),
17497                frame_index: 1,
17498                tar_stream_offset: healthy.len() as u64,
17499                member_group_size: broken.len() as u64,
17500                file_data_size: b"broken payload\n".len() as u64,
17501            },
17502        ];
17503
17504        let (index_shard_plaintext, first_path_hash, last_path_hash) =
17505            build_test_index_shard(&files, &frames, &envelopes);
17506        let index_shard = encrypt_test_object(
17507            &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
17508            &subkeys.index_shard_key,
17509            &subkeys.index_nonce_seed,
17510            b"idxshard",
17511            0,
17512            BlockKind::IndexShardData,
17513            &mut next_block_index,
17514            &crypto_header,
17515            &volume_header,
17516        );
17517        insert_records(&mut blocks, &index_shard.records);
17518
17519        let shard_entry = ShardEntry {
17520            shard_index: 0,
17521            first_block_index: index_shard.extent.first_block_index,
17522            data_block_count: index_shard.extent.data_block_count,
17523            parity_block_count: 0,
17524            encrypted_size: index_shard.extent.encrypted_size,
17525            decompressed_size: index_shard_plaintext.len() as u32,
17526            file_count: files.len() as u32,
17527            first_path_hash,
17528            last_path_hash,
17529        };
17530        let mut root_header = IndexRootHeader::empty();
17531        root_header.frame_count = frames.len() as u64;
17532        root_header.envelope_count = envelopes.len() as u64;
17533        root_header.file_count = files.len() as u64;
17534        root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
17535            + broken_payload.extent.data_block_count as u64;
17536        root_header.tar_total_size = tar_stream.len() as u64;
17537        root_header.content_sha256 = sha256_bytes(&tar_stream);
17538        let index_root = IndexRoot {
17539            header: root_header,
17540            shards: vec![shard_entry],
17541            directory_hint_shards: Vec::new(),
17542        };
17543
17544        let index_root_plaintext = index_root.to_bytes();
17545        let index_root_object = encrypt_test_object(
17546            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
17547            &subkeys.index_root_key,
17548            &subkeys.index_nonce_seed,
17549            b"idxroot",
17550            0,
17551            BlockKind::IndexRootData,
17552            &mut next_block_index,
17553            &crypto_header,
17554            &volume_header,
17555        );
17556        insert_records(&mut blocks, &index_root_object.records);
17557
17558        let archive_uuid = volume_header.archive_uuid;
17559        let session_id = volume_header.session_id;
17560        let opened = OpenedArchive {
17561            options: ReaderOptions::default(),
17562            observed_archive_bytes: 1_000_000,
17563            observed_volume_count: 1,
17564            subkeys,
17565            blocks,
17566            lazy_blocks: None,
17567            crypto_header_bytes: Vec::new(),
17568            volume_header,
17569            crypto_header,
17570            manifest_footer: ManifestFooter {
17571                archive_uuid,
17572                session_id,
17573                volume_index: 0,
17574                is_authoritative: 1,
17575                total_volumes: 1,
17576                index_root_first_block: index_root_object.extent.first_block_index,
17577                index_root_data_block_count: index_root_object.extent.data_block_count,
17578                index_root_parity_block_count: 0,
17579                index_root_encrypted_size: index_root_object.extent.encrypted_size,
17580                index_root_decompressed_size: index_root_plaintext.len() as u32,
17581                manifest_hmac: [0u8; 32],
17582            },
17583            volume_trailer: Some(VolumeTrailer {
17584                archive_uuid,
17585                session_id,
17586                volume_index: 0,
17587                block_count: next_block_index,
17588                bytes_written: 0,
17589                manifest_footer_offset: 0,
17590                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
17591                closed_at_ns: 0,
17592                root_auth_footer_offset: 0,
17593                root_auth_footer_length: 0,
17594                root_auth_flags: 0,
17595                trailer_hmac: [0u8; 32],
17596            }),
17597            root_auth_footer: None,
17598            index_root,
17599            payload_dictionary: None,
17600        };
17601        (opened, broken_payload_block)
17602    }
17603
17604    fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
17605        let locating = opened.index_root.shards[0].clone();
17606        let mut shard = opened.load_index_shard(&locating).unwrap();
17607        mutate(&mut shard);
17608        let plaintext = shard.to_bytes();
17609        let mut next_block_index = opened
17610            .blocks
17611            .keys()
17612            .last()
17613            .copied()
17614            .map(|index| index + 1)
17615            .unwrap_or(0);
17616        let replacement = encrypt_test_object(
17617            &compress_zstd_frame(&plaintext, 1).unwrap(),
17618            &opened.subkeys.index_shard_key,
17619            &opened.subkeys.index_nonce_seed,
17620            b"idxshard",
17621            locating.shard_index,
17622            BlockKind::IndexShardData,
17623            &mut next_block_index,
17624            &opened.crypto_header,
17625            &opened.volume_header,
17626        );
17627        insert_records(&mut opened.blocks, &replacement.records);
17628        opened.index_root.shards[0] = ShardEntry {
17629            shard_index: locating.shard_index,
17630            first_block_index: replacement.extent.first_block_index,
17631            data_block_count: replacement.extent.data_block_count,
17632            parity_block_count: 0,
17633            encrypted_size: replacement.extent.encrypted_size,
17634            decompressed_size: plaintext.len() as u32,
17635            file_count: shard.files.len() as u32,
17636            first_path_hash: shard.files.first().unwrap().path_hash,
17637            last_path_hash: shard.files.last().unwrap().path_hash,
17638        };
17639    }
17640
17641    fn rewrite_as_single_healthy_file(
17642        opened: &mut OpenedArchive,
17643        mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
17644    ) {
17645        let healthy_path = b"healthy.txt";
17646        let healthy_payload = b"healthy payload\n";
17647        let healthy_member = test_member(healthy_path, healthy_payload);
17648        replace_first_index_shard(opened, |shard| {
17649            let file_index = (0..shard.files.len())
17650                .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
17651                .unwrap();
17652            let mut file = shard.files[file_index].clone();
17653            let frame = shard
17654                .frames
17655                .iter()
17656                .find(|entry| entry.frame_index == 0)
17657                .unwrap()
17658                .clone();
17659            let envelope = shard
17660                .envelopes
17661                .iter()
17662                .find(|entry| entry.envelope_index == 0)
17663                .unwrap()
17664                .clone();
17665            let mut path = healthy_path.to_vec();
17666
17667            file.path_offset = 0;
17668            file.path_length = path.len() as u32;
17669            file.first_frame_index = 0;
17670            file.frame_count = 1;
17671            file.offset_in_first_frame_plaintext = 0;
17672            file.tar_member_group_size = healthy_member.len() as u64;
17673            file.file_data_size = healthy_payload.len() as u64;
17674            file.flags = 0;
17675            mutate(&mut file, &mut path);
17676            file.path_offset = 0;
17677            file.path_length = path.len() as u32;
17678            file.path_hash = hash_prefix(&path);
17679
17680            shard.files = vec![file];
17681            shard.frames = vec![frame];
17682            shard.envelopes = vec![envelope];
17683            shard.string_pool = path;
17684        });
17685
17686        opened.index_root.header.file_count = 1;
17687        opened.index_root.header.frame_count = 1;
17688        opened.index_root.header.envelope_count = 1;
17689        opened.index_root.header.payload_block_count = 1;
17690        opened.index_root.header.tar_total_size = healthy_member.len() as u64;
17691        opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
17692    }
17693
17694    fn test_volume_header() -> VolumeHeader {
17695        VolumeHeader {
17696            format_version: FORMAT_VERSION,
17697            volume_format_rev: VOLUME_FORMAT_REV,
17698            volume_index: 0,
17699            stripe_width: 1,
17700            archive_uuid: [0x31; 16],
17701            session_id: [0x42; 16],
17702            crypto_header_offset: VOLUME_HEADER_LEN as u32,
17703            crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
17704            header_crc32c: 0,
17705        }
17706    }
17707
17708    fn test_crypto_header() -> CryptoHeaderFixed {
17709        CryptoHeaderFixed {
17710            length: CRYPTO_HEADER_FIXED_LEN as u32,
17711            compression_algo: CompressionAlgo::ZstdFramed,
17712            aead_algo: AeadAlgo::AesGcmSiv256,
17713            fec_algo: FecAlgo::ReedSolomonGF16,
17714            kdf_algo: KdfAlgo::Raw,
17715            chunk_size: 4096,
17716            envelope_target_size: 8192,
17717            block_size: 4096,
17718            fec_data_shards: 4,
17719            fec_parity_shards: 0,
17720            index_fec_data_shards: 4,
17721            index_fec_parity_shards: 0,
17722            index_root_fec_data_shards: 4,
17723            index_root_fec_parity_shards: 0,
17724            stripe_width: 1,
17725            volume_loss_tolerance: 0,
17726            bit_rot_buffer_pct: 0,
17727            has_dictionary: 0,
17728            max_path_length: 4096,
17729            expected_volume_size: 0,
17730        }
17731    }
17732
17733    #[allow(clippy::too_many_arguments)]
17734    fn encrypt_test_object(
17735        plaintext: &[u8],
17736        key: &[u8; 32],
17737        nonce_seed: &[u8; 32],
17738        domain: &[u8],
17739        counter: u64,
17740        data_kind: BlockKind,
17741        next_block_index: &mut u64,
17742        crypto_header: &CryptoHeaderFixed,
17743        volume_header: &VolumeHeader,
17744    ) -> TestObject {
17745        let block_size = crypto_header.block_size as usize;
17746        let encrypted = encrypt_padded_aead_object(
17747            AeadObjectContext {
17748                algo: crypto_header.aead_algo,
17749                key,
17750                nonce_seed,
17751                domain,
17752                archive_uuid: &volume_header.archive_uuid,
17753                session_id: &volume_header.session_id,
17754                counter,
17755            },
17756            block_size,
17757            plaintext,
17758        )
17759        .unwrap();
17760        assert_eq!(encrypted.len() % block_size, 0);
17761
17762        let first_block_index = *next_block_index;
17763        let data_block_count = encrypted.len() / block_size;
17764        let records = encrypted
17765            .chunks(block_size)
17766            .enumerate()
17767            .map(|(index, payload)| BlockRecord {
17768                block_index: first_block_index + index as u64,
17769                kind: data_kind,
17770                flags: if index + 1 == data_block_count {
17771                    0x01
17772                } else {
17773                    0
17774                },
17775                payload: payload.to_vec(),
17776                record_crc32c: 0,
17777            })
17778            .collect::<Vec<_>>();
17779        *next_block_index += data_block_count as u64;
17780
17781        TestObject {
17782            extent: ObjectExtent {
17783                first_block_index,
17784                data_block_count: data_block_count as u32,
17785                parity_block_count: 0,
17786                encrypted_size: encrypted.len() as u32,
17787            },
17788            records,
17789        }
17790    }
17791
17792    fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
17793        for record in records {
17794            assert!(blocks.insert(record.block_index, record.clone()).is_none());
17795        }
17796    }
17797
17798    #[allow(clippy::too_many_arguments)]
17799    fn build_metadata_object_from_payload(
17800        payload: &[u8],
17801        _subkeys: &Subkeys,
17802        volume_header: &VolumeHeader,
17803        crypto_header: &CryptoHeaderFixed,
17804        key: &[u8; 32],
17805        nonce_seed: &[u8; 32],
17806        domain: &[u8],
17807        counter: u64,
17808        data_kind: BlockKind,
17809        next_block_index: &mut u64,
17810    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
17811        let compressed = compress_zstd_frame(payload, 1).unwrap();
17812        build_metadata_object_from_compressed(
17813            &compressed,
17814            key,
17815            nonce_seed,
17816            domain,
17817            counter,
17818            data_kind,
17819            next_block_index,
17820            crypto_header,
17821            volume_header,
17822        )
17823    }
17824
17825    #[allow(clippy::too_many_arguments)]
17826    fn build_metadata_object_from_compressed(
17827        compressed: &[u8],
17828        key: &[u8; 32],
17829        nonce_seed: &[u8; 32],
17830        domain: &[u8],
17831        counter: u64,
17832        data_kind: BlockKind,
17833        next_block_index: &mut u64,
17834        crypto_header: &CryptoHeaderFixed,
17835        volume_header: &VolumeHeader,
17836    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
17837        let object = encrypt_test_object(
17838            compressed,
17839            key,
17840            nonce_seed,
17841            domain,
17842            counter,
17843            data_kind,
17844            next_block_index,
17845            crypto_header,
17846            volume_header,
17847        );
17848
17849        let mut blocks = BTreeMap::new();
17850        for record in object.records {
17851            blocks.insert(record.block_index, record);
17852        }
17853        (object.extent, blocks)
17854    }
17855
17856    #[allow(clippy::too_many_arguments)]
17857    fn assert_metadata_object_from_compressed(
17858        compressed: &[u8],
17859        decompressed_size: usize,
17860        _subkeys: &Subkeys,
17861        volume_header: &VolumeHeader,
17862        crypto_header: &CryptoHeaderFixed,
17863        key: &[u8; 32],
17864        nonce_seed: &[u8; 32],
17865        domain: &[u8],
17866        counter: u64,
17867        data_kind: BlockKind,
17868        parity_kind: BlockKind,
17869        class_data_shards: u16,
17870        class_parity_shards: u16,
17871        next_block_index: &mut u64,
17872        expected: FormatError,
17873    ) {
17874        let (extent, blocks) = build_metadata_object_from_compressed(
17875            compressed,
17876            key,
17877            nonce_seed,
17878            domain,
17879            counter,
17880            data_kind,
17881            next_block_index,
17882            crypto_header,
17883            volume_header,
17884        );
17885        let error = load_metadata_object_from_parts(
17886            &blocks,
17887            ObjectLoadContext {
17888                volume_header,
17889                crypto_header,
17890                extent,
17891                data_kind,
17892                parity_kind,
17893                key,
17894                nonce_seed,
17895                domain,
17896                counter,
17897                class_data_shard_max: class_data_shards,
17898                class_parity_shard_max: class_parity_shards,
17899            },
17900            decompressed_size as u32,
17901        )
17902        .unwrap_err();
17903        assert_eq!(error, expected);
17904    }
17905
17906    fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
17907        let record = blocks.get_mut(&block_index).unwrap();
17908        assert_eq!(record.kind, BlockKind::PayloadData);
17909        record.payload[0] ^= 0x55;
17910    }
17911
17912    fn build_test_index_shard(
17913        files: &[TestFileMeta],
17914        frames: &[FrameEntry],
17915        envelopes: &[EnvelopeEntry],
17916    ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
17917        let mut sorted = files
17918            .iter()
17919            .map(|file| (hash_prefix(&file.path), file))
17920            .collect::<Vec<_>>();
17921        sorted.sort_by(|left, right| {
17922            (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
17923                right.0,
17924                right.1.path.as_slice(),
17925                right.1.tar_stream_offset,
17926            ))
17927        });
17928
17929        let mut string_pool = Vec::new();
17930        let mut file_entries = Vec::with_capacity(sorted.len());
17931        for (path_hash, file) in &sorted {
17932            let path_offset = string_pool.len() as u32;
17933            string_pool.extend_from_slice(&file.path);
17934            file_entries.push(FileEntry {
17935                path_hash: *path_hash,
17936                path_offset,
17937                path_length: file.path.len() as u32,
17938                first_frame_index: file.frame_index,
17939                frame_count: 1,
17940                offset_in_first_frame_plaintext: 0,
17941                tar_member_group_size: file.member_group_size,
17942                file_data_size: file.file_data_size,
17943                flags: 0,
17944            });
17945        }
17946
17947        let header = IndexShardHeader {
17948            version: 1,
17949            shard_index: 0,
17950            file_count: file_entries.len() as u32,
17951            frame_count: frames.len() as u32,
17952            envelope_count: envelopes.len() as u32,
17953            file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
17954            frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
17955                as u32,
17956            envelope_table_offset: (INDEX_SHARD_HEADER_LEN
17957                + file_entries.len() * FILE_ENTRY_LEN
17958                + frames.len() * FRAME_ENTRY_LEN) as u32,
17959            string_pool_offset: (INDEX_SHARD_HEADER_LEN
17960                + file_entries.len() * FILE_ENTRY_LEN
17961                + frames.len() * FRAME_ENTRY_LEN
17962                + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
17963            string_pool_size: string_pool.len() as u32,
17964        };
17965
17966        let mut bytes = Vec::new();
17967        bytes.extend_from_slice(&header.to_bytes());
17968        for entry in &file_entries {
17969            bytes.extend_from_slice(&entry.to_bytes());
17970        }
17971        for entry in frames {
17972            bytes.extend_from_slice(&entry.to_bytes());
17973        }
17974        for entry in envelopes {
17975            bytes.extend_from_slice(&entry.to_bytes());
17976        }
17977        bytes.extend_from_slice(&string_pool);
17978
17979        (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
17980    }
17981
17982    fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
17983        let mut out = Vec::new();
17984        out.extend_from_slice(&test_tar_header(path, data.len() as u64));
17985        out.extend_from_slice(data);
17986        out.resize(out.len() + padding_to_512(data.len()), 0);
17987        out
17988    }
17989
17990    fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
17991        let mut header = [0u8; 512];
17992        header[..path.len()].copy_from_slice(path);
17993        write_test_tar_octal(&mut header[100..108], 0o644);
17994        write_test_tar_octal(&mut header[108..116], 0);
17995        write_test_tar_octal(&mut header[116..124], 0);
17996        write_test_tar_octal(&mut header[124..136], size);
17997        write_test_tar_octal(&mut header[136..148], 0);
17998        header[148..156].fill(b' ');
17999        header[156] = b'0';
18000        header[257..263].copy_from_slice(b"ustar\0");
18001        header[263..265].copy_from_slice(b"00");
18002        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
18003        write_test_tar_checksum(&mut header[148..156], checksum);
18004        header
18005    }
18006
18007    fn write_test_tar_octal(field: &mut [u8], value: u64) {
18008        let digits = format!("{value:o}");
18009        field.fill(0);
18010        let start = field.len() - 1 - digits.len();
18011        field[..start].fill(b'0');
18012        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
18013    }
18014
18015    fn write_test_tar_checksum(field: &mut [u8], value: u64) {
18016        let digits = format!("{value:06o}");
18017        field[0..6].copy_from_slice(digits.as_bytes());
18018        field[6] = 0;
18019        field[7] = b' ';
18020    }
18021
18022    fn padding_to_512(len: usize) -> usize {
18023        let remainder = len % 512;
18024        if remainder == 0 {
18025            0
18026        } else {
18027            512 - remainder
18028        }
18029    }
18030}