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_RECOVERY_HEADER_LEN, CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN,
19    CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_HEADER_HMAC_LEN, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN,
20    MANIFEST_FOOTER_LEN, MASTER_KEY_LEN, READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN,
21    READER_MAX_KEY_WRAP_TABLE_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN,
22    VOLUME_FORMAT_REV_44, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
23};
24use crate::metadata::{
25    hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
26    EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
27};
28use crate::non_seekable_reader::{
29    StreamedEnvelopeSummary, StreamedFrameSummary, StreamedPayloadSummary,
30};
31use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
32use crate::root_auth::{
33    archive_root_for_revision, critical_metadata_digest, data_block_merkle_root_for_revision,
34    fec_layout_digest_for_revision, index_digest_for_revision,
35    root_auth_descriptor_digest_for_revision, signer_identity_digest, ArchiveRootInputs,
36    CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
37};
38use crate::tar_model::{
39    parse_tar_member_group, restore_streaming_tar_member_group,
40    stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
41    MetadataDiagnostic, NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
42    TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
43    TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
44};
45use crate::wire::{
46    compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
47    CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
48    CryptoHeader, CryptoHeaderFixed, ExtensionTlv, KeyWrapTableV1, ManifestFooter,
49    RootAuthFooterV1, VolumeHeader, VolumeTrailer,
50};
51
52const TRAILER_HMAC_COVERED_LEN: usize = 96;
53const MANIFEST_HMAC_COVERED_LEN: usize = 104;
54const SIDECAR_HMAC_COVERED_LEN: usize = 92;
55const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
56const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
57const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
58const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
59
60fn default_jobs() -> usize {
61    std::thread::available_parallelism()
62        .map(|jobs| jobs.get())
63        .unwrap_or(1)
64}
65
66pub trait ArchiveReadAt: Send + Sync + 'static {
67    fn len(&self) -> Result<u64, FormatError>;
68    fn is_empty(&self) -> Result<bool, FormatError> {
69        Ok(self.len()? == 0)
70    }
71    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
72}
73
74pub type RecipientWrapCandidateMasterKey = [u8; MASTER_KEY_LEN];
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct RecipientWrapArchiveIdentity {
78    pub archive_uuid: [u8; 16],
79    pub session_id: [u8; 16],
80    pub format_version: u16,
81    pub volume_format_rev: u16,
82}
83
84#[derive(Debug, Clone, Copy)]
85pub struct RecipientWrapRecordContext<'a> {
86    pub archive_identity: RecipientWrapArchiveIdentity,
87    pub record: &'a crate::wire::RecipientRecordV1,
88}
89
90impl ArchiveReadAt for File {
91    fn len(&self) -> Result<u64, FormatError> {
92        self.metadata()
93            .map(|metadata| metadata.len())
94            .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
95    }
96
97    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
98        file_read_exact_at(self, offset, buf)
99    }
100}
101
102#[cfg(unix)]
103fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
104    use std::os::unix::fs::FileExt;
105
106    file_read_exact_at_with(offset, buf, |chunk, offset| file.read_at(chunk, offset))
107}
108
109#[cfg(windows)]
110fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
111    use std::os::windows::fs::FileExt;
112
113    file_read_exact_at_with(offset, buf, |chunk, offset| file.seek_read(chunk, offset))
114}
115
116#[cfg(any(unix, windows))]
117fn file_read_exact_at_with<F>(
118    mut offset: u64,
119    mut buf: &mut [u8],
120    mut read_at: F,
121) -> Result<(), FormatError>
122where
123    F: FnMut(&mut [u8], u64) -> std::io::Result<usize>,
124{
125    while !buf.is_empty() {
126        let read =
127            read_at(buf, offset).map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
128        if read == 0 {
129            return Err(FormatError::InvalidArchive("archive read failed"));
130        }
131        offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
132        let rest = std::mem::take(&mut buf).split_at_mut(read).1;
133        buf = rest;
134    }
135    Ok(())
136}
137
138#[cfg(not(any(unix, windows)))]
139fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
140    let mut file = file
141        .try_clone()
142        .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
143    std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
144        .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
145    file.read_exact(buf)
146        .map_err(|_| FormatError::InvalidArchive("archive read failed"))
147}
148
149impl ArchiveReadAt for Vec<u8> {
150    fn len(&self) -> Result<u64, FormatError> {
151        u64::try_from(self.len())
152            .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
153    }
154
155    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
156        let offset = to_usize(offset, "archive")?;
157        let end = checked_add(offset, buf.len(), "archive")?;
158        let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
159            structure: "archive",
160            expected: end,
161            actual: self.len(),
162        })?;
163        buf.copy_from_slice(source);
164        Ok(())
165    }
166}
167
168impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
169    fn len(&self) -> Result<u64, FormatError> {
170        (**self).len()
171    }
172
173    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
174        (**self).read_exact_at(offset, buf)
175    }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct ReaderOptions {
180    pub max_trailing_garbage_scan: usize,
181    pub max_verify_tar_size: usize,
182    pub max_total_extraction_size: u64,
183    pub jobs: usize,
184}
185
186impl Default for ReaderOptions {
187    fn default() -> Self {
188        Self {
189            max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
190            max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
191            max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
192            jobs: default_jobs(),
193        }
194    }
195}
196
197pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
198    if options.jobs == 0 {
199        return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
200    }
201    Ok(())
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct ArchiveEntry {
206    pub path: String,
207    pub file_data_size: u64,
208    pub kind: TarEntryKind,
209    pub mode: u32,
210    pub mtime: u64,
211    pub diagnostics: Vec<MetadataDiagnostic>,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct ArchiveIndexEntry {
216    pub path: String,
217    pub name: String,
218    pub file_data_size: u64,
219    pub kind: TarEntryKind,
220    pub mode: u32,
221    pub mtime: u64,
222    pub path_hash: [u8; 8],
223    pub tar_member_group_size: u64,
224    pub first_frame_index: u64,
225    pub frame_count: u32,
226    pub offset_in_first_frame_plaintext: u32,
227    pub layout: ArchiveIndexEntryLayout,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct ArchiveIndexEntryLayout {
232    pub compressed_size: u64,
233    pub decompressed_frame_size: u64,
234    pub envelope_count: u32,
235    pub first_envelope_index: Option<u64>,
236    pub last_envelope_index: Option<u64>,
237    pub first_payload_block_index: Option<u64>,
238    pub payload_data_block_count: u64,
239    pub payload_parity_block_count: u64,
240    pub payload_encrypted_size: u64,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct ExtractedArchiveMember {
245    pub path: String,
246    pub kind: TarEntryKind,
247    pub data: Vec<u8>,
248    pub link_target: Option<String>,
249    pub diagnostics: Vec<MetadataDiagnostic>,
250}
251
252/// Receives logical regular-file bytes while the archive reader extracts data.
253///
254/// Callbacks report uncompressed member payload bytes after they are accepted by
255/// the destination writer. Each selected file is capped by its authenticated
256/// `file_data_size`.
257pub trait ArchiveExtractProgressSink {
258    /// Reports newly extracted payload bytes for one archive member.
259    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
260}
261
262impl<F> ArchiveExtractProgressSink for F
263where
264    F: FnMut(&str, u64),
265{
266    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
267        self(archive_path, bytes);
268    }
269}
270
271#[derive(Debug, Clone)]
272pub struct OpenedArchive {
273    options: ReaderOptions,
274    observed_archive_bytes: u64,
275    observed_volume_count: u32,
276    subkeys: Subkeys,
277    blocks: BTreeMap<u64, BlockRecord>,
278    lazy_blocks: Option<Arc<SeekableBlockSource>>,
279    crypto_header_bytes: Vec<u8>,
280    pub volume_header: VolumeHeader,
281    pub crypto_header: CryptoHeaderFixed,
282    pub manifest_footer: ManifestFooter,
283    pub volume_trailer: Option<VolumeTrailer>,
284    pub root_auth_footer: Option<RootAuthFooterV1>,
285    pub index_root: IndexRoot,
286    payload_dictionary: Option<Vec<u8>>,
287}
288
289#[derive(Debug)]
290pub struct ArchiveContentVerification<'a> {
291    archive: &'a OpenedArchive,
292    mode: ContentVerificationMode,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296enum ContentVerificationMode {
297    Full,
298    Fast,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct ArchiveRepairPatch {
303    pub volume_index: u32,
304    pub block_index: u64,
305    pub record_offset: u64,
306    pub record_bytes: Vec<u8>,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum RootAuthDiagnostic {
311    RootAuthContentVerified,
312    RootAuthDeferredFullArchiveScanRequired,
313    AuthenticatedMetadataNotRootSigned,
314    RecoveryMarginNotRootAuthenticated,
315    ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
316    RecoveryMarginChecked,
317    RecoveryMarginFailed,
318    RecoveryMarginUnchecked,
319}
320
321impl RootAuthDiagnostic {
322    pub const fn label(self) -> &'static str {
323        match self {
324            Self::RootAuthContentVerified => "root_auth_content_verified",
325            Self::RootAuthDeferredFullArchiveScanRequired => {
326                "root_auth_deferred_full_archive_scan_required"
327            }
328            Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
329            Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
330            Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
331                "replicated_global_copy_unchecked_due_to_volume_loss"
332            }
333            Self::RecoveryMarginChecked => "recovery_margin_checked",
334            Self::RecoveryMarginFailed => "recovery_margin_failed",
335            Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
336        }
337    }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum PublicNoKeyDiagnostic {
342    PublicDataBlockCommitmentVerified,
343    PublicPhysicalCompletenessUnverified,
344    PublicRecoveryMarginUnchecked,
345}
346
347impl PublicNoKeyDiagnostic {
348    pub const fn label(self) -> &'static str {
349        match self {
350            Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
351            Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
352            Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
353        }
354    }
355}
356
357#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct RootAuthVerification {
359    pub format_version: u16,
360    pub volume_format_rev: u16,
361    pub archive_root: [u8; 32],
362    pub authenticator_id: u16,
363    pub signer_identity_type: u16,
364    pub signer_identity_bytes: Vec<u8>,
365    pub total_data_block_count: u64,
366    pub diagnostics: Vec<RootAuthDiagnostic>,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct PublicNoKeyVerification {
371    pub format_version: u16,
372    pub volume_format_rev: u16,
373    pub archive_root: [u8; 32],
374    pub authenticator_id: u16,
375    pub signer_identity_type: u16,
376    pub signer_identity_bytes: Vec<u8>,
377    pub total_data_block_count: u64,
378    pub diagnostics: Vec<PublicNoKeyDiagnostic>,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382struct RootAuthMaterial {
383    critical_metadata_digest: [u8; 32],
384    index_digest: [u8; 32],
385    fec_layout_digest: [u8; 32],
386    data_block_merkle_root: [u8; 32],
387    signer_identity_digest: [u8; 32],
388    archive_root: [u8; 32],
389    total_data_block_count: u64,
390}
391
392#[derive(Debug, Clone, Copy)]
393struct ObjectExtent {
394    first_block_index: u64,
395    data_block_count: u32,
396    parity_block_count: u32,
397    encrypted_size: u32,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401enum ParityReadPolicy {
402    Always,
403    RepairOnly,
404}
405
406pub(crate) struct StreamedArchiveOpenParts {
407    pub(crate) options: ReaderOptions,
408    pub(crate) observed_archive_bytes: u64,
409    pub(crate) subkeys: Subkeys,
410    pub(crate) blocks: BTreeMap<u64, BlockRecord>,
411    pub(crate) crypto_header_bytes: Vec<u8>,
412    pub(crate) volume_header: VolumeHeader,
413    pub(crate) crypto_header: CryptoHeaderFixed,
414    pub(crate) manifest_footer: ManifestFooter,
415    pub(crate) volume_trailer: VolumeTrailer,
416    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
417}
418
419#[derive(Clone, Copy)]
420struct WinningIndexEntry {
421    start: u64,
422    file_data_size: u64,
423    mtime: u64,
424    shard_index: usize,
425    file_index: usize,
426}
427
428struct LocatedIndexFile {
429    shard: IndexShard,
430    file_index: usize,
431    start: u64,
432}
433
434struct ExtractProgressWriter<'a, W> {
435    inner: &'a mut W,
436    archive_path: &'a str,
437    file_data_size: u64,
438    reported_bytes: u64,
439    progress: &'a mut dyn ArchiveExtractProgressSink,
440}
441
442impl<'a, W> ExtractProgressWriter<'a, W> {
443    fn new(
444        inner: &'a mut W,
445        archive_path: &'a str,
446        file_data_size: u64,
447        progress: &'a mut dyn ArchiveExtractProgressSink,
448    ) -> Self {
449        Self {
450            inner,
451            archive_path,
452            file_data_size,
453            reported_bytes: 0,
454            progress,
455        }
456    }
457
458    fn report(&mut self, bytes: u64) {
459        if bytes == 0 || self.file_data_size == 0 {
460            return;
461        }
462        let capped_next = self
463            .reported_bytes
464            .saturating_add(bytes)
465            .min(self.file_data_size);
466        let delta = capped_next.saturating_sub(self.reported_bytes);
467        if delta == 0 {
468            return;
469        }
470        self.reported_bytes = capped_next;
471        self.progress.file_bytes_extracted(self.archive_path, delta);
472    }
473}
474
475impl<W: Write> Write for ExtractProgressWriter<'_, W> {
476    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
477        let written = self.inner.write(buf)?;
478        self.report(written as u64);
479        Ok(written)
480    }
481
482    fn flush(&mut self) -> std::io::Result<()> {
483        self.inner.flush()
484    }
485}
486
487struct DecodedTarMemberGroupReader<'a> {
488    archive: &'a OpenedArchive,
489    shard: &'a IndexShard,
490    file: &'a FileEntry,
491    decompressor: zstd::bulk::Decompressor<'static>,
492    next_frame_offset: u64,
493    cached_envelope_index: Option<u64>,
494    cached_envelope_plaintext: Vec<u8>,
495    current_frame: Vec<u8>,
496    current_frame_offset: usize,
497    remaining_group_bytes: u64,
498}
499
500struct SeekableVolumeSource {
501    reader: Arc<dyn ArchiveReadAt>,
502    volume_index: u32,
503    block_records_start: u64,
504    block_count: u64,
505    record_len: u64,
506    block_size: usize,
507}
508
509impl std::fmt::Debug for SeekableVolumeSource {
510    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511        f.debug_struct("SeekableVolumeSource")
512            .field("volume_index", &self.volume_index)
513            .field("block_records_start", &self.block_records_start)
514            .field("block_count", &self.block_count)
515            .field("record_len", &self.record_len)
516            .field("block_size", &self.block_size)
517            .finish_non_exhaustive()
518    }
519}
520
521#[derive(Debug)]
522struct SeekableBlockSource {
523    stripe_width: u32,
524    volumes: Vec<Option<SeekableVolumeSource>>,
525}
526
527trait BlockProvider {
528    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
529}
530
531struct OpenedBlockProvider<'a> {
532    memory_blocks: &'a BTreeMap<u64, BlockRecord>,
533    lazy_blocks: Option<&'a SeekableBlockSource>,
534}
535
536impl SeekableBlockSource {
537    fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
538        if self.stripe_width == 0 {
539            return Err(FormatError::ZeroStripeWidth);
540        }
541        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
542            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
543        let Some(volume) = self
544            .volumes
545            .get(volume_index as usize)
546            .and_then(Option::as_ref)
547        else {
548            return Err(FormatError::InvalidArchive(
549                "repair output requires all archive volumes",
550            ));
551        };
552        let slot = block_index / self.stripe_width as u64;
553        if slot >= volume.block_count {
554            return Err(FormatError::InvalidArchive(
555                "BlockRecord global coverage has a gap",
556            ));
557        }
558        Ok((volume_index, volume.record_offset(slot)?))
559    }
560
561    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
562        if self.stripe_width == 0 {
563            return Err(FormatError::ZeroStripeWidth);
564        }
565        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
566            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
567        let Some(volume) = self
568            .volumes
569            .get(volume_index as usize)
570            .and_then(Option::as_ref)
571        else {
572            return Ok(None);
573        };
574        let slot = block_index / self.stripe_width as u64;
575        if slot >= volume.block_count {
576            return Ok(None);
577        }
578        match volume.read_slot(slot, block_index) {
579            Ok(record) => Ok(Some(record)),
580            Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
581            Err(err) => Err(err),
582        }
583    }
584
585    fn is_complete_volume_set(&self) -> bool {
586        self.volumes.iter().all(Option::is_some)
587    }
588
589    fn total_block_count(&self) -> Result<u64, FormatError> {
590        self.volumes
591            .iter()
592            .map(|volume| {
593                volume
594                    .as_ref()
595                    .map(|volume| volume.block_count)
596                    .ok_or(FormatError::InvalidArchive(
597                        "missing volume in complete set",
598                    ))
599            })
600            .try_fold(0u64, |sum, count| {
601                checked_u64_add(sum, count?, "BlockRecord count overflow")
602            })
603    }
604}
605
606impl SeekableVolumeSource {
607    fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
608        self.block_records_start
609            .checked_add(checked_u64_mul(
610                slot,
611                self.record_len,
612                "BlockRecord offset overflow",
613            )?)
614            .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
615    }
616
617    fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
618        let record_offset = self.record_offset(slot)?;
619        let raw = read_at_vec_unchecked(
620            self.reader.as_ref(),
621            record_offset,
622            usize::try_from(self.record_len)
623                .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
624        )?;
625        let record = BlockRecord::parse(&raw, self.block_size)?;
626        if record.block_index != expected_block_index {
627            return Err(FormatError::InvalidArchive(
628                "BlockRecord index does not match volume position",
629            ));
630        }
631        Ok(record)
632    }
633}
634
635impl BlockProvider for BTreeMap<u64, BlockRecord> {
636    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
637        Ok(self.get(&block_index).cloned())
638    }
639}
640
641impl BlockProvider for OpenedBlockProvider<'_> {
642    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
643        if let Some(record) = self.memory_blocks.get(&block_index) {
644            return Ok(Some(record.clone()));
645        }
646        match self.lazy_blocks {
647            Some(source) => source.block(block_index),
648            None => Ok(None),
649        }
650    }
651}
652
653fn subkeys_for_open(
654    master_key: Option<&MasterKey>,
655    aead_algo: AeadAlgo,
656    archive_uuid: &[u8; 16],
657    session_id: &[u8; 16],
658) -> Result<Subkeys, FormatError> {
659    if aead_algo.is_encrypted() {
660        Subkeys::derive(
661            master_key.ok_or(FormatError::KeyMaterialMismatch)?,
662            archive_uuid,
663            session_id,
664        )
665    } else {
666        Ok(Subkeys::unencrypted_placeholder())
667    }
668}
669
670type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
671pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
672const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
673    "fast full extract requires unique archive paths";
674
675fn parse_volume_format_dispatch(
676    volume_header: &VolumeHeader,
677) -> Result<VolumeFormatRevision, FormatError> {
678    let revision = volume_header.parse_volume_format_revision()?;
679    match revision {
680        VolumeFormatRevision::V44 => Ok(revision),
681    }
682}
683
684#[derive(Debug)]
685struct PayloadIndexTables {
686    shards: Vec<IndexShard>,
687    file_count: u64,
688    frames: BTreeMap<u64, FrameEntry>,
689    envelopes: BTreeMap<u64, EnvelopeEntry>,
690}
691
692pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
693    OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
694}
695
696pub fn open_archive_with_recipient_wrap_resolver<F>(
697    bytes: &[u8],
698    resolver: F,
699) -> Result<OpenedArchive, FormatError>
700where
701    F: FnMut(
702        RecipientWrapRecordContext<'_>,
703    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
704{
705    OpenedArchive::open_with_recipient_wrap_resolver_options(
706        bytes,
707        resolver,
708        ReaderOptions::default(),
709    )
710}
711
712pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
713    require_unencrypted_volume_profile(bytes)?;
714    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
715    OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
716}
717
718pub fn open_archive_volumes(
719    volumes: &[&[u8]],
720    master_key: &MasterKey,
721) -> Result<OpenedArchive, FormatError> {
722    OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
723}
724
725pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
726    for volume in volumes {
727        require_unencrypted_volume_profile(volume)?;
728    }
729    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
730    OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
731}
732
733pub fn open_archive_with_bootstrap_sidecar(
734    bytes: &[u8],
735    bootstrap_sidecar: &[u8],
736    master_key: &MasterKey,
737) -> Result<OpenedArchive, FormatError> {
738    OpenedArchive::open_with_bootstrap_sidecar_options(
739        bytes,
740        bootstrap_sidecar,
741        master_key,
742        ReaderOptions::default(),
743    )
744}
745
746fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
747    if bytes.len() < VOLUME_HEADER_LEN {
748        return Err(FormatError::InvalidLength {
749            structure: "archive",
750            expected: VOLUME_HEADER_LEN,
751            actual: bytes.len(),
752        });
753    }
754    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
755    parse_volume_format_dispatch(&volume_header)?;
756    let crypto_start = volume_header.crypto_header_offset as usize;
757    let crypto_len = volume_header.crypto_header_length as usize;
758    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
759    let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
760    if crypto_header.fixed.aead_algo == AeadAlgo::None
761        && crypto_header.fixed.kdf_algo == KdfAlgo::None
762    {
763        Ok(())
764    } else {
765        Err(FormatError::KeyMaterialMismatch)
766    }
767}
768
769pub fn open_seekable_archive<R: ArchiveReadAt>(
770    reader: R,
771    master_key: &MasterKey,
772) -> Result<OpenedArchive, FormatError> {
773    OpenedArchive::open_seekable_volumes_with_options(
774        vec![reader],
775        master_key,
776        ReaderOptions::default(),
777    )
778}
779
780pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
781    readers: Vec<R>,
782    master_key: &MasterKey,
783) -> Result<OpenedArchive, FormatError> {
784    OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
785}
786
787pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
788    reader: R,
789    bootstrap_sidecar: &[u8],
790    master_key: &MasterKey,
791) -> Result<OpenedArchive, FormatError> {
792    open_seekable_archive_with_bootstrap_sidecar_options(
793        reader,
794        bootstrap_sidecar,
795        master_key,
796        ReaderOptions::default(),
797    )
798}
799
800pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
801    reader: R,
802    bootstrap_sidecar: &[u8],
803    master_key: &MasterKey,
804    options: ReaderOptions,
805) -> Result<OpenedArchive, FormatError> {
806    OpenedArchive::open_seekable_volumes_with_options_for_mode(
807        vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
808        master_key,
809        options,
810        Some(bootstrap_sidecar),
811    )
812}
813
814pub fn open_seekable_archive_with_recipient_wrap_resolver_options<R, F>(
815    reader: R,
816    resolver: F,
817    options: ReaderOptions,
818) -> Result<OpenedArchive, FormatError>
819where
820    R: ArchiveReadAt,
821    F: FnMut(
822        RecipientWrapRecordContext<'_>,
823    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
824{
825    OpenedArchive::open_seekable_with_recipient_wrap_resolver_options(reader, resolver, options)
826}
827
828pub fn open_seekable_archive_volumes_with_recipient_wrap_resolver_options<R, F>(
829    readers: Vec<R>,
830    resolver: F,
831    options: ReaderOptions,
832) -> Result<OpenedArchive, FormatError>
833where
834    R: ArchiveReadAt,
835    F: FnMut(
836        RecipientWrapRecordContext<'_>,
837    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
838{
839    OpenedArchive::open_seekable_volumes_with_recipient_wrap_resolver_options(
840        readers, resolver, options,
841    )
842}
843
844pub fn open_non_seekable_archive(
845    bytes: &[u8],
846    master_key: &MasterKey,
847    bootstrap_sidecar: Option<&[u8]>,
848) -> Result<OpenedArchive, FormatError> {
849    match bootstrap_sidecar {
850        Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
851            bytes,
852            sidecar,
853            master_key,
854            ReaderOptions::default(),
855            BootstrapSidecarUse::NonSeekableRandomAccess,
856        ),
857        None => Err(FormatError::ReaderUnsupported(
858            "non-seekable random access requires a bootstrap sidecar",
859        )),
860    }
861}
862
863pub fn public_no_key_verify_archive_with<F>(
864    bytes: &[u8],
865    verifier: F,
866) -> Result<PublicNoKeyVerification, FormatError>
867where
868    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
869{
870    public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
871}
872
873pub fn public_no_key_verify_volumes_with<F>(
874    volumes: &[&[u8]],
875    verifier: F,
876) -> Result<PublicNoKeyVerification, FormatError>
877where
878    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
879{
880    public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
881}
882
883/// Decode a single-volume, dictionary-free non-seekable archive image into tar
884/// bytes after authenticating its terminal ManifestFooter and VolumeTrailer.
885///
886/// This is a whole-buffer helper, not a live provisional-output API.
887/// Callers receive no decoded bytes if terminal authentication fails.
888pub fn sequential_extract_tar_stream(
889    bytes: &[u8],
890    master_key: &MasterKey,
891) -> Result<Vec<u8>, FormatError> {
892    sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
893}
894
895impl OpenedArchive {
896    fn block_provider(&self) -> OpenedBlockProvider<'_> {
897        OpenedBlockProvider {
898            memory_blocks: &self.blocks,
899            lazy_blocks: self.lazy_blocks.as_deref(),
900        }
901    }
902
903    fn missing_volume_count(&self) -> u32 {
904        self.crypto_header
905            .stripe_width
906            .saturating_sub(self.observed_volume_count)
907    }
908
909    fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
910        let mut diagnostics = vec![
911            RootAuthDiagnostic::RootAuthContentVerified,
912            RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
913            RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
914        ];
915        if self.missing_volume_count() > 0 {
916            diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
917        }
918        diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
919        diagnostics
920    }
921
922    pub fn open_with_options(
923        bytes: &[u8],
924        master_key: &MasterKey,
925        options: ReaderOptions,
926    ) -> Result<Self, FormatError> {
927        Self::open_volumes_with_options(&[bytes], master_key, options)
928    }
929
930    pub fn open_with_recipient_wrap_resolver_options<F>(
931        bytes: &[u8],
932        mut resolver: F,
933        options: ReaderOptions,
934    ) -> Result<Self, FormatError>
935    where
936        F: FnMut(
937            RecipientWrapRecordContext<'_>,
938        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
939    {
940        validate_reader_options(options)?;
941        let observed_archive_bytes = observed_archive_size(std::iter::once(bytes.len() as u64))?;
942        let parsed =
943            parse_seekable_volume_with_recipient_wrap_resolver(bytes, &mut resolver, options)?;
944        let ParsedSeekableVolume {
945            volume_header,
946            crypto_header,
947            crypto_header_bytes,
948            key_wrap_table_bytes: _,
949            subkeys,
950            manifest_footer,
951            manifest_footer_error,
952            root_auth_footer,
953            root_auth_footer_bytes: _,
954            volume_trailer,
955            blocks,
956            erased_block_indices,
957        } = parsed;
958        let manifest_footer = match manifest_footer {
959            Some(footer) => footer,
960            None => {
961                return Err(manifest_footer_error.unwrap_or(FormatError::InvalidArchive(
962                    "no authenticated ManifestFooter found",
963                )));
964            }
965        };
966        let observed_volume_count = 1;
967        let missing_volume_count = crypto_header
968            .stripe_width
969            .checked_sub(observed_volume_count)
970            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
971        if missing_volume_count > crypto_header.volume_loss_tolerance as u32 {
972            return Err(FormatError::InvalidArchive(
973                "missing volume count exceeds volume_loss_tolerance",
974            ));
975        }
976        if missing_volume_count == 0 {
977            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
978        }
979
980        let limits = metadata_limits(&crypto_header);
981        let index_root_plaintext = load_metadata_object_from_parts(
982            &blocks,
983            ObjectLoadContext::index_root(
984                &volume_header,
985                &crypto_header,
986                &subkeys,
987                ObjectExtent {
988                    first_block_index: manifest_footer.index_root_first_block,
989                    data_block_count: manifest_footer.index_root_data_block_count,
990                    parity_block_count: manifest_footer.index_root_parity_block_count,
991                    encrypted_size: manifest_footer.index_root_encrypted_size,
992                },
993            ),
994            manifest_footer.index_root_decompressed_size,
995        )?;
996        let index_root = IndexRoot::parse(
997            &index_root_plaintext,
998            crypto_header.has_dictionary != 0,
999            limits,
1000        )?;
1001        let payload_dictionary = load_archive_dictionary(
1002            &blocks,
1003            &subkeys,
1004            &volume_header,
1005            &crypto_header,
1006            &index_root,
1007        )?;
1008
1009        Ok(Self {
1010            options,
1011            observed_archive_bytes,
1012            observed_volume_count,
1013            subkeys,
1014            blocks,
1015            lazy_blocks: None,
1016            crypto_header_bytes,
1017            volume_header,
1018            crypto_header,
1019            manifest_footer,
1020            volume_trailer: Some(volume_trailer),
1021            root_auth_footer,
1022            index_root,
1023            payload_dictionary,
1024        })
1025    }
1026
1027    pub fn open_seekable_with_recipient_wrap_resolver_options<R, F>(
1028        reader: R,
1029        mut resolver: F,
1030        options: ReaderOptions,
1031    ) -> Result<Self, FormatError>
1032    where
1033        R: ArchiveReadAt,
1034        F: FnMut(
1035            RecipientWrapRecordContext<'_>,
1036        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1037    {
1038        validate_reader_options(options)?;
1039        let reader = Arc::new(reader) as Arc<dyn ArchiveReadAt>;
1040        let observed_len = reader.len()?;
1041        let observed_archive_bytes = observed_archive_size([observed_len])?;
1042        let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1043            reader.clone(),
1044            &mut resolver,
1045            options,
1046        )?;
1047        let manifest_footer = match parsed.manifest_footer.take() {
1048            Some(footer) => footer,
1049            None => {
1050                return Err(parsed.manifest_footer_error.take().unwrap_or(
1051                    FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1052                ));
1053            }
1054        };
1055        let observed_volume_count = 1;
1056        let missing_volume_count = parsed
1057            .crypto_header
1058            .stripe_width
1059            .checked_sub(observed_volume_count)
1060            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1061        if missing_volume_count > parsed.crypto_header.volume_loss_tolerance as u32 {
1062            return Err(FormatError::InvalidArchive(
1063                "missing volume count exceeds volume_loss_tolerance",
1064            ));
1065        }
1066
1067        let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1068        let mut lazy_volume_slots = Vec::new();
1069        lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1070        let slot = parsed.volume_header.volume_index as usize;
1071        if slot >= lazy_volume_slots.len() {
1072            return Err(FormatError::InvalidArchive(
1073                "authenticated volume index exceeds stripe_width",
1074            ));
1075        }
1076        lazy_volume_slots[slot] = Some(SeekableVolumeSource {
1077            reader: parsed.reader.clone(),
1078            volume_index: parsed.volume_header.volume_index,
1079            block_records_start: parsed.block_records_start,
1080            block_count: parsed.volume_trailer.block_count,
1081            record_len,
1082            block_size: parsed.crypto_header.block_size as usize,
1083        });
1084        let lazy_source = Arc::new(SeekableBlockSource {
1085            stripe_width: parsed.crypto_header.stripe_width,
1086            volumes: lazy_volume_slots,
1087        });
1088        let blocks = BTreeMap::new();
1089        let block_provider = OpenedBlockProvider {
1090            memory_blocks: &blocks,
1091            lazy_blocks: Some(lazy_source.as_ref()),
1092        };
1093        let limits = metadata_limits(&parsed.crypto_header);
1094        let index_root_plaintext = load_metadata_object_from_parts(
1095            &block_provider,
1096            ObjectLoadContext::index_root(
1097                &parsed.volume_header,
1098                &parsed.crypto_header,
1099                &parsed.subkeys,
1100                index_root_extent_from_manifest(&manifest_footer),
1101            ),
1102            manifest_footer.index_root_decompressed_size,
1103        )?;
1104        let index_root = IndexRoot::parse(
1105            &index_root_plaintext,
1106            parsed.crypto_header.has_dictionary != 0,
1107            limits,
1108        )?;
1109        let block_provider = OpenedBlockProvider {
1110            memory_blocks: &blocks,
1111            lazy_blocks: Some(lazy_source.as_ref()),
1112        };
1113        let payload_dictionary = load_archive_dictionary(
1114            &block_provider,
1115            &parsed.subkeys,
1116            &parsed.volume_header,
1117            &parsed.crypto_header,
1118            &index_root,
1119        )?;
1120
1121        Ok(Self {
1122            options,
1123            observed_archive_bytes,
1124            observed_volume_count,
1125            subkeys: parsed.subkeys,
1126            blocks,
1127            lazy_blocks: Some(lazy_source),
1128            crypto_header_bytes: parsed.crypto_header_bytes,
1129            volume_header: parsed.volume_header,
1130            crypto_header: parsed.crypto_header,
1131            manifest_footer,
1132            volume_trailer: Some(parsed.volume_trailer),
1133            root_auth_footer: parsed.root_auth_footer,
1134            index_root,
1135            payload_dictionary,
1136        })
1137    }
1138
1139    pub fn open_seekable_volumes_with_recipient_wrap_resolver_options<R, F>(
1140        readers: Vec<R>,
1141        mut resolver: F,
1142        options: ReaderOptions,
1143    ) -> Result<Self, FormatError>
1144    where
1145        R: ArchiveReadAt,
1146        F: FnMut(
1147            RecipientWrapRecordContext<'_>,
1148        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1149    {
1150        validate_reader_options(options)?;
1151        if readers.is_empty() {
1152            return Err(FormatError::InvalidArchive("no volumes supplied"));
1153        }
1154        let readers = readers
1155            .into_iter()
1156            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1157            .collect::<Vec<_>>();
1158        let observed_archive_bytes = observed_archive_size(
1159            readers
1160                .iter()
1161                .map(|reader| reader.len())
1162                .collect::<Result<Vec<_>, _>>()?,
1163        )?;
1164        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1165        let mut manifest_authority: Option<ManifestFooter> = None;
1166        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1167        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1168        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1169        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1170        let mut saw_root_auth_absent = false;
1171        let mut first_manifest_footer_error: Option<FormatError> = None;
1172        let mut seen_volume_indexes = BTreeSet::new();
1173        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1174
1175        for reader in readers {
1176            let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1177                reader,
1178                &mut resolver,
1179                options,
1180            )?;
1181            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1182                return Err(FormatError::InvalidArchive(
1183                    "duplicate authenticated volume index",
1184                ));
1185            }
1186
1187            if let Some(first) = &first {
1188                validate_volume_set_member_metadata(
1189                    &first.volume_header,
1190                    &first.crypto_header,
1191                    &first.crypto_header_bytes,
1192                    &parsed.volume_header,
1193                    &parsed.crypto_header,
1194                    &parsed.crypto_header_bytes,
1195                )?;
1196                validate_key_wrap_table_bytes_match(
1197                    &first.key_wrap_table_bytes,
1198                    &parsed.key_wrap_table_bytes,
1199                )?;
1200            } else {
1201                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1202            }
1203
1204            if let Some(footer) = &parsed.manifest_footer {
1205                if let Some(authority) = &manifest_authority {
1206                    if !manifest_bootstrap_fields_match(authority, footer) {
1207                        return Err(FormatError::InvalidArchive(
1208                            "ManifestFooter bootstrap fields differ",
1209                        ));
1210                    }
1211                } else {
1212                    manifest_authority = Some(footer.clone());
1213                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1214                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1215                }
1216            } else if first_manifest_footer_error.is_none() {
1217                first_manifest_footer_error = parsed.manifest_footer_error.take();
1218            }
1219
1220            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1221                (Some(footer), Some(bytes)) => {
1222                    if saw_root_auth_absent {
1223                        return Err(FormatError::InvalidArchive(
1224                            "root-auth footer presence differs across volumes",
1225                        ));
1226                    }
1227                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1228                        if authority_bytes != bytes {
1229                            return Err(FormatError::InvalidArchive(
1230                                "RootAuthFooter copies differ",
1231                            ));
1232                        }
1233                    } else {
1234                        root_auth_authority = Some(footer.clone());
1235                        root_auth_authority_bytes = Some(bytes.clone());
1236                    }
1237                }
1238                (None, None) => {
1239                    if root_auth_authority_bytes.is_some() {
1240                        return Err(FormatError::InvalidArchive(
1241                            "root-auth footer presence differs across volumes",
1242                        ));
1243                    }
1244                    saw_root_auth_absent = true;
1245                }
1246                _ => {
1247                    return Err(FormatError::InvalidArchive(
1248                        "root-auth footer terminal state is inconsistent",
1249                    ));
1250                }
1251            }
1252
1253            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1254            let source = SeekableVolumeSource {
1255                reader: parsed.reader.clone(),
1256                volume_index: parsed.volume_header.volume_index,
1257                block_records_start: parsed.block_records_start,
1258                block_count: parsed.volume_trailer.block_count,
1259                record_len,
1260                block_size: parsed.crypto_header.block_size as usize,
1261            };
1262            let slot = parsed.volume_header.volume_index as usize;
1263            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1264            {
1265                return Err(FormatError::InvalidArchive(
1266                    "duplicate authenticated volume index",
1267                ));
1268            }
1269
1270            if first.is_none() {
1271                first = Some(parsed);
1272            }
1273        }
1274
1275        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1276        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1277            Some(err) => err,
1278            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1279        })?;
1280        let authority_volume_header = manifest_authority_volume_header.ok_or(
1281            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1282        )?;
1283        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1284            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1285        )?;
1286        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1287            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1288        let missing_volume_count = first
1289            .crypto_header
1290            .stripe_width
1291            .checked_sub(observed_volume_count)
1292            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1293        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1294            return Err(FormatError::InvalidArchive(
1295                "missing volume count exceeds volume_loss_tolerance",
1296            ));
1297        }
1298
1299        let blocks = BTreeMap::new();
1300        let lazy_source = Arc::new(SeekableBlockSource {
1301            stripe_width: first.crypto_header.stripe_width,
1302            volumes: lazy_volume_slots,
1303        });
1304        let block_provider = OpenedBlockProvider {
1305            memory_blocks: &blocks,
1306            lazy_blocks: Some(lazy_source.as_ref()),
1307        };
1308        let limits = metadata_limits(&first.crypto_header);
1309        let index_root_plaintext = load_metadata_object_from_parts(
1310            &block_provider,
1311            ObjectLoadContext::index_root(
1312                &first.volume_header,
1313                &first.crypto_header,
1314                &first.subkeys,
1315                index_root_extent_from_manifest(&manifest_footer),
1316            ),
1317            manifest_footer.index_root_decompressed_size,
1318        )?;
1319        let index_root = IndexRoot::parse(
1320            &index_root_plaintext,
1321            first.crypto_header.has_dictionary != 0,
1322            limits,
1323        )?;
1324        let block_provider = OpenedBlockProvider {
1325            memory_blocks: &blocks,
1326            lazy_blocks: Some(lazy_source.as_ref()),
1327        };
1328        let payload_dictionary = load_archive_dictionary(
1329            &block_provider,
1330            &first.subkeys,
1331            &first.volume_header,
1332            &first.crypto_header,
1333            &index_root,
1334        )?;
1335
1336        Ok(Self {
1337            options,
1338            observed_archive_bytes,
1339            observed_volume_count,
1340            subkeys: first.subkeys,
1341            blocks,
1342            lazy_blocks: Some(lazy_source),
1343            crypto_header_bytes: first.crypto_header_bytes,
1344            volume_header: authority_volume_header,
1345            crypto_header: first.crypto_header,
1346            manifest_footer,
1347            volume_trailer: Some(authority_volume_trailer),
1348            root_auth_footer: root_auth_authority,
1349            index_root,
1350            payload_dictionary,
1351        })
1352    }
1353
1354    pub fn open_volumes_with_options(
1355        volumes: &[&[u8]],
1356        master_key: &MasterKey,
1357        options: ReaderOptions,
1358    ) -> Result<Self, FormatError> {
1359        validate_reader_options(options)?;
1360        if volumes.is_empty() {
1361            return Err(FormatError::InvalidArchive("no volumes supplied"));
1362        }
1363
1364        let observed_archive_bytes =
1365            observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
1366        let mut first: Option<ParsedSeekableVolume> = None;
1367        let mut manifest_authority: Option<ManifestFooter> = None;
1368        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1369        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1370        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1371        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1372        let mut saw_root_auth_absent = false;
1373        let mut first_manifest_footer_error: Option<FormatError> = None;
1374        let mut seen_volume_indexes = BTreeSet::new();
1375        let mut blocks = BTreeMap::new();
1376        let mut erased_block_indices = BTreeSet::new();
1377
1378        for volume_bytes in volumes {
1379            let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
1380            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1381                return Err(FormatError::InvalidArchive(
1382                    "duplicate authenticated volume index",
1383                ));
1384            }
1385
1386            if let Some(first) = &first {
1387                validate_volume_set_member(first, &parsed)?;
1388            }
1389
1390            if let Some(footer) = &parsed.manifest_footer {
1391                if let Some(authority) = &manifest_authority {
1392                    if !manifest_bootstrap_fields_match(authority, footer) {
1393                        return Err(FormatError::InvalidArchive(
1394                            "ManifestFooter bootstrap fields differ",
1395                        ));
1396                    }
1397                } else {
1398                    manifest_authority = Some(footer.clone());
1399                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1400                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1401                }
1402            } else if first_manifest_footer_error.is_none() {
1403                first_manifest_footer_error = parsed.manifest_footer_error.take();
1404            }
1405
1406            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1407                (Some(footer), Some(bytes)) => {
1408                    if saw_root_auth_absent {
1409                        return Err(FormatError::InvalidArchive(
1410                            "root-auth footer presence differs across volumes",
1411                        ));
1412                    }
1413                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1414                        if authority_bytes != bytes {
1415                            return Err(FormatError::InvalidArchive(
1416                                "RootAuthFooter copies differ",
1417                            ));
1418                        }
1419                    } else {
1420                        root_auth_authority = Some(footer.clone());
1421                        root_auth_authority_bytes = Some(bytes.clone());
1422                    }
1423                }
1424                (None, None) => {
1425                    if root_auth_authority_bytes.is_some() {
1426                        return Err(FormatError::InvalidArchive(
1427                            "root-auth footer presence differs across volumes",
1428                        ));
1429                    }
1430                    saw_root_auth_absent = true;
1431                }
1432                _ => {
1433                    return Err(FormatError::InvalidArchive(
1434                        "root-auth footer terminal state is inconsistent",
1435                    ));
1436                }
1437            }
1438
1439            for (block_index, record) in &parsed.blocks {
1440                if blocks.insert(*block_index, record.clone()).is_some() {
1441                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1442                }
1443            }
1444            for block_index in &parsed.erased_block_indices {
1445                erased_block_indices.insert(*block_index);
1446            }
1447
1448            if first.is_none() {
1449                first = Some(parsed);
1450            }
1451        }
1452
1453        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1454        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1455            Some(err) => err,
1456            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1457        })?;
1458        let authority_volume_header = manifest_authority_volume_header.ok_or(
1459            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1460        )?;
1461        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1462            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1463        )?;
1464        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1465            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1466        let missing_volume_count = first
1467            .crypto_header
1468            .stripe_width
1469            .checked_sub(observed_volume_count)
1470            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1471        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1472            return Err(FormatError::InvalidArchive(
1473                "missing volume count exceeds volume_loss_tolerance",
1474            ));
1475        }
1476        if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
1477            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
1478        }
1479
1480        let limits = metadata_limits(&first.crypto_header);
1481        let index_root_plaintext = load_metadata_object_from_parts(
1482            &blocks,
1483            ObjectLoadContext::index_root(
1484                &first.volume_header,
1485                &first.crypto_header,
1486                &first.subkeys,
1487                ObjectExtent {
1488                    first_block_index: manifest_footer.index_root_first_block,
1489                    data_block_count: manifest_footer.index_root_data_block_count,
1490                    parity_block_count: manifest_footer.index_root_parity_block_count,
1491                    encrypted_size: manifest_footer.index_root_encrypted_size,
1492                },
1493            ),
1494            manifest_footer.index_root_decompressed_size,
1495        )?;
1496        let index_root = IndexRoot::parse(
1497            &index_root_plaintext,
1498            first.crypto_header.has_dictionary != 0,
1499            limits,
1500        )?;
1501        let payload_dictionary = load_archive_dictionary(
1502            &blocks,
1503            &first.subkeys,
1504            &first.volume_header,
1505            &first.crypto_header,
1506            &index_root,
1507        )?;
1508
1509        Ok(Self {
1510            options,
1511            observed_archive_bytes,
1512            observed_volume_count,
1513            subkeys: first.subkeys,
1514            blocks,
1515            lazy_blocks: None,
1516            crypto_header_bytes: first.crypto_header_bytes,
1517            volume_header: authority_volume_header,
1518            crypto_header: first.crypto_header,
1519            manifest_footer,
1520            volume_trailer: Some(authority_volume_trailer),
1521            root_auth_footer: root_auth_authority,
1522            index_root,
1523            payload_dictionary,
1524        })
1525    }
1526
1527    pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
1528        readers: Vec<R>,
1529        master_key: &MasterKey,
1530        options: ReaderOptions,
1531    ) -> Result<Self, FormatError> {
1532        let readers = readers
1533            .into_iter()
1534            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1535            .collect::<Vec<_>>();
1536        Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1537    }
1538
1539    fn open_seekable_volumes_with_options_for_mode(
1540        readers: Vec<Arc<dyn ArchiveReadAt>>,
1541        master_key: &MasterKey,
1542        options: ReaderOptions,
1543        bootstrap_sidecar: Option<&[u8]>,
1544    ) -> Result<Self, FormatError> {
1545        validate_reader_options(options)?;
1546        if readers.is_empty() {
1547            return Err(FormatError::InvalidArchive("no volumes supplied"));
1548        }
1549        if bootstrap_sidecar.is_some() && readers.len() > 1 {
1550            return Err(FormatError::ReaderUnsupported(
1551                "multi-volume inputs with bootstrap sidecar are not supported",
1552            ));
1553        }
1554
1555        let observed_archive_bytes = observed_archive_size(
1556            readers
1557                .iter()
1558                .map(|reader| reader.len())
1559                .collect::<Result<Vec<_>, _>>()?
1560                .into_iter()
1561                .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1562        )?;
1563        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1564        let mut manifest_authority: Option<ManifestFooter> = None;
1565        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1566        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1567        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1568        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1569        let mut saw_root_auth_absent = false;
1570        let mut first_manifest_footer_error: Option<FormatError> = None;
1571        let mut seen_volume_indexes = BTreeSet::new();
1572        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1573
1574        for reader in readers {
1575            let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1576            if bootstrap_sidecar.is_some() {
1577                validate_bootstrap_single_volume_input(
1578                    &parsed.volume_header,
1579                    &parsed.crypto_header,
1580                )?;
1581            }
1582            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1583                return Err(FormatError::InvalidArchive(
1584                    "duplicate authenticated volume index",
1585                ));
1586            }
1587
1588            if let Some(first) = &first {
1589                validate_volume_set_member_metadata(
1590                    &first.volume_header,
1591                    &first.crypto_header,
1592                    &first.crypto_header_bytes,
1593                    &parsed.volume_header,
1594                    &parsed.crypto_header,
1595                    &parsed.crypto_header_bytes,
1596                )?;
1597                validate_key_wrap_table_bytes_match(
1598                    &first.key_wrap_table_bytes,
1599                    &parsed.key_wrap_table_bytes,
1600                )?;
1601            } else {
1602                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1603            }
1604
1605            if let Some(footer) = &parsed.manifest_footer {
1606                if let Some(authority) = &manifest_authority {
1607                    if !manifest_bootstrap_fields_match(authority, footer) {
1608                        return Err(FormatError::InvalidArchive(
1609                            "ManifestFooter bootstrap fields differ",
1610                        ));
1611                    }
1612                } else {
1613                    manifest_authority = Some(footer.clone());
1614                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1615                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1616                }
1617            } else if first_manifest_footer_error.is_none() {
1618                first_manifest_footer_error = parsed.manifest_footer_error.take();
1619            }
1620
1621            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1622                (Some(footer), Some(bytes)) => {
1623                    if saw_root_auth_absent {
1624                        return Err(FormatError::InvalidArchive(
1625                            "root-auth footer presence differs across volumes",
1626                        ));
1627                    }
1628                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1629                        if authority_bytes != bytes {
1630                            return Err(FormatError::InvalidArchive(
1631                                "RootAuthFooter copies differ",
1632                            ));
1633                        }
1634                    } else {
1635                        root_auth_authority = Some(footer.clone());
1636                        root_auth_authority_bytes = Some(bytes.clone());
1637                    }
1638                }
1639                (None, None) => {
1640                    if root_auth_authority_bytes.is_some() {
1641                        return Err(FormatError::InvalidArchive(
1642                            "root-auth footer presence differs across volumes",
1643                        ));
1644                    }
1645                    saw_root_auth_absent = true;
1646                }
1647                _ => {
1648                    return Err(FormatError::InvalidArchive(
1649                        "root-auth footer terminal state is inconsistent",
1650                    ));
1651                }
1652            }
1653
1654            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1655            let source = SeekableVolumeSource {
1656                reader: parsed.reader.clone(),
1657                volume_index: parsed.volume_header.volume_index,
1658                block_records_start: parsed.block_records_start,
1659                block_count: parsed.volume_trailer.block_count,
1660                record_len,
1661                block_size: parsed.crypto_header.block_size as usize,
1662            };
1663            let slot = parsed.volume_header.volume_index as usize;
1664            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1665            {
1666                return Err(FormatError::InvalidArchive(
1667                    "duplicate authenticated volume index",
1668                ));
1669            }
1670
1671            if first.is_none() {
1672                first = Some(parsed);
1673            }
1674        }
1675
1676        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1677        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1678            Some(err) => err,
1679            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1680        })?;
1681        let authority_volume_header = manifest_authority_volume_header.ok_or(
1682            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1683        )?;
1684        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1685            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1686        )?;
1687        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1688            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1689        let missing_volume_count = first
1690            .crypto_header
1691            .stripe_width
1692            .checked_sub(observed_volume_count)
1693            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1694        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1695            return Err(FormatError::InvalidArchive(
1696                "missing volume count exceeds volume_loss_tolerance",
1697            ));
1698        }
1699
1700        let mut blocks = BTreeMap::new();
1701        let sidecar = if let Some(bytes) = bootstrap_sidecar {
1702            let sidecar = parse_bootstrap_sidecar(
1703                bytes,
1704                &first.volume_header,
1705                &first.crypto_header,
1706                &first.subkeys,
1707            )?;
1708            sidecar
1709                .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1710            if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1711                if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1712                    return Err(FormatError::InvalidArchive(
1713                        "bootstrap sidecar conflicts with terminal ManifestFooter",
1714                    ));
1715                }
1716            }
1717            Some((bytes, sidecar))
1718        } else {
1719            None
1720        };
1721
1722        if let Some((sidecar_bytes, sidecar)) = &sidecar {
1723            if let Some((offset, length)) = sidecar.index_root_records_section {
1724                let index_root_records = parse_sidecar_block_records(
1725                    sidecar_bytes,
1726                    first.crypto_header.block_size as usize,
1727                    SidecarBlockRecordsSection {
1728                        offset,
1729                        length,
1730                        extent: index_root_extent_from_manifest(&manifest_footer),
1731                        data_kind: BlockKind::IndexRootData,
1732                        parity_kind: BlockKind::IndexRootParity,
1733                        structure: "IndexRoot",
1734                    },
1735                )?;
1736                insert_sidecar_records(&mut blocks, index_root_records)?;
1737            }
1738        }
1739
1740        let lazy_source = Arc::new(SeekableBlockSource {
1741            stripe_width: first.crypto_header.stripe_width,
1742            volumes: lazy_volume_slots,
1743        });
1744        let block_provider = OpenedBlockProvider {
1745            memory_blocks: &blocks,
1746            lazy_blocks: Some(lazy_source.as_ref()),
1747        };
1748        let limits = metadata_limits(&first.crypto_header);
1749        let index_root_plaintext = load_metadata_object_from_parts(
1750            &block_provider,
1751            ObjectLoadContext::index_root(
1752                &first.volume_header,
1753                &first.crypto_header,
1754                &first.subkeys,
1755                index_root_extent_from_manifest(&manifest_footer),
1756            ),
1757            manifest_footer.index_root_decompressed_size,
1758        )?;
1759        let index_root = IndexRoot::parse(
1760            &index_root_plaintext,
1761            first.crypto_header.has_dictionary != 0,
1762            limits,
1763        )?;
1764        if first.crypto_header.has_dictionary != 0 {
1765            if let Some((sidecar_bytes, sidecar)) = &sidecar {
1766                if let Some((offset, length)) = sidecar.dictionary_records_section {
1767                    let dictionary_records = parse_sidecar_block_records(
1768                        sidecar_bytes,
1769                        first.crypto_header.block_size as usize,
1770                        SidecarBlockRecordsSection {
1771                            offset,
1772                            length,
1773                            extent: dictionary_extent_from_index_root(&index_root)?,
1774                            data_kind: BlockKind::DictionaryData,
1775                            parity_kind: BlockKind::DictionaryParity,
1776                            structure: "Dictionary",
1777                        },
1778                    )?;
1779                    insert_sidecar_records(&mut blocks, dictionary_records)?;
1780                }
1781            }
1782        }
1783        let block_provider = OpenedBlockProvider {
1784            memory_blocks: &blocks,
1785            lazy_blocks: Some(lazy_source.as_ref()),
1786        };
1787        let payload_dictionary = load_archive_dictionary(
1788            &block_provider,
1789            &first.subkeys,
1790            &first.volume_header,
1791            &first.crypto_header,
1792            &index_root,
1793        )?;
1794
1795        Ok(Self {
1796            options,
1797            observed_archive_bytes,
1798            observed_volume_count,
1799            subkeys: first.subkeys,
1800            blocks,
1801            lazy_blocks: Some(lazy_source),
1802            crypto_header_bytes: first.crypto_header_bytes,
1803            volume_header: authority_volume_header,
1804            crypto_header: first.crypto_header,
1805            manifest_footer,
1806            volume_trailer: Some(authority_volume_trailer),
1807            root_auth_footer: root_auth_authority,
1808            index_root,
1809            payload_dictionary,
1810        })
1811    }
1812
1813    pub fn open_with_bootstrap_sidecar_options(
1814        bytes: &[u8],
1815        bootstrap_sidecar: &[u8],
1816        master_key: &MasterKey,
1817        options: ReaderOptions,
1818    ) -> Result<Self, FormatError> {
1819        Self::open_with_bootstrap_sidecar_options_for_mode(
1820            bytes,
1821            bootstrap_sidecar,
1822            master_key,
1823            options,
1824            BootstrapSidecarUse::SeekableAssist,
1825        )
1826    }
1827
1828    fn open_with_bootstrap_sidecar_options_for_mode(
1829        bytes: &[u8],
1830        bootstrap_sidecar: &[u8],
1831        master_key: &MasterKey,
1832        options: ReaderOptions,
1833        sidecar_use: BootstrapSidecarUse,
1834    ) -> Result<Self, FormatError> {
1835        let observed_archive_bytes =
1836            observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1837        if bytes.len() < VOLUME_HEADER_LEN {
1838            return Err(FormatError::InvalidLength {
1839                structure: "archive",
1840                expected: VOLUME_HEADER_LEN,
1841                actual: bytes.len(),
1842            });
1843        }
1844
1845        let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1846        parse_volume_format_dispatch(&volume_header)?;
1847        let crypto_start = volume_header.crypto_header_offset as usize;
1848        let crypto_len = volume_header.crypto_header_length as usize;
1849        let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1850        let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1851        let subkeys = subkeys_for_open(
1852            Some(master_key),
1853            parsed_crypto.fixed.aead_algo,
1854            &volume_header.archive_uuid,
1855            &volume_header.session_id,
1856        )?;
1857        verify_integrity_tag(
1858            HmacDomain::CryptoHeader,
1859            parsed_crypto.fixed.aead_algo,
1860            volume_header.volume_format_rev,
1861            Some(&subkeys.mac_key),
1862            &volume_header.archive_uuid,
1863            &volume_header.session_id,
1864            parsed_crypto.hmac_covered_bytes,
1865            &parsed_crypto.header_hmac,
1866        )?;
1867        parsed_crypto.validate_extension_semantics()?;
1868        reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1869        validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1870        validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1871
1872        let sidecar = parse_bootstrap_sidecar(
1873            bootstrap_sidecar,
1874            &volume_header,
1875            &parsed_crypto.fixed,
1876            &subkeys,
1877        )?;
1878        sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1879        let block_records_start = startup_block_records_start(
1880            &volume_header,
1881            &parsed_crypto.kdf_params,
1882            |start, length| {
1883                let start = to_usize(start, "KeyWrapTableV1")?;
1884                Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
1885            },
1886        )?;
1887
1888        let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1889            bytes,
1890            to_usize(block_records_start, "BlockRecord")?,
1891            parsed_crypto.fixed.block_size as usize,
1892            &volume_header,
1893        )?;
1894        let terminal_material = match sidecar_use {
1895            BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1896                bytes,
1897                terminal_offset,
1898                observed_block_count,
1899                KeyHoldingTerminalContext {
1900                    subkeys: &subkeys,
1901                    volume_header: &volume_header,
1902                    crypto_header: &parsed_crypto.fixed,
1903                    crypto_header_bytes: crypto_bytes,
1904                },
1905                options,
1906            )?),
1907            BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1908                bytes,
1909                terminal_offset,
1910                observed_block_count,
1911                KeyHoldingTerminalContext {
1912                    subkeys: &subkeys,
1913                    volume_header: &volume_header,
1914                    crypto_header: &parsed_crypto.fixed,
1915                    crypto_header_bytes: crypto_bytes,
1916                },
1917                options,
1918            )
1919            .ok(),
1920        };
1921        let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1922        let manifest_authority = match sidecar_use {
1923            BootstrapSidecarUse::SeekableAssist => {
1924                let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1925                    "terminal ManifestFooter/VolumeTrailer is required",
1926                ))?;
1927                if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1928                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1929                        return Err(FormatError::InvalidArchive(
1930                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1931                        ));
1932                    }
1933                }
1934                terminal_manifest.clone()
1935            }
1936            BootstrapSidecarUse::NonSeekableRandomAccess => {
1937                let sidecar_manifest = sidecar
1938                    .manifest_footer
1939                    .as_ref()
1940                    .ok_or(FormatError::ReaderUnsupported(
1941                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1942                ))?;
1943                if let Some(terminal_manifest) = terminal_manifest {
1944                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1945                        return Err(FormatError::InvalidArchive(
1946                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1947                        ));
1948                    }
1949                }
1950                sidecar_manifest.clone()
1951            }
1952        };
1953        manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1954
1955        if let Some((offset, length)) = sidecar.index_root_records_section {
1956            let index_root_records = parse_sidecar_block_records(
1957                bootstrap_sidecar,
1958                parsed_crypto.fixed.block_size as usize,
1959                SidecarBlockRecordsSection {
1960                    offset,
1961                    length,
1962                    extent: index_root_extent_from_manifest(&manifest_authority),
1963                    data_kind: BlockKind::IndexRootData,
1964                    parity_kind: BlockKind::IndexRootParity,
1965                    structure: "IndexRoot",
1966                },
1967            )?;
1968            insert_sidecar_records(&mut blocks, index_root_records)?;
1969        }
1970
1971        let limits = metadata_limits(&parsed_crypto.fixed);
1972        let index_root_plaintext = load_metadata_object_from_parts(
1973            &blocks,
1974            ObjectLoadContext::index_root(
1975                &volume_header,
1976                &parsed_crypto.fixed,
1977                &subkeys,
1978                index_root_extent_from_manifest(&manifest_authority),
1979            ),
1980            manifest_authority.index_root_decompressed_size,
1981        )?;
1982        let index_root = IndexRoot::parse(
1983            &index_root_plaintext,
1984            parsed_crypto.fixed.has_dictionary != 0,
1985            limits,
1986        )?;
1987        if parsed_crypto.fixed.has_dictionary != 0 {
1988            if let Some((offset, length)) = sidecar.dictionary_records_section {
1989                let dictionary_records = parse_sidecar_block_records(
1990                    bootstrap_sidecar,
1991                    parsed_crypto.fixed.block_size as usize,
1992                    SidecarBlockRecordsSection {
1993                        offset,
1994                        length,
1995                        extent: dictionary_extent_from_index_root(&index_root)?,
1996                        data_kind: BlockKind::DictionaryData,
1997                        parity_kind: BlockKind::DictionaryParity,
1998                        structure: "dictionary",
1999                    },
2000                )?;
2001                insert_sidecar_records(&mut blocks, dictionary_records)?;
2002            }
2003        }
2004        let payload_dictionary = load_archive_dictionary(
2005            &blocks,
2006            &subkeys,
2007            &volume_header,
2008            &parsed_crypto.fixed,
2009            &index_root,
2010        )?;
2011
2012        Ok(Self {
2013            options,
2014            observed_archive_bytes,
2015            observed_volume_count: 1,
2016            subkeys,
2017            blocks,
2018            lazy_blocks: None,
2019            crypto_header_bytes: crypto_bytes.to_vec(),
2020            volume_header,
2021            crypto_header: parsed_crypto.fixed,
2022            manifest_footer: manifest_authority,
2023            volume_trailer: terminal_material
2024                .as_ref()
2025                .map(|(_, trailer, _)| trailer.clone()),
2026            root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
2027            index_root,
2028            payload_dictionary,
2029        })
2030    }
2031
2032    /// Return path and payload-size entries from encrypted index metadata only.
2033    ///
2034    /// Unlike [`Self::list_files`], this does not decode tar member groups, so
2035    /// it does not read or decrypt payload envelopes after the index shards are
2036    /// available.
2037    pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
2038        let shards = self.load_all_index_shards()?;
2039        final_index_entry_winners(&shards)?
2040            .into_iter()
2041            .map(|(path, winner)| {
2042                archive_index_entry_from_loaded_file_with_path(
2043                    path,
2044                    &shards[winner.shard_index],
2045                    winner.file_index,
2046                )
2047            })
2048            .collect()
2049    }
2050
2051    /// Look up one archive path using encrypted index metadata only.
2052    pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
2053        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2054        self.locate_index_file(&normalized)?
2055            .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
2056            .transpose()
2057    }
2058
2059    pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
2060        let shards = self.load_all_index_shards()?;
2061        final_index_entry_winners(&shards)?
2062            .into_iter()
2063            .map(|(path, winner)| {
2064                let shard = &shards[winner.shard_index];
2065                let member =
2066                    self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
2067                Ok(ArchiveEntry {
2068                    path,
2069                    file_data_size: winner.file_data_size,
2070                    kind: member.kind,
2071                    mode: member.mode,
2072                    mtime: member.mtime,
2073                    diagnostics: member.diagnostics,
2074                })
2075            })
2076            .collect()
2077    }
2078
2079    /// Return only the regular-file payload bytes for `path`.
2080    ///
2081    /// This is a payload-only convenience for callers that do not need tar
2082    /// metadata fidelity diagnostics. Use [`Self::extract_file_with_diagnostics`]
2083    /// or [`Self::extract_member`] when unsupported local PAX/GNU metadata must
2084    /// be reported to users.
2085    pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
2086        self.extract_member(path)?
2087            .map(|member| {
2088                if member.kind != TarEntryKind::Regular {
2089                    return Err(FormatError::ReaderUnsupported(
2090                        "extract_file returns only regular file payloads",
2091                    ));
2092                }
2093                Ok(member.data)
2094            })
2095            .transpose()
2096    }
2097
2098    /// Return regular-file payload bytes together with parsed tar metadata
2099    /// diagnostics for `path`.
2100    pub fn extract_file_with_diagnostics(
2101        &self,
2102        path: &str,
2103    ) -> Result<Option<ExtractedRegularFile>, FormatError> {
2104        self.extract_member(path)?
2105            .map(|member| {
2106                if member.kind != TarEntryKind::Regular {
2107                    return Err(FormatError::ReaderUnsupported(
2108                        "extract_file_with_diagnostics returns only regular file payloads",
2109                    ));
2110                }
2111                Ok((member.data, member.diagnostics))
2112            })
2113            .transpose()
2114    }
2115
2116    /// Stream regular-file payload bytes for `path` into `writer`.
2117    ///
2118    /// This keeps extraction memory bounded by the selected payload envelope,
2119    /// one decompressed frame, and small tar metadata buffers. It returns the
2120    /// same metadata diagnostics as [`Self::extract_file_with_diagnostics`].
2121    pub fn extract_file_to_writer<W: Write>(
2122        &self,
2123        path: &str,
2124        writer: &mut W,
2125    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2126        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2127        self.locate_index_file(&normalized)?
2128            .map(|located| {
2129                self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
2130            })
2131            .transpose()
2132    }
2133
2134    /// Stream regular-file payload bytes for `path` into `writer` while
2135    /// reporting extracted logical payload bytes.
2136    pub fn extract_file_to_writer_with_progress<W: Write>(
2137        &self,
2138        path: &str,
2139        writer: &mut W,
2140        progress: &mut dyn ArchiveExtractProgressSink,
2141    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2142        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2143        self.locate_index_file(&normalized)?
2144            .map(|located| {
2145                self.stream_loaded_file_to_writer_with_progress(
2146                    &located.shard,
2147                    located.file_index,
2148                    writer,
2149                    progress,
2150                )
2151            })
2152            .transpose()
2153    }
2154
2155    pub fn extract_member(
2156        &self,
2157        path: &str,
2158    ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
2159        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2160        self.locate_index_file(&normalized)?
2161            .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
2162            .transpose()
2163    }
2164
2165    pub fn extract_file_to(
2166        &self,
2167        path: &str,
2168        root: &std::path::Path,
2169        options: SafeExtractionOptions,
2170    ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2171        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2172        self.locate_index_file(&normalized)?
2173            .map(|located| {
2174                self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
2175            })
2176            .transpose()
2177    }
2178
2179    pub fn extract_indexed_files_to(
2180        &self,
2181        root: &std::path::Path,
2182        options: SafeExtractionOptions,
2183        jobs: usize,
2184    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2185        if jobs == 0 {
2186            return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2187        }
2188
2189        let shards = self.load_all_index_shards()?;
2190        let mut entries = final_index_entry_winners(&shards)?
2191            .into_iter()
2192            .collect::<Vec<_>>();
2193        entries.sort_by_key(|(_, entry)| entry.start);
2194        self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2195    }
2196
2197    pub fn verify(&self) -> Result<(), FormatError> {
2198        self.verify_content().map(|_| ())
2199    }
2200
2201    pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2202        self.verify_content_with_parity_policy(
2203            ParityReadPolicy::Always,
2204            ContentVerificationMode::Full,
2205        )
2206    }
2207
2208    pub fn verify_content_fast(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2209        if self.fast_verify_defers_payload_semantics() {
2210            self.verify_payload_record_integrity_only()?;
2211            return Ok(ArchiveContentVerification {
2212                archive: self,
2213                mode: ContentVerificationMode::Fast,
2214            });
2215        }
2216        self.verify_content_with_parity_policy(
2217            ParityReadPolicy::RepairOnly,
2218            ContentVerificationMode::Fast,
2219        )
2220    }
2221
2222    pub fn fast_verify_defers_payload_semantics(&self) -> bool {
2223        self.root_auth_footer.is_none()
2224            && self.crypto_header.has_dictionary == 0
2225            && !self.crypto_header.aead_algo.is_encrypted()
2226            && self.crypto_header.fec_parity_shards == 0
2227            && self.crypto_header.index_fec_parity_shards == 0
2228            && self.crypto_header.index_root_fec_parity_shards == 0
2229            && self.manifest_footer.index_root_parity_block_count == 0
2230    }
2231
2232    fn verify_payload_record_integrity_only(&self) -> Result<(), FormatError> {
2233        let tables = self.load_payload_index_tables()?;
2234        let block_provider = self.block_provider();
2235        let block_size = self.crypto_header.block_size as u64;
2236        for envelope in tables.envelopes.values() {
2237            if envelope.parity_block_count != 0 {
2238                return Err(FormatError::InvalidArchive(
2239                    "fast payload record scan requires zero parity",
2240                ));
2241            }
2242            let expected_encrypted_size = checked_u64_mul(
2243                envelope.data_block_count as u64,
2244                block_size,
2245                "payload envelope encrypted size",
2246            )?;
2247            if envelope.encrypted_size as u64 != expected_encrypted_size {
2248                return Err(FormatError::InvalidArchive(
2249                    "payload envelope encrypted_size mismatch",
2250                ));
2251            }
2252            for offset in 0..envelope.data_block_count {
2253                let block_index =
2254                    checked_u64_add(envelope.first_block_index, offset as u64, "payload")?;
2255                let record = block_provider
2256                    .block(block_index)?
2257                    .ok_or(FormatError::InvalidArchive("payload data block is missing"))?;
2258                if record.kind != BlockKind::PayloadData {
2259                    return Err(FormatError::InvalidArchive(
2260                        "payload data block has unexpected kind",
2261                    ));
2262                }
2263                let should_be_last = offset + 1 == envelope.data_block_count;
2264                if record.is_last_data() != should_be_last {
2265                    return Err(FormatError::InvalidArchive(
2266                        "payload last-data flag is not on the final data block",
2267                    ));
2268                }
2269            }
2270        }
2271        Ok(())
2272    }
2273
2274    fn verify_content_with_parity_policy(
2275        &self,
2276        parity_policy: ParityReadPolicy,
2277        mode: ContentVerificationMode,
2278    ) -> Result<ArchiveContentVerification<'_>, FormatError> {
2279        let tables = self.load_payload_index_tables()?;
2280        let streamed = self.scan_seekable_payload(
2281            &tables,
2282            u64::MAX,
2283            NoopTarStreamObserver,
2284            true,
2285            parity_policy,
2286        )?;
2287        self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
2288        Ok(ArchiveContentVerification {
2289            archive: self,
2290            mode,
2291        })
2292    }
2293
2294    pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
2295        let lazy_source = self
2296            .lazy_blocks
2297            .as_ref()
2298            .ok_or(FormatError::ReaderUnsupported(
2299                "repair output requires seekable archive input",
2300            ))?;
2301        if !lazy_source.is_complete_volume_set() {
2302            return Err(FormatError::ReaderUnsupported(
2303                "repair output requires all archive volumes",
2304            ));
2305        }
2306
2307        let shards = self.load_all_index_shards()?;
2308        let rows = self.root_auth_fec_layout_rows(&shards)?;
2309        let block_provider = self.block_provider();
2310        let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
2311        for row in rows.into_iter().filter(|row| row.present) {
2312            self.collect_repair_patches_for_object(
2313                &block_provider,
2314                lazy_source,
2315                row,
2316                &mut patches,
2317            )?;
2318        }
2319        Ok(patches.into_values().collect())
2320    }
2321
2322    pub fn extract_all_to(
2323        &self,
2324        root: &std::path::Path,
2325        options: SafeExtractionOptions,
2326    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2327        let tables = self.load_payload_index_tables()?;
2328        if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
2329            return Err(FormatError::ReaderUnsupported(
2330                FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
2331            ));
2332        }
2333
2334        let dry_run = self.scan_seekable_payload(
2335            &tables,
2336            total_extraction_size_cap(self.options, self.observed_archive_bytes),
2337            NoopTarStreamObserver,
2338            false,
2339            ParityReadPolicy::RepairOnly,
2340        )?;
2341        self.validate_streamed_payload_summary(&tables, &dry_run, true, false)?;
2342
2343        let observer = TarStreamFilesystemRestoreObserver::new(root, options);
2344        let streamed = self.scan_seekable_payload(
2345            &tables,
2346            total_extraction_size_cap(self.options, self.observed_archive_bytes),
2347            observer,
2348            false,
2349            ParityReadPolicy::RepairOnly,
2350        )?;
2351        streamed
2352            .tar
2353            .members
2354            .into_iter()
2355            .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
2356            .collect()
2357    }
2358
2359    fn collect_repair_patches_for_object(
2360        &self,
2361        blocks: &impl BlockProvider,
2362        source: &SeekableBlockSource,
2363        row: FecLayoutObjectRow,
2364        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2365    ) -> Result<(), FormatError> {
2366        let (data_kind, parity_kind, data_max, parity_max) =
2367            self.fec_object_class_shape(row.object_class)?;
2368        let extent = ObjectExtent {
2369            first_block_index: row.first_block_index,
2370            data_block_count: row.data_block_count,
2371            parity_block_count: row.parity_block_count,
2372            encrypted_size: row.encrypted_size,
2373        };
2374        validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
2375
2376        let block_size = self.crypto_header.block_size as usize;
2377        let data_count = extent.data_block_count as usize;
2378        let parity_count = extent.parity_block_count as usize;
2379        let mut data_shards = Vec::with_capacity(data_count);
2380        let mut parity_shards = Vec::with_capacity(parity_count);
2381
2382        for offset in 0..data_count {
2383            let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2384            match blocks.block(block_index)? {
2385                Some(record) => {
2386                    if record.kind != data_kind {
2387                        return Err(FormatError::InvalidArchive(
2388                            "object data block has unexpected kind",
2389                        ));
2390                    }
2391                    let should_be_last = offset + 1 == data_count;
2392                    if record.is_last_data() != should_be_last {
2393                        return Err(FormatError::InvalidArchive(
2394                            "object last-data flag is not on the final data block",
2395                        ));
2396                    }
2397                    data_shards.push(Some(record.payload.clone()));
2398                }
2399                None => data_shards.push(None),
2400            }
2401        }
2402
2403        for offset in 0..parity_count {
2404            let block_index = checked_u64_add(
2405                extent.first_block_index,
2406                data_count as u64 + offset as u64,
2407                "object",
2408            )?;
2409            match blocks.block(block_index)? {
2410                Some(record) => {
2411                    if record.kind != parity_kind {
2412                        return Err(FormatError::InvalidArchive(
2413                            "object parity block has unexpected kind",
2414                        ));
2415                    }
2416                    if record.is_last_data() {
2417                        return Err(FormatError::InvalidArchive(
2418                            "object parity block has last-data flag",
2419                        ));
2420                    }
2421                    parity_shards.push(Some(record.payload.clone()));
2422                }
2423                None => parity_shards.push(None),
2424            }
2425        }
2426
2427        let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2428        for (offset, payload) in repaired_data.iter().enumerate() {
2429            if data_shards[offset].is_none() {
2430                let block_index =
2431                    checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2432                let flags = if offset + 1 == data_count { 0x01 } else { 0 };
2433                self.insert_repair_patch(
2434                    patches,
2435                    source,
2436                    block_index,
2437                    data_kind,
2438                    flags,
2439                    payload.clone(),
2440                )?;
2441            }
2442        }
2443
2444        if parity_count > 0 {
2445            let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
2446            for (offset, payload) in repaired_parity.into_iter().enumerate() {
2447                if parity_shards[offset].as_ref() != Some(&payload) {
2448                    let block_index = checked_u64_add(
2449                        extent.first_block_index,
2450                        data_count as u64 + offset as u64,
2451                        "object",
2452                    )?;
2453                    self.insert_repair_patch(
2454                        patches,
2455                        source,
2456                        block_index,
2457                        parity_kind,
2458                        0,
2459                        payload,
2460                    )?;
2461                }
2462            }
2463        }
2464
2465        Ok(())
2466    }
2467
2468    fn insert_repair_patch(
2469        &self,
2470        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2471        source: &SeekableBlockSource,
2472        block_index: u64,
2473        kind: BlockKind,
2474        flags: u8,
2475        payload: Vec<u8>,
2476    ) -> Result<(), FormatError> {
2477        let (volume_index, record_offset) = source.record_location(block_index)?;
2478        let record = BlockRecord {
2479            block_index,
2480            kind,
2481            flags,
2482            payload,
2483            record_crc32c: 0,
2484        };
2485        let patch = ArchiveRepairPatch {
2486            volume_index,
2487            block_index,
2488            record_offset,
2489            record_bytes: record.to_bytes(),
2490        };
2491        if let Some(existing) = patches.insert(block_index, patch.clone()) {
2492            if existing != patch {
2493                return Err(FormatError::InvalidArchive(
2494                    "conflicting repair patch for BlockRecord",
2495                ));
2496            }
2497        }
2498        Ok(())
2499    }
2500
2501    fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
2502        if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
2503            && self.index_root.directory_hint_shards.is_empty()
2504        {
2505            return Err(FormatError::InvalidArchive(
2506                "IndexRoot file_count requires directory hints",
2507            ));
2508        }
2509
2510        let shards = self.load_all_index_shards()?;
2511        let mut file_count = 0u64;
2512        let mut frames = BTreeMap::<u64, FrameEntry>::new();
2513        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2514
2515        for shard in &shards {
2516            file_count = file_count
2517                .checked_add(shard.files.len() as u64)
2518                .ok_or(FormatError::InvalidArchive("file count overflow"))?;
2519            for frame in &shard.frames {
2520                if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
2521                    if existing != *frame {
2522                        return Err(FormatError::InvalidArchive(
2523                            "duplicate FrameEntry rows do not match",
2524                        ));
2525                    }
2526                }
2527            }
2528            for envelope in &shard.envelopes {
2529                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2530                {
2531                    if existing != *envelope {
2532                        return Err(FormatError::InvalidArchive(
2533                            "duplicate EnvelopeEntry rows do not match",
2534                        ));
2535                    }
2536                }
2537            }
2538        }
2539        validate_global_file_table_order(&shards)?;
2540
2541        if file_count != self.index_root.header.file_count {
2542            return Err(FormatError::InvalidArchive(
2543                "IndexRoot file_count does not match decoded shards",
2544            ));
2545        }
2546        verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
2547        verify_dense_keys(
2548            &envelopes,
2549            self.index_root.header.envelope_count,
2550            "EnvelopeEntry",
2551        )?;
2552        validate_envelope_frame_coverage(&frames, &envelopes)?;
2553        self.validate_encrypted_object_block_ranges(&envelopes)?;
2554
2555        let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
2556            sum.checked_add(envelope.data_block_count as u64)
2557                .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2558        })?;
2559        if payload_block_count != self.index_root.header.payload_block_count {
2560            return Err(FormatError::InvalidArchive(
2561                "IndexRoot payload_block_count does not match envelopes",
2562            ));
2563        }
2564
2565        Ok(PayloadIndexTables {
2566            shards,
2567            file_count,
2568            frames,
2569            envelopes,
2570        })
2571    }
2572
2573    fn scan_seekable_payload<O: TarStreamObserver>(
2574        &self,
2575        tables: &PayloadIndexTables,
2576        extraction_cap: u64,
2577        observer: O,
2578        hash_content: bool,
2579        parity_policy: ParityReadPolicy,
2580    ) -> Result<StreamedPayloadSummary, FormatError> {
2581        let mut tar = TarStreamSummaryValidator::with_observer(
2582            self.crypto_header.max_path_length,
2583            extraction_cap,
2584            usize::MAX,
2585            self.index_root.header.file_count,
2586            observer,
2587        );
2588        let mut content_hasher = hash_content.then(Sha256::new);
2589        let mut streamed_frames = Vec::with_capacity(tables.frames.len());
2590        let streamed_envelopes = tables
2591            .envelopes
2592            .values()
2593            .map(|envelope| StreamedEnvelopeSummary {
2594                envelope_index: envelope.envelope_index,
2595                first_block_index: envelope.first_block_index,
2596                data_block_count: envelope.data_block_count,
2597                parity_block_count: envelope.parity_block_count,
2598                encrypted_size: envelope.encrypted_size,
2599                plaintext_size: envelope.plaintext_size,
2600                first_frame_index: envelope.first_frame_index,
2601                frame_count: envelope.frame_count,
2602            })
2603            .collect::<Vec<_>>();
2604        let mut cached_envelope_index = None;
2605        let mut cached_envelope_plaintext = Vec::new();
2606        let mut decompressor = self.new_payload_decompressor()?;
2607
2608        for frame in tables.frames.values() {
2609            let envelope =
2610                tables
2611                    .envelopes
2612                    .get(&frame.envelope_index)
2613                    .ok_or(FormatError::InvalidArchive(
2614                        "FrameEntry references missing EnvelopeEntry",
2615                    ))?;
2616            if cached_envelope_index != Some(envelope.envelope_index) {
2617                cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
2618                cached_envelope_index = Some(envelope.envelope_index);
2619            }
2620            let compressed = slice(
2621                &cached_envelope_plaintext,
2622                frame.offset_in_envelope as usize,
2623                frame.compressed_size as usize,
2624                "FrameEntry",
2625            )?;
2626            let tar_stream_offset = tar.tar_total_size();
2627            let decoded = self.decompress_payload_frame_with(
2628                &mut decompressor,
2629                compressed,
2630                frame.decompressed_size,
2631            )?;
2632            if decoded.is_empty() {
2633                return Err(FormatError::InvalidArchive(
2634                    "zstd payload frame decompressed to zero bytes",
2635                ));
2636            }
2637            if let Some(hasher) = &mut content_hasher {
2638                hasher.update(&decoded);
2639            }
2640            tar.observe(&decoded)?;
2641            streamed_frames.push(StreamedFrameSummary {
2642                frame_index: frame.frame_index,
2643                envelope_index: frame.envelope_index,
2644                offset_in_envelope: frame.offset_in_envelope,
2645                compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2646                    FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2647                })?,
2648                decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2649                    FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2650                })?,
2651                tar_stream_offset,
2652            });
2653        }
2654
2655        let mut content_sha256 = [0u8; 32];
2656        if let Some(hasher) = content_hasher {
2657            let digest = hasher.finalize();
2658            content_sha256.copy_from_slice(&digest);
2659        }
2660        Ok(StreamedPayloadSummary {
2661            tar: tar.finish()?,
2662            content_sha256,
2663            envelopes: streamed_envelopes,
2664            frames: streamed_frames,
2665        })
2666    }
2667
2668    fn validate_streamed_payload_summary(
2669        &self,
2670        tables: &PayloadIndexTables,
2671        streamed: &StreamedPayloadSummary,
2672        enforce_total_extraction_cap: bool,
2673        enforce_content_sha256: bool,
2674    ) -> Result<(), FormatError> {
2675        if enforce_total_extraction_cap
2676            && streamed.tar.total_extraction_size
2677                > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2678        {
2679            return Err(FormatError::ReaderUnsupported(
2680                "total extraction size exceeds configured cap",
2681            ));
2682        }
2683
2684        let streamed_payload_block_count =
2685            streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2686                sum.checked_add(envelope.data_block_count as u64)
2687                    .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2688            })?;
2689        if streamed_payload_block_count != self.index_root.header.payload_block_count {
2690            return Err(FormatError::InvalidArchive(
2691                "streamed payload block count does not match IndexRoot",
2692            ));
2693        }
2694
2695        if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2696            return Err(FormatError::InvalidArchive(
2697                "IndexRoot tar_total_size does not match streamed tar stream",
2698            ));
2699        }
2700        if enforce_content_sha256
2701            && streamed.content_sha256 != self.index_root.header.content_sha256
2702        {
2703            return Err(FormatError::InvalidArchive(
2704                "IndexRoot content_sha256 does not match decoded tar stream",
2705            ));
2706        }
2707
2708        let streamed_envelopes = streamed.envelope_map()?;
2709        for envelope in tables.envelopes.values() {
2710            let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2711                FormatError::InvalidArchive(
2712                    "metadata references missing streamed payload envelope",
2713                ),
2714            )?;
2715            if actual.first_block_index != envelope.first_block_index
2716                || actual.data_block_count != envelope.data_block_count
2717                || actual.parity_block_count != envelope.parity_block_count
2718                || actual.encrypted_size != envelope.encrypted_size
2719                || actual.plaintext_size != envelope.plaintext_size
2720                || actual.first_frame_index != envelope.first_frame_index
2721                || actual.frame_count != envelope.frame_count
2722            {
2723                return Err(FormatError::InvalidArchive(
2724                    "EnvelopeEntry does not match streamed payload envelope",
2725                ));
2726            }
2727        }
2728
2729        let streamed_frames = streamed.frame_map()?;
2730        for frame in tables.frames.values() {
2731            let actual =
2732                streamed_frames
2733                    .get(&frame.frame_index)
2734                    .ok_or(FormatError::InvalidArchive(
2735                        "metadata references missing streamed payload frame",
2736                    ))?;
2737            if actual.envelope_index != frame.envelope_index
2738                || actual.offset_in_envelope != frame.offset_in_envelope
2739                || actual.compressed_size != frame.compressed_size
2740                || actual.decompressed_size != frame.decompressed_size
2741                || actual.tar_stream_offset != frame.tar_stream_offset
2742                || streamed.frame_flags(actual)? != frame.flags
2743            {
2744                return Err(FormatError::InvalidArchive(
2745                    "FrameEntry does not match streamed payload frame",
2746                ));
2747            }
2748        }
2749
2750        let streamed_members = streamed.member_start_map()?;
2751        if streamed.tar.members.len() as u64 != tables.file_count {
2752            return Err(FormatError::InvalidArchive(
2753                "streamed tar member count does not match decoded shards",
2754            ));
2755        }
2756        let mut file_extents = Vec::new();
2757        let mut directory_hint_map = DirectoryHintMap::new();
2758        for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2759            let shard_row_index = u32::try_from(shard_row_index)
2760                .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2761            for idx in 0..shard.files.len() {
2762                let file = &shard.files[idx];
2763                let start =
2764                    shard
2765                        .tar_member_group_start(idx)
2766                        .ok_or(FormatError::InvalidArchive(
2767                            "FileEntry tar member start is missing",
2768                        ))?;
2769                file_extents.push((start, file.tar_member_group_size));
2770                let path = shard
2771                    .file_path(idx)
2772                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2773                let member = streamed_members
2774                    .get(&start)
2775                    .ok_or(FormatError::InvalidArchive(
2776                        "FileEntry tar member start is missing from streamed tar",
2777                    ))?;
2778                if member.path != path {
2779                    return Err(FormatError::InvalidArchive(
2780                        "tar member path does not match FileEntry path",
2781                    ));
2782                }
2783                if member.logical_size != file.file_data_size {
2784                    return Err(FormatError::InvalidArchive(
2785                        "tar member size does not match FileEntry file_data_size",
2786                    ));
2787                }
2788                if member.group_size != file.tar_member_group_size {
2789                    return Err(FormatError::InvalidArchive(
2790                        "FileEntry does not match streamed tar member",
2791                    ));
2792                }
2793                add_expected_directory_hint_rows(
2794                    &mut directory_hint_map,
2795                    shard_row_index,
2796                    path,
2797                    member.kind,
2798                );
2799            }
2800        }
2801        validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2802        if !self.index_root.directory_hint_shards.is_empty() {
2803            let hint_tables = self.load_all_directory_hint_tables()?;
2804            validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2805        }
2806
2807        Ok(())
2808    }
2809
2810    pub(crate) fn from_streamed_parts(
2811        parts: StreamedArchiveOpenParts,
2812    ) -> Result<Self, FormatError> {
2813        let limits = metadata_limits(&parts.crypto_header);
2814        let index_root_plaintext = load_metadata_object_from_parts(
2815            &parts.blocks,
2816            ObjectLoadContext::index_root(
2817                &parts.volume_header,
2818                &parts.crypto_header,
2819                &parts.subkeys,
2820                ObjectExtent {
2821                    first_block_index: parts.manifest_footer.index_root_first_block,
2822                    data_block_count: parts.manifest_footer.index_root_data_block_count,
2823                    parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2824                    encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2825                },
2826            ),
2827            parts.manifest_footer.index_root_decompressed_size,
2828        )?;
2829        let index_root = IndexRoot::parse(
2830            &index_root_plaintext,
2831            parts.crypto_header.has_dictionary != 0,
2832            limits,
2833        )?;
2834        let payload_dictionary = load_archive_dictionary(
2835            &parts.blocks,
2836            &parts.subkeys,
2837            &parts.volume_header,
2838            &parts.crypto_header,
2839            &index_root,
2840        )?;
2841
2842        Ok(Self {
2843            options: parts.options,
2844            observed_archive_bytes: parts.observed_archive_bytes,
2845            observed_volume_count: 1,
2846            subkeys: parts.subkeys,
2847            blocks: parts.blocks,
2848            lazy_blocks: None,
2849            crypto_header_bytes: parts.crypto_header_bytes,
2850            volume_header: parts.volume_header,
2851            crypto_header: parts.crypto_header,
2852            manifest_footer: parts.manifest_footer,
2853            volume_trailer: Some(parts.volume_trailer),
2854            root_auth_footer: parts.root_auth_footer,
2855            index_root,
2856            payload_dictionary,
2857        })
2858    }
2859
2860    pub(crate) fn verify_streamed_payload_summary(
2861        &self,
2862        streamed: &StreamedPayloadSummary,
2863    ) -> Result<(), FormatError> {
2864        let tables = self.load_payload_index_tables()?;
2865        self.validate_streamed_payload_summary(&tables, streamed, true, true)
2866    }
2867
2868    pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2869    where
2870        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2871    {
2872        let content_verification = self.verify_content()?;
2873        self.verify_root_auth_with_verified_content(&content_verification, verifier)
2874    }
2875
2876    pub fn verify_root_auth_with_verified_content<F>(
2877        &self,
2878        content_verification: &ArchiveContentVerification<'_>,
2879        mut verifier: F,
2880    ) -> Result<RootAuthVerification, FormatError>
2881    where
2882        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2883    {
2884        if !std::ptr::eq(content_verification.archive, self) {
2885            return Err(FormatError::InvalidArchive(
2886                "content verification does not match archive",
2887            ));
2888        }
2889        if content_verification.mode != ContentVerificationMode::Full {
2890            return Err(FormatError::ReaderUnsupported(
2891                "RootAuth verification requires full archive content verification",
2892            ));
2893        }
2894        let footer = self
2895            .root_auth_footer
2896            .as_ref()
2897            .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2898        let material = self.recompute_root_auth_material(footer)?;
2899        if material.critical_metadata_digest != footer.critical_metadata_digest
2900            || material.index_digest != footer.index_digest
2901            || material.fec_layout_digest != footer.fec_layout_digest
2902            || material.data_block_merkle_root != footer.data_block_merkle_root
2903            || material.signer_identity_digest != footer.signer_identity_digest
2904            || material.archive_root != footer.archive_root
2905            || material.total_data_block_count != footer.total_data_block_count
2906        {
2907            return Err(FormatError::InvalidArchive(
2908                "RootAuthFooter commitments do not match recomputed archive root",
2909            ));
2910        }
2911        if !verifier(footer, &material.archive_root)? {
2912            return Err(FormatError::InvalidArchive(
2913                "root-auth authenticator verification failed",
2914            ));
2915        }
2916        Ok(RootAuthVerification {
2917            format_version: footer.format_version,
2918            volume_format_rev: footer.volume_format_rev,
2919            archive_root: material.archive_root,
2920            authenticator_id: footer.authenticator_id,
2921            signer_identity_type: footer.signer_identity_type,
2922            signer_identity_bytes: footer.signer_identity_bytes.clone(),
2923            total_data_block_count: footer.total_data_block_count,
2924            diagnostics: self.root_auth_success_diagnostics(),
2925        })
2926    }
2927
2928    fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2929        parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2930            self.load_index_shard(entry)
2931        })
2932    }
2933
2934    fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2935        let block_provider = self.block_provider();
2936        let plaintext = load_metadata_object_from_parts(
2937            &block_provider,
2938            ObjectLoadContext::index_shard(
2939                &self.volume_header,
2940                &self.crypto_header,
2941                &self.subkeys,
2942                entry,
2943            ),
2944            entry.decompressed_size,
2945        )?;
2946        IndexShard::parse(&plaintext, entry, self.metadata_limits())
2947    }
2948
2949    fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2950        parallel_map_ref(
2951            &self.index_root.directory_hint_shards,
2952            self.options.jobs,
2953            |entry| self.load_directory_hint_table(entry),
2954        )
2955    }
2956
2957    fn load_directory_hint_table(
2958        &self,
2959        entry: &DirectoryHintShardEntry,
2960    ) -> Result<DirectoryHintTable, FormatError> {
2961        let block_provider = self.block_provider();
2962        let plaintext = load_metadata_object_from_parts(
2963            &block_provider,
2964            ObjectLoadContext::directory_hint(
2965                &self.volume_header,
2966                &self.crypto_header,
2967                &self.subkeys,
2968                entry,
2969            ),
2970            entry.decompressed_size,
2971        )?;
2972        DirectoryHintTable::parse(
2973            &plaintext,
2974            entry,
2975            self.index_root.header.shard_count,
2976            self.metadata_limits(),
2977        )
2978    }
2979
2980    fn load_payload_envelope(
2981        &self,
2982        envelope: &EnvelopeEntry,
2983        parity_policy: ParityReadPolicy,
2984    ) -> Result<Vec<u8>, FormatError> {
2985        let block_provider = self.block_provider();
2986        let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2987            &block_provider,
2988            ObjectLoadContext::payload(
2989                &self.volume_header,
2990                &self.crypto_header,
2991                &self.subkeys,
2992                envelope,
2993            ),
2994            parity_policy,
2995        )?;
2996        if plaintext.len() != envelope.plaintext_size as usize {
2997            return Err(FormatError::InvalidArchive(
2998                "payload envelope plaintext_size mismatch",
2999            ));
3000        }
3001        Ok(plaintext)
3002    }
3003
3004    fn locate_index_file(
3005        &self,
3006        normalized: &[u8],
3007    ) -> Result<Option<LocatedIndexFile>, FormatError> {
3008        let candidate_indexes = self
3009            .index_root
3010            .candidate_shards_for_path(normalized, self.metadata_limits())?;
3011        let mut winner: Option<LocatedIndexFile> = None;
3012
3013        for row_index in candidate_indexes {
3014            let locating =
3015                self.index_root
3016                    .shards
3017                    .get(row_index)
3018                    .ok_or(FormatError::InvalidArchive(
3019                        "candidate shard row is out of bounds",
3020                    ))?;
3021            let shard = self.load_index_shard(locating)?;
3022            if let Some(file_index) = shard.lookup_file_index(normalized) {
3023                let start =
3024                    shard
3025                        .tar_member_group_start(file_index)
3026                        .ok_or(FormatError::InvalidArchive(
3027                            "FileEntry tar member start is missing",
3028                        ))?;
3029                if winner
3030                    .as_ref()
3031                    .map(|existing| start > existing.start)
3032                    .unwrap_or(true)
3033                {
3034                    winner = Some(LocatedIndexFile {
3035                        shard,
3036                        file_index,
3037                        start,
3038                    });
3039                }
3040            }
3041        }
3042
3043        Ok(winner)
3044    }
3045
3046    fn extract_loaded_member(
3047        &self,
3048        shard: &IndexShard,
3049        file_index: usize,
3050    ) -> Result<ExtractedArchiveMember, FormatError> {
3051        let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
3052        Ok(ExtractedArchiveMember {
3053            path: utf8_path(&member.path)?,
3054            kind: member.kind,
3055            data: member.data,
3056            link_target: member
3057                .link_target
3058                .map(|target| utf8_path(&target))
3059                .transpose()?,
3060            diagnostics: member.diagnostics,
3061        })
3062    }
3063
3064    fn extract_loaded_owned_tar_member(
3065        &self,
3066        shard: &IndexShard,
3067        file_index: usize,
3068    ) -> Result<OwnedTarMember, FormatError> {
3069        self.decode_loaded_owned_tar_member(shard, file_index, true)
3070    }
3071
3072    fn stream_loaded_file_to_writer<W: Write>(
3073        &self,
3074        shard: &IndexShard,
3075        file_index: usize,
3076        writer: &mut W,
3077    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3078        let file = shard
3079            .files
3080            .get(file_index)
3081            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3082        self.validate_total_extraction_size(file.file_data_size)?;
3083        let expected_path = shard
3084            .file_path(file_index)
3085            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3086        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3087        stream_regular_tar_member_group_to_writer(
3088            &mut reader,
3089            expected_path,
3090            file.file_data_size,
3091            file.tar_member_group_size,
3092            self.crypto_header.max_path_length,
3093            writer,
3094        )
3095    }
3096
3097    fn stream_loaded_file_to_writer_with_progress<W: Write>(
3098        &self,
3099        shard: &IndexShard,
3100        file_index: usize,
3101        writer: &mut W,
3102        progress: &mut dyn ArchiveExtractProgressSink,
3103    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3104        let file = shard
3105            .files
3106            .get(file_index)
3107            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3108        self.validate_total_extraction_size(file.file_data_size)?;
3109        let expected_path = shard
3110            .file_path(file_index)
3111            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3112        let archive_path = utf8_path(expected_path)?;
3113        let mut progress_writer =
3114            ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
3115        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3116        stream_regular_tar_member_group_to_writer(
3117            &mut reader,
3118            expected_path,
3119            file.file_data_size,
3120            file.tar_member_group_size,
3121            self.crypto_header.max_path_length,
3122            &mut progress_writer,
3123        )
3124    }
3125
3126    fn stream_loaded_file_to_path(
3127        &self,
3128        shard: &IndexShard,
3129        file_index: usize,
3130        root: &std::path::Path,
3131        options: SafeExtractionOptions,
3132    ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3133        let file = shard
3134            .files
3135            .get(file_index)
3136            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3137        self.validate_total_extraction_size(file.file_data_size)?;
3138        let expected_path = shard
3139            .file_path(file_index)
3140            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3141        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3142        restore_streaming_tar_member_group(
3143            root,
3144            expected_path,
3145            file.file_data_size,
3146            file.tar_member_group_size,
3147            self.crypto_header.max_path_length,
3148            options,
3149            &mut reader,
3150        )
3151        .map_err(format_error_from_extract_error)
3152    }
3153
3154    fn extract_winning_index_entries_to(
3155        &self,
3156        shards: &[IndexShard],
3157        entries: Vec<(String, WinningIndexEntry)>,
3158        root: &std::path::Path,
3159        options: SafeExtractionOptions,
3160        _jobs: usize,
3161    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
3162        if entries.is_empty() {
3163            return Ok(Vec::new());
3164        }
3165        entries
3166            .into_iter()
3167            .map(|(path, entry)| {
3168                let shard = shards
3169                    .get(entry.shard_index)
3170                    .ok_or(FormatError::InvalidArchive(
3171                        "winning FileEntry shard is out of bounds",
3172                    ))?;
3173                let diagnostics =
3174                    self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
3175                Ok((path, diagnostics))
3176            })
3177            .collect()
3178    }
3179
3180    fn decode_loaded_owned_tar_member(
3181        &self,
3182        shard: &IndexShard,
3183        file_index: usize,
3184        enforce_extraction_cap: bool,
3185    ) -> Result<OwnedTarMember, FormatError> {
3186        let file = shard
3187            .files
3188            .get(file_index)
3189            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3190        if enforce_extraction_cap {
3191            self.validate_total_extraction_size(file.file_data_size)?;
3192        }
3193        let expected_path = shard
3194            .file_path(file_index)
3195            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3196        let frames = frame_range_for_file(shard, file)?;
3197        let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
3198        let mut decoded = Vec::new();
3199
3200        for frame in frames {
3201            let envelope = envelope_by_index(shard, frame.envelope_index)?;
3202            if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
3203                entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
3204            }
3205            let envelope_plaintext = envelope_cache
3206                .get(&envelope.envelope_index)
3207                .expect("inserted above");
3208            let compressed = slice(
3209                envelope_plaintext,
3210                frame.offset_in_envelope as usize,
3211                frame.compressed_size as usize,
3212                "FrameEntry",
3213            )?;
3214            decoded.extend_from_slice(
3215                &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
3216            );
3217        }
3218
3219        let offset = file.offset_in_first_frame_plaintext as usize;
3220        let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
3221        let group = slice(&decoded, offset, group_len, "FileEntry")?;
3222        let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
3223        if member.path != expected_path {
3224            return Err(FormatError::InvalidArchive(
3225                "tar member path does not match FileEntry path",
3226            ));
3227        }
3228        if member.logical_size != file.file_data_size {
3229            return Err(FormatError::InvalidArchive(
3230                "tar member size does not match FileEntry file_data_size",
3231            ));
3232        }
3233        Ok(member.to_owned_member())
3234    }
3235
3236    fn metadata_limits(&self) -> MetadataLimits {
3237        metadata_limits(&self.crypto_header)
3238    }
3239
3240    fn recompute_root_auth_material(
3241        &self,
3242        footer: &RootAuthFooterV1,
3243    ) -> Result<RootAuthMaterial, FormatError> {
3244        if footer.format_version != self.volume_header.format_version {
3245            return Err(FormatError::InvalidArchive(
3246                "RootAuthFooter format_version differs from authenticated VolumeHeader",
3247            ));
3248        }
3249        if footer.volume_format_rev != self.volume_header.volume_format_rev {
3250            return Err(FormatError::InvalidArchive(
3251                "RootAuthFooter volume_format_rev differs from authenticated VolumeHeader",
3252            ));
3253        }
3254        let format_version = self.volume_header.format_version;
3255        let volume_format_rev = self.volume_header.volume_format_rev;
3256        let footer_length = footer.footer_length()?;
3257        let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
3258            format_version,
3259            volume_format_rev,
3260            footer.authenticator_id,
3261            footer.signer_identity_type,
3262            &footer.signer_identity_bytes,
3263            u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3264                FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3265            })?,
3266            footer_length,
3267        )?;
3268        let signer_identity_digest =
3269            signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3270        let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
3271        let crypto_pre_hmac_len = self
3272            .crypto_header_bytes
3273            .len()
3274            .checked_sub(CRYPTO_HEADER_HMAC_LEN)
3275            .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
3276        let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
3277            archive_uuid: self.volume_header.archive_uuid,
3278            session_id: self.volume_header.session_id,
3279            format_version,
3280            volume_format_rev,
3281            stripe_width: self.crypto_header.stripe_width,
3282            total_volumes: self.manifest_footer.total_volumes,
3283            compression_algo: self.crypto_header.compression_algo,
3284            aead_algo: self.crypto_header.aead_algo,
3285            fec_algo: self.crypto_header.fec_algo,
3286            kdf_algo: self.crypto_header.kdf_algo,
3287            crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
3288            chunk_size: self.crypto_header.chunk_size,
3289            envelope_target_size: self.crypto_header.envelope_target_size,
3290            block_size: self.crypto_header.block_size,
3291            fec_data_shards: self.crypto_header.fec_data_shards,
3292            fec_parity_shards: self.crypto_header.fec_parity_shards,
3293            index_fec_data_shards: self.crypto_header.index_fec_data_shards,
3294            index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
3295            index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
3296            index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
3297            volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
3298            bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
3299            has_dictionary: self.crypto_header.has_dictionary,
3300            manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
3301            index_root_first_block: self.manifest_footer.index_root_first_block,
3302            index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
3303            index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
3304            index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
3305            index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
3306            root_auth_descriptor_digest,
3307        })?;
3308        let index_root_plaintext = self.index_root.to_bytes();
3309        let index_digest =
3310            index_digest_for_revision(format_version, volume_format_rev, &index_root_plaintext)?;
3311        let shards = self.load_all_index_shards()?;
3312        let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
3313        let fec_layout_digest =
3314            fec_layout_digest_for_revision(format_version, volume_format_rev, &fec_layout_rows)?;
3315        let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
3316        let total_data_block_count = u64::try_from(data_leaves.len())
3317            .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
3318        let data_block_merkle_root =
3319            data_block_merkle_root_for_revision(format_version, volume_format_rev, &data_leaves)?;
3320        let archive_root = archive_root_for_revision(ArchiveRootInputs {
3321            archive_uuid: self.volume_header.archive_uuid,
3322            session_id: self.volume_header.session_id,
3323            format_version,
3324            volume_format_rev,
3325            compression_algo: self.crypto_header.compression_algo,
3326            aead_algo: self.crypto_header.aead_algo,
3327            fec_algo: self.crypto_header.fec_algo,
3328            kdf_algo: self.crypto_header.kdf_algo,
3329            critical_metadata_digest,
3330            index_digest,
3331            fec_layout_digest,
3332            total_data_block_count,
3333            data_block_merkle_root,
3334            root_auth_descriptor_digest,
3335            signer_identity_digest,
3336        })?;
3337        Ok(RootAuthMaterial {
3338            critical_metadata_digest,
3339            index_digest,
3340            fec_layout_digest,
3341            data_block_merkle_root,
3342            signer_identity_digest,
3343            archive_root,
3344            total_data_block_count,
3345        })
3346    }
3347
3348    fn root_auth_fec_layout_rows(
3349        &self,
3350        shards: &[IndexShard],
3351    ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
3352        let mut rows = Vec::new();
3353        rows.push(FecLayoutObjectRow {
3354            object_class: 1,
3355            present: true,
3356            object_id: 0,
3357            first_block_index: self.manifest_footer.index_root_first_block,
3358            data_block_count: self.manifest_footer.index_root_data_block_count,
3359            parity_block_count: self.manifest_footer.index_root_parity_block_count,
3360            encrypted_size: self.manifest_footer.index_root_encrypted_size,
3361            plain_size: self.manifest_footer.index_root_decompressed_size,
3362        });
3363        if self.crypto_header.has_dictionary != 0 {
3364            rows.push(FecLayoutObjectRow {
3365                object_class: 2,
3366                present: true,
3367                object_id: 0,
3368                first_block_index: self.index_root.header.dictionary_first_block,
3369                data_block_count: self.index_root.header.dictionary_data_block_count,
3370                parity_block_count: self.index_root.header.dictionary_parity_block_count,
3371                encrypted_size: self.index_root.header.dictionary_encrypted_size,
3372                plain_size: self.index_root.header.dictionary_decompressed_size,
3373            });
3374        } else {
3375            rows.push(FecLayoutObjectRow {
3376                object_class: 2,
3377                present: false,
3378                object_id: 0,
3379                first_block_index: 0,
3380                data_block_count: 0,
3381                parity_block_count: 0,
3382                encrypted_size: 0,
3383                plain_size: 0,
3384            });
3385        }
3386        for entry in &self.index_root.shards {
3387            rows.push(FecLayoutObjectRow {
3388                object_class: 3,
3389                present: true,
3390                object_id: entry.shard_index,
3391                first_block_index: entry.first_block_index,
3392                data_block_count: entry.data_block_count,
3393                parity_block_count: entry.parity_block_count,
3394                encrypted_size: entry.encrypted_size,
3395                plain_size: entry.decompressed_size,
3396            });
3397        }
3398        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
3399        for shard in shards {
3400            for envelope in &shard.envelopes {
3401                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
3402                {
3403                    if existing != *envelope {
3404                        return Err(FormatError::InvalidArchive(
3405                            "duplicate EnvelopeEntry rows do not match",
3406                        ));
3407                    }
3408                }
3409            }
3410        }
3411        for envelope in envelopes.values() {
3412            rows.push(FecLayoutObjectRow {
3413                object_class: 4,
3414                present: true,
3415                object_id: envelope.envelope_index,
3416                first_block_index: envelope.first_block_index,
3417                data_block_count: envelope.data_block_count,
3418                parity_block_count: envelope.parity_block_count,
3419                encrypted_size: envelope.encrypted_size,
3420                plain_size: envelope.plaintext_size,
3421            });
3422        }
3423        for entry in &self.index_root.directory_hint_shards {
3424            rows.push(FecLayoutObjectRow {
3425                object_class: 5,
3426                present: true,
3427                object_id: entry.hint_shard_index,
3428                first_block_index: entry.first_block_index,
3429                data_block_count: entry.data_block_count,
3430                parity_block_count: entry.parity_block_count,
3431                encrypted_size: entry.encrypted_size,
3432                plain_size: entry.decompressed_size,
3433            });
3434        }
3435        Ok(rows)
3436    }
3437
3438    fn fec_object_class_shape(
3439        &self,
3440        object_class: u8,
3441    ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
3442        match object_class {
3443            1 => Ok((
3444                BlockKind::IndexRootData,
3445                BlockKind::IndexRootParity,
3446                self.crypto_header.index_root_fec_data_shards,
3447                self.crypto_header.index_root_fec_parity_shards,
3448            )),
3449            2 => Ok((
3450                BlockKind::DictionaryData,
3451                BlockKind::DictionaryParity,
3452                self.crypto_header.index_root_fec_data_shards,
3453                self.crypto_header.index_root_fec_parity_shards,
3454            )),
3455            3 => Ok((
3456                BlockKind::IndexShardData,
3457                BlockKind::IndexShardParity,
3458                self.crypto_header.index_fec_data_shards,
3459                self.crypto_header.index_fec_parity_shards,
3460            )),
3461            4 => Ok((
3462                BlockKind::PayloadData,
3463                BlockKind::PayloadParity,
3464                self.crypto_header.fec_data_shards,
3465                self.crypto_header.fec_parity_shards,
3466            )),
3467            5 => Ok((
3468                BlockKind::DirectoryHintData,
3469                BlockKind::DirectoryHintParity,
3470                self.crypto_header.index_fec_data_shards,
3471                self.crypto_header.index_fec_parity_shards,
3472            )),
3473            _ => Err(FormatError::InvalidArchive(
3474                "unknown root-auth FEC row class",
3475            )),
3476        }
3477    }
3478
3479    fn root_auth_data_block_leaves(
3480        &self,
3481        rows: &[FecLayoutObjectRow],
3482    ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
3483        let block_provider = self.block_provider();
3484        let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
3485        let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
3486            let row = **row;
3487            let (data_kind, parity_kind, data_max, parity_max) =
3488                self.fec_object_class_shape(row.object_class)?;
3489            let extent = ObjectExtent {
3490                first_block_index: row.first_block_index,
3491                data_block_count: row.data_block_count,
3492                parity_block_count: row.parity_block_count,
3493                encrypted_size: row.encrypted_size,
3494            };
3495            let repaired = load_repaired_object_data_shards_from_parts(
3496                &block_provider,
3497                &self.crypto_header,
3498                extent,
3499                data_kind,
3500                parity_kind,
3501                data_max,
3502                parity_max,
3503            )?;
3504            let mut leaves = Vec::new();
3505            for (offset, payload) in repaired.into_iter().enumerate() {
3506                leaves.push(DataBlockMerkleLeaf {
3507                    block_index: checked_u64_add(
3508                        row.first_block_index,
3509                        offset as u64,
3510                        "root-auth data block",
3511                    )?,
3512                    kind: data_kind,
3513                    flags: if offset + 1 == row.data_block_count as usize {
3514                        0x01
3515                    } else {
3516                        0
3517                    },
3518                    payload,
3519                });
3520            }
3521            Ok(leaves)
3522        })?;
3523        let mut leaves = Vec::new();
3524        for mut chunk in chunks {
3525            leaves.append(&mut chunk);
3526        }
3527        leaves.sort_by_key(|leaf| leaf.block_index);
3528        Ok(leaves)
3529    }
3530
3531    fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
3532        let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
3533        if logical_size > cap {
3534            return Err(FormatError::ReaderUnsupported(
3535                "total extraction size exceeds configured cap",
3536            ));
3537        }
3538        Ok(())
3539    }
3540
3541    fn decompress_payload_frame(
3542        &self,
3543        compressed: &[u8],
3544        decompressed_size: u32,
3545    ) -> Result<Vec<u8>, FormatError> {
3546        let mut decompressor = self.new_payload_decompressor()?;
3547        self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
3548    }
3549
3550    fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
3551        match &self.payload_dictionary {
3552            Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
3553            None => zstd::bulk::Decompressor::new(),
3554        }
3555        .map_err(|_| FormatError::ZstdDecompressionFailure)
3556    }
3557
3558    fn decompress_payload_frame_with(
3559        &self,
3560        decompressor: &mut zstd::bulk::Decompressor<'static>,
3561        compressed: &[u8],
3562        decompressed_size: u32,
3563    ) -> Result<Vec<u8>, FormatError> {
3564        validate_exact_zstd_frame(compressed)?;
3565        let expected = decompressed_size as usize;
3566        let decoded = decompressor
3567            .decompress(compressed, expected)
3568            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
3569        if decoded.len() != expected {
3570            return Err(FormatError::ZstdDecompressedSizeMismatch {
3571                expected,
3572                actual: decoded.len(),
3573            });
3574        }
3575        Ok(decoded)
3576    }
3577
3578    fn validate_encrypted_object_block_ranges(
3579        &self,
3580        envelopes: &BTreeMap<u64, EnvelopeEntry>,
3581    ) -> Result<(), FormatError> {
3582        let mut ranges = Vec::new();
3583        ranges.push(object_block_range(
3584            self.manifest_footer.index_root_first_block,
3585            self.manifest_footer.index_root_data_block_count,
3586            self.manifest_footer.index_root_parity_block_count,
3587            "IndexRoot",
3588        )?);
3589        for shard in &self.index_root.shards {
3590            ranges.push(object_block_range(
3591                shard.first_block_index,
3592                shard.data_block_count,
3593                shard.parity_block_count,
3594                "IndexShard",
3595            )?);
3596        }
3597        for hint in &self.index_root.directory_hint_shards {
3598            ranges.push(object_block_range(
3599                hint.first_block_index,
3600                hint.data_block_count,
3601                hint.parity_block_count,
3602                "DirectoryHintShardEntry",
3603            )?);
3604        }
3605        if self.crypto_header.has_dictionary != 0 {
3606            ranges.push(object_block_range(
3607                self.index_root.header.dictionary_first_block,
3608                self.index_root.header.dictionary_data_block_count,
3609                self.index_root.header.dictionary_parity_block_count,
3610                "dictionary",
3611            )?);
3612        }
3613        for envelope in envelopes.values() {
3614            ranges.push(object_block_range(
3615                envelope.first_block_index,
3616                envelope.data_block_count,
3617                envelope.parity_block_count,
3618                "EnvelopeEntry",
3619            )?);
3620        }
3621        validate_non_overlapping_object_ranges(&mut ranges)?;
3622        if let Some(source) = &self.lazy_blocks {
3623            if source.is_complete_volume_set() {
3624                validate_exact_coverage_ranges_u64(
3625                    &mut ranges,
3626                    source.total_block_count()?,
3627                    "encrypted object block ranges do not cover complete archive exactly",
3628                )?;
3629            }
3630        }
3631        Ok(())
3632    }
3633}
3634
3635impl<'a> DecodedTarMemberGroupReader<'a> {
3636    fn new(
3637        archive: &'a OpenedArchive,
3638        shard: &'a IndexShard,
3639        file: &'a FileEntry,
3640    ) -> Result<Self, FormatError> {
3641        Ok(Self {
3642            archive,
3643            shard,
3644            file,
3645            decompressor: archive.new_payload_decompressor()?,
3646            next_frame_offset: 0,
3647            cached_envelope_index: None,
3648            cached_envelope_plaintext: Vec::new(),
3649            current_frame: Vec::new(),
3650            current_frame_offset: 0,
3651            remaining_group_bytes: file.tar_member_group_size,
3652        })
3653    }
3654
3655    fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3656        while self.current_frame_offset >= self.current_frame.len() {
3657            if self.next_frame_offset >= self.file.frame_count as u64 {
3658                return Err(
3659                    FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3660                );
3661            }
3662            let frame_index = self
3663                .file
3664                .first_frame_index
3665                .checked_add(self.next_frame_offset)
3666                .ok_or(FormatError::InvalidArchive(
3667                    "FileEntry frame range overflow",
3668                ))?;
3669            let frame = frame_by_index(self.shard, frame_index)?;
3670            let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3671            if self.cached_envelope_index != Some(envelope.envelope_index) {
3672                self.cached_envelope_plaintext = self
3673                    .archive
3674                    .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3675                self.cached_envelope_index = Some(envelope.envelope_index);
3676            }
3677            let compressed = slice(
3678                &self.cached_envelope_plaintext,
3679                frame.offset_in_envelope as usize,
3680                frame.compressed_size as usize,
3681                "FrameEntry",
3682            )?;
3683            let decoded = self.archive.decompress_payload_frame_with(
3684                &mut self.decompressor,
3685                compressed,
3686                frame.decompressed_size,
3687            )?;
3688            let offset = if self.next_frame_offset == 0 {
3689                self.file.offset_in_first_frame_plaintext as usize
3690            } else {
3691                0
3692            };
3693            if offset > decoded.len() {
3694                return Err(FormatError::InvalidArchive(
3695                    "offset in first frame is outside the first referenced frame",
3696                )
3697                .into());
3698            }
3699            self.next_frame_offset += 1;
3700            self.current_frame = decoded;
3701            self.current_frame_offset = offset;
3702        }
3703        Ok(())
3704    }
3705}
3706
3707impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3708    fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3709        if buf.is_empty() {
3710            return Ok(0);
3711        }
3712        if self.remaining_group_bytes == 0 {
3713            return Ok(0);
3714        }
3715        self.ensure_frame_available()?;
3716        let available = self.current_frame.len() - self.current_frame_offset;
3717        let len = available
3718            .min(buf.len())
3719            .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3720        if len == 0 {
3721            return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3722        }
3723        buf[..len].copy_from_slice(
3724            &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3725        );
3726        self.current_frame_offset += len;
3727        self.remaining_group_bytes -= len as u64;
3728        Ok(len)
3729    }
3730}
3731
3732fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3733    shard
3734        .frames
3735        .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3736        .map(|idx| &shard.frames[idx])
3737        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3738}
3739
3740fn envelope_by_index(
3741    shard: &IndexShard,
3742    envelope_index: u64,
3743) -> Result<&EnvelopeEntry, FormatError> {
3744    shard
3745        .envelopes
3746        .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3747        .map(|idx| &shard.envelopes[idx])
3748        .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3749}
3750
3751fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3752    match err {
3753        ExtractError::Format(err) => err,
3754        ExtractError::Output(_) => {
3755            FormatError::FilesystemExtractionFailed("failed to write regular file")
3756        }
3757    }
3758}
3759
3760fn final_index_entry_winners(
3761    shards: &[IndexShard],
3762) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3763    let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3764    for (shard_index, shard) in shards.iter().enumerate() {
3765        for (idx, file) in shard.files.iter().enumerate() {
3766            let path = utf8_path(
3767                shard
3768                    .file_path(idx)
3769                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3770            )?;
3771            let start = shard
3772                .tar_member_group_start(idx)
3773                .ok_or(FormatError::InvalidArchive(
3774                    "FileEntry tar member start is missing",
3775                ))?;
3776            if let Some(winner) = final_entries.get_mut(&path) {
3777                if start >= winner.start {
3778                    winner.start = start;
3779                    winner.file_data_size = file.file_data_size;
3780                    winner.mtime = file.mtime;
3781                    winner.shard_index = shard_index;
3782                    winner.file_index = idx;
3783                }
3784            } else {
3785                final_entries.insert(
3786                    path,
3787                    WinningIndexEntry {
3788                        start,
3789                        file_data_size: file.file_data_size,
3790                        mtime: file.mtime,
3791                        shard_index,
3792                        file_index: idx,
3793                    },
3794                );
3795            }
3796        }
3797    }
3798    Ok(final_entries)
3799}
3800
3801fn archive_index_entry_from_loaded_file(
3802    shard: &IndexShard,
3803    file_index: usize,
3804) -> Result<ArchiveIndexEntry, FormatError> {
3805    let path = utf8_path(
3806        shard
3807            .file_path(file_index)
3808            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3809    )?;
3810    archive_index_entry_from_loaded_file_with_path(path, shard, file_index)
3811}
3812
3813fn archive_index_entry_from_loaded_file_with_path(
3814    path: String,
3815    shard: &IndexShard,
3816    file_index: usize,
3817) -> Result<ArchiveIndexEntry, FormatError> {
3818    let file = shard
3819        .files
3820        .get(file_index)
3821        .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3822    let layout = archive_index_entry_layout(shard, file)?;
3823    Ok(ArchiveIndexEntry {
3824        name: archive_entry_name(&path),
3825        path,
3826        file_data_size: file.file_data_size,
3827        kind: file.kind,
3828        mode: file.mode,
3829        mtime: file.mtime,
3830        path_hash: file.path_hash,
3831        tar_member_group_size: file.tar_member_group_size,
3832        first_frame_index: file.first_frame_index,
3833        frame_count: file.frame_count,
3834        offset_in_first_frame_plaintext: file.offset_in_first_frame_plaintext,
3835        layout,
3836    })
3837}
3838
3839fn archive_index_entry_layout(
3840    shard: &IndexShard,
3841    file: &FileEntry,
3842) -> Result<ArchiveIndexEntryLayout, FormatError> {
3843    let frames = frame_range_for_file(shard, file)?;
3844    if let [frame] = frames {
3845        let envelope = envelope_by_index(shard, frame.envelope_index)?;
3846        return Ok(ArchiveIndexEntryLayout {
3847            compressed_size: frame.compressed_size as u64,
3848            decompressed_frame_size: frame.decompressed_size as u64,
3849            envelope_count: 1,
3850            first_envelope_index: Some(envelope.envelope_index),
3851            last_envelope_index: Some(envelope.envelope_index),
3852            first_payload_block_index: Some(envelope.first_block_index),
3853            payload_data_block_count: envelope.data_block_count as u64,
3854            payload_parity_block_count: envelope.parity_block_count as u64,
3855            payload_encrypted_size: envelope.encrypted_size as u64,
3856        });
3857    }
3858
3859    let mut compressed_size = 0u64;
3860    let mut decompressed_frame_size = 0u64;
3861    let mut envelope_indexes = BTreeSet::new();
3862
3863    for frame in frames {
3864        compressed_size = checked_u64_add(
3865            compressed_size,
3866            frame.compressed_size as u64,
3867            "ArchiveIndexEntry.compressed_size",
3868        )?;
3869        decompressed_frame_size = checked_u64_add(
3870            decompressed_frame_size,
3871            frame.decompressed_size as u64,
3872            "ArchiveIndexEntry.decompressed_frame_size",
3873        )?;
3874        envelope_indexes.insert(frame.envelope_index);
3875    }
3876
3877    let mut first_payload_block_index = None::<u64>;
3878    let mut payload_data_block_count = 0u64;
3879    let mut payload_parity_block_count = 0u64;
3880    let mut payload_encrypted_size = 0u64;
3881
3882    for envelope_index in &envelope_indexes {
3883        let envelope = envelope_by_index(shard, *envelope_index)?;
3884        first_payload_block_index = Some(
3885            first_payload_block_index
3886                .map(|existing| existing.min(envelope.first_block_index))
3887                .unwrap_or(envelope.first_block_index),
3888        );
3889        payload_data_block_count = checked_u64_add(
3890            payload_data_block_count,
3891            envelope.data_block_count as u64,
3892            "ArchiveIndexEntry.payload_data_block_count",
3893        )?;
3894        payload_parity_block_count = checked_u64_add(
3895            payload_parity_block_count,
3896            envelope.parity_block_count as u64,
3897            "ArchiveIndexEntry.payload_parity_block_count",
3898        )?;
3899        payload_encrypted_size = checked_u64_add(
3900            payload_encrypted_size,
3901            envelope.encrypted_size as u64,
3902            "ArchiveIndexEntry.payload_encrypted_size",
3903        )?;
3904    }
3905
3906    Ok(ArchiveIndexEntryLayout {
3907        compressed_size,
3908        decompressed_frame_size,
3909        envelope_count: u32::try_from(envelope_indexes.len()).map_err(|_| {
3910            FormatError::InvalidArchive("ArchiveIndexEntry envelope count overflow")
3911        })?,
3912        first_envelope_index: envelope_indexes.iter().next().copied(),
3913        last_envelope_index: envelope_indexes.iter().next_back().copied(),
3914        first_payload_block_index,
3915        payload_data_block_count,
3916        payload_parity_block_count,
3917        payload_encrypted_size,
3918    })
3919}
3920
3921fn archive_entry_name(path: &str) -> String {
3922    path.rsplit('/').next().unwrap_or(path).to_owned()
3923}
3924
3925#[derive(Debug)]
3926struct ParsedSeekableVolume {
3927    volume_header: VolumeHeader,
3928    crypto_header: CryptoHeaderFixed,
3929    crypto_header_bytes: Vec<u8>,
3930    key_wrap_table_bytes: Option<Vec<u8>>,
3931    subkeys: Subkeys,
3932    manifest_footer: Option<ManifestFooter>,
3933    manifest_footer_error: Option<FormatError>,
3934    root_auth_footer: Option<RootAuthFooterV1>,
3935    root_auth_footer_bytes: Option<Vec<u8>>,
3936    volume_trailer: VolumeTrailer,
3937    blocks: BTreeMap<u64, BlockRecord>,
3938    erased_block_indices: BTreeSet<u64>,
3939}
3940
3941struct ParsedSeekableReadAtVolume {
3942    reader: Arc<dyn ArchiveReadAt>,
3943    volume_header: VolumeHeader,
3944    crypto_header: CryptoHeaderFixed,
3945    crypto_header_bytes: Vec<u8>,
3946    key_wrap_table_bytes: Option<Vec<u8>>,
3947    subkeys: Subkeys,
3948    manifest_footer: Option<ManifestFooter>,
3949    manifest_footer_error: Option<FormatError>,
3950    root_auth_footer: Option<RootAuthFooterV1>,
3951    root_auth_footer_bytes: Option<Vec<u8>>,
3952    volume_trailer: VolumeTrailer,
3953    block_records_start: u64,
3954}
3955
3956struct ParsedOpenPrefix {
3957    volume_header: VolumeHeader,
3958    crypto_header: CryptoHeaderFixed,
3959    crypto_header_bytes: Vec<u8>,
3960    key_wrap_table_bytes: Option<Vec<u8>>,
3961    block_records_start: u64,
3962    subkeys: Subkeys,
3963}
3964
3965struct ParsedReadAtOpenPrefix {
3966    volume_header: VolumeHeader,
3967    crypto_header: CryptoHeaderFixed,
3968    crypto_header_bytes: Vec<u8>,
3969    key_wrap_table_bytes: Option<Vec<u8>>,
3970    block_records_start: u64,
3971    subkeys: Subkeys,
3972}
3973
3974pub(crate) struct StartupKeyWrapTable {
3975    pub(crate) table: KeyWrapTableV1,
3976    pub(crate) bytes: Vec<u8>,
3977    pub(crate) block_records_start: u64,
3978}
3979
3980pub(crate) fn startup_block_records_start(
3981    volume_header: &VolumeHeader,
3982    kdf_params: &KdfParams,
3983    read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3984) -> Result<u64, FormatError> {
3985    Ok(
3986        startup_key_wrap_table(volume_header, kdf_params, read_key_wrap_table)?
3987            .map(|startup| startup.block_records_start)
3988            .unwrap_or_else(|| {
3989                volume_header.crypto_header_offset as u64
3990                    + volume_header.crypto_header_length as u64
3991            }),
3992    )
3993}
3994
3995pub(crate) fn startup_key_wrap_table(
3996    volume_header: &VolumeHeader,
3997    kdf_params: &KdfParams,
3998    mut read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3999) -> Result<Option<StartupKeyWrapTable>, FormatError> {
4000    let crypto_end = checked_u64_add(
4001        volume_header.crypto_header_offset as u64,
4002        volume_header.crypto_header_length as u64,
4003        "CryptoHeader",
4004    )?;
4005    let &KdfParams::RecipientWrap {
4006        key_wrap_table_length,
4007        ..
4008    } = kdf_params
4009    else {
4010        return Ok(None);
4011    };
4012    if volume_header.volume_format_rev != VOLUME_FORMAT_REV_44 {
4013        return Err(FormatError::InvalidArchive(
4014            "RecipientWrap KdfParams require volume_format_rev 44",
4015        ));
4016    }
4017    let key_wrap_table_length_usize =
4018        to_usize(u64::from(key_wrap_table_length), "KeyWrapTableV1 length")?;
4019    let key_wrap_table_bytes = read_key_wrap_table(crypto_end, key_wrap_table_length_usize)?;
4020    Ok(Some(parse_startup_key_wrap_table_bytes(
4021        volume_header,
4022        kdf_params,
4023        key_wrap_table_bytes,
4024    )?))
4025}
4026
4027pub(crate) fn parse_startup_key_wrap_table_bytes(
4028    volume_header: &VolumeHeader,
4029    kdf_params: &KdfParams,
4030    key_wrap_table_bytes: Vec<u8>,
4031) -> Result<StartupKeyWrapTable, FormatError> {
4032    let crypto_end = checked_u64_add(
4033        volume_header.crypto_header_offset as u64,
4034        volume_header.crypto_header_length as u64,
4035        "CryptoHeader",
4036    )?;
4037    let KdfParams::RecipientWrap {
4038        key_wrap_table_length,
4039        key_wrap_table_record_count,
4040        key_wrap_table_digest,
4041        ..
4042    } = kdf_params
4043    else {
4044        return Err(FormatError::KeyMaterialMismatch);
4045    };
4046    let key_wrap_table = KeyWrapTableV1::parse(
4047        &key_wrap_table_bytes,
4048        &volume_header.archive_uuid,
4049        &volume_header.session_id,
4050        *key_wrap_table_length,
4051        *key_wrap_table_record_count,
4052    )?;
4053    if compute_key_wrap_table_digest(*key_wrap_table_length, &key_wrap_table_bytes)
4054        != *key_wrap_table_digest
4055    {
4056        return Err(FormatError::IntegrityDigestMismatch {
4057            structure: "KeyWrapTableV1",
4058        });
4059    }
4060    let block_records_start = checked_u64_add(
4061        crypto_end,
4062        key_wrap_table.table_length as u64,
4063        "KeyWrapTableV1",
4064    )?;
4065    Ok(StartupKeyWrapTable {
4066        table: key_wrap_table,
4067        bytes: key_wrap_table_bytes,
4068        block_records_start,
4069    })
4070}
4071
4072fn parse_seekable_volume(
4073    bytes: &[u8],
4074    master_key: &MasterKey,
4075    options: ReaderOptions,
4076) -> Result<ParsedSeekableVolume, FormatError> {
4077    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
4078        return Err(FormatError::InvalidLength {
4079            structure: "archive",
4080            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4081            actual: bytes.len(),
4082        });
4083    }
4084
4085    let prefix = match parse_open_prefix(bytes, master_key) {
4086        Ok(prefix) => prefix,
4087        Err(prefix_err) => {
4088            if matches!(
4089                prefix_err,
4090                FormatError::UnsupportedVolumeFormatRevision { .. }
4091            ) {
4092                return Err(prefix_err);
4093            }
4094            if matches!(prefix_err, FormatError::KeyMaterialMismatch)
4095                && prefix_uses_recipient_wrap(bytes)
4096            {
4097                return Err(prefix_err);
4098            }
4099            return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
4100                .or(Err(prefix_err));
4101        }
4102    };
4103    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4104    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4105        Ok(parsed) => Ok(parsed),
4106        Err(prefix_err) => {
4107            match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
4108                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4109                    Ok(recovered)
4110                }
4111                Ok(_) | Err(_) => Err(prefix_err),
4112            }
4113        }
4114    }
4115}
4116
4117fn parse_seekable_volume_with_recipient_wrap_resolver<F>(
4118    bytes: &[u8],
4119    resolver: &mut F,
4120    options: ReaderOptions,
4121) -> Result<ParsedSeekableVolume, FormatError>
4122where
4123    F: FnMut(
4124        RecipientWrapRecordContext<'_>,
4125    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4126{
4127    let prefix = match parse_open_prefix_with_recipient_wrap_resolver(bytes, resolver) {
4128        Ok(prefix) => prefix,
4129        Err(prefix_err) => {
4130            if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4131                return Err(prefix_err);
4132            }
4133            return parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4134                bytes, resolver, options,
4135            )
4136            .or(Err(prefix_err));
4137        }
4138    };
4139    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4140    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4141        Ok(parsed) => Ok(parsed),
4142        Err(prefix_err) => {
4143            match parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4144                bytes, resolver, options,
4145            ) {
4146                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4147                    Ok(recovered)
4148                }
4149                Ok(_) | Err(_) => Err(prefix_err),
4150            }
4151        }
4152    }
4153}
4154
4155fn parse_seekable_volume_with_prefix(
4156    bytes: &[u8],
4157    prefix: ParsedOpenPrefix,
4158    options: ReaderOptions,
4159) -> Result<ParsedSeekableVolume, FormatError> {
4160    let ParsedOpenPrefix {
4161        volume_header,
4162        crypto_header,
4163        crypto_header_bytes,
4164        key_wrap_table_bytes,
4165        block_records_start,
4166        subkeys,
4167    } = prefix;
4168    let crypto_bytes = crypto_header_bytes.as_slice();
4169
4170    let terminal = locate_v41_terminal(
4171        bytes,
4172        KeyHoldingTerminalContext {
4173            subkeys: &subkeys,
4174            volume_header: &volume_header,
4175            crypto_header: &crypto_header,
4176            crypto_header_bytes: crypto_bytes,
4177        },
4178        options,
4179    )?;
4180    finish_parse_seekable_volume(
4181        bytes,
4182        volume_header,
4183        crypto_header,
4184        crypto_header_bytes,
4185        key_wrap_table_bytes,
4186        block_records_start,
4187        subkeys,
4188        terminal,
4189    )
4190}
4191
4192fn parse_seekable_volume_from_recovered_terminal(
4193    bytes: &[u8],
4194    master_key: &MasterKey,
4195    options: ReaderOptions,
4196) -> Result<ParsedSeekableVolume, FormatError> {
4197    let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
4198    parse_volume_format_dispatch(&authority.volume_header)?;
4199    let startup_key_wrap_table = startup_key_wrap_table(
4200        &authority.volume_header,
4201        &authority.kdf_params,
4202        |start, length| {
4203            let start = to_usize(start, "KeyWrapTableV1")?;
4204            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4205        },
4206    )?;
4207    let crypto_end = checked_u64_add(
4208        authority.volume_header.crypto_header_offset as u64,
4209        authority.volume_header.crypto_header_length as u64,
4210        "CryptoHeader",
4211    )?;
4212    let (key_wrap_table_bytes, block_records_start) = startup_key_wrap_table
4213        .map(|startup| (Some(startup.bytes), startup.block_records_start))
4214        .unwrap_or((None, crypto_end));
4215    finish_parse_seekable_volume(
4216        bytes,
4217        authority.volume_header,
4218        authority.crypto_header,
4219        authority.crypto_header_bytes,
4220        key_wrap_table_bytes,
4221        block_records_start,
4222        authority.subkeys,
4223        authority.terminal,
4224    )
4225}
4226
4227fn parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4228    bytes: &[u8],
4229    resolver: &mut F,
4230    options: ReaderOptions,
4231) -> Result<ParsedSeekableVolume, FormatError>
4232where
4233    F: FnMut(
4234        RecipientWrapRecordContext<'_>,
4235    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4236{
4237    let authority = locate_v41_recipient_wrap_terminal_authority(bytes, resolver, options)?;
4238    finish_parse_seekable_volume(
4239        bytes,
4240        authority.volume_header,
4241        authority.crypto_header,
4242        authority.crypto_header_bytes,
4243        Some(authority.key_wrap_table_bytes),
4244        authority.block_records_start,
4245        authority.subkeys,
4246        authority.terminal,
4247    )
4248}
4249
4250fn recipient_wrap_prefix_error_precludes_recovery(error: &FormatError) -> bool {
4251    matches!(
4252        error,
4253        FormatError::UnsupportedVolumeFormatRevision { .. }
4254            | FormatError::ReaderUnsupported(_)
4255            | FormatError::InvalidArchive(
4256                "VolumeHeader and CryptoHeader stripe_width differ"
4257                    | "fec_parity_shards does not match v41 compute_parity"
4258                    | "index_fec_parity_shards does not match v41 compute_parity"
4259                    | "index_root_fec_parity_shards does not match v41 compute_parity"
4260            )
4261    )
4262}
4263
4264fn parse_open_prefix(
4265    bytes: &[u8],
4266    master_key: &MasterKey,
4267) -> Result<ParsedOpenPrefix, FormatError> {
4268    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4269    parse_volume_format_dispatch(&volume_header)?;
4270    let crypto_start = volume_header.crypto_header_offset as usize;
4271    let crypto_len = volume_header.crypto_header_length as usize;
4272    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4273    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4274    if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4275        return Err(FormatError::KeyMaterialMismatch);
4276    }
4277    let subkeys = subkeys_for_open(
4278        Some(master_key),
4279        parsed_crypto.fixed.aead_algo,
4280        &volume_header.archive_uuid,
4281        &volume_header.session_id,
4282    )?;
4283    verify_integrity_tag(
4284        HmacDomain::CryptoHeader,
4285        parsed_crypto.fixed.aead_algo,
4286        volume_header.volume_format_rev,
4287        Some(&subkeys.mac_key),
4288        &volume_header.archive_uuid,
4289        &volume_header.session_id,
4290        parsed_crypto.hmac_covered_bytes,
4291        &parsed_crypto.header_hmac,
4292    )?;
4293    parsed_crypto.validate_extension_semantics()?;
4294    validate_seekable_supported_volume(
4295        &volume_header,
4296        &parsed_crypto.fixed,
4297        &parsed_crypto.extensions,
4298    )?;
4299    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4300    let block_records_start = startup_block_records_start(
4301        &volume_header,
4302        &parsed_crypto.kdf_params,
4303        |start, length| {
4304            let start = to_usize(start, "KeyWrapTableV1")?;
4305            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4306        },
4307    )?;
4308    let crypto_header = parsed_crypto.fixed.clone();
4309    Ok(ParsedOpenPrefix {
4310        volume_header,
4311        crypto_header,
4312        crypto_header_bytes: crypto_bytes.to_vec(),
4313        key_wrap_table_bytes: None,
4314        block_records_start,
4315        subkeys,
4316    })
4317}
4318
4319fn prefix_uses_recipient_wrap(bytes: &[u8]) -> bool {
4320    let Ok(volume_header_bytes) = slice(bytes, 0, VOLUME_HEADER_LEN, "archive") else {
4321        return false;
4322    };
4323    let Ok(volume_header) = VolumeHeader::parse(volume_header_bytes) else {
4324        return false;
4325    };
4326    let crypto_start = volume_header.crypto_header_offset as usize;
4327    let crypto_len = volume_header.crypto_header_length as usize;
4328    let Ok(crypto_bytes) = slice(bytes, crypto_start, crypto_len, "CryptoHeader") else {
4329        return false;
4330    };
4331    let Ok(parsed_crypto) = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)
4332    else {
4333        return false;
4334    };
4335    matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4336}
4337
4338fn parse_open_prefix_with_recipient_wrap_resolver<F>(
4339    bytes: &[u8],
4340    resolver: &mut F,
4341) -> Result<ParsedOpenPrefix, FormatError>
4342where
4343    F: FnMut(
4344        RecipientWrapRecordContext<'_>,
4345    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4346{
4347    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4348    parse_volume_format_dispatch(&volume_header)?;
4349    let crypto_start = volume_header.crypto_header_offset as usize;
4350    let crypto_len = volume_header.crypto_header_length as usize;
4351    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4352    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4353    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4354        || !parsed_crypto.fixed.aead_algo.is_encrypted()
4355    {
4356        return Err(FormatError::KeyMaterialMismatch);
4357    }
4358
4359    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4360    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4361
4362    let startup_key_wrap_table = startup_key_wrap_table(
4363        &volume_header,
4364        &parsed_crypto.kdf_params,
4365        |start, length| {
4366            let start = to_usize(start, "KeyWrapTableV1")?;
4367            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4368        },
4369    )?
4370    .ok_or(FormatError::KeyMaterialMismatch)?;
4371    let key_wrap_table = startup_key_wrap_table.table;
4372    let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4373    let block_records_start = startup_key_wrap_table.block_records_start;
4374
4375    let subkeys = recipient_wrap_subkeys_from_table(
4376        &volume_header,
4377        &parsed_crypto,
4378        &key_wrap_table,
4379        resolver,
4380    )?;
4381    parsed_crypto.validate_extension_semantics()?;
4382    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4383
4384    let crypto_header = parsed_crypto.fixed.clone();
4385    Ok(ParsedOpenPrefix {
4386        volume_header,
4387        crypto_header,
4388        crypto_header_bytes: crypto_bytes.to_vec(),
4389        key_wrap_table_bytes,
4390        block_records_start,
4391        subkeys,
4392    })
4393}
4394
4395pub(crate) fn recipient_wrap_subkeys_from_table<F>(
4396    volume_header: &VolumeHeader,
4397    parsed_crypto: &CryptoHeader<'_>,
4398    key_wrap_table: &KeyWrapTableV1,
4399    resolver: &mut F,
4400) -> Result<Subkeys, FormatError>
4401where
4402    F: FnMut(
4403            RecipientWrapRecordContext<'_>,
4404        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
4405        + ?Sized,
4406{
4407    let archive_identity = RecipientWrapArchiveIdentity {
4408        archive_uuid: volume_header.archive_uuid,
4409        session_id: volume_header.session_id,
4410        format_version: volume_header.format_version,
4411        volume_format_rev: volume_header.volume_format_rev,
4412    };
4413
4414    for record in &key_wrap_table.recipient_records {
4415        let candidates = resolver(RecipientWrapRecordContext {
4416            archive_identity,
4417            record,
4418        })?;
4419        for candidate in candidates {
4420            let master_key = MasterKey::from_raw_key(&candidate)?;
4421            let subkeys = subkeys_for_open(
4422                Some(&master_key),
4423                parsed_crypto.fixed.aead_algo,
4424                &volume_header.archive_uuid,
4425                &volume_header.session_id,
4426            )?;
4427            if verify_integrity_tag(
4428                HmacDomain::CryptoHeader,
4429                parsed_crypto.fixed.aead_algo,
4430                volume_header.volume_format_rev,
4431                Some(&subkeys.mac_key),
4432                &volume_header.archive_uuid,
4433                &volume_header.session_id,
4434                parsed_crypto.hmac_covered_bytes,
4435                &parsed_crypto.header_hmac,
4436            )
4437            .is_ok()
4438            {
4439                return Ok(subkeys);
4440            }
4441        }
4442    }
4443    Err(FormatError::KeyMaterialMismatch)
4444}
4445
4446#[allow(clippy::too_many_arguments)]
4447fn finish_parse_seekable_volume(
4448    bytes: &[u8],
4449    volume_header: VolumeHeader,
4450    crypto_header: CryptoHeaderFixed,
4451    crypto_header_bytes: Vec<u8>,
4452    key_wrap_table_bytes: Option<Vec<u8>>,
4453    block_records_start: u64,
4454    subkeys: Subkeys,
4455    terminal: V41Terminal,
4456) -> Result<ParsedSeekableVolume, FormatError> {
4457    let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
4458    let volume_trailer = terminal.volume_trailer.clone();
4459    validate_trailer_identity(&volume_header, &volume_trailer)?;
4460
4461    let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
4462    let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
4463    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4464        if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
4465            || volume_trailer
4466                .root_auth_footer_offset
4467                .checked_add(volume_trailer.root_auth_footer_length as u64)
4468                .ok_or(FormatError::InvalidArchive(
4469                    "RootAuthFooter terminal boundary overflow",
4470                ))?
4471                != trailer_offset as u64
4472        {
4473            return Err(FormatError::InvalidArchive(
4474                "RootAuthFooter does not sit before selected trailer",
4475            ));
4476        }
4477    } else if manifest_end != trailer_offset {
4478        return Err(FormatError::InvalidArchive(
4479            "ManifestFooter does not end at selected trailer",
4480        ));
4481    }
4482    let manifest_bytes = &terminal.manifest_footer_bytes;
4483    let (manifest_footer, manifest_footer_error) =
4484        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4485        {
4486            Ok(footer) => (Some(footer), None),
4487            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4488            Err(err) => return Err(err),
4489        };
4490
4491    let block_region = parse_block_region(
4492        bytes,
4493        to_usize(block_records_start, "BlockRecord")?,
4494        manifest_offset,
4495        crypto_header.block_size as usize,
4496        &volume_header,
4497        &volume_trailer,
4498    )?;
4499
4500    Ok(ParsedSeekableVolume {
4501        volume_header,
4502        crypto_header,
4503        crypto_header_bytes,
4504        key_wrap_table_bytes,
4505        subkeys,
4506        manifest_footer,
4507        manifest_footer_error,
4508        root_auth_footer: terminal.root_auth_footer,
4509        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4510        volume_trailer,
4511        blocks: block_region.blocks,
4512        erased_block_indices: block_region.erased_block_indices,
4513    })
4514}
4515
4516fn parse_seekable_read_at_volume(
4517    reader: Arc<dyn ArchiveReadAt>,
4518    master_key: &MasterKey,
4519    options: ReaderOptions,
4520) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4521    let observed_len = reader.len()?;
4522    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4523        return Err(FormatError::InvalidLength {
4524            structure: "archive",
4525            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4526            actual: to_usize(observed_len, "archive")?,
4527        });
4528    }
4529
4530    let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
4531        Ok(prefix) => prefix,
4532        Err(prefix_err) => {
4533            if matches!(
4534                prefix_err,
4535                FormatError::UnsupportedVolumeFormatRevision { .. }
4536            ) {
4537                return Err(prefix_err);
4538            }
4539            return parse_seekable_read_at_volume_from_recovered_terminal(
4540                reader,
4541                observed_len,
4542                master_key,
4543                options,
4544            )
4545            .or(Err(prefix_err));
4546        }
4547    };
4548    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4549    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4550        Ok(parsed) => Ok(parsed),
4551        Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
4552            reader,
4553            observed_len,
4554            master_key,
4555            options,
4556        ) {
4557            Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4558                Ok(recovered)
4559            }
4560            Ok(_) | Err(_) => Err(prefix_err),
4561        },
4562    }
4563}
4564
4565fn parse_seekable_read_at_volume_with_recipient_wrap_resolver<F>(
4566    reader: Arc<dyn ArchiveReadAt>,
4567    resolver: &mut F,
4568    options: ReaderOptions,
4569) -> Result<ParsedSeekableReadAtVolume, FormatError>
4570where
4571    F: FnMut(
4572        RecipientWrapRecordContext<'_>,
4573    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4574{
4575    let observed_len = reader.len()?;
4576    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4577        return Err(FormatError::InvalidLength {
4578            structure: "archive",
4579            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4580            actual: to_usize(observed_len, "archive")?,
4581        });
4582    }
4583
4584    let prefix = match parse_read_at_open_prefix_with_recipient_wrap_resolver(
4585        reader.as_ref(),
4586        resolver,
4587    ) {
4588        Ok(prefix) => prefix,
4589        Err(prefix_err) => {
4590            if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4591                return Err(prefix_err);
4592            }
4593            return parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4594                reader,
4595                observed_len,
4596                resolver,
4597                options,
4598            )
4599            .or(Err(prefix_err));
4600        }
4601    };
4602    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4603    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4604        Ok(parsed) => Ok(parsed),
4605        Err(prefix_err) => {
4606            match parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4607                reader,
4608                observed_len,
4609                resolver,
4610                options,
4611            ) {
4612                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4613                    Ok(recovered)
4614                }
4615                Ok(_) | Err(_) => Err(prefix_err),
4616            }
4617        }
4618    }
4619}
4620
4621fn parse_seekable_read_at_volume_with_prefix(
4622    reader: Arc<dyn ArchiveReadAt>,
4623    observed_len: u64,
4624    prefix: ParsedReadAtOpenPrefix,
4625    options: ReaderOptions,
4626) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4627    let ParsedReadAtOpenPrefix {
4628        volume_header,
4629        crypto_header,
4630        crypto_header_bytes,
4631        key_wrap_table_bytes,
4632        block_records_start,
4633        subkeys,
4634    } = prefix;
4635
4636    let terminal = locate_v41_terminal_read_at(
4637        reader.as_ref(),
4638        observed_len,
4639        KeyHoldingTerminalContext {
4640            subkeys: &subkeys,
4641            volume_header: &volume_header,
4642            crypto_header: &crypto_header,
4643            crypto_header_bytes: &crypto_header_bytes,
4644        },
4645        options,
4646    )?;
4647    finish_parse_seekable_read_at_volume(
4648        reader,
4649        volume_header,
4650        crypto_header,
4651        crypto_header_bytes,
4652        key_wrap_table_bytes,
4653        block_records_start,
4654        subkeys,
4655        terminal,
4656    )
4657}
4658
4659fn parse_seekable_read_at_volume_from_recovered_terminal(
4660    reader: Arc<dyn ArchiveReadAt>,
4661    observed_len: u64,
4662    master_key: &MasterKey,
4663    options: ReaderOptions,
4664) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4665    let authority =
4666        locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
4667    parse_volume_format_dispatch(&authority.volume_header)?;
4668    if matches!(authority.kdf_params, KdfParams::RecipientWrap { .. }) {
4669        return Err(FormatError::KeyMaterialMismatch);
4670    }
4671    let block_records_start = startup_block_records_start(
4672        &authority.volume_header,
4673        &authority.kdf_params,
4674        |start, length| read_at_vec(reader.as_ref(), start, length, "KeyWrapTableV1"),
4675    )?;
4676    finish_parse_seekable_read_at_volume(
4677        reader,
4678        authority.volume_header,
4679        authority.crypto_header,
4680        authority.crypto_header_bytes,
4681        None,
4682        block_records_start,
4683        authority.subkeys,
4684        authority.terminal,
4685    )
4686}
4687
4688fn parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4689    reader: Arc<dyn ArchiveReadAt>,
4690    observed_len: u64,
4691    resolver: &mut F,
4692    options: ReaderOptions,
4693) -> Result<ParsedSeekableReadAtVolume, FormatError>
4694where
4695    F: FnMut(
4696        RecipientWrapRecordContext<'_>,
4697    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4698{
4699    let authority = locate_v41_recipient_wrap_terminal_authority_read_at(
4700        reader.as_ref(),
4701        observed_len,
4702        resolver,
4703        options,
4704    )?;
4705    finish_parse_seekable_read_at_volume(
4706        reader,
4707        authority.volume_header,
4708        authority.crypto_header,
4709        authority.crypto_header_bytes,
4710        Some(authority.key_wrap_table_bytes),
4711        authority.block_records_start,
4712        authority.subkeys,
4713        authority.terminal,
4714    )
4715}
4716
4717fn parse_read_at_open_prefix(
4718    reader: &dyn ArchiveReadAt,
4719    master_key: &MasterKey,
4720) -> Result<ParsedReadAtOpenPrefix, FormatError> {
4721    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4722    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4723    parse_volume_format_dispatch(&volume_header)?;
4724    let crypto_start = volume_header.crypto_header_offset as u64;
4725    let crypto_len = volume_header.crypto_header_length as u64;
4726    let crypto_bytes = read_at_vec(
4727        reader,
4728        crypto_start,
4729        to_usize(crypto_len, "CryptoHeader")?,
4730        "CryptoHeader",
4731    )?;
4732    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4733    if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4734        return Err(FormatError::KeyMaterialMismatch);
4735    }
4736    let subkeys = subkeys_for_open(
4737        Some(master_key),
4738        parsed_crypto.fixed.aead_algo,
4739        &volume_header.archive_uuid,
4740        &volume_header.session_id,
4741    )?;
4742    verify_integrity_tag(
4743        HmacDomain::CryptoHeader,
4744        parsed_crypto.fixed.aead_algo,
4745        volume_header.volume_format_rev,
4746        Some(&subkeys.mac_key),
4747        &volume_header.archive_uuid,
4748        &volume_header.session_id,
4749        parsed_crypto.hmac_covered_bytes,
4750        &parsed_crypto.header_hmac,
4751    )?;
4752    parsed_crypto.validate_extension_semantics()?;
4753    validate_seekable_supported_volume(
4754        &volume_header,
4755        &parsed_crypto.fixed,
4756        &parsed_crypto.extensions,
4757    )?;
4758    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4759    let block_records_start = startup_block_records_start(
4760        &volume_header,
4761        &parsed_crypto.kdf_params,
4762        |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4763    )?;
4764    let crypto_header = parsed_crypto.fixed.clone();
4765    drop(parsed_crypto);
4766    Ok(ParsedReadAtOpenPrefix {
4767        volume_header,
4768        crypto_header,
4769        crypto_header_bytes: crypto_bytes,
4770        key_wrap_table_bytes: None,
4771        block_records_start,
4772        subkeys,
4773    })
4774}
4775
4776fn parse_read_at_open_prefix_with_recipient_wrap_resolver<F>(
4777    reader: &dyn ArchiveReadAt,
4778    resolver: &mut F,
4779) -> Result<ParsedReadAtOpenPrefix, FormatError>
4780where
4781    F: FnMut(
4782        RecipientWrapRecordContext<'_>,
4783    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4784{
4785    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4786    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4787    parse_volume_format_dispatch(&volume_header)?;
4788    let crypto_start = volume_header.crypto_header_offset as u64;
4789    let crypto_len = volume_header.crypto_header_length as u64;
4790    let crypto_bytes = read_at_vec(
4791        reader,
4792        crypto_start,
4793        to_usize(crypto_len, "CryptoHeader")?,
4794        "CryptoHeader",
4795    )?;
4796    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4797    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4798        || !parsed_crypto.fixed.aead_algo.is_encrypted()
4799    {
4800        return Err(FormatError::KeyMaterialMismatch);
4801    }
4802
4803    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4804    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4805
4806    let startup_key_wrap_table = startup_key_wrap_table(
4807        &volume_header,
4808        &parsed_crypto.kdf_params,
4809        |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4810    )?
4811    .ok_or(FormatError::KeyMaterialMismatch)?;
4812    let key_wrap_table = startup_key_wrap_table.table;
4813    let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4814    let block_records_start = startup_key_wrap_table.block_records_start;
4815
4816    let subkeys = recipient_wrap_subkeys_from_table(
4817        &volume_header,
4818        &parsed_crypto,
4819        &key_wrap_table,
4820        resolver,
4821    )?;
4822    parsed_crypto.validate_extension_semantics()?;
4823    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4824
4825    let crypto_header = parsed_crypto.fixed.clone();
4826    Ok(ParsedReadAtOpenPrefix {
4827        volume_header,
4828        crypto_header,
4829        crypto_header_bytes: crypto_bytes,
4830        key_wrap_table_bytes,
4831        block_records_start,
4832        subkeys,
4833    })
4834}
4835
4836#[allow(clippy::too_many_arguments)]
4837fn finish_parse_seekable_read_at_volume(
4838    reader: Arc<dyn ArchiveReadAt>,
4839    volume_header: VolumeHeader,
4840    crypto_header: CryptoHeaderFixed,
4841    crypto_header_bytes: Vec<u8>,
4842    key_wrap_table_bytes: Option<Vec<u8>>,
4843    block_records_start: u64,
4844    subkeys: Subkeys,
4845    terminal: V41Terminal,
4846) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4847    let volume_trailer = terminal.volume_trailer.clone();
4848    validate_trailer_identity(&volume_header, &volume_trailer)?;
4849
4850    let manifest_offset = volume_trailer.manifest_footer_offset;
4851    let manifest_end = checked_u64_add(
4852        manifest_offset,
4853        MANIFEST_FOOTER_LEN as u64,
4854        "ManifestFooter",
4855    )?;
4856    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4857        if volume_trailer.root_auth_footer_offset != manifest_end
4858            || volume_trailer
4859                .root_auth_footer_offset
4860                .checked_add(volume_trailer.root_auth_footer_length as u64)
4861                .ok_or(FormatError::InvalidArchive(
4862                    "RootAuthFooter terminal boundary overflow",
4863                ))?
4864                != terminal.image.volume_trailer_offset
4865        {
4866            return Err(FormatError::InvalidArchive(
4867                "RootAuthFooter does not sit before selected trailer",
4868            ));
4869        }
4870    } else if manifest_end != terminal.image.volume_trailer_offset {
4871        return Err(FormatError::InvalidArchive(
4872            "ManifestFooter does not end at selected trailer",
4873        ));
4874    }
4875    validate_seekable_block_region_layout(
4876        block_records_start,
4877        manifest_offset,
4878        crypto_header.block_size as usize,
4879        &volume_trailer,
4880    )?;
4881
4882    let manifest_bytes = &terminal.manifest_footer_bytes;
4883    let (manifest_footer, manifest_footer_error) =
4884        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4885        {
4886            Ok(footer) => (Some(footer), None),
4887            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4888            Err(err) => return Err(err),
4889        };
4890
4891    Ok(ParsedSeekableReadAtVolume {
4892        reader,
4893        volume_header,
4894        crypto_header,
4895        crypto_header_bytes,
4896        key_wrap_table_bytes,
4897        subkeys,
4898        manifest_footer,
4899        manifest_footer_error,
4900        root_auth_footer: terminal.root_auth_footer,
4901        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4902        volume_trailer,
4903        block_records_start,
4904    })
4905}
4906
4907#[derive(Debug)]
4908struct ParsedPublicNoKeyVolume {
4909    volume_header: VolumeHeader,
4910    crypto_header: CryptoHeaderFixed,
4911    kdf_params: KdfParams,
4912    root_auth_footer: RootAuthFooterV1,
4913    root_auth_footer_bytes: Vec<u8>,
4914    blocks: BTreeMap<u64, BlockRecord>,
4915}
4916
4917pub fn public_no_key_verify_volumes_with_options<F>(
4918    volumes: &[&[u8]],
4919    mut verifier: F,
4920    options: ReaderOptions,
4921) -> Result<PublicNoKeyVerification, FormatError>
4922where
4923    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
4924{
4925    validate_reader_options(options)?;
4926    if volumes.is_empty() {
4927        return Err(FormatError::InvalidArchive("no volumes supplied"));
4928    }
4929    let mut parsed = Vec::with_capacity(volumes.len());
4930    for volume in volumes {
4931        parsed.push(parse_public_no_key_volume(volume, options)?);
4932    }
4933    let first = parsed
4934        .first()
4935        .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
4936    if parsed.len() != first.crypto_header.stripe_width as usize {
4937        return Err(FormatError::ReaderUnsupported(
4938            "public no-key verification requires a complete volume set",
4939        ));
4940    }
4941
4942    let mut seen_volume_indexes = BTreeSet::new();
4943    let mut blocks = BTreeMap::new();
4944    for volume in &parsed {
4945        if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
4946            || volume.volume_header.session_id != first.volume_header.session_id
4947            || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
4948            || !public_kdf_profiles_agree(&volume.kdf_params, &first.kdf_params)
4949        {
4950            return Err(FormatError::InvalidArchive(
4951                "public no-key volume global metadata differs",
4952            ));
4953        }
4954        if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
4955            return Err(FormatError::InvalidArchive(
4956                "public no-key RootAuthFooter copies differ",
4957            ));
4958        }
4959        if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
4960            return Err(FormatError::InvalidArchive(
4961                "duplicate public no-key volume index",
4962            ));
4963        }
4964        for (block_index, record) in &volume.blocks {
4965            if blocks.insert(*block_index, record.clone()).is_some() {
4966                return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
4967            }
4968        }
4969    }
4970    validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
4971
4972    let footer = &first.root_auth_footer;
4973    let mut data_leaves = blocks
4974        .values()
4975        .filter(|record| record.kind.is_data())
4976        .map(|record| DataBlockMerkleLeaf {
4977            block_index: record.block_index,
4978            kind: record.kind,
4979            flags: record.flags,
4980            payload: record.payload.clone(),
4981        })
4982        .collect::<Vec<_>>();
4983    data_leaves.sort_by_key(|leaf| leaf.block_index);
4984    let total_data_block_count = u64::try_from(data_leaves.len())
4985        .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
4986    let observed_data_root = data_block_merkle_root_for_revision(
4987        footer.format_version,
4988        footer.volume_format_rev,
4989        &data_leaves,
4990    )?;
4991    if total_data_block_count != footer.total_data_block_count
4992        || observed_data_root != footer.data_block_merkle_root
4993    {
4994        return Err(FormatError::InvalidArchive(
4995            "public no-key data-block commitment mismatch",
4996        ));
4997    }
4998    let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
4999    if archive_root != footer.archive_root {
5000        return Err(FormatError::InvalidArchive(
5001            "public no-key archive_root mismatch",
5002        ));
5003    }
5004    if !verifier(footer, &archive_root)? {
5005        return Err(FormatError::InvalidArchive(
5006            "public no-key authenticator verification failed",
5007        ));
5008    }
5009    Ok(PublicNoKeyVerification {
5010        format_version: footer.format_version,
5011        volume_format_rev: footer.volume_format_rev,
5012        archive_root,
5013        authenticator_id: footer.authenticator_id,
5014        signer_identity_type: footer.signer_identity_type,
5015        signer_identity_bytes: footer.signer_identity_bytes.clone(),
5016        total_data_block_count,
5017        diagnostics: vec![
5018            PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
5019            PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
5020            PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
5021        ],
5022    })
5023}
5024
5025fn parse_public_no_key_volume(
5026    bytes: &[u8],
5027    options: ReaderOptions,
5028) -> Result<ParsedPublicNoKeyVolume, FormatError> {
5029    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
5030        return Err(FormatError::InvalidLength {
5031            structure: "archive",
5032            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
5033            actual: bytes.len(),
5034        });
5035    }
5036    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
5037    parse_volume_format_dispatch(&volume_header)?;
5038    let crypto_start = volume_header.crypto_header_offset as usize;
5039    let crypto_len = volume_header.crypto_header_length as usize;
5040    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
5041    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
5042    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
5043    parsed_crypto.validate_extension_semantics()?;
5044    validate_seekable_supported_volume(
5045        &volume_header,
5046        &parsed_crypto.fixed,
5047        &parsed_crypto.extensions,
5048    )?;
5049    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5050
5051    let terminal = locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto, options)?;
5052    let block_records_start = match &parsed_crypto.kdf_params {
5053        KdfParams::RecipientWrap {
5054            key_wrap_table_length,
5055            ..
5056        } => checked_add(
5057            crypto_end,
5058            *key_wrap_table_length as usize,
5059            "KeyWrapTableV1",
5060        )?,
5061        _ => crypto_end,
5062    };
5063    let block_region = parse_public_block_observation(
5064        bytes,
5065        block_records_start,
5066        &terminal.image,
5067        parsed_crypto.fixed.block_size as usize,
5068        &volume_header,
5069    )?;
5070    Ok(ParsedPublicNoKeyVolume {
5071        volume_header,
5072        crypto_header: parsed_crypto.fixed,
5073        kdf_params: parsed_crypto.kdf_params,
5074        root_auth_footer: terminal.root_auth_footer,
5075        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
5076        blocks: block_region,
5077    })
5078}
5079
5080fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
5081    left.length == right.length
5082        && left.stripe_width == right.stripe_width
5083        && left.block_size == right.block_size
5084        && left.compression_algo == right.compression_algo
5085        && left.aead_algo == right.aead_algo
5086        && left.fec_algo == right.fec_algo
5087        && left.kdf_algo == right.kdf_algo
5088}
5089
5090fn public_kdf_profiles_agree(left: &KdfParams, right: &KdfParams) -> bool {
5091    match (left, right) {
5092        (
5093            KdfParams::RecipientWrap {
5094                key_wrap_table_length: left_length,
5095                key_wrap_table_record_count: left_count,
5096                key_wrap_table_version: left_version,
5097                key_wrap_table_digest: left_digest,
5098            },
5099            KdfParams::RecipientWrap {
5100                key_wrap_table_length: right_length,
5101                key_wrap_table_record_count: right_count,
5102                key_wrap_table_version: right_version,
5103                key_wrap_table_digest: right_digest,
5104            },
5105        ) => {
5106            left_length == right_length
5107                && left_count == right_count
5108                && left_version == right_version
5109                && left_digest == right_digest
5110        }
5111        (KdfParams::RecipientWrap { .. }, _) | (_, KdfParams::RecipientWrap { .. }) => false,
5112        _ => true,
5113    }
5114}
5115
5116fn recompute_public_archive_root(
5117    footer: &RootAuthFooterV1,
5118    crypto_header: &CryptoHeaderFixed,
5119) -> Result<[u8; 32], FormatError> {
5120    let descriptor_digest = root_auth_descriptor_digest_for_revision(
5121        footer.format_version,
5122        footer.volume_format_rev,
5123        footer.authenticator_id,
5124        footer.signer_identity_type,
5125        &footer.signer_identity_bytes,
5126        u32::try_from(footer.authenticator_value.len()).map_err(|_| {
5127            FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
5128        })?,
5129        footer.footer_length()?,
5130    )?;
5131    let signer_digest =
5132        signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
5133    if signer_digest != footer.signer_identity_digest {
5134        return Err(FormatError::InvalidArchive(
5135            "public no-key signer identity digest mismatch",
5136        ));
5137    }
5138    archive_root_for_revision(ArchiveRootInputs {
5139        archive_uuid: footer.archive_uuid,
5140        session_id: footer.session_id,
5141        format_version: footer.format_version,
5142        volume_format_rev: footer.volume_format_rev,
5143        compression_algo: crypto_header.compression_algo,
5144        aead_algo: crypto_header.aead_algo,
5145        fec_algo: crypto_header.fec_algo,
5146        kdf_algo: crypto_header.kdf_algo,
5147        critical_metadata_digest: footer.critical_metadata_digest,
5148        index_digest: footer.index_digest,
5149        fec_layout_digest: footer.fec_layout_digest,
5150        total_data_block_count: footer.total_data_block_count,
5151        data_block_merkle_root: footer.data_block_merkle_root,
5152        root_auth_descriptor_digest: descriptor_digest,
5153        signer_identity_digest: signer_digest,
5154    })
5155}
5156
5157fn parse_valid_manifest_footer(
5158    volume_header: &VolumeHeader,
5159    crypto_header: &CryptoHeaderFixed,
5160    subkeys: &Subkeys,
5161    manifest_bytes: &[u8],
5162) -> Result<ManifestFooter, FormatError> {
5163    let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5164    validate_manifest_footer(
5165        volume_header,
5166        crypto_header,
5167        &manifest_footer,
5168        subkeys,
5169        volume_header.volume_format_rev,
5170        manifest_bytes,
5171    )?;
5172    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5173    Ok(manifest_footer)
5174}
5175
5176fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
5177    matches!(
5178        error,
5179        FormatError::BadMagic {
5180            structure: "ManifestFooter",
5181        } | FormatError::NonZeroReserved {
5182            structure: "ManifestFooter",
5183        } | FormatError::InvalidAuthoritativeFlag(_)
5184            | FormatError::HmacMismatch {
5185                structure: "ManifestFooter",
5186            }
5187            | FormatError::IntegrityDigestMismatch {
5188                structure: "ManifestFooter",
5189            }
5190    )
5191}
5192
5193fn validate_seekable_supported_volume(
5194    volume_header: &VolumeHeader,
5195    crypto_header: &CryptoHeaderFixed,
5196    extensions: &[ExtensionTlv<'_>],
5197) -> Result<(), FormatError> {
5198    reject_unsupported_raw_stream_profile(extensions)?;
5199    if crypto_header.stripe_width != volume_header.stripe_width {
5200        return Err(FormatError::InvalidArchive(
5201            "VolumeHeader and CryptoHeader stripe_width differ",
5202        ));
5203    }
5204    Ok(())
5205}
5206
5207pub(crate) fn validate_crypto_class_parity_exactness(
5208    crypto_header: &CryptoHeaderFixed,
5209) -> Result<(), FormatError> {
5210    let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
5211    if crypto_header.fec_parity_shards as u32 != fec {
5212        return Err(FormatError::InvalidArchive(
5213            "fec_parity_shards does not match v41 compute_parity",
5214        ));
5215    }
5216    let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
5217    if crypto_header.index_fec_parity_shards as u32 != index {
5218        return Err(FormatError::InvalidArchive(
5219            "index_fec_parity_shards does not match v41 compute_parity",
5220        ));
5221    }
5222    let index_root = required_object_parity(
5223        crypto_header.index_root_fec_data_shards as u64,
5224        crypto_header,
5225    )?;
5226    if crypto_header.index_root_fec_parity_shards as u32 != index_root {
5227        return Err(FormatError::InvalidArchive(
5228            "index_root_fec_parity_shards does not match v41 compute_parity",
5229        ));
5230    }
5231    Ok(())
5232}
5233
5234fn validate_volume_set_member(
5235    first: &ParsedSeekableVolume,
5236    candidate: &ParsedSeekableVolume,
5237) -> Result<(), FormatError> {
5238    validate_volume_set_member_metadata(
5239        &first.volume_header,
5240        &first.crypto_header,
5241        &first.crypto_header_bytes,
5242        &candidate.volume_header,
5243        &candidate.crypto_header,
5244        &candidate.crypto_header_bytes,
5245    )?;
5246    validate_key_wrap_table_bytes_match(
5247        &first.key_wrap_table_bytes,
5248        &candidate.key_wrap_table_bytes,
5249    )
5250}
5251
5252fn validate_key_wrap_table_bytes_match(
5253    first_key_wrap_table_bytes: &Option<Vec<u8>>,
5254    candidate_key_wrap_table_bytes: &Option<Vec<u8>>,
5255) -> Result<(), FormatError> {
5256    if candidate_key_wrap_table_bytes != first_key_wrap_table_bytes {
5257        return Err(FormatError::InvalidArchive("KeyWrapTableV1 copies differ"));
5258    }
5259    Ok(())
5260}
5261
5262fn validate_volume_set_member_metadata(
5263    first_volume_header: &VolumeHeader,
5264    first_crypto_header: &CryptoHeaderFixed,
5265    first_crypto_header_bytes: &[u8],
5266    candidate_volume_header: &VolumeHeader,
5267    candidate_crypto_header: &CryptoHeaderFixed,
5268    candidate_crypto_header_bytes: &[u8],
5269) -> Result<(), FormatError> {
5270    if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
5271        || candidate_volume_header.session_id != first_volume_header.session_id
5272    {
5273        return Err(FormatError::InvalidArchive(
5274            "mixed archive or session IDs in volume set",
5275        ));
5276    }
5277    if candidate_crypto_header_bytes != first_crypto_header_bytes
5278        || candidate_crypto_header != first_crypto_header
5279    {
5280        return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
5281    }
5282    Ok(())
5283}
5284
5285pub(crate) fn manifest_bootstrap_fields_match(
5286    left: &ManifestFooter,
5287    right: &ManifestFooter,
5288) -> bool {
5289    left.archive_uuid == right.archive_uuid
5290        && left.session_id == right.session_id
5291        && left.is_authoritative == right.is_authoritative
5292        && left.total_volumes == right.total_volumes
5293        && left.index_root_first_block == right.index_root_first_block
5294        && left.index_root_data_block_count == right.index_root_data_block_count
5295        && left.index_root_parity_block_count == right.index_root_parity_block_count
5296        && left.index_root_encrypted_size == right.index_root_encrypted_size
5297        && left.index_root_decompressed_size == right.index_root_decompressed_size
5298}
5299
5300fn validate_complete_global_block_coverage(
5301    blocks: &BTreeMap<u64, BlockRecord>,
5302    erased_block_indices: &BTreeSet<u64>,
5303) -> Result<(), FormatError> {
5304    let mut expected = 0u64;
5305    let mut block_iter = blocks.keys().copied().peekable();
5306    let mut erasure_iter = erased_block_indices.iter().copied().peekable();
5307
5308    loop {
5309        let next_block = block_iter.peek().copied();
5310        let next_erasure = erasure_iter.peek().copied();
5311        let next = match (next_block, next_erasure) {
5312            (Some(block), Some(erasure)) if block == erasure => {
5313                return Err(FormatError::InvalidArchive(
5314                    "BlockRecord index is both present and erased",
5315                ));
5316            }
5317            (Some(block), Some(erasure)) => block.min(erasure),
5318            (Some(block), None) => block,
5319            (None, Some(erasure)) => erasure,
5320            (None, None) => return Ok(()),
5321        };
5322
5323        if next != expected {
5324            return Err(FormatError::InvalidArchive(
5325                "complete volume set has missing global blocks",
5326            ));
5327        }
5328        if next_block == Some(next) {
5329            block_iter.next();
5330        }
5331        if next_erasure == Some(next) {
5332            erasure_iter.next();
5333        }
5334        expected = expected
5335            .checked_add(1)
5336            .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
5337    }
5338}
5339
5340#[derive(Debug)]
5341struct V41Terminal {
5342    image: CriticalMetadataImage,
5343    manifest_footer_bytes: Vec<u8>,
5344    root_auth_footer_bytes: Option<Vec<u8>>,
5345    root_auth_footer: Option<RootAuthFooterV1>,
5346    volume_trailer: VolumeTrailer,
5347}
5348
5349pub(crate) struct SequentialTerminalMaterial {
5350    pub(crate) manifest_footer: ManifestFooter,
5351    pub(crate) volume_trailer: VolumeTrailer,
5352    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
5353}
5354
5355#[derive(Debug)]
5356struct V41PublicTerminal {
5357    image: CriticalMetadataImage,
5358    root_auth_footer_bytes: Vec<u8>,
5359    root_auth_footer: RootAuthFooterV1,
5360}
5361
5362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5363struct CmraDecoderTuple {
5364    shard_size: u32,
5365    data_shard_count: u16,
5366    parity_shard_count: u16,
5367    image_length: u32,
5368    image_sha256: [u8; 32],
5369}
5370
5371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5372struct CmraIdentityHints {
5373    archive_uuid: [u8; 16],
5374    session_id: [u8; 16],
5375    volume_index: u32,
5376}
5377
5378impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
5379    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5380        Self {
5381            shard_size: header.shard_size,
5382            data_shard_count: header.data_shard_count,
5383            parity_shard_count: header.parity_shard_count,
5384            image_length: header.image_length,
5385            image_sha256: header.image_sha256,
5386        }
5387    }
5388}
5389
5390impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
5391    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5392        Self {
5393            archive_uuid: header.archive_uuid_hint,
5394            session_id: header.session_id_hint,
5395            volume_index: header.volume_index_hint,
5396        }
5397    }
5398}
5399
5400impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
5401    fn from(locator: CriticalRecoveryLocator) -> Self {
5402        Self {
5403            shard_size: locator.cmra_shard_size,
5404            data_shard_count: locator.cmra_data_shard_count,
5405            parity_shard_count: locator.cmra_parity_shard_count,
5406            image_length: locator.cmra_image_length,
5407            image_sha256: locator.cmra_image_sha256,
5408        }
5409    }
5410}
5411
5412impl From<CriticalRecoveryLocator> for CmraIdentityHints {
5413    fn from(locator: CriticalRecoveryLocator) -> Self {
5414        Self {
5415            archive_uuid: locator.archive_uuid_hint,
5416            session_id: locator.session_id_hint,
5417            volume_index: locator.volume_index_hint,
5418        }
5419    }
5420}
5421
5422#[derive(Debug)]
5423struct RecoveredCmra {
5424    image: CriticalMetadataImage,
5425    tuple: CmraDecoderTuple,
5426    header_hints: Option<CmraIdentityHints>,
5427    cmra_length: u64,
5428}
5429
5430#[derive(Debug)]
5431struct TerminalCandidate {
5432    terminal: V41Terminal,
5433    anchor: usize,
5434    locator_sequence: Option<u32>,
5435    cmra_offset: u64,
5436    cmra_length: u64,
5437}
5438
5439#[derive(Debug)]
5440struct PublicTerminalCandidate {
5441    terminal: V41PublicTerminal,
5442    anchor: usize,
5443    cmra_offset: u64,
5444    cmra_length: u64,
5445}
5446
5447#[derive(Debug)]
5448struct RecoveredTerminalAuthority {
5449    terminal: V41Terminal,
5450    volume_header: VolumeHeader,
5451    crypto_header: CryptoHeaderFixed,
5452    crypto_header_bytes: Vec<u8>,
5453    subkeys: Subkeys,
5454    kdf_params: KdfParams,
5455}
5456
5457#[derive(Debug)]
5458struct RecoveredRecipientWrapTerminalAuthority {
5459    terminal: V41Terminal,
5460    volume_header: VolumeHeader,
5461    crypto_header: CryptoHeaderFixed,
5462    crypto_header_bytes: Vec<u8>,
5463    key_wrap_table_bytes: Vec<u8>,
5464    block_records_start: u64,
5465    subkeys: Subkeys,
5466}
5467
5468#[derive(Debug)]
5469struct TerminalAuthorityCandidate {
5470    authority: RecoveredTerminalAuthority,
5471    anchor: usize,
5472    cmra_offset: u64,
5473    cmra_length: u64,
5474}
5475
5476#[derive(Debug)]
5477struct RecipientWrapTerminalAuthorityCandidate {
5478    authority: RecoveredRecipientWrapTerminalAuthority,
5479    anchor: usize,
5480    cmra_offset: u64,
5481    cmra_length: u64,
5482}
5483
5484#[derive(Debug, Clone, Copy)]
5485enum CmraRecoveryMode {
5486    KeyHolding,
5487    PublicNoKey,
5488}
5489
5490#[derive(Clone, Copy)]
5491pub(crate) struct KeyHoldingTerminalContext<'a> {
5492    pub(crate) subkeys: &'a Subkeys,
5493    pub(crate) volume_header: &'a VolumeHeader,
5494    pub(crate) crypto_header: &'a CryptoHeaderFixed,
5495    pub(crate) crypto_header_bytes: &'a [u8],
5496}
5497
5498fn locate_v41_terminal(
5499    bytes: &[u8],
5500    context: KeyHoldingTerminalContext<'_>,
5501    options: ReaderOptions,
5502) -> Result<V41Terminal, FormatError> {
5503    locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
5504}
5505
5506fn locate_v41_terminal_read_at(
5507    reader: &dyn ArchiveReadAt,
5508    len: u64,
5509    context: KeyHoldingTerminalContext<'_>,
5510    options: ReaderOptions,
5511) -> Result<V41Terminal, FormatError> {
5512    let mut candidates = Vec::new();
5513    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5514        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5515        collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
5516    }
5517    if len >= LOCATOR_PAIR_LEN as u64 {
5518        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5519        collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
5520    }
5521
5522    if candidates.is_empty() {
5523        let scan = max_critical_recovery_scan(options)? as u64;
5524        let scan_start = len.saturating_sub(scan);
5525        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5526        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5527        let mut offset = tail.len().saturating_sub(4);
5528        while offset < tail.len() {
5529            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5530            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5531                collect_v41_locator_candidate_read_at(
5532                    reader,
5533                    absolute_offset,
5534                    2,
5535                    context,
5536                    &mut candidates,
5537                );
5538            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5539                if let Ok(candidate) =
5540                    parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
5541                {
5542                    candidates.push(candidate);
5543                }
5544            }
5545            if offset == 0 {
5546                break;
5547            }
5548            offset -= 1;
5549        }
5550    }
5551
5552    choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5553}
5554
5555fn locate_v41_terminal_authority(
5556    bytes: &[u8],
5557    master_key: &MasterKey,
5558    options: ReaderOptions,
5559) -> Result<RecoveredTerminalAuthority, FormatError> {
5560    let mut candidates = Vec::new();
5561    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5562        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5563        collect_v41_locator_authority_candidate(
5564            bytes,
5565            final_offset,
5566            0,
5567            master_key,
5568            &mut candidates,
5569        );
5570    }
5571    if bytes.len() >= LOCATOR_PAIR_LEN {
5572        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5573        collect_v41_locator_authority_candidate(
5574            bytes,
5575            mirror_offset,
5576            1,
5577            master_key,
5578            &mut candidates,
5579        );
5580    }
5581
5582    if candidates.is_empty() {
5583        let scan = max_critical_recovery_scan(options)?;
5584        let scan_start = bytes.len().saturating_sub(scan);
5585        let mut offset = bytes.len().saturating_sub(4);
5586        while offset >= scan_start {
5587            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5588                collect_v41_locator_authority_candidate(
5589                    bytes,
5590                    offset,
5591                    2,
5592                    master_key,
5593                    &mut candidates,
5594                );
5595            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5596                if let Ok(candidate) =
5597                    parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
5598                {
5599                    candidates.push(candidate);
5600                }
5601            }
5602            if offset == 0 {
5603                break;
5604            }
5605            offset -= 1;
5606        }
5607    }
5608
5609    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5610}
5611
5612fn locate_v41_recipient_wrap_terminal_authority<F>(
5613    bytes: &[u8],
5614    resolver: &mut F,
5615    options: ReaderOptions,
5616) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5617where
5618    F: FnMut(
5619        RecipientWrapRecordContext<'_>,
5620    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5621{
5622    let mut candidates = Vec::new();
5623    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5624        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5625        collect_v41_recipient_wrap_locator_authority_candidate(
5626            bytes,
5627            final_offset,
5628            0,
5629            resolver,
5630            &mut candidates,
5631        );
5632    }
5633    if bytes.len() >= LOCATOR_PAIR_LEN {
5634        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5635        collect_v41_recipient_wrap_locator_authority_candidate(
5636            bytes,
5637            mirror_offset,
5638            1,
5639            resolver,
5640            &mut candidates,
5641        );
5642    }
5643
5644    if candidates.is_empty() {
5645        let scan = max_critical_recovery_scan(options)?;
5646        let scan_start = bytes.len().saturating_sub(scan);
5647        let mut offset = bytes.len().saturating_sub(4);
5648        while offset >= scan_start {
5649            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5650                collect_v41_recipient_wrap_locator_authority_candidate(
5651                    bytes,
5652                    offset,
5653                    2,
5654                    resolver,
5655                    &mut candidates,
5656                );
5657            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5658                if let Ok(candidate) = parse_locatorless_cmra_recipient_wrap_authority_candidate(
5659                    bytes, offset, resolver,
5660                ) {
5661                    candidates.push(candidate);
5662                }
5663            }
5664            if offset == 0 {
5665                break;
5666            }
5667            offset -= 1;
5668        }
5669    }
5670
5671    choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5672        .map(|candidate| candidate.authority)
5673}
5674
5675fn locate_v41_recipient_wrap_terminal_authority_read_at<F>(
5676    reader: &dyn ArchiveReadAt,
5677    len: u64,
5678    resolver: &mut F,
5679    options: ReaderOptions,
5680) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5681where
5682    F: FnMut(
5683        RecipientWrapRecordContext<'_>,
5684    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5685{
5686    let mut candidates = Vec::new();
5687    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5688        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5689        collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5690            reader,
5691            final_offset,
5692            0,
5693            resolver,
5694            &mut candidates,
5695        );
5696    }
5697    if len >= LOCATOR_PAIR_LEN as u64 {
5698        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5699        collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5700            reader,
5701            mirror_offset,
5702            1,
5703            resolver,
5704            &mut candidates,
5705        );
5706    }
5707
5708    if candidates.is_empty() {
5709        let scan = max_critical_recovery_scan(options)? as u64;
5710        let scan_start = len.saturating_sub(scan);
5711        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5712        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5713        let mut offset = tail.len().saturating_sub(4);
5714        while offset < tail.len() {
5715            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5716            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5717                collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5718                    reader,
5719                    absolute_offset,
5720                    2,
5721                    resolver,
5722                    &mut candidates,
5723                );
5724            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5725                if let Ok(candidate) =
5726                    parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at(
5727                        reader,
5728                        absolute_offset,
5729                        resolver,
5730                    )
5731                {
5732                    candidates.push(candidate);
5733                }
5734            }
5735            if offset == 0 {
5736                break;
5737            }
5738            offset -= 1;
5739        }
5740    }
5741
5742    choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5743        .map(|candidate| candidate.authority)
5744}
5745
5746fn locate_v41_terminal_authority_read_at(
5747    reader: &dyn ArchiveReadAt,
5748    len: u64,
5749    master_key: &MasterKey,
5750    options: ReaderOptions,
5751) -> Result<RecoveredTerminalAuthority, FormatError> {
5752    let mut candidates = Vec::new();
5753    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5754        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5755        collect_v41_locator_authority_candidate_read_at(
5756            reader,
5757            final_offset,
5758            0,
5759            master_key,
5760            &mut candidates,
5761        );
5762    }
5763    if len >= LOCATOR_PAIR_LEN as u64 {
5764        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5765        collect_v41_locator_authority_candidate_read_at(
5766            reader,
5767            mirror_offset,
5768            1,
5769            master_key,
5770            &mut candidates,
5771        );
5772    }
5773
5774    if candidates.is_empty() {
5775        let scan = max_critical_recovery_scan(options)? as u64;
5776        let scan_start = len.saturating_sub(scan);
5777        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5778        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5779        let mut offset = tail.len().saturating_sub(4);
5780        while offset < tail.len() {
5781            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5782            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5783                collect_v41_locator_authority_candidate_read_at(
5784                    reader,
5785                    absolute_offset,
5786                    2,
5787                    master_key,
5788                    &mut candidates,
5789                );
5790            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5791                if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
5792                    reader,
5793                    absolute_offset,
5794                    master_key,
5795                ) {
5796                    candidates.push(candidate);
5797                }
5798            }
5799            if offset == 0 {
5800                break;
5801            }
5802            offset -= 1;
5803        }
5804    }
5805
5806    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5807}
5808
5809fn locate_v41_terminal_candidate(
5810    bytes: &[u8],
5811    context: KeyHoldingTerminalContext<'_>,
5812    options: ReaderOptions,
5813) -> Result<TerminalCandidate, FormatError> {
5814    let mut candidates = Vec::new();
5815    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5816        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5817        collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
5818    }
5819    if bytes.len() >= LOCATOR_PAIR_LEN {
5820        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5821        collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
5822    }
5823
5824    if candidates.is_empty() {
5825        let scan = max_critical_recovery_scan(options)?;
5826        let scan_start = bytes.len().saturating_sub(scan);
5827        let mut offset = bytes.len().saturating_sub(4);
5828        while offset >= scan_start {
5829            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5830                collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
5831            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5832                if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
5833                    candidates.push(candidate);
5834                }
5835            }
5836            if offset == 0 {
5837                break;
5838            }
5839            offset -= 1;
5840        }
5841    }
5842
5843    choose_v41_terminal_candidate(candidates)
5844}
5845
5846fn locate_v41_public_terminal(
5847    bytes: &[u8],
5848    volume_header: &VolumeHeader,
5849    crypto_header: &CryptoHeader<'_>,
5850    options: ReaderOptions,
5851) -> Result<V41PublicTerminal, FormatError> {
5852    let mut candidates = Vec::new();
5853    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5854        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5855        collect_v41_public_locator_candidate(
5856            bytes,
5857            final_offset,
5858            0,
5859            volume_header,
5860            crypto_header,
5861            &mut candidates,
5862        );
5863    }
5864    if bytes.len() >= LOCATOR_PAIR_LEN {
5865        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5866        collect_v41_public_locator_candidate(
5867            bytes,
5868            mirror_offset,
5869            1,
5870            volume_header,
5871            crypto_header,
5872            &mut candidates,
5873        );
5874    }
5875
5876    if candidates.is_empty() {
5877        let scan = max_critical_recovery_scan(options)?;
5878        let scan_start = bytes.len().saturating_sub(scan);
5879        let mut offset = bytes.len().saturating_sub(4);
5880        while offset >= scan_start {
5881            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5882                collect_v41_public_locator_candidate(
5883                    bytes,
5884                    offset,
5885                    2,
5886                    volume_header,
5887                    crypto_header,
5888                    &mut candidates,
5889                );
5890            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5891                if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
5892                    bytes,
5893                    offset,
5894                    volume_header,
5895                    crypto_header,
5896                ) {
5897                    candidates.push(candidate);
5898                }
5899            }
5900            if offset == 0 {
5901                break;
5902            }
5903            offset -= 1;
5904        }
5905    }
5906
5907    choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5908}
5909
5910fn collect_v41_locator_candidate(
5911    bytes: &[u8],
5912    offset: usize,
5913    expected_sequence: u32,
5914    context: KeyHoldingTerminalContext<'_>,
5915    candidates: &mut Vec<TerminalCandidate>,
5916) {
5917    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5918        return;
5919    };
5920    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5921        return;
5922    };
5923    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5924        return;
5925    }
5926    if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
5927        candidates.push(candidate);
5928    }
5929}
5930
5931fn collect_v41_locator_candidate_read_at(
5932    reader: &dyn ArchiveReadAt,
5933    offset: u64,
5934    expected_sequence: u32,
5935    context: KeyHoldingTerminalContext<'_>,
5936    candidates: &mut Vec<TerminalCandidate>,
5937) {
5938    let Ok(raw) = read_at_vec(
5939        reader,
5940        offset,
5941        CRITICAL_RECOVERY_LOCATOR_LEN,
5942        "CriticalRecoveryLocator",
5943    ) else {
5944        return;
5945    };
5946    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5947        return;
5948    };
5949    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5950        return;
5951    }
5952    if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
5953        candidates.push(candidate);
5954    }
5955}
5956
5957fn collect_v41_locator_authority_candidate(
5958    bytes: &[u8],
5959    offset: usize,
5960    expected_sequence: u32,
5961    master_key: &MasterKey,
5962    candidates: &mut Vec<TerminalAuthorityCandidate>,
5963) {
5964    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5965        return;
5966    };
5967    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5968        return;
5969    };
5970    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5971        return;
5972    }
5973    if let Ok(candidate) =
5974        parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
5975    {
5976        candidates.push(candidate);
5977    }
5978}
5979
5980fn collect_v41_recipient_wrap_locator_authority_candidate<F>(
5981    bytes: &[u8],
5982    offset: usize,
5983    expected_sequence: u32,
5984    resolver: &mut F,
5985    candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
5986) where
5987    F: FnMut(
5988        RecipientWrapRecordContext<'_>,
5989    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5990{
5991    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5992        return;
5993    };
5994    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5995        return;
5996    };
5997    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5998        return;
5999    }
6000    if let Ok(candidate) =
6001        parse_locator_cmra_recipient_wrap_authority_candidate(bytes, offset, locator, resolver)
6002    {
6003        candidates.push(candidate);
6004    }
6005}
6006
6007fn collect_v41_recipient_wrap_locator_authority_candidate_read_at<F>(
6008    reader: &dyn ArchiveReadAt,
6009    offset: u64,
6010    expected_sequence: u32,
6011    resolver: &mut F,
6012    candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
6013) where
6014    F: FnMut(
6015        RecipientWrapRecordContext<'_>,
6016    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6017{
6018    let Ok(raw) = read_at_vec(
6019        reader,
6020        offset,
6021        CRITICAL_RECOVERY_LOCATOR_LEN,
6022        "CriticalRecoveryLocator",
6023    ) else {
6024        return;
6025    };
6026    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6027        return;
6028    };
6029    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6030        return;
6031    }
6032    if let Ok(candidate) = parse_locator_cmra_recipient_wrap_authority_candidate_read_at(
6033        reader, offset, locator, resolver,
6034    ) {
6035        candidates.push(candidate);
6036    }
6037}
6038
6039fn collect_v41_locator_authority_candidate_read_at(
6040    reader: &dyn ArchiveReadAt,
6041    offset: u64,
6042    expected_sequence: u32,
6043    master_key: &MasterKey,
6044    candidates: &mut Vec<TerminalAuthorityCandidate>,
6045) {
6046    let Ok(raw) = read_at_vec(
6047        reader,
6048        offset,
6049        CRITICAL_RECOVERY_LOCATOR_LEN,
6050        "CriticalRecoveryLocator",
6051    ) else {
6052        return;
6053    };
6054    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6055        return;
6056    };
6057    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6058        return;
6059    }
6060    if let Ok(candidate) =
6061        parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
6062    {
6063        candidates.push(candidate);
6064    }
6065}
6066
6067fn collect_v41_public_locator_candidate(
6068    bytes: &[u8],
6069    offset: usize,
6070    expected_sequence: u32,
6071    volume_header: &VolumeHeader,
6072    crypto_header: &CryptoHeader<'_>,
6073    candidates: &mut Vec<PublicTerminalCandidate>,
6074) {
6075    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6076        return;
6077    };
6078    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6079        return;
6080    };
6081    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6082        return;
6083    }
6084    if let Ok(candidate) =
6085        parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
6086    {
6087        candidates.push(candidate);
6088    }
6089}
6090
6091fn choose_v41_terminal_candidate(
6092    mut candidates: Vec<TerminalCandidate>,
6093) -> Result<TerminalCandidate, FormatError> {
6094    candidates.sort_by_key(|candidate| candidate.anchor);
6095    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6096        "no valid v41 CMRA candidate found",
6097    ))?;
6098    if let Some(previous) = candidates.last() {
6099        if previous.anchor == winner.anchor
6100            && (previous.cmra_offset != winner.cmra_offset
6101                || previous.cmra_length != winner.cmra_length)
6102        {
6103            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6104        }
6105    }
6106    Ok(winner)
6107}
6108
6109fn choose_v41_terminal_authority_candidate(
6110    mut candidates: Vec<TerminalAuthorityCandidate>,
6111) -> Result<TerminalAuthorityCandidate, FormatError> {
6112    candidates.sort_by_key(|candidate| candidate.anchor);
6113    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6114        "no valid v41 CMRA candidate found",
6115    ))?;
6116    if let Some(previous) = candidates.last() {
6117        if previous.anchor == winner.anchor
6118            && (previous.cmra_offset != winner.cmra_offset
6119                || previous.cmra_length != winner.cmra_length)
6120        {
6121            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6122        }
6123    }
6124    Ok(winner)
6125}
6126
6127fn choose_v41_recipient_wrap_terminal_authority_candidate(
6128    mut candidates: Vec<RecipientWrapTerminalAuthorityCandidate>,
6129) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError> {
6130    candidates.sort_by_key(|candidate| candidate.anchor);
6131    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6132        "no valid v41 CMRA candidate found",
6133    ))?;
6134    if let Some(previous) = candidates.last() {
6135        if previous.anchor == winner.anchor
6136            && (previous.cmra_offset != winner.cmra_offset
6137                || previous.cmra_length != winner.cmra_length)
6138        {
6139            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6140        }
6141    }
6142    Ok(winner)
6143}
6144
6145fn choose_v41_public_terminal_candidate(
6146    mut candidates: Vec<PublicTerminalCandidate>,
6147) -> Result<PublicTerminalCandidate, FormatError> {
6148    candidates.sort_by_key(|candidate| candidate.anchor);
6149    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6150        "no valid v41 public CMRA candidate found",
6151    ))?;
6152    if let Some(previous) = candidates.last() {
6153        if previous.anchor == winner.anchor
6154            && (previous.cmra_offset != winner.cmra_offset
6155                || previous.cmra_length != winner.cmra_length)
6156        {
6157            return Err(FormatError::InvalidArchive(
6158                "ambiguous v41 public CMRA candidates",
6159            ));
6160        }
6161    }
6162    Ok(winner)
6163}
6164
6165fn parse_locator_cmra_candidate(
6166    bytes: &[u8],
6167    locator_offset: usize,
6168    locator: CriticalRecoveryLocator,
6169    context: KeyHoldingTerminalContext<'_>,
6170) -> Result<TerminalCandidate, FormatError> {
6171    let tuple = CmraDecoderTuple::from(locator);
6172    validate_cmra_decoder_tuple(tuple)?;
6173    let expected_cmra_length = cmra_serialized_length(tuple)?;
6174    if locator.cmra_length as u64 != expected_cmra_length {
6175        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6176    }
6177    validate_locator_position(locator_offset, locator)?;
6178    let recovered = recover_cmra(
6179        bytes,
6180        locator.cmra_offset,
6181        Some(tuple),
6182        CmraRecoveryMode::KeyHolding,
6183    )?;
6184    if recovered.tuple != tuple {
6185        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6186    }
6187    if expected_cmra_length != recovered.cmra_length {
6188        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6189    }
6190    validate_locator_image_boundary(locator, &recovered.image)?;
6191    validate_cmra_identity_hints(
6192        recovered.header_hints,
6193        Some(CmraIdentityHints::from(locator)),
6194        &recovered.image,
6195    )?;
6196    let terminal =
6197        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
6198    Ok(TerminalCandidate {
6199        terminal,
6200        anchor: locator_offset
6201            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6202            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6203        locator_sequence: Some(locator.locator_sequence),
6204        cmra_offset: locator.cmra_offset,
6205        cmra_length: recovered.cmra_length,
6206    })
6207}
6208
6209fn parse_locator_cmra_candidate_read_at(
6210    reader: &dyn ArchiveReadAt,
6211    locator_offset: u64,
6212    locator: CriticalRecoveryLocator,
6213    context: KeyHoldingTerminalContext<'_>,
6214) -> Result<TerminalCandidate, FormatError> {
6215    let tuple = CmraDecoderTuple::from(locator);
6216    validate_cmra_decoder_tuple(tuple)?;
6217    let expected_cmra_length = cmra_serialized_length(tuple)?;
6218    if locator.cmra_length as u64 != expected_cmra_length {
6219        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6220    }
6221    validate_locator_position(
6222        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6223        locator,
6224    )?;
6225    let recovered = recover_cmra_read_at(
6226        reader,
6227        locator.cmra_offset,
6228        Some(tuple),
6229        CmraRecoveryMode::KeyHolding,
6230    )?;
6231    if recovered.tuple != tuple {
6232        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6233    }
6234    if expected_cmra_length != recovered.cmra_length {
6235        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6236    }
6237    validate_locator_image_boundary(locator, &recovered.image)?;
6238    validate_cmra_identity_hints(
6239        recovered.header_hints,
6240        Some(CmraIdentityHints::from(locator)),
6241        &recovered.image,
6242    )?;
6243    let terminal = validate_recovered_terminal_read_at(
6244        recovered.image,
6245        recovered.tuple,
6246        reader,
6247        context,
6248        false,
6249    )?;
6250    Ok(TerminalCandidate {
6251        terminal,
6252        anchor: to_usize(
6253            checked_u64_add(
6254                locator_offset,
6255                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6256                "locator anchor overflow",
6257            )?,
6258            "locator anchor overflow",
6259        )?,
6260        locator_sequence: Some(locator.locator_sequence),
6261        cmra_offset: locator.cmra_offset,
6262        cmra_length: recovered.cmra_length,
6263    })
6264}
6265
6266fn parse_locator_cmra_authority_candidate(
6267    bytes: &[u8],
6268    locator_offset: usize,
6269    locator: CriticalRecoveryLocator,
6270    master_key: &MasterKey,
6271) -> Result<TerminalAuthorityCandidate, FormatError> {
6272    let tuple = CmraDecoderTuple::from(locator);
6273    validate_cmra_decoder_tuple(tuple)?;
6274    let expected_cmra_length = cmra_serialized_length(tuple)?;
6275    if locator.cmra_length as u64 != expected_cmra_length {
6276        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6277    }
6278    validate_locator_position(locator_offset, locator)?;
6279    let recovered = recover_cmra(
6280        bytes,
6281        locator.cmra_offset,
6282        Some(tuple),
6283        CmraRecoveryMode::KeyHolding,
6284    )?;
6285    if recovered.tuple != tuple {
6286        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6287    }
6288    if expected_cmra_length != recovered.cmra_length {
6289        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6290    }
6291    validate_locator_image_boundary(locator, &recovered.image)?;
6292    validate_cmra_identity_hints(
6293        recovered.header_hints,
6294        Some(CmraIdentityHints::from(locator)),
6295        &recovered.image,
6296    )?;
6297    let cmra_length = recovered.cmra_length;
6298    let authority =
6299        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6300    Ok(TerminalAuthorityCandidate {
6301        authority,
6302        anchor: locator_offset
6303            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6304            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6305        cmra_offset: locator.cmra_offset,
6306        cmra_length,
6307    })
6308}
6309
6310fn parse_locator_cmra_recipient_wrap_authority_candidate<F>(
6311    bytes: &[u8],
6312    locator_offset: usize,
6313    locator: CriticalRecoveryLocator,
6314    resolver: &mut F,
6315) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6316where
6317    F: FnMut(
6318        RecipientWrapRecordContext<'_>,
6319    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6320{
6321    let tuple = CmraDecoderTuple::from(locator);
6322    validate_cmra_decoder_tuple(tuple)?;
6323    let expected_cmra_length = cmra_serialized_length(tuple)?;
6324    if locator.cmra_length as u64 != expected_cmra_length {
6325        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6326    }
6327    validate_locator_position(locator_offset, locator)?;
6328    let recovered = recover_cmra(
6329        bytes,
6330        locator.cmra_offset,
6331        Some(tuple),
6332        CmraRecoveryMode::KeyHolding,
6333    )?;
6334    if recovered.tuple != tuple {
6335        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6336    }
6337    if expected_cmra_length != recovered.cmra_length {
6338        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6339    }
6340    validate_locator_image_boundary(locator, &recovered.image)?;
6341    validate_cmra_identity_hints(
6342        recovered.header_hints,
6343        Some(CmraIdentityHints::from(locator)),
6344        &recovered.image,
6345    )?;
6346    let cmra_length = recovered.cmra_length;
6347    let authority = validate_recovered_recipient_wrap_terminal_authority(
6348        recovered.image,
6349        recovered.tuple,
6350        resolver,
6351        false,
6352    )?;
6353    Ok(RecipientWrapTerminalAuthorityCandidate {
6354        authority,
6355        anchor: locator_offset
6356            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6357            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6358        cmra_offset: locator.cmra_offset,
6359        cmra_length,
6360    })
6361}
6362
6363fn parse_locator_cmra_recipient_wrap_authority_candidate_read_at<F>(
6364    reader: &dyn ArchiveReadAt,
6365    locator_offset: u64,
6366    locator: CriticalRecoveryLocator,
6367    resolver: &mut F,
6368) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6369where
6370    F: FnMut(
6371        RecipientWrapRecordContext<'_>,
6372    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6373{
6374    let tuple = CmraDecoderTuple::from(locator);
6375    validate_cmra_decoder_tuple(tuple)?;
6376    let expected_cmra_length = cmra_serialized_length(tuple)?;
6377    if locator.cmra_length as u64 != expected_cmra_length {
6378        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6379    }
6380    validate_locator_position(
6381        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6382        locator,
6383    )?;
6384    let recovered = recover_cmra_read_at(
6385        reader,
6386        locator.cmra_offset,
6387        Some(tuple),
6388        CmraRecoveryMode::KeyHolding,
6389    )?;
6390    if recovered.tuple != tuple {
6391        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6392    }
6393    if expected_cmra_length != recovered.cmra_length {
6394        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6395    }
6396    validate_locator_image_boundary(locator, &recovered.image)?;
6397    validate_cmra_identity_hints(
6398        recovered.header_hints,
6399        Some(CmraIdentityHints::from(locator)),
6400        &recovered.image,
6401    )?;
6402    let cmra_length = recovered.cmra_length;
6403    let authority = validate_recovered_recipient_wrap_terminal_authority(
6404        recovered.image,
6405        recovered.tuple,
6406        resolver,
6407        false,
6408    )?;
6409    Ok(RecipientWrapTerminalAuthorityCandidate {
6410        authority,
6411        anchor: to_usize(
6412            checked_u64_add(
6413                locator_offset,
6414                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6415                "locator anchor overflow",
6416            )?,
6417            "locator anchor overflow",
6418        )?,
6419        cmra_offset: locator.cmra_offset,
6420        cmra_length,
6421    })
6422}
6423
6424fn parse_locator_cmra_authority_candidate_read_at(
6425    reader: &dyn ArchiveReadAt,
6426    locator_offset: u64,
6427    locator: CriticalRecoveryLocator,
6428    master_key: &MasterKey,
6429) -> Result<TerminalAuthorityCandidate, FormatError> {
6430    let tuple = CmraDecoderTuple::from(locator);
6431    validate_cmra_decoder_tuple(tuple)?;
6432    let expected_cmra_length = cmra_serialized_length(tuple)?;
6433    if locator.cmra_length as u64 != expected_cmra_length {
6434        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6435    }
6436    validate_locator_position(
6437        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6438        locator,
6439    )?;
6440    let recovered = recover_cmra_read_at(
6441        reader,
6442        locator.cmra_offset,
6443        Some(tuple),
6444        CmraRecoveryMode::KeyHolding,
6445    )?;
6446    if recovered.tuple != tuple {
6447        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6448    }
6449    if expected_cmra_length != recovered.cmra_length {
6450        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6451    }
6452    validate_locator_image_boundary(locator, &recovered.image)?;
6453    validate_cmra_identity_hints(
6454        recovered.header_hints,
6455        Some(CmraIdentityHints::from(locator)),
6456        &recovered.image,
6457    )?;
6458    let cmra_length = recovered.cmra_length;
6459    let authority =
6460        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6461    Ok(TerminalAuthorityCandidate {
6462        authority,
6463        anchor: to_usize(
6464            checked_u64_add(
6465                locator_offset,
6466                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6467                "locator anchor overflow",
6468            )?,
6469            "locator anchor overflow",
6470        )?,
6471        cmra_offset: locator.cmra_offset,
6472        cmra_length,
6473    })
6474}
6475
6476fn parse_public_locator_cmra_candidate(
6477    bytes: &[u8],
6478    locator_offset: usize,
6479    locator: CriticalRecoveryLocator,
6480    volume_header: &VolumeHeader,
6481    crypto_header: &CryptoHeader<'_>,
6482) -> Result<PublicTerminalCandidate, FormatError> {
6483    let tuple = CmraDecoderTuple::from(locator);
6484    validate_cmra_decoder_tuple(tuple)?;
6485    let expected_cmra_length = cmra_serialized_length(tuple)?;
6486    if locator.cmra_length as u64 != expected_cmra_length {
6487        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6488    }
6489    validate_locator_position(locator_offset, locator)?;
6490    let recovered = recover_cmra(
6491        bytes,
6492        locator.cmra_offset,
6493        Some(tuple),
6494        CmraRecoveryMode::PublicNoKey,
6495    )?;
6496    if recovered.tuple != tuple {
6497        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6498    }
6499    if expected_cmra_length != recovered.cmra_length {
6500        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6501    }
6502    validate_locator_image_boundary(locator, &recovered.image)?;
6503    validate_cmra_identity_hints(
6504        recovered.header_hints,
6505        Some(CmraIdentityHints::from(locator)),
6506        &recovered.image,
6507    )?;
6508    let terminal = validate_recovered_public_terminal(
6509        recovered.image,
6510        bytes,
6511        volume_header,
6512        crypto_header,
6513        false,
6514    )?;
6515    Ok(PublicTerminalCandidate {
6516        terminal,
6517        anchor: locator_offset
6518            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6519            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6520        cmra_offset: locator.cmra_offset,
6521        cmra_length: recovered.cmra_length,
6522    })
6523}
6524
6525fn parse_locatorless_cmra_candidate(
6526    bytes: &[u8],
6527    cmra_offset: usize,
6528    context: KeyHoldingTerminalContext<'_>,
6529) -> Result<TerminalCandidate, FormatError> {
6530    let recovered = recover_cmra(
6531        bytes,
6532        cmra_offset as u64,
6533        None,
6534        CmraRecoveryMode::KeyHolding,
6535    )?;
6536    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6537        return Err(FormatError::InvalidArchive(
6538            "locatorless CMRA boundary mismatch",
6539        ));
6540    }
6541    if recovered
6542        .image
6543        .volume_trailer_offset
6544        .checked_add(VOLUME_TRAILER_LEN as u64)
6545        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6546        != cmra_offset as u64
6547    {
6548        return Err(FormatError::InvalidArchive(
6549            "locatorless trailer boundary mismatch",
6550        ));
6551    }
6552    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6553    let terminal =
6554        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
6555    Ok(TerminalCandidate {
6556        terminal,
6557        anchor: cmra_offset
6558            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6559            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6560        locator_sequence: None,
6561        cmra_offset: cmra_offset as u64,
6562        cmra_length: recovered.cmra_length,
6563    })
6564}
6565
6566fn parse_locatorless_cmra_candidate_read_at(
6567    reader: &dyn ArchiveReadAt,
6568    cmra_offset: u64,
6569    context: KeyHoldingTerminalContext<'_>,
6570) -> Result<TerminalCandidate, FormatError> {
6571    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6572    if recovered.image.body_bytes_before_cmra != cmra_offset {
6573        return Err(FormatError::InvalidArchive(
6574            "locatorless CMRA boundary mismatch",
6575        ));
6576    }
6577    if recovered
6578        .image
6579        .volume_trailer_offset
6580        .checked_add(VOLUME_TRAILER_LEN as u64)
6581        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6582        != cmra_offset
6583    {
6584        return Err(FormatError::InvalidArchive(
6585            "locatorless trailer boundary mismatch",
6586        ));
6587    }
6588    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6589    let terminal = validate_recovered_terminal_read_at(
6590        recovered.image,
6591        recovered.tuple,
6592        reader,
6593        context,
6594        true,
6595    )?;
6596    Ok(TerminalCandidate {
6597        terminal,
6598        anchor: to_usize(
6599            checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
6600            "CMRA anchor overflow",
6601        )?,
6602        locator_sequence: None,
6603        cmra_offset,
6604        cmra_length: recovered.cmra_length,
6605    })
6606}
6607
6608fn parse_locatorless_cmra_authority_candidate(
6609    bytes: &[u8],
6610    cmra_offset: usize,
6611    master_key: &MasterKey,
6612) -> Result<TerminalAuthorityCandidate, FormatError> {
6613    let recovered = recover_cmra(
6614        bytes,
6615        cmra_offset as u64,
6616        None,
6617        CmraRecoveryMode::KeyHolding,
6618    )?;
6619    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6620        return Err(FormatError::InvalidArchive(
6621            "locatorless CMRA boundary mismatch",
6622        ));
6623    }
6624    if recovered
6625        .image
6626        .volume_trailer_offset
6627        .checked_add(VOLUME_TRAILER_LEN as u64)
6628        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6629        != cmra_offset as u64
6630    {
6631        return Err(FormatError::InvalidArchive(
6632            "locatorless trailer boundary mismatch",
6633        ));
6634    }
6635    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6636    let cmra_length = recovered.cmra_length;
6637    let authority =
6638        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6639    Ok(TerminalAuthorityCandidate {
6640        authority,
6641        anchor: cmra_offset
6642            .checked_add(to_usize(cmra_length, "CMRA")?)
6643            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6644        cmra_offset: cmra_offset as u64,
6645        cmra_length,
6646    })
6647}
6648
6649fn parse_locatorless_cmra_recipient_wrap_authority_candidate<F>(
6650    bytes: &[u8],
6651    cmra_offset: usize,
6652    resolver: &mut F,
6653) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6654where
6655    F: FnMut(
6656        RecipientWrapRecordContext<'_>,
6657    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6658{
6659    let recovered = recover_cmra(
6660        bytes,
6661        cmra_offset as u64,
6662        None,
6663        CmraRecoveryMode::KeyHolding,
6664    )?;
6665    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6666        return Err(FormatError::InvalidArchive(
6667            "locatorless CMRA boundary mismatch",
6668        ));
6669    }
6670    if recovered
6671        .image
6672        .volume_trailer_offset
6673        .checked_add(VOLUME_TRAILER_LEN as u64)
6674        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6675        != cmra_offset as u64
6676    {
6677        return Err(FormatError::InvalidArchive(
6678            "locatorless trailer boundary mismatch",
6679        ));
6680    }
6681    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6682    let cmra_length = recovered.cmra_length;
6683    let authority = validate_recovered_recipient_wrap_terminal_authority(
6684        recovered.image,
6685        recovered.tuple,
6686        resolver,
6687        true,
6688    )?;
6689    Ok(RecipientWrapTerminalAuthorityCandidate {
6690        authority,
6691        anchor: cmra_offset
6692            .checked_add(to_usize(cmra_length, "CMRA")?)
6693            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6694        cmra_offset: cmra_offset as u64,
6695        cmra_length,
6696    })
6697}
6698
6699fn parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at<F>(
6700    reader: &dyn ArchiveReadAt,
6701    cmra_offset: u64,
6702    resolver: &mut F,
6703) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6704where
6705    F: FnMut(
6706        RecipientWrapRecordContext<'_>,
6707    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6708{
6709    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6710    if recovered.image.body_bytes_before_cmra != cmra_offset {
6711        return Err(FormatError::InvalidArchive(
6712            "locatorless CMRA boundary mismatch",
6713        ));
6714    }
6715    if recovered
6716        .image
6717        .volume_trailer_offset
6718        .checked_add(VOLUME_TRAILER_LEN as u64)
6719        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6720        != cmra_offset
6721    {
6722        return Err(FormatError::InvalidArchive(
6723            "locatorless trailer boundary mismatch",
6724        ));
6725    }
6726    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6727    let cmra_length = recovered.cmra_length;
6728    let authority = validate_recovered_recipient_wrap_terminal_authority(
6729        recovered.image,
6730        recovered.tuple,
6731        resolver,
6732        true,
6733    )?;
6734    Ok(RecipientWrapTerminalAuthorityCandidate {
6735        authority,
6736        anchor: to_usize(
6737            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6738            "CMRA anchor overflow",
6739        )?,
6740        cmra_offset,
6741        cmra_length,
6742    })
6743}
6744
6745fn parse_locatorless_cmra_authority_candidate_read_at(
6746    reader: &dyn ArchiveReadAt,
6747    cmra_offset: u64,
6748    master_key: &MasterKey,
6749) -> Result<TerminalAuthorityCandidate, FormatError> {
6750    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6751    if recovered.image.body_bytes_before_cmra != cmra_offset {
6752        return Err(FormatError::InvalidArchive(
6753            "locatorless CMRA boundary mismatch",
6754        ));
6755    }
6756    if recovered
6757        .image
6758        .volume_trailer_offset
6759        .checked_add(VOLUME_TRAILER_LEN as u64)
6760        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6761        != cmra_offset
6762    {
6763        return Err(FormatError::InvalidArchive(
6764            "locatorless trailer boundary mismatch",
6765        ));
6766    }
6767    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6768    let cmra_length = recovered.cmra_length;
6769    let authority =
6770        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6771    Ok(TerminalAuthorityCandidate {
6772        authority,
6773        anchor: to_usize(
6774            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6775            "CMRA anchor overflow",
6776        )?,
6777        cmra_offset,
6778        cmra_length,
6779    })
6780}
6781
6782fn parse_public_locatorless_cmra_candidate(
6783    bytes: &[u8],
6784    cmra_offset: usize,
6785    volume_header: &VolumeHeader,
6786    crypto_header: &CryptoHeader<'_>,
6787) -> Result<PublicTerminalCandidate, FormatError> {
6788    let recovered = recover_cmra(
6789        bytes,
6790        cmra_offset as u64,
6791        None,
6792        CmraRecoveryMode::PublicNoKey,
6793    )?;
6794    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6795        return Err(FormatError::InvalidArchive(
6796            "locatorless CMRA boundary mismatch",
6797        ));
6798    }
6799    if recovered
6800        .image
6801        .volume_trailer_offset
6802        .checked_add(VOLUME_TRAILER_LEN as u64)
6803        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6804        != cmra_offset as u64
6805    {
6806        return Err(FormatError::InvalidArchive(
6807            "locatorless trailer boundary mismatch",
6808        ));
6809    }
6810    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6811    let terminal = validate_recovered_public_terminal(
6812        recovered.image,
6813        bytes,
6814        volume_header,
6815        crypto_header,
6816        true,
6817    )?;
6818    Ok(PublicTerminalCandidate {
6819        terminal,
6820        anchor: cmra_offset
6821            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6822            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6823        cmra_offset: cmra_offset as u64,
6824        cmra_length: recovered.cmra_length,
6825    })
6826}
6827
6828fn validate_locator_position(
6829    locator_offset: usize,
6830    locator: CriticalRecoveryLocator,
6831) -> Result<(), FormatError> {
6832    if locator.cmra_offset != locator.body_bytes_before_cmra {
6833        return Err(FormatError::InvalidArchive(
6834            "locator CMRA boundary mismatch",
6835        ));
6836    }
6837    if locator
6838        .volume_trailer_offset
6839        .checked_add(VOLUME_TRAILER_LEN as u64)
6840        .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
6841        != locator.cmra_offset
6842    {
6843        return Err(FormatError::InvalidArchive(
6844            "locator trailer boundary mismatch",
6845        ));
6846    }
6847    let expected_offset = match locator.locator_sequence {
6848        1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
6849        0 => locator
6850            .cmra_offset
6851            .checked_add(locator.cmra_length as u64)
6852            .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
6853        _ => None,
6854    }
6855    .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
6856    if expected_offset != locator_offset as u64 {
6857        return Err(FormatError::InvalidArchive(
6858            "locator position does not match sequence",
6859        ));
6860    }
6861    Ok(())
6862}
6863
6864fn validate_locator_image_boundary(
6865    locator: CriticalRecoveryLocator,
6866    image: &CriticalMetadataImage,
6867) -> Result<(), FormatError> {
6868    if locator.volume_format_rev != image.volume_format_rev
6869        || locator.volume_trailer_offset != image.volume_trailer_offset
6870        || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
6871        || image
6872            .volume_trailer_offset
6873            .checked_add(VOLUME_TRAILER_LEN as u64)
6874            .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
6875            != locator.cmra_offset
6876    {
6877        return Err(FormatError::InvalidArchive(
6878            "locator and CMRA image boundaries differ",
6879        ));
6880    }
6881    Ok(())
6882}
6883
6884fn validate_cmra_identity_hints(
6885    header_hints: Option<CmraIdentityHints>,
6886    locator_hints: Option<CmraIdentityHints>,
6887    image: &CriticalMetadataImage,
6888) -> Result<(), FormatError> {
6889    if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
6890        if header != locator {
6891            return Err(FormatError::InvalidArchive(
6892                "CMRA header and locator identity hints differ",
6893            ));
6894        }
6895    }
6896    for hints in [header_hints, locator_hints].into_iter().flatten() {
6897        if hints.archive_uuid != image.archive_uuid
6898            || hints.session_id != image.session_id
6899            || hints.volume_index != image.volume_index
6900        {
6901            return Err(FormatError::InvalidArchive(
6902                "CMRA identity hints do not match recovered image",
6903            ));
6904        }
6905    }
6906    Ok(())
6907}
6908
6909fn recover_cmra(
6910    bytes: &[u8],
6911    cmra_offset: u64,
6912    locator_tuple: Option<CmraDecoderTuple>,
6913    mode: CmraRecoveryMode,
6914) -> Result<RecoveredCmra, FormatError> {
6915    let offset = to_usize(cmra_offset, "CMRA")?;
6916    let header_bytes = slice(
6917        bytes,
6918        offset,
6919        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6920        "CriticalMetadataRecoveryHeader",
6921    )?;
6922    let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
6923    validate_cmra_decoder_tuple(tuple)?;
6924    let cmra_length = cmra_serialized_length(tuple)?;
6925    let cmra_len = to_usize(cmra_length, "CMRA")?;
6926    let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
6927    recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
6928}
6929
6930fn recover_cmra_read_at(
6931    reader: &dyn ArchiveReadAt,
6932    cmra_offset: u64,
6933    locator_tuple: Option<CmraDecoderTuple>,
6934    mode: CmraRecoveryMode,
6935) -> Result<RecoveredCmra, FormatError> {
6936    let header_bytes = read_at_vec(
6937        reader,
6938        cmra_offset,
6939        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6940        "CriticalMetadataRecoveryHeader",
6941    )?;
6942    let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
6943    validate_cmra_decoder_tuple(tuple)?;
6944    let cmra_length = cmra_serialized_length(tuple)?;
6945    let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
6946    recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
6947}
6948
6949fn recover_cmra_header_tuple(
6950    header_bytes: &[u8],
6951    locator_tuple: Option<CmraDecoderTuple>,
6952) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
6953    let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
6954    Ok(match (parsed_header, locator_tuple) {
6955        (Ok(header), Some(locator_tuple)) => {
6956            let header_tuple = CmraDecoderTuple::from(header);
6957            if header_tuple != locator_tuple {
6958                return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6959            }
6960            (locator_tuple, Some(CmraIdentityHints::from(header)))
6961        }
6962        (Ok(header), None) => (
6963            CmraDecoderTuple::from(header),
6964            Some(CmraIdentityHints::from(header)),
6965        ),
6966        (Err(_), Some(tuple)) => (tuple, None),
6967        (Err(err), _) => return Err(err),
6968    })
6969}
6970
6971fn recover_cmra_from_bytes(
6972    cmra_bytes: &[u8],
6973    tuple: CmraDecoderTuple,
6974    header_hints: Option<CmraIdentityHints>,
6975    cmra_length: u64,
6976    mode: CmraRecoveryMode,
6977) -> Result<RecoveredCmra, FormatError> {
6978    let shard_size = tuple.shard_size as usize;
6979    let mut data_shards = vec![None; tuple.data_shard_count as usize];
6980    let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
6981    let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
6982    for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
6983        let raw = slice(
6984            cmra_bytes,
6985            cursor,
6986            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
6987            "CriticalMetadataRecoveryShard",
6988        )?;
6989        let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
6990        if let Some(shard) = shard {
6991            validate_cmra_shard(&shard, idx, tuple)?;
6992            if shard.shard_role == 0 {
6993                let data_slot = data_shards
6994                    .get_mut(idx)
6995                    .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
6996                *data_slot = Some(shard.payload);
6997            } else {
6998                let parity_idx = idx - tuple.data_shard_count as usize;
6999                let parity_slot =
7000                    parity_shards
7001                        .get_mut(parity_idx)
7002                        .ok_or(FormatError::InvalidArchive(
7003                            "CMRA parity shard out of range",
7004                        ))?;
7005                *parity_slot = Some(shard.payload);
7006            }
7007        }
7008        cursor = checked_add(
7009            cursor,
7010            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
7011            "CriticalMetadataRecoveryShard",
7012        )?;
7013    }
7014    let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
7015    let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
7016    for shard in repaired {
7017        image_bytes.extend_from_slice(&shard);
7018    }
7019    image_bytes.truncate(tuple.image_length as usize);
7020    if sha256_bytes(&image_bytes) != tuple.image_sha256 {
7021        return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
7022    }
7023    let image = CriticalMetadataImage::parse(&image_bytes)?;
7024    validate_critical_metadata_image(&image, mode)?;
7025    Ok(RecoveredCmra {
7026        image,
7027        tuple,
7028        header_hints,
7029        cmra_length,
7030    })
7031}
7032
7033fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
7034    let shard_size = tuple.shard_size as u64;
7035    if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
7036        return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
7037    }
7038    let image_length = tuple.image_length as u64;
7039    let min = critical_image_min();
7040    let cap = critical_image_cap()?;
7041    if image_length < min || image_length > cap {
7042        return Err(FormatError::InvalidArchive(
7043            "CMRA image_length is outside bounds",
7044        ));
7045    }
7046    let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
7047    if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
7048        return Err(FormatError::InvalidArchive(
7049            "CMRA data_shard_count does not match image length",
7050        ));
7051    }
7052    let max_parity = 2u64.max(ceil_div_u64(
7053        checked_u64_mul(
7054            expected_data_shards,
7055            READER_MAX_CMRA_PARITY_PCT as u64,
7056            "CMRA parity overflow",
7057        )?,
7058        100,
7059    )?);
7060    if tuple.parity_shard_count as u64 > max_parity {
7061        return Err(FormatError::ReaderResourceLimitExceeded {
7062            field: "CMRA parity shard count",
7063            cap: max_parity,
7064            actual: tuple.parity_shard_count as u64,
7065        });
7066    }
7067    let total = expected_data_shards
7068        .checked_add(tuple.parity_shard_count as u64)
7069        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
7070    if total > 65_535 {
7071        return Err(FormatError::FecTooManyShards(total as usize));
7072    }
7073    Ok(())
7074}
7075
7076fn validate_cmra_writer_parity_lower_bound(
7077    tuple: CmraDecoderTuple,
7078    bit_rot_buffer_pct: u8,
7079) -> Result<(), FormatError> {
7080    let min_parity = 2u64.max(ceil_div_u64(
7081        checked_u64_mul(
7082            tuple.data_shard_count as u64,
7083            bit_rot_buffer_pct as u64,
7084            "CMRA parity lower-bound overflow",
7085        )?,
7086        100,
7087    )?);
7088    if (tuple.parity_shard_count as u64) < min_parity {
7089        return Err(FormatError::InvalidArchive(
7090            "CMRA parity shard count is below authenticated bit-rot lower bound",
7091        ));
7092    }
7093    Ok(())
7094}
7095
7096fn validate_cmra_shard(
7097    shard: &CriticalMetadataRecoveryShard,
7098    serialized_idx: usize,
7099    tuple: CmraDecoderTuple,
7100) -> Result<(), FormatError> {
7101    if shard.shard_index as usize != serialized_idx {
7102        return Err(FormatError::InvalidArchive(
7103            "CMRA shards are not in canonical order",
7104        ));
7105    }
7106    let data_count = tuple.data_shard_count as usize;
7107    let shard_size = tuple.shard_size as usize;
7108    if serialized_idx < data_count {
7109        if shard.shard_role != 0 {
7110            return Err(FormatError::InvalidArchive(
7111                "CMRA data shard has wrong role",
7112            ));
7113        }
7114        let expected_len = if serialized_idx + 1 == data_count {
7115            let used = tuple.image_length as usize - serialized_idx * shard_size;
7116            if used == 0 {
7117                shard_size
7118            } else {
7119                used
7120            }
7121        } else {
7122            shard_size
7123        };
7124        if shard.shard_payload_length as usize != expected_len {
7125            return Err(FormatError::InvalidArchive(
7126                "CMRA data shard payload length is non-canonical",
7127            ));
7128        }
7129        if serialized_idx + 1 == data_count
7130            && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
7131        {
7132            return Err(FormatError::InvalidArchive(
7133                "CMRA final data shard padding is non-zero",
7134            ));
7135        }
7136    } else {
7137        if shard.shard_role != 1 {
7138            return Err(FormatError::InvalidArchive(
7139                "CMRA parity shard has wrong role",
7140            ));
7141        }
7142        if shard.shard_payload_length as usize != shard_size {
7143            return Err(FormatError::InvalidArchive(
7144                "CMRA parity shard payload length is non-canonical",
7145            ));
7146        }
7147    }
7148    Ok(())
7149}
7150
7151fn validate_critical_metadata_image(
7152    image: &CriticalMetadataImage,
7153    mode: CmraRecoveryMode,
7154) -> Result<(), FormatError> {
7155    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7156    let key_wrap_layout_present = image.layout_flags & 0x0000_0002 != 0;
7157    let key_wrap_region = image.region(6);
7158    if key_wrap_layout_present != key_wrap_region.is_some() {
7159        return Err(FormatError::InvalidArchive(
7160            "CriticalMetadataImage key-wrap layout flag mismatch",
7161        ));
7162    }
7163    let key_wrap_present = key_wrap_layout_present;
7164    if image.volume_header_offset != 0
7165        || image.volume_header_length != VOLUME_HEADER_LEN as u32
7166        || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
7167        || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7168        || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
7169        || image.body_bytes_before_cmra
7170            != image
7171                .volume_trailer_offset
7172                .checked_add(VOLUME_TRAILER_LEN as u64)
7173                .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7174    {
7175        return Err(FormatError::InvalidArchive(
7176            "CriticalMetadataImage fixed layout is invalid",
7177        ));
7178    }
7179    if root_auth_present {
7180        if image.root_auth_footer_offset == 0
7181            || image.root_auth_footer_length == 0
7182            || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7183        {
7184            return Err(FormatError::InvalidArchive(
7185                "CriticalMetadataImage root-auth range is invalid",
7186            ));
7187        }
7188    } else if image.root_auth_footer_offset != 0
7189        || image.root_auth_footer_length != 0
7190        || image.root_auth_footer_sha256 != [0u8; 32]
7191    {
7192        return Err(FormatError::InvalidArchive(
7193            "CriticalMetadataImage root-auth fields must be zero when absent",
7194        ));
7195    }
7196    let block_record_len = image_block_record_len_from_region(image)?;
7197    let block_record_len_u64 = u64::try_from(block_record_len)
7198        .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
7199    match mode {
7200        CmraRecoveryMode::KeyHolding => {
7201            let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
7202                FormatError::InvalidArchive("BlockRecord region length overflow"),
7203            )?;
7204            if image.block_records_length != expected_len {
7205                return Err(FormatError::InvalidArchive(
7206                    "CriticalMetadataImage terminal equations are invalid",
7207                ));
7208            }
7209        }
7210        CmraRecoveryMode::PublicNoKey => {
7211            if image.block_records_length % block_record_len_u64 != 0 {
7212                return Err(FormatError::InvalidArchive(
7213                    "CriticalMetadataImage BlockRecord region is not aligned",
7214                ));
7215            }
7216        }
7217    }
7218    let crypto_header_end = image
7219        .crypto_header_offset
7220        .checked_add(image.crypto_header_length as u64)
7221        .ok_or(FormatError::InvalidArchive(
7222            "CryptoHeader boundary overflow",
7223        ))?;
7224    let expected_block_records_offset = if key_wrap_present {
7225        let key_wrap_region = key_wrap_region.ok_or(FormatError::InvalidArchive(
7226            "missing CriticalMetadataImage key-wrap region",
7227        ))?;
7228        if image.key_wrap_table_offset != crypto_header_end
7229            || image.key_wrap_table_length == 0
7230            || key_wrap_region.offset != image.key_wrap_table_offset
7231            || key_wrap_region.bytes.len() != image.key_wrap_table_length as usize
7232        {
7233            return Err(FormatError::InvalidArchive(
7234                "CriticalMetadataImage key-wrap region is malformed",
7235            ));
7236        }
7237        image
7238            .key_wrap_table_offset
7239            .checked_add(image.key_wrap_table_length as u64)
7240            .ok_or(FormatError::InvalidArchive(
7241                "KeyWrapTableV1 boundary overflow",
7242            ))?
7243    } else {
7244        if image.key_wrap_table_offset != 0
7245            || image.key_wrap_table_length != 0
7246            || image.key_wrap_table_sha256 != [0u8; 32]
7247        {
7248            return Err(FormatError::InvalidArchive(
7249                "CriticalMetadataImage key-wrap fields must be zero when absent",
7250            ));
7251        }
7252        crypto_header_end
7253    };
7254    if image.block_records_offset != expected_block_records_offset
7255        || image.manifest_footer_offset
7256            != image
7257                .block_records_offset
7258                .checked_add(image.block_records_length)
7259                .ok_or(FormatError::InvalidArchive(
7260                    "ManifestFooter boundary overflow",
7261                ))?
7262    {
7263        return Err(FormatError::InvalidArchive(
7264            "CriticalMetadataImage terminal equations are invalid",
7265        ));
7266    }
7267    let manifest_end = image
7268        .manifest_footer_offset
7269        .checked_add(MANIFEST_FOOTER_LEN as u64)
7270        .ok_or(FormatError::InvalidArchive(
7271            "RootAuthFooter boundary overflow",
7272        ))?;
7273    if root_auth_present {
7274        if image.root_auth_footer_offset != manifest_end
7275            || image
7276                .root_auth_footer_offset
7277                .checked_add(image.root_auth_footer_length as u64)
7278                .ok_or(FormatError::InvalidArchive(
7279                    "VolumeTrailer boundary overflow",
7280                ))?
7281                != image.volume_trailer_offset
7282        {
7283            return Err(FormatError::InvalidArchive(
7284                "CriticalMetadataImage root-auth terminal equations are invalid",
7285            ));
7286        }
7287    } else if image.volume_trailer_offset != manifest_end {
7288        return Err(FormatError::InvalidArchive(
7289            "CriticalMetadataImage unsigned terminal equations are invalid",
7290        ));
7291    }
7292    let expected_types: &[u16] = match (key_wrap_present, root_auth_present) {
7293        (false, false) => &[1, 2, 3, 5],
7294        (false, true) => &[1, 2, 3, 4, 5],
7295        (true, false) => &[1, 2, 6, 3, 5],
7296        (true, true) => &[1, 2, 6, 3, 4, 5],
7297    };
7298    if image.regions.len() != expected_types.len()
7299        || image
7300            .regions
7301            .iter()
7302            .map(|region| region.region_type)
7303            .ne(expected_types.iter().copied())
7304    {
7305        return Err(FormatError::InvalidArchive(
7306            "CriticalMetadataImage regions are not canonical",
7307        ));
7308    }
7309    validate_image_region(
7310        image,
7311        1,
7312        image.volume_header_offset,
7313        image.volume_header_length,
7314    )?;
7315    validate_image_region(
7316        image,
7317        2,
7318        image.crypto_header_offset,
7319        image.crypto_header_length,
7320    )?;
7321    validate_image_region(
7322        image,
7323        3,
7324        image.manifest_footer_offset,
7325        image.manifest_footer_length,
7326    )?;
7327    if key_wrap_present {
7328        validate_image_region(
7329            image,
7330            6,
7331            image.key_wrap_table_offset,
7332            image.key_wrap_table_length,
7333        )?;
7334    }
7335    if root_auth_present {
7336        validate_image_region(
7337            image,
7338            4,
7339            image.root_auth_footer_offset,
7340            image.root_auth_footer_length,
7341        )?;
7342    }
7343    validate_image_region(
7344        image,
7345        5,
7346        image.volume_trailer_offset,
7347        image.volume_trailer_length,
7348    )?;
7349    if sha256_region(image, 1)? != image.volume_header_sha256
7350        || sha256_region(image, 2)? != image.crypto_header_sha256
7351        || (key_wrap_present && sha256_region(image, 6)? != image.key_wrap_table_sha256)
7352        || (!key_wrap_present && image.key_wrap_table_sha256 != [0u8; 32])
7353        || sha256_region(image, 3)? != image.manifest_footer_sha256
7354        || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
7355        || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
7356        || sha256_region(image, 5)? != image.volume_trailer_sha256
7357    {
7358        return Err(FormatError::InvalidArchive(
7359            "CriticalMetadataImage region digest mismatch",
7360        ));
7361    }
7362    Ok(())
7363}
7364
7365fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
7366    let crypto_region = image
7367        .region(2)
7368        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7369    let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7370    crypto.fixed.validate_supported_profile()?;
7371    Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
7372}
7373
7374fn validate_image_region(
7375    image: &CriticalMetadataImage,
7376    region_type: u16,
7377    offset: u64,
7378    length: u32,
7379) -> Result<(), FormatError> {
7380    let region = image
7381        .region(region_type)
7382        .ok_or(FormatError::InvalidArchive(
7383            "missing CriticalMetadataImage region",
7384        ))?;
7385    if region.offset != offset || region.bytes.len() != length as usize {
7386        return Err(FormatError::InvalidArchive(
7387            "CriticalMetadataImage region range mismatch",
7388        ));
7389    }
7390    Ok(())
7391}
7392
7393fn validate_image_identity(
7394    image: &CriticalMetadataImage,
7395    volume_header: &VolumeHeader,
7396    crypto_header: &CryptoHeaderFixed,
7397) -> Result<(), FormatError> {
7398    if image.volume_format_rev != volume_header.volume_format_rev
7399        || image.archive_uuid != volume_header.archive_uuid
7400        || image.session_id != volume_header.session_id
7401        || image.volume_index != volume_header.volume_index
7402        || image.stripe_width != volume_header.stripe_width
7403        || image.stripe_width != crypto_header.stripe_width
7404    {
7405        return Err(FormatError::InvalidArchive(
7406            "CriticalMetadataImage identity does not match selected volume",
7407        ));
7408    }
7409    Ok(())
7410}
7411
7412fn validate_image_key_wrap_table(
7413    image: &CriticalMetadataImage,
7414    volume_header: &VolumeHeader,
7415    kdf_params: &KdfParams,
7416) -> Result<(), FormatError> {
7417    match kdf_params {
7418        KdfParams::RecipientWrap {
7419            key_wrap_table_length,
7420            key_wrap_table_record_count,
7421            key_wrap_table_digest,
7422            ..
7423        } => {
7424            if image.layout_flags & 0x0000_0002 == 0
7425                || image.key_wrap_table_length != *key_wrap_table_length
7426            {
7427                return Err(FormatError::InvalidArchive(
7428                    "CriticalMetadataImage key-wrap fields do not match KdfParams",
7429                ));
7430            }
7431            let region = image.region(6).ok_or(FormatError::InvalidArchive(
7432                "missing CriticalMetadataImage key-wrap region",
7433            ))?;
7434            if region.offset != image.key_wrap_table_offset
7435                || region.bytes.len() != *key_wrap_table_length as usize
7436            {
7437                return Err(FormatError::InvalidArchive(
7438                    "CriticalMetadataImage key-wrap region is malformed",
7439                ));
7440            }
7441            if compute_key_wrap_table_digest(*key_wrap_table_length, &region.bytes)
7442                != *key_wrap_table_digest
7443            {
7444                return Err(FormatError::IntegrityDigestMismatch {
7445                    structure: "KeyWrapTableV1",
7446                });
7447            }
7448            KeyWrapTableV1::parse(
7449                &region.bytes,
7450                &volume_header.archive_uuid,
7451                &volume_header.session_id,
7452                *key_wrap_table_length,
7453                *key_wrap_table_record_count,
7454            )?;
7455            Ok(())
7456        }
7457        _ => {
7458            if image.layout_flags & 0x0000_0002 != 0
7459                || image.region(6).is_some()
7460                || image.key_wrap_table_offset != 0
7461                || image.key_wrap_table_length != 0
7462                || image.key_wrap_table_sha256 != [0u8; 32]
7463            {
7464                return Err(FormatError::InvalidArchive(
7465                    "CriticalMetadataImage key-wrap fields must be zero when absent",
7466                ));
7467            }
7468            Ok(())
7469        }
7470    }
7471}
7472
7473fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
7474    Ok(sha256_bytes(
7475        &image
7476            .region(region_type)
7477            .ok_or(FormatError::InvalidArchive(
7478                "missing CriticalMetadataImage region",
7479            ))?
7480            .bytes,
7481    ))
7482}
7483
7484fn validate_recovered_terminal_authority(
7485    image: CriticalMetadataImage,
7486    tuple: CmraDecoderTuple,
7487    master_key: &MasterKey,
7488    require_cmra_boundary_magic: bool,
7489) -> Result<RecoveredTerminalAuthority, FormatError> {
7490    let volume_header_region = image
7491        .region(1)
7492        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7493    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7494    let crypto_region = image
7495        .region(2)
7496        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7497    let crypto_header_bytes = crypto_region.bytes.clone();
7498    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7499    let kdf_params = parsed_crypto.kdf_params.clone();
7500    let subkeys = subkeys_for_open(
7501        Some(master_key),
7502        parsed_crypto.fixed.aead_algo,
7503        &volume_header.archive_uuid,
7504        &volume_header.session_id,
7505    )?;
7506    verify_integrity_tag(
7507        HmacDomain::CryptoHeader,
7508        parsed_crypto.fixed.aead_algo,
7509        volume_header.volume_format_rev,
7510        Some(&subkeys.mac_key),
7511        &volume_header.archive_uuid,
7512        &volume_header.session_id,
7513        parsed_crypto.hmac_covered_bytes,
7514        &parsed_crypto.header_hmac,
7515    )?;
7516    parsed_crypto.validate_extension_semantics()?;
7517    validate_seekable_supported_volume(
7518        &volume_header,
7519        &parsed_crypto.fixed,
7520        &parsed_crypto.extensions,
7521    )?;
7522    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7523    let crypto_header = parsed_crypto.fixed.clone();
7524    if crypto_header.bit_rot_buffer_pct == 0 {
7525        return Err(FormatError::InvalidArchive(
7526            "CMRA startup recovery requires a nonzero bit-rot budget",
7527        ));
7528    }
7529    drop(parsed_crypto);
7530
7531    let terminal = validate_recovered_terminal_inner(
7532        image,
7533        tuple,
7534        require_cmra_boundary_magic,
7535        true,
7536        KeyHoldingTerminalContext {
7537            subkeys: &subkeys,
7538            volume_header: &volume_header,
7539            crypto_header: &crypto_header,
7540            crypto_header_bytes: &crypto_header_bytes,
7541        },
7542    )?;
7543    Ok(RecoveredTerminalAuthority {
7544        terminal,
7545        volume_header,
7546        crypto_header,
7547        crypto_header_bytes,
7548        subkeys,
7549        kdf_params,
7550    })
7551}
7552
7553fn validate_recovered_recipient_wrap_terminal_authority<F>(
7554    image: CriticalMetadataImage,
7555    tuple: CmraDecoderTuple,
7556    resolver: &mut F,
7557    require_cmra_boundary_magic: bool,
7558) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
7559where
7560    F: FnMut(
7561        RecipientWrapRecordContext<'_>,
7562    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
7563{
7564    let volume_header_region = image
7565        .region(1)
7566        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7567    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7568    let crypto_region = image
7569        .region(2)
7570        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7571    let crypto_header_bytes = crypto_region.bytes.clone();
7572    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7573    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
7574        || !parsed_crypto.fixed.aead_algo.is_encrypted()
7575    {
7576        return Err(FormatError::KeyMaterialMismatch);
7577    }
7578    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
7579    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7580    if parsed_crypto.fixed.bit_rot_buffer_pct == 0 {
7581        return Err(FormatError::InvalidArchive(
7582            "CMRA startup recovery requires a nonzero bit-rot budget",
7583        ));
7584    }
7585    validate_cmra_writer_parity_lower_bound(tuple, parsed_crypto.fixed.bit_rot_buffer_pct)?;
7586    validate_image_key_wrap_table(&image, &volume_header, &parsed_crypto.kdf_params)?;
7587    let key_wrap_region = image.region(6).ok_or(FormatError::InvalidArchive(
7588        "missing CriticalMetadataImage key-wrap region",
7589    ))?;
7590    let startup_key_wrap_table = parse_startup_key_wrap_table_bytes(
7591        &volume_header,
7592        &parsed_crypto.kdf_params,
7593        key_wrap_region.bytes.clone(),
7594    )?;
7595    let subkeys = recipient_wrap_subkeys_from_table(
7596        &volume_header,
7597        &parsed_crypto,
7598        &startup_key_wrap_table.table,
7599        resolver,
7600    )?;
7601    parsed_crypto.validate_extension_semantics()?;
7602    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
7603    let crypto_header = parsed_crypto.fixed.clone();
7604
7605    let terminal = validate_recovered_terminal_inner(
7606        image,
7607        tuple,
7608        require_cmra_boundary_magic,
7609        true,
7610        KeyHoldingTerminalContext {
7611            subkeys: &subkeys,
7612            volume_header: &volume_header,
7613            crypto_header: &crypto_header,
7614            crypto_header_bytes: &crypto_header_bytes,
7615        },
7616    )?;
7617    Ok(RecoveredRecipientWrapTerminalAuthority {
7618        terminal,
7619        volume_header,
7620        crypto_header,
7621        crypto_header_bytes,
7622        key_wrap_table_bytes: startup_key_wrap_table.bytes,
7623        block_records_start: startup_key_wrap_table.block_records_start,
7624        subkeys,
7625    })
7626}
7627
7628fn validate_recovered_terminal(
7629    image: CriticalMetadataImage,
7630    tuple: CmraDecoderTuple,
7631    bytes: &[u8],
7632    context: KeyHoldingTerminalContext<'_>,
7633    require_cmra_boundary_magic: bool,
7634) -> Result<V41Terminal, FormatError> {
7635    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7636    let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
7637    validate_recovered_terminal_inner(
7638        image,
7639        tuple,
7640        require_cmra_boundary_magic,
7641        cmra_boundary_magic_ok,
7642        context,
7643    )
7644}
7645
7646fn validate_recovered_terminal_read_at(
7647    image: CriticalMetadataImage,
7648    tuple: CmraDecoderTuple,
7649    reader: &dyn ArchiveReadAt,
7650    context: KeyHoldingTerminalContext<'_>,
7651    require_cmra_boundary_magic: bool,
7652) -> Result<V41Terminal, FormatError> {
7653    let mut magic = [0u8; 4];
7654    reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
7655    validate_recovered_terminal_inner(
7656        image,
7657        tuple,
7658        require_cmra_boundary_magic,
7659        magic == *b"TZCR",
7660        context,
7661    )
7662}
7663
7664fn validate_recovered_terminal_inner(
7665    image: CriticalMetadataImage,
7666    tuple: CmraDecoderTuple,
7667    require_cmra_boundary_magic: bool,
7668    cmra_boundary_magic_ok: bool,
7669    context: KeyHoldingTerminalContext<'_>,
7670) -> Result<V41Terminal, FormatError> {
7671    let subkeys = context.subkeys;
7672    let volume_header = context.volume_header;
7673    let crypto_header = context.crypto_header;
7674    let volume_header_region = image
7675        .region(1)
7676        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7677    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7678    if &recovered_volume_header != volume_header {
7679        return Err(FormatError::InvalidArchive(
7680            "CMRA VolumeHeader differs from parsed VolumeHeader",
7681        ));
7682    }
7683    validate_image_identity(&image, volume_header, crypto_header)?;
7684    let crypto_region = image
7685        .region(2)
7686        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7687    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7688    if recovered_crypto.fixed != *crypto_header {
7689        return Err(FormatError::InvalidArchive(
7690            "CMRA CryptoHeader differs from parsed CryptoHeader",
7691        ));
7692    }
7693    let recovered_pre_hmac_len = crypto_region
7694        .bytes
7695        .len()
7696        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7697        .ok_or(FormatError::InvalidArchive(
7698            "CMRA CryptoHeader is too short",
7699        ))?;
7700    let parsed_pre_hmac_len = context
7701        .crypto_header_bytes
7702        .len()
7703        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7704        .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
7705    if recovered_pre_hmac_len != parsed_pre_hmac_len
7706        || crypto_region.bytes[..recovered_pre_hmac_len]
7707            != context.crypto_header_bytes[..parsed_pre_hmac_len]
7708    {
7709        return Err(FormatError::InvalidArchive(
7710            "CMRA CryptoHeader differs from parsed CryptoHeader",
7711        ));
7712    }
7713    verify_integrity_tag(
7714        HmacDomain::CryptoHeader,
7715        recovered_crypto.fixed.aead_algo,
7716        volume_header.volume_format_rev,
7717        Some(&subkeys.mac_key),
7718        &volume_header.archive_uuid,
7719        &volume_header.session_id,
7720        recovered_crypto.hmac_covered_bytes,
7721        &recovered_crypto.header_hmac,
7722    )?;
7723    validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
7724    recovered_crypto.validate_extension_semantics()?;
7725    validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7726
7727    let manifest_region = image
7728        .region(3)
7729        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7730    let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
7731    validate_manifest_footer(
7732        volume_header,
7733        crypto_header,
7734        &manifest_footer,
7735        subkeys,
7736        volume_header.volume_format_rev,
7737        &manifest_region.bytes,
7738    )?;
7739    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
7740
7741    let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
7742        let root_auth_region = image
7743            .region(4)
7744            .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7745        let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7746        if footer.format_version != volume_header.format_version
7747            || footer.volume_format_rev != volume_header.volume_format_rev
7748        {
7749            return Err(FormatError::InvalidArchive(
7750                "RootAuthFooter format/revision does not match VolumeHeader",
7751            ));
7752        }
7753        if footer.archive_uuid != volume_header.archive_uuid
7754            || footer.session_id != volume_header.session_id
7755            || footer.footer_length()? != image.root_auth_footer_length
7756        {
7757            return Err(FormatError::InvalidArchive(
7758                "RootAuthFooter identity or length does not match terminal image",
7759            ));
7760        }
7761        Some(footer)
7762    } else {
7763        None
7764    };
7765
7766    let trailer_region = image
7767        .region(5)
7768        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7769    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7770    verify_integrity_tag(
7771        HmacDomain::VolumeTrailer,
7772        crypto_header.aead_algo,
7773        volume_header.volume_format_rev,
7774        Some(&subkeys.mac_key),
7775        &volume_header.archive_uuid,
7776        &volume_header.session_id,
7777        &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
7778        &trailer.trailer_hmac,
7779    )?;
7780    validate_trailer_identity(volume_header, &trailer)?;
7781    validate_v41_trailer_equations(&image, &trailer)?;
7782
7783    if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
7784        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7785    }
7786
7787    let manifest_footer_bytes = manifest_region.bytes.clone();
7788    let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
7789    Ok(V41Terminal {
7790        image,
7791        manifest_footer_bytes,
7792        root_auth_footer_bytes,
7793        root_auth_footer,
7794        volume_trailer: trailer,
7795    })
7796}
7797
7798fn validate_recovered_public_terminal(
7799    image: CriticalMetadataImage,
7800    bytes: &[u8],
7801    volume_header: &VolumeHeader,
7802    public_crypto_header: &CryptoHeader<'_>,
7803    require_cmra_boundary_magic: bool,
7804) -> Result<V41PublicTerminal, FormatError> {
7805    if image.layout_flags & 0x0000_0001 == 0 {
7806        return Err(FormatError::ReaderUnsupported(
7807            "public no-key verification requires root-auth",
7808        ));
7809    }
7810    let volume_header_region = image
7811        .region(1)
7812        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7813    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7814    if &recovered_volume_header != volume_header {
7815        return Err(FormatError::InvalidArchive(
7816            "CMRA VolumeHeader differs from parsed VolumeHeader",
7817        ));
7818    }
7819    validate_image_identity(&image, volume_header, &public_crypto_header.fixed)?;
7820    let crypto_region = image
7821        .region(2)
7822        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7823    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7824    if !public_crypto_headers_agree(&recovered_crypto.fixed, &public_crypto_header.fixed)
7825        || !public_kdf_profiles_agree(
7826            &recovered_crypto.kdf_params,
7827            &public_crypto_header.kdf_params,
7828        )
7829    {
7830        return Err(FormatError::InvalidArchive(
7831            "CMRA CryptoHeader differs from parsed CryptoHeader",
7832        ));
7833    }
7834    recovered_crypto.validate_extension_semantics()?;
7835    validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7836
7837    image
7838        .region(3)
7839        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7840
7841    let root_auth_region = image
7842        .region(4)
7843        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7844    let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7845    if root_auth_footer.format_version != volume_header.format_version
7846        || root_auth_footer.volume_format_rev != volume_header.volume_format_rev
7847    {
7848        return Err(FormatError::InvalidArchive(
7849            "public RootAuthFooter format/revision does not match VolumeHeader",
7850        ));
7851    }
7852    if root_auth_footer.archive_uuid != volume_header.archive_uuid
7853        || root_auth_footer.session_id != volume_header.session_id
7854        || root_auth_footer.footer_length()? != image.root_auth_footer_length
7855    {
7856        return Err(FormatError::InvalidArchive(
7857            "public RootAuthFooter identity or length does not match terminal image",
7858        ));
7859    }
7860
7861    let trailer_region = image
7862        .region(5)
7863        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7864    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7865    validate_trailer_identity(volume_header, &trailer)?;
7866    validate_v41_public_trailer_profile(&image, &trailer)?;
7867
7868    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7869    if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
7870        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7871    }
7872
7873    let root_auth_footer_bytes = root_auth_region.bytes.clone();
7874    Ok(V41PublicTerminal {
7875        image,
7876        root_auth_footer_bytes,
7877        root_auth_footer,
7878    })
7879}
7880
7881fn validate_v41_trailer_equations(
7882    image: &CriticalMetadataImage,
7883    trailer: &VolumeTrailer,
7884) -> Result<(), FormatError> {
7885    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7886    if trailer.bytes_written != image.volume_trailer_offset
7887        || trailer.manifest_footer_offset != image.manifest_footer_offset
7888        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7889        || trailer.block_count != image.block_count
7890    {
7891        return Err(FormatError::InvalidArchive(
7892            "VolumeTrailer does not match v41 terminal layout",
7893        ));
7894    }
7895    if root_auth_present {
7896        if trailer.root_auth_flags != 0x0000_0001
7897            || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7898            || trailer.root_auth_footer_length != image.root_auth_footer_length
7899            || image.root_auth_footer_offset
7900                != image
7901                    .manifest_footer_offset
7902                    .checked_add(MANIFEST_FOOTER_LEN as u64)
7903                    .ok_or(FormatError::InvalidArchive(
7904                        "RootAuthFooter trailer boundary overflow",
7905                    ))?
7906            || image
7907                .root_auth_footer_offset
7908                .checked_add(image.root_auth_footer_length as u64)
7909                .ok_or(FormatError::InvalidArchive(
7910                    "RootAuthFooter trailer boundary overflow",
7911                ))?
7912                != image.volume_trailer_offset
7913        {
7914            return Err(FormatError::InvalidArchive(
7915                "VolumeTrailer root-auth fields do not match v41 terminal layout",
7916            ));
7917        }
7918    } else if trailer.root_auth_footer_offset != 0
7919        || trailer.root_auth_footer_length != 0
7920        || trailer.root_auth_flags != 0
7921    {
7922        return Err(FormatError::InvalidArchive(
7923            "VolumeTrailer root-auth fields must be zero when absent",
7924        ));
7925    }
7926    Ok(())
7927}
7928
7929fn validate_v41_public_trailer_profile(
7930    image: &CriticalMetadataImage,
7931    trailer: &VolumeTrailer,
7932) -> Result<(), FormatError> {
7933    if trailer.bytes_written != image.volume_trailer_offset
7934        || trailer.manifest_footer_offset != image.manifest_footer_offset
7935        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7936    {
7937        return Err(FormatError::InvalidArchive(
7938            "VolumeTrailer does not match v41 public terminal layout",
7939        ));
7940    }
7941    if trailer.root_auth_flags != 0x0000_0001
7942        || trailer.root_auth_footer_offset == 0
7943        || trailer.root_auth_footer_length == 0
7944        || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7945        || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7946        || trailer.root_auth_footer_length != image.root_auth_footer_length
7947        || image.root_auth_footer_offset
7948            != image
7949                .manifest_footer_offset
7950                .checked_add(MANIFEST_FOOTER_LEN as u64)
7951                .ok_or(FormatError::InvalidArchive(
7952                    "RootAuthFooter trailer boundary overflow",
7953                ))?
7954        || image
7955            .root_auth_footer_offset
7956            .checked_add(image.root_auth_footer_length as u64)
7957            .ok_or(FormatError::InvalidArchive(
7958                "RootAuthFooter trailer boundary overflow",
7959            ))?
7960            != image.volume_trailer_offset
7961    {
7962        return Err(FormatError::InvalidArchive(
7963            "VolumeTrailer root-auth fields do not match v41 public terminal layout",
7964        ));
7965    }
7966    Ok(())
7967}
7968
7969fn critical_image_min() -> u64 {
7970    const MIN_CRYPTO_HEADER_LEN: u64 = 116;
7971    CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
7972        + 4 * SERIALIZED_REGION_HEADER_LEN as u64
7973        + VOLUME_HEADER_LEN as u64
7974        + MIN_CRYPTO_HEADER_LEN
7975        + MANIFEST_FOOTER_LEN as u64
7976        + VOLUME_TRAILER_LEN as u64
7977        + IMAGE_CRC_LEN as u64
7978}
7979
7980fn critical_image_cap() -> Result<u64, FormatError> {
7981    [
7982        CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
7983        6 * SERIALIZED_REGION_HEADER_LEN as u64,
7984        VOLUME_HEADER_LEN as u64,
7985        READER_MAX_CRYPTO_HEADER_LEN as u64,
7986        READER_MAX_KEY_WRAP_TABLE_LEN as u64,
7987        MANIFEST_FOOTER_LEN as u64,
7988        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
7989        VOLUME_TRAILER_LEN as u64,
7990        IMAGE_CRC_LEN as u64,
7991    ]
7992    .into_iter()
7993    .try_fold(0u64, |total, value| {
7994        total
7995            .checked_add(value)
7996            .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
7997    })
7998}
7999
8000fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
8001    let shard_total = (tuple.data_shard_count as u64)
8002        .checked_add(tuple.parity_shard_count as u64)
8003        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
8004    let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
8005        .checked_add(tuple.shard_size as u64)
8006        .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
8007    checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
8008        .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
8009        .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
8010}
8011
8012fn cmra_worst_case_cap() -> Result<u64, FormatError> {
8013    let cap = critical_image_cap()?;
8014    let mut worst = 0u64;
8015    let mut shard_size = 512u64;
8016    while shard_size <= 4096 {
8017        let data = ceil_div_u64(cap, shard_size)?;
8018        let parity = 2u64.max(ceil_div_u64(
8019            checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
8020            100,
8021        )?);
8022        let tuple = CmraDecoderTuple {
8023            shard_size: shard_size as u32,
8024            data_shard_count: u16::try_from(data)
8025                .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
8026            parity_shard_count: u16::try_from(parity)
8027                .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
8028            image_length: u32::try_from(cap)
8029                .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
8030            image_sha256: [0u8; 32],
8031        };
8032        worst = worst.max(cmra_serialized_length(tuple)?);
8033        shard_size += 2;
8034    }
8035    Ok(worst)
8036}
8037
8038pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
8039    let total = [
8040        MANIFEST_FOOTER_LEN as u64,
8041        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
8042        VOLUME_TRAILER_LEN as u64,
8043        cmra_worst_case_cap()?,
8044        LOCATOR_PAIR_LEN as u64,
8045    ]
8046    .into_iter()
8047    .try_fold(0u64, |sum, value| {
8048        sum.checked_add(value)
8049            .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
8050    })?;
8051    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
8052}
8053
8054fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
8055    let worst = cmra_worst_case_cap()?;
8056    let total = options
8057        .max_trailing_garbage_scan
8058        .try_into()
8059        .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8060        .and_then(|scan: u64| {
8061            scan.checked_add(worst)
8062                .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
8063                .ok_or(FormatError::InvalidArchive("scan cap overflow"))
8064        })?;
8065    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8066}
8067
8068fn validate_bootstrap_single_volume_input(
8069    volume_header: &VolumeHeader,
8070    crypto_header: &CryptoHeaderFixed,
8071) -> Result<(), FormatError> {
8072    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
8073        return Err(FormatError::ReaderUnsupported(
8074            "bootstrap sidecar reader supports only single-volume archive input",
8075        ));
8076    }
8077    if crypto_header.stripe_width != volume_header.stripe_width {
8078        return Err(FormatError::InvalidArchive(
8079            "VolumeHeader and CryptoHeader stripe_width differ",
8080        ));
8081    }
8082    Ok(())
8083}
8084
8085#[derive(Debug)]
8086struct ParsedBootstrapSidecar {
8087    manifest_footer: Option<ManifestFooter>,
8088    index_root_records_section: Option<(u64, u64)>,
8089    dictionary_records_section: Option<(u64, u64)>,
8090}
8091
8092pub(crate) struct NonSeekableBootstrapMaterial {
8093    pub(crate) manifest_footer: ManifestFooter,
8094    pub(crate) payload_dictionary: Option<Vec<u8>>,
8095}
8096
8097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8098enum BootstrapSidecarUse {
8099    SeekableAssist,
8100    NonSeekableRandomAccess,
8101}
8102
8103impl ParsedBootstrapSidecar {
8104    fn require_sections_for(
8105        &self,
8106        sidecar_use: BootstrapSidecarUse,
8107        crypto_header: &CryptoHeaderFixed,
8108    ) -> Result<(), FormatError> {
8109        if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
8110            if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
8111                return Err(FormatError::ReaderUnsupported(
8112                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8113                ));
8114            }
8115            if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
8116                return Err(FormatError::ReaderUnsupported(
8117                    "dictionary bootstrap required",
8118                ));
8119            }
8120        }
8121        Ok(())
8122    }
8123}
8124
8125pub(crate) fn parse_non_seekable_bootstrap_material(
8126    bootstrap_sidecar: &[u8],
8127    volume_header: &VolumeHeader,
8128    crypto_header: &CryptoHeaderFixed,
8129    subkeys: &Subkeys,
8130) -> Result<NonSeekableBootstrapMaterial, FormatError> {
8131    validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
8132    let sidecar =
8133        parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
8134    sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
8135    let manifest_footer = sidecar
8136        .manifest_footer
8137        .clone()
8138        .ok_or(FormatError::ReaderUnsupported(
8139            "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8140        ))?;
8141
8142    let mut blocks = BTreeMap::new();
8143    let (offset, length) =
8144        sidecar
8145            .index_root_records_section
8146            .ok_or(FormatError::ReaderUnsupported(
8147                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8148            ))?;
8149    let index_root_records = parse_sidecar_block_records(
8150        bootstrap_sidecar,
8151        crypto_header.block_size as usize,
8152        SidecarBlockRecordsSection {
8153            offset,
8154            length,
8155            extent: index_root_extent_from_manifest(&manifest_footer),
8156            data_kind: BlockKind::IndexRootData,
8157            parity_kind: BlockKind::IndexRootParity,
8158            structure: "IndexRoot",
8159        },
8160    )?;
8161    insert_sidecar_records(&mut blocks, index_root_records)?;
8162
8163    let limits = metadata_limits(crypto_header);
8164    let index_root_plaintext = load_metadata_object_from_parts(
8165        &blocks,
8166        ObjectLoadContext::index_root(
8167            volume_header,
8168            crypto_header,
8169            subkeys,
8170            index_root_extent_from_manifest(&manifest_footer),
8171        ),
8172        manifest_footer.index_root_decompressed_size,
8173    )?;
8174    let index_root = IndexRoot::parse(
8175        &index_root_plaintext,
8176        crypto_header.has_dictionary != 0,
8177        limits,
8178    )?;
8179
8180    if crypto_header.has_dictionary != 0 {
8181        let (offset, length) =
8182            sidecar
8183                .dictionary_records_section
8184                .ok_or(FormatError::ReaderUnsupported(
8185                    "dictionary bootstrap required",
8186                ))?;
8187        let dictionary_records = parse_sidecar_block_records(
8188            bootstrap_sidecar,
8189            crypto_header.block_size as usize,
8190            SidecarBlockRecordsSection {
8191                offset,
8192                length,
8193                extent: dictionary_extent_from_index_root(&index_root)?,
8194                data_kind: BlockKind::DictionaryData,
8195                parity_kind: BlockKind::DictionaryParity,
8196                structure: "dictionary",
8197            },
8198        )?;
8199        insert_sidecar_records(&mut blocks, dictionary_records)?;
8200    }
8201    let payload_dictionary =
8202        load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
8203
8204    Ok(NonSeekableBootstrapMaterial {
8205        manifest_footer,
8206        payload_dictionary,
8207    })
8208}
8209
8210fn parse_bootstrap_sidecar(
8211    bytes: &[u8],
8212    volume_header: &VolumeHeader,
8213    crypto_header: &CryptoHeaderFixed,
8214    subkeys: &Subkeys,
8215) -> Result<ParsedBootstrapSidecar, FormatError> {
8216    let header_bytes = slice(
8217        bytes,
8218        0,
8219        BOOTSTRAP_SIDECAR_HEADER_LEN,
8220        "BootstrapSidecarHeader",
8221    )?;
8222    let header = BootstrapSidecarHeader::parse(header_bytes)?;
8223    if header.archive_uuid != volume_header.archive_uuid
8224        || header.session_id != volume_header.session_id
8225    {
8226        return Err(FormatError::InvalidArchive(
8227            "bootstrap sidecar identity does not match VolumeHeader",
8228        ));
8229    }
8230    verify_integrity_tag(
8231        HmacDomain::BootstrapSidecar,
8232        crypto_header.aead_algo,
8233        volume_header.volume_format_rev,
8234        Some(&subkeys.mac_key),
8235        &volume_header.archive_uuid,
8236        &volume_header.session_id,
8237        &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
8238        &header.sidecar_hmac,
8239    )?;
8240    header.validate_packed_layout(bytes.len() as u64)?;
8241    validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
8242
8243    if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
8244        return Err(FormatError::InvalidArchive(
8245            "bootstrap sidecar has dictionary records while has_dictionary is false",
8246        ));
8247    }
8248
8249    let manifest_footer = if header.has_manifest_footer() {
8250        let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
8251        let manifest_bytes = slice(
8252            bytes,
8253            manifest_offset,
8254            MANIFEST_FOOTER_LEN,
8255            "ManifestFooter",
8256        )?;
8257        let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
8258        validate_sidecar_manifest_footer(
8259            volume_header,
8260            crypto_header,
8261            &manifest_footer,
8262            subkeys,
8263            volume_header.volume_format_rev,
8264            manifest_bytes,
8265        )?;
8266        manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
8267        Some(manifest_footer)
8268    } else {
8269        None
8270    };
8271
8272    Ok(ParsedBootstrapSidecar {
8273        manifest_footer,
8274        index_root_records_section: header.has_index_root_records().then_some((
8275            header.index_root_records_offset,
8276            header.index_root_records_length,
8277        )),
8278        dictionary_records_section: header.has_dictionary_records().then_some((
8279            header.dictionary_records_offset,
8280            header.dictionary_records_length,
8281        )),
8282    })
8283}
8284
8285fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
8286    ObjectExtent {
8287        first_block_index: manifest_footer.index_root_first_block,
8288        data_block_count: manifest_footer.index_root_data_block_count,
8289        parity_block_count: manifest_footer.index_root_parity_block_count,
8290        encrypted_size: manifest_footer.index_root_encrypted_size,
8291    }
8292}
8293
8294fn insert_sidecar_records(
8295    blocks: &mut BTreeMap<u64, BlockRecord>,
8296    records: Vec<BlockRecord>,
8297) -> Result<(), FormatError> {
8298    for record in records {
8299        if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
8300            if existing != record {
8301                return Err(FormatError::InvalidArchive(
8302                    "bootstrap sidecar conflicts with volume BlockRecord",
8303                ));
8304            }
8305        }
8306    }
8307    Ok(())
8308}
8309
8310fn validate_sidecar_manifest_footer(
8311    volume_header: &VolumeHeader,
8312    crypto_header: &CryptoHeaderFixed,
8313    footer: &ManifestFooter,
8314    subkeys: &Subkeys,
8315    volume_format_rev: u16,
8316    raw: &[u8],
8317) -> Result<(), FormatError> {
8318    if footer.archive_uuid != volume_header.archive_uuid
8319        || footer.session_id != volume_header.session_id
8320    {
8321        return Err(FormatError::InvalidArchive(
8322            "sidecar ManifestFooter identity does not match VolumeHeader",
8323        ));
8324    }
8325    if footer.volume_index != 0 {
8326        return Err(FormatError::InvalidArchive(
8327            "sidecar ManifestFooter volume_index must be zero",
8328        ));
8329    }
8330    if footer.total_volumes != crypto_header.stripe_width {
8331        return Err(FormatError::InvalidArchive(
8332            "sidecar ManifestFooter total_volumes does not match stripe_width",
8333        ));
8334    }
8335    if footer.is_authoritative != 1 {
8336        return Err(FormatError::InvalidArchive(
8337            "sidecar ManifestFooter is not authoritative",
8338        ));
8339    }
8340    verify_integrity_tag(
8341        HmacDomain::ManifestFooter,
8342        crypto_header.aead_algo,
8343        volume_format_rev,
8344        Some(&subkeys.mac_key),
8345        &volume_header.archive_uuid,
8346        &volume_header.session_id,
8347        &raw[..MANIFEST_HMAC_COVERED_LEN],
8348        &footer.manifest_hmac,
8349    )
8350}
8351
8352fn validate_sidecar_size_cap(
8353    header: &BootstrapSidecarHeader,
8354    crypto_header: &CryptoHeaderFixed,
8355    file_size: u64,
8356) -> Result<(), FormatError> {
8357    let record_len = checked_u64_add(
8358        crypto_header.block_size as u64,
8359        BLOCK_RECORD_FRAMING_LEN as u64,
8360        "bootstrap sidecar cap overflow",
8361    )?;
8362    let max_index_records = crypto_header.index_root_fec_data_shards as u64
8363        + crypto_header.index_root_fec_parity_shards as u64;
8364    let max_record_section_bytes = checked_u64_mul(
8365        max_index_records,
8366        record_len,
8367        "bootstrap sidecar cap overflow",
8368    )?;
8369    if header.index_root_records_length % record_len != 0 {
8370        return Err(FormatError::InvalidArchive(
8371            "bootstrap sidecar IndexRoot records length is not aligned",
8372        ));
8373    }
8374    if header.index_root_records_length / record_len > max_index_records {
8375        return Err(FormatError::InvalidArchive(
8376            "bootstrap sidecar IndexRoot records exceed resource cap",
8377        ));
8378    }
8379    if header.dictionary_records_length % record_len != 0 {
8380        return Err(FormatError::InvalidArchive(
8381            "bootstrap sidecar dictionary records length is not aligned",
8382        ));
8383    }
8384    if header.dictionary_records_length / record_len > max_index_records {
8385        return Err(FormatError::InvalidArchive(
8386            "bootstrap sidecar dictionary records exceed resource cap",
8387        ));
8388    }
8389
8390    let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
8391    if header.has_manifest_footer() {
8392        cap = cap
8393            .checked_add(MANIFEST_FOOTER_LEN as u64)
8394            .ok_or(FormatError::InvalidArchive(
8395                "bootstrap sidecar cap overflow",
8396            ))?;
8397    }
8398    if header.has_index_root_records() {
8399        cap = checked_u64_add(
8400            cap,
8401            max_record_section_bytes,
8402            "bootstrap sidecar cap overflow",
8403        )?;
8404    }
8405    if header.has_dictionary_records() {
8406        cap = checked_u64_add(
8407            cap,
8408            max_record_section_bytes,
8409            "bootstrap sidecar cap overflow",
8410        )?;
8411    }
8412    if file_size > cap {
8413        return Err(FormatError::InvalidArchive(
8414            "bootstrap sidecar exceeds resource cap",
8415        ));
8416    }
8417    Ok(())
8418}
8419
8420#[derive(Debug, Clone, Copy)]
8421struct SidecarBlockRecordsSection {
8422    offset: u64,
8423    length: u64,
8424    extent: ObjectExtent,
8425    data_kind: BlockKind,
8426    parity_kind: BlockKind,
8427    structure: &'static str,
8428}
8429
8430fn parse_sidecar_block_records(
8431    sidecar_bytes: &[u8],
8432    block_size: usize,
8433    section: SidecarBlockRecordsSection,
8434) -> Result<Vec<BlockRecord>, FormatError> {
8435    let record_len = block_size
8436        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8437        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8438    if section.length % record_len as u64 != 0 {
8439        return Err(FormatError::InvalidArchive(
8440            "sidecar BlockRecord section is not aligned",
8441        ));
8442    }
8443    let expected_count =
8444        section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
8445    let actual_count = usize::try_from(section.length / record_len as u64)
8446        .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
8447    if actual_count != expected_count {
8448        return Err(FormatError::InvalidArchive(
8449            "sidecar BlockRecord section does not match declared extent",
8450        ));
8451    }
8452    let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
8453    let raw = slice(
8454        sidecar_bytes,
8455        start,
8456        to_usize(section.length, "BootstrapSidecarHeader")?,
8457        "BootstrapSidecarHeader",
8458    )?;
8459    let mut records = Vec::with_capacity(expected_count);
8460
8461    for idx in 0..expected_count {
8462        let record = BlockRecord::parse(
8463            slice(raw, idx * record_len, record_len, "BlockRecord")?,
8464            block_size,
8465        )?;
8466        let expected_block_index = checked_u64_add(
8467            section.extent.first_block_index,
8468            idx as u64,
8469            section.structure,
8470        )?;
8471        if record.block_index != expected_block_index {
8472            return Err(FormatError::InvalidArchive(
8473                "sidecar BlockRecord section has missing or duplicate blocks",
8474            ));
8475        }
8476        let expected_kind = if idx < section.extent.data_block_count as usize {
8477            section.data_kind
8478        } else {
8479            section.parity_kind
8480        };
8481        if record.kind != expected_kind {
8482            return Err(FormatError::InvalidArchive(
8483                "sidecar BlockRecord section has wrong kind",
8484            ));
8485        }
8486        let should_be_last = idx + 1 == section.extent.data_block_count as usize;
8487        if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
8488        {
8489            return Err(FormatError::InvalidArchive(
8490                "sidecar BlockRecord section has wrong last-data flag",
8491            ));
8492        }
8493        records.push(record);
8494    }
8495
8496    Ok(records)
8497}
8498
8499fn validate_trailer_identity(
8500    volume_header: &VolumeHeader,
8501    trailer: &VolumeTrailer,
8502) -> Result<(), FormatError> {
8503    if trailer.archive_uuid != volume_header.archive_uuid
8504        || trailer.session_id != volume_header.session_id
8505        || trailer.volume_index != volume_header.volume_index
8506    {
8507        return Err(FormatError::InvalidArchive(
8508            "VolumeTrailer identity does not match VolumeHeader",
8509        ));
8510    }
8511    Ok(())
8512}
8513
8514fn validate_manifest_footer(
8515    volume_header: &VolumeHeader,
8516    crypto_header: &CryptoHeaderFixed,
8517    footer: &ManifestFooter,
8518    subkeys: &Subkeys,
8519    volume_format_rev: u16,
8520    raw: &[u8],
8521) -> Result<(), FormatError> {
8522    if footer.archive_uuid != volume_header.archive_uuid
8523        || footer.session_id != volume_header.session_id
8524        || footer.volume_index != volume_header.volume_index
8525    {
8526        return Err(FormatError::InvalidArchive(
8527            "ManifestFooter identity does not match VolumeHeader",
8528        ));
8529    }
8530    if footer.total_volumes != volume_header.stripe_width {
8531        return Err(FormatError::InvalidArchive(
8532            "ManifestFooter total_volumes does not match stripe_width",
8533        ));
8534    }
8535    if footer.is_authoritative != 1 {
8536        return Err(FormatError::InvalidArchive(
8537            "ManifestFooter is not authoritative",
8538        ));
8539    }
8540    verify_integrity_tag(
8541        HmacDomain::ManifestFooter,
8542        crypto_header.aead_algo,
8543        volume_format_rev,
8544        Some(&subkeys.mac_key),
8545        &volume_header.archive_uuid,
8546        &volume_header.session_id,
8547        &raw[..MANIFEST_HMAC_COVERED_LEN],
8548        &footer.manifest_hmac,
8549    )
8550}
8551
8552#[derive(Debug)]
8553struct ParsedBlockRegion {
8554    blocks: BTreeMap<u64, BlockRecord>,
8555    erased_block_indices: BTreeSet<u64>,
8556}
8557
8558fn parse_block_region(
8559    bytes: &[u8],
8560    start: usize,
8561    end: usize,
8562    block_size: usize,
8563    volume_header: &VolumeHeader,
8564    trailer: &VolumeTrailer,
8565) -> Result<ParsedBlockRegion, FormatError> {
8566    if end < start {
8567        return Err(FormatError::InvalidArchive(
8568            "ManifestFooter starts before BlockRecord region",
8569        ));
8570    }
8571    let record_len = block_size
8572        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8573        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8574    let region_len = end - start;
8575    if region_len % record_len != 0 {
8576        return Err(FormatError::InvalidArchive(
8577            "BlockRecord region length is not aligned",
8578        ));
8579    }
8580    let observed_count = region_len / record_len;
8581    if observed_count as u64 != trailer.block_count {
8582        return Err(FormatError::InvalidArchive(
8583            "VolumeTrailer block_count does not match BlockRecord region",
8584        ));
8585    }
8586
8587    let mut blocks = BTreeMap::new();
8588    let mut erased_block_indices = BTreeSet::new();
8589    for idx in 0..observed_count {
8590        let offset = start + idx * record_len;
8591        let expected_block_index = checked_u64_add(
8592            volume_header.volume_index as u64,
8593            checked_u64_mul(
8594                idx as u64,
8595                volume_header.stripe_width as u64,
8596                "BlockRecord index overflow",
8597            )?,
8598            "BlockRecord index overflow",
8599        )?;
8600        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8601        match BlockRecord::parse(raw, block_size) {
8602            Ok(record) => {
8603                if record.block_index != expected_block_index {
8604                    return Err(FormatError::InvalidArchive(
8605                        "BlockRecord index does not match volume position",
8606                    ));
8607                }
8608                if blocks.insert(record.block_index, record).is_some() {
8609                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8610                }
8611            }
8612            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
8613                if !erased_block_indices.insert(expected_block_index) {
8614                    return Err(FormatError::InvalidArchive(
8615                        "duplicate erased BlockRecord index",
8616                    ));
8617                }
8618            }
8619            Err(err) => return Err(err),
8620        }
8621    }
8622
8623    Ok(ParsedBlockRegion {
8624        blocks,
8625        erased_block_indices,
8626    })
8627}
8628
8629fn validate_seekable_block_region_layout(
8630    start: u64,
8631    end: u64,
8632    block_size: usize,
8633    trailer: &VolumeTrailer,
8634) -> Result<(), FormatError> {
8635    if end < start {
8636        return Err(FormatError::InvalidArchive(
8637            "ManifestFooter starts before BlockRecord region",
8638        ));
8639    }
8640    let record_len = block_record_len(block_size)?;
8641    let region_len = end - start;
8642    if region_len % record_len != 0 {
8643        return Err(FormatError::InvalidArchive(
8644            "BlockRecord region length is not aligned",
8645        ));
8646    }
8647    let observed_count = region_len / record_len;
8648    if observed_count != trailer.block_count {
8649        return Err(FormatError::InvalidArchive(
8650            "VolumeTrailer block_count does not match BlockRecord region",
8651        ));
8652    }
8653    Ok(())
8654}
8655
8656fn parse_public_block_observation(
8657    bytes: &[u8],
8658    start: usize,
8659    image: &CriticalMetadataImage,
8660    block_size: usize,
8661    volume_header: &VolumeHeader,
8662) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
8663    let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
8664    if start != image_start {
8665        return Err(FormatError::InvalidArchive(
8666            "public BlockRecord observation start mismatch",
8667        ));
8668    }
8669    let scan_limit_u64 = image
8670        .block_records_offset
8671        .checked_add(image.block_records_length)
8672        .ok_or(FormatError::InvalidArchive(
8673            "public BlockRecord observation limit overflow",
8674        ))?;
8675    if scan_limit_u64 != image.manifest_footer_offset {
8676        return Err(FormatError::InvalidArchive(
8677            "public BlockRecord observation limit mismatch",
8678        ));
8679    }
8680    let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
8681    if scan_limit < start {
8682        return Err(FormatError::InvalidArchive(
8683            "public BlockRecord observation limit before start",
8684        ));
8685    }
8686    let record_len = block_size
8687        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8688        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8689    let region_len = scan_limit - start;
8690    if region_len % record_len != 0 {
8691        return Err(FormatError::InvalidArchive(
8692            "public BlockRecord observation window is not aligned",
8693        ));
8694    }
8695
8696    let mut blocks = BTreeMap::new();
8697    let mut offset = start;
8698    let mut observed_slot = 0u64;
8699    while offset < scan_limit {
8700        let magic_end = checked_add(offset, 4, "BlockRecord")?;
8701        if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
8702            break;
8703        }
8704        let record_end = checked_add(offset, record_len, "BlockRecord")?;
8705        if record_end > scan_limit {
8706            return Err(FormatError::InvalidArchive(
8707                "public BlockRecord observation slot is incomplete",
8708            ));
8709        }
8710        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8711        let record = BlockRecord::parse(raw, block_size)?;
8712        let expected_block_index = checked_u64_add(
8713            volume_header.volume_index as u64,
8714            checked_u64_mul(
8715                observed_slot,
8716                volume_header.stripe_width as u64,
8717                "BlockRecord index overflow",
8718            )?,
8719            "BlockRecord index overflow",
8720        )?;
8721        if record.block_index != expected_block_index {
8722            return Err(FormatError::InvalidArchive(
8723                "public BlockRecord index does not match volume position",
8724            ));
8725        }
8726        if blocks.insert(record.block_index, record).is_some() {
8727            return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8728        }
8729        offset = record_end;
8730        observed_slot = observed_slot
8731            .checked_add(1)
8732            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8733    }
8734
8735    let mut scan = if offset < scan_limit {
8736        checked_add(offset, record_len, "BlockRecord")?
8737    } else {
8738        scan_limit
8739    };
8740    while scan < scan_limit {
8741        let magic_end = checked_add(scan, 4, "BlockRecord")?;
8742        let record_end = checked_add(scan, record_len, "BlockRecord")?;
8743        if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
8744            let raw = slice(bytes, scan, record_len, "BlockRecord")?;
8745            if BlockRecord::parse(raw, block_size).is_ok() {
8746                return Err(FormatError::InvalidArchive(
8747                    "public observation has ambiguous extra BlockRecord",
8748                ));
8749            }
8750        }
8751        scan = record_end;
8752    }
8753
8754    Ok(blocks)
8755}
8756
8757pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
8758    matches!(
8759        error,
8760        FormatError::BadCrc {
8761            structure: "BlockRecord",
8762        } | FormatError::BadMagic {
8763            structure: "BlockRecord",
8764        } | FormatError::NonZeroReserved {
8765            structure: "BlockRecord",
8766        }
8767    )
8768}
8769
8770fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
8771    let len = block_size
8772        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8773        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8774    u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
8775}
8776
8777fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
8778    lhs.checked_mul(rhs)
8779        .ok_or(FormatError::InvalidArchive(reason))
8780}
8781
8782fn parse_stream_block_prefix(
8783    bytes: &[u8],
8784    start: usize,
8785    block_size: usize,
8786    volume_header: &VolumeHeader,
8787) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
8788    let record_len = block_size
8789        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8790        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8791    let mut blocks = BTreeMap::new();
8792    let mut offset = start;
8793    let mut observed_block_count = 0u64;
8794
8795    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
8796        let expected_block_index =
8797            expected_stream_block_index(volume_header, observed_block_count)?;
8798        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8799        match BlockRecord::parse(raw, block_size) {
8800            Ok(record) => {
8801                if record.block_index != expected_block_index {
8802                    return Err(FormatError::InvalidArchive(
8803                        "BlockRecord index does not match stream position",
8804                    ));
8805                }
8806                if blocks.insert(record.block_index, record).is_some() {
8807                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8808                }
8809            }
8810            Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
8811            Err(err) => return Err(err),
8812        }
8813        offset = checked_add(offset, record_len, "BlockRecord")?;
8814        observed_block_count = observed_block_count
8815            .checked_add(1)
8816            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8817    }
8818
8819    Ok((blocks, offset, observed_block_count))
8820}
8821
8822pub(crate) fn expected_stream_block_index(
8823    volume_header: &VolumeHeader,
8824    observed_block_count: u64,
8825) -> Result<u64, FormatError> {
8826    checked_u64_add(
8827        volume_header.volume_index as u64,
8828        checked_u64_mul(
8829            observed_block_count,
8830            volume_header.stripe_width as u64,
8831            "BlockRecord index overflow",
8832        )?,
8833        "BlockRecord index overflow",
8834    )
8835}
8836
8837fn parse_sequential_block_or_erasure(
8838    bytes: &[u8],
8839    offset: usize,
8840    record_len: usize,
8841    block_size: usize,
8842    volume_header: &VolumeHeader,
8843    observed_block_count: u64,
8844) -> Result<Option<BlockRecord>, FormatError> {
8845    let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
8846    let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8847    match BlockRecord::parse(raw, block_size) {
8848        Ok(record) => {
8849            if record.block_index != expected_block_index {
8850                return Err(FormatError::InvalidArchive(
8851                    "BlockRecord index does not match stream position",
8852                ));
8853            }
8854            Ok(Some(record))
8855        }
8856        Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
8857        Err(err) => Err(err),
8858    }
8859}
8860
8861fn parse_terminal_material(
8862    bytes: &[u8],
8863    manifest_offset: usize,
8864    observed_block_count: u64,
8865    context: KeyHoldingTerminalContext<'_>,
8866    options: ReaderOptions,
8867) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
8868    let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
8869    if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
8870        return Err(FormatError::InvalidArchive(
8871            "sequential terminal does not end at EOF",
8872        ));
8873    }
8874    let terminal = candidate.terminal;
8875    if terminal.image.manifest_footer_offset != manifest_offset as u64 {
8876        return Err(FormatError::InvalidArchive(
8877            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8878        ));
8879    }
8880    if terminal.volume_trailer.block_count != observed_block_count {
8881        return Err(FormatError::InvalidArchive(
8882            "VolumeTrailer block_count does not match observed stream",
8883        ));
8884    }
8885    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8886    Ok((
8887        manifest_footer,
8888        terminal.volume_trailer,
8889        terminal.root_auth_footer,
8890    ))
8891}
8892
8893pub(crate) fn parse_terminal_material_read_at(
8894    reader: &dyn ArchiveReadAt,
8895    input_len: u64,
8896    manifest_offset: u64,
8897    observed_block_count: u64,
8898    context: KeyHoldingTerminalContext<'_>,
8899) -> Result<SequentialTerminalMaterial, FormatError> {
8900    let mut candidates = Vec::new();
8901    if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
8902        collect_v41_locator_candidate_read_at(
8903            reader,
8904            input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
8905            0,
8906            context,
8907            &mut candidates,
8908        );
8909    }
8910    if input_len >= LOCATOR_PAIR_LEN as u64 {
8911        collect_v41_locator_candidate_read_at(
8912            reader,
8913            input_len - LOCATOR_PAIR_LEN as u64,
8914            1,
8915            context,
8916            &mut candidates,
8917        );
8918    }
8919
8920    let candidate = choose_v41_terminal_candidate(candidates)?;
8921    if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
8922        return Err(FormatError::InvalidArchive(
8923            "sequential terminal does not end at EOF",
8924        ));
8925    }
8926    let terminal = candidate.terminal;
8927    if terminal.image.manifest_footer_offset != manifest_offset {
8928        return Err(FormatError::InvalidArchive(
8929            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8930        ));
8931    }
8932    if terminal.volume_trailer.block_count != observed_block_count {
8933        return Err(FormatError::InvalidArchive(
8934            "VolumeTrailer block_count does not match observed stream",
8935        ));
8936    }
8937    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8938    Ok(SequentialTerminalMaterial {
8939        manifest_footer,
8940        volume_trailer: terminal.volume_trailer,
8941        root_auth_footer: terminal.root_auth_footer,
8942    })
8943}
8944
8945fn terminal_candidate_reaches_eof(
8946    candidate: &TerminalCandidate,
8947    input_len: usize,
8948) -> Result<bool, FormatError> {
8949    let expected_end =
8950        match candidate.locator_sequence {
8951            Some(0) => candidate.anchor,
8952            Some(1) => candidate
8953                .anchor
8954                .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
8955                .ok_or(FormatError::InvalidArchive(
8956                    "terminal EOF boundary overflow",
8957                ))?,
8958            None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
8959                FormatError::InvalidArchive("terminal EOF boundary overflow"),
8960            )?,
8961            Some(_) => {
8962                return Err(FormatError::InvalidArchive(
8963                    "invalid terminal locator sequence",
8964                ))
8965            }
8966        };
8967    Ok(expected_end == input_len)
8968}
8969
8970#[derive(Debug, Default)]
8971struct PendingSequentialEnvelope {
8972    data_shards: Vec<Option<Vec<u8>>>,
8973    parity_shards: Vec<Option<Vec<u8>>>,
8974    saw_last_data: bool,
8975    awaiting_tentative_parity: bool,
8976}
8977
8978impl PendingSequentialEnvelope {
8979    fn is_empty(&self) -> bool {
8980        self.data_shards.is_empty() && self.parity_shards.is_empty()
8981    }
8982}
8983
8984fn handle_sequential_payload_erasure(
8985    pending: &mut PendingSequentialEnvelope,
8986    crypto_header: &CryptoHeaderFixed,
8987    metadata_seen: bool,
8988) -> Result<(), FormatError> {
8989    if metadata_seen || pending.saw_last_data {
8990        return Err(FormatError::BadCrc {
8991            structure: "BlockRecord",
8992        });
8993    }
8994    if !sequential_payload_parity_is_guaranteed(crypto_header) {
8995        return Err(FormatError::BadCrc {
8996            structure: "BlockRecord",
8997        });
8998    }
8999    pending.data_shards.push(None);
9000    pending.awaiting_tentative_parity = true;
9001    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
9002        return Err(FormatError::InvalidArchive(
9003            "sequential payload envelope exceeds data-shard cap",
9004        ));
9005    }
9006    Ok(())
9007}
9008
9009fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
9010    crypto_header.fec_parity_shards > 0
9011        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
9012}
9013
9014fn sequential_extract_tar_stream_with_options(
9015    bytes: &[u8],
9016    master_key: &MasterKey,
9017    options: ReaderOptions,
9018) -> Result<Vec<u8>, FormatError> {
9019    validate_reader_options(options)?;
9020    if bytes.len() < VOLUME_HEADER_LEN {
9021        return Err(FormatError::InvalidLength {
9022            structure: "archive",
9023            expected: VOLUME_HEADER_LEN,
9024            actual: bytes.len(),
9025        });
9026    }
9027
9028    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
9029    let crypto_start = volume_header.crypto_header_offset as usize;
9030    let crypto_len = volume_header.crypto_header_length as usize;
9031    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
9032    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
9033    let subkeys = subkeys_for_open(
9034        Some(master_key),
9035        parsed_crypto.fixed.aead_algo,
9036        &volume_header.archive_uuid,
9037        &volume_header.session_id,
9038    )?;
9039    verify_integrity_tag(
9040        HmacDomain::CryptoHeader,
9041        parsed_crypto.fixed.aead_algo,
9042        volume_header.volume_format_rev,
9043        Some(&subkeys.mac_key),
9044        &volume_header.archive_uuid,
9045        &volume_header.session_id,
9046        parsed_crypto.hmac_covered_bytes,
9047        &parsed_crypto.header_hmac,
9048    )?;
9049    parsed_crypto.validate_extension_semantics()?;
9050    validate_sequential_supported_volume(
9051        &volume_header,
9052        &parsed_crypto.fixed,
9053        &parsed_crypto.extensions,
9054    )?;
9055    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
9056    let block_records_start = startup_block_records_start(
9057        &volume_header,
9058        &parsed_crypto.kdf_params,
9059        |start, length| {
9060            let start = to_usize(start, "KeyWrapTableV1")?;
9061            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
9062        },
9063    )?;
9064
9065    let block_size = parsed_crypto.fixed.block_size as usize;
9066    let record_len = block_size
9067        .checked_add(BLOCK_RECORD_FRAMING_LEN)
9068        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
9069    let mut offset = to_usize(block_records_start, "BlockRecord")?;
9070    let mut observed_block_count = 0u64;
9071    let mut metadata_seen = false;
9072    let mut pending = PendingSequentialEnvelope::default();
9073    let mut next_envelope_index = 0u64;
9074    let mut tar_stream = Vec::new();
9075    let max_tar_stream_size = options.max_verify_tar_size;
9076    let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
9077    let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
9078    let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
9079        parsed_crypto.fixed.max_path_length,
9080        total_extraction_cap,
9081    );
9082
9083    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9084        let record = parse_sequential_block_or_erasure(
9085            bytes,
9086            offset,
9087            record_len,
9088            block_size,
9089            &volume_header,
9090            observed_block_count,
9091        )?;
9092        observed_block_count = observed_block_count
9093            .checked_add(1)
9094            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9095        let Some(record) = record else {
9096            handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
9097            offset = checked_add(offset, record_len, "BlockRecord")?;
9098            continue;
9099        };
9100
9101        match record.kind {
9102            BlockKind::PayloadData => {
9103                if metadata_seen {
9104                    return Err(FormatError::InvalidArchive(
9105                        "payload BlockRecord appears after metadata",
9106                    ));
9107                }
9108                if pending.awaiting_tentative_parity {
9109                    return Err(FormatError::InvalidArchive(
9110                        "sequential payload envelope boundary is ambiguous after CRC erasure",
9111                    ));
9112                }
9113                if pending.saw_last_data {
9114                    finalize_sequential_envelope(
9115                        &mut pending,
9116                        SequentialEnvelopeDecodeContext {
9117                            crypto_header: &parsed_crypto.fixed,
9118                            subkeys: &subkeys,
9119                            volume_header: &volume_header,
9120                            next_envelope_index: &mut next_envelope_index,
9121                            tar_stream: &mut tar_stream,
9122                            max_tar_stream_size,
9123                            tar_stream_total_validator: &mut tar_stream_total_validator,
9124                        },
9125                    )?;
9126                }
9127                let is_last_data = record.is_last_data();
9128                pending.data_shards.push(Some(record.payload));
9129                if is_last_data {
9130                    pending.saw_last_data = true;
9131                }
9132                if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
9133                    return Err(FormatError::InvalidArchive(
9134                        "sequential payload envelope exceeds data-shard cap",
9135                    ));
9136                }
9137            }
9138            BlockKind::PayloadParity => {
9139                if metadata_seen {
9140                    return Err(FormatError::InvalidArchive(
9141                        "payload parity BlockRecord appears after metadata",
9142                    ));
9143                }
9144                if pending.awaiting_tentative_parity {
9145                    pending.awaiting_tentative_parity = false;
9146                    pending.saw_last_data = true;
9147                } else if pending.data_shards.is_empty() || !pending.saw_last_data {
9148                    return Err(FormatError::InvalidArchive(
9149                        "payload parity appears before envelope data is complete",
9150                    ));
9151                }
9152                pending.parity_shards.push(Some(record.payload));
9153                if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
9154                    return Err(FormatError::InvalidArchive(
9155                        "sequential payload envelope exceeds parity-shard cap",
9156                    ));
9157                }
9158            }
9159            _ => {
9160                if !pending.is_empty() {
9161                    finalize_sequential_envelope(
9162                        &mut pending,
9163                        SequentialEnvelopeDecodeContext {
9164                            crypto_header: &parsed_crypto.fixed,
9165                            subkeys: &subkeys,
9166                            volume_header: &volume_header,
9167                            next_envelope_index: &mut next_envelope_index,
9168                            tar_stream: &mut tar_stream,
9169                            max_tar_stream_size,
9170                            tar_stream_total_validator: &mut tar_stream_total_validator,
9171                        },
9172                    )?;
9173                }
9174                metadata_seen = true;
9175            }
9176        }
9177
9178        offset = checked_add(offset, record_len, "BlockRecord")?;
9179    }
9180
9181    if !pending.is_empty() {
9182        finalize_sequential_envelope(
9183            &mut pending,
9184            SequentialEnvelopeDecodeContext {
9185                crypto_header: &parsed_crypto.fixed,
9186                subkeys: &subkeys,
9187                volume_header: &volume_header,
9188                next_envelope_index: &mut next_envelope_index,
9189                tar_stream: &mut tar_stream,
9190                max_tar_stream_size,
9191                tar_stream_total_validator: &mut tar_stream_total_validator,
9192            },
9193        )?;
9194    }
9195
9196    parse_terminal_material(
9197        bytes,
9198        offset,
9199        observed_block_count,
9200        KeyHoldingTerminalContext {
9201            subkeys: &subkeys,
9202            volume_header: &volume_header,
9203            crypto_header: &parsed_crypto.fixed,
9204            crypto_header_bytes: crypto_bytes,
9205        },
9206        options,
9207    )?;
9208    // This public helper is intentionally whole-buffer: decoded payload bytes
9209    // stay internal until terminal ManifestFooter and VolumeTrailer HMACs pass.
9210    validate_tar_stream_total_extraction_size(
9211        &tar_stream,
9212        parsed_crypto.fixed.max_path_length,
9213        total_extraction_cap,
9214    )?;
9215    Ok(tar_stream)
9216}
9217
9218fn validate_sequential_supported_volume(
9219    volume_header: &VolumeHeader,
9220    crypto_header: &CryptoHeaderFixed,
9221    extensions: &[ExtensionTlv<'_>],
9222) -> Result<(), FormatError> {
9223    reject_unsupported_raw_stream_profile(extensions)?;
9224    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
9225        return Err(FormatError::ReaderUnsupported(
9226            "sequential reader supports only single-volume archive input",
9227        ));
9228    }
9229    if crypto_header.stripe_width != volume_header.stripe_width {
9230        return Err(FormatError::InvalidArchive(
9231            "VolumeHeader and CryptoHeader stripe_width differ",
9232        ));
9233    }
9234    if crypto_header.has_dictionary != 0 {
9235        return Err(FormatError::ReaderUnsupported(
9236            "dictionary bootstrap required for non-seekable sequential extraction",
9237        ));
9238    }
9239    Ok(())
9240}
9241
9242struct SequentialEnvelopeDecodeContext<'a> {
9243    crypto_header: &'a CryptoHeaderFixed,
9244    subkeys: &'a Subkeys,
9245    volume_header: &'a VolumeHeader,
9246    next_envelope_index: &'a mut u64,
9247    tar_stream: &'a mut Vec<u8>,
9248    max_tar_stream_size: usize,
9249    tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
9250}
9251
9252fn finalize_sequential_envelope(
9253    pending: &mut PendingSequentialEnvelope,
9254    context: SequentialEnvelopeDecodeContext<'_>,
9255) -> Result<(), FormatError> {
9256    if !pending.saw_last_data {
9257        return Err(FormatError::InvalidArchive(
9258            "sequential payload envelope is missing last-data flag",
9259        ));
9260    }
9261    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
9262        return Err(FormatError::InvalidArchive(
9263            "sequential payload envelope exceeds data-shard cap",
9264        ));
9265    }
9266    if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
9267        return Err(FormatError::InvalidArchive(
9268            "sequential payload envelope exceeds parity-shard cap",
9269        ));
9270    }
9271    let required_parity =
9272        required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
9273    if pending.parity_shards.len() < required_parity as usize {
9274        return Err(FormatError::InvalidArchive(
9275            "sequential payload envelope has insufficient parity for recovery settings",
9276        ));
9277    }
9278
9279    let repaired = repair_data_gf16(
9280        &pending.data_shards,
9281        &pending.parity_shards,
9282        context.crypto_header.block_size as usize,
9283    )?;
9284    let mut encrypted =
9285        Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
9286    for shard in repaired {
9287        encrypted.extend_from_slice(&shard);
9288    }
9289    let plaintext = decrypt_padded_aead_object(
9290        AeadObjectContext {
9291            algo: context.crypto_header.aead_algo,
9292            key: &context.subkeys.enc_key,
9293            nonce_seed: &context.subkeys.nonce_seed,
9294            domain: b"envelope",
9295            archive_uuid: &context.volume_header.archive_uuid,
9296            session_id: &context.volume_header.session_id,
9297            counter: *context.next_envelope_index,
9298        },
9299        &encrypted,
9300    )?;
9301    decode_concatenated_zstd_frames_with_cap(
9302        &plaintext,
9303        None,
9304        context.tar_stream,
9305        context.max_tar_stream_size,
9306        Some(context.tar_stream_total_validator),
9307    )?;
9308    *context.next_envelope_index = (*context.next_envelope_index)
9309        .checked_add(1)
9310        .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
9311    *pending = PendingSequentialEnvelope::default();
9312    Ok(())
9313}
9314
9315fn decode_concatenated_zstd_frames_with_cap(
9316    plaintext: &[u8],
9317    dictionary: Option<&[u8]>,
9318    output: &mut Vec<u8>,
9319    max_output_len: usize,
9320    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9321) -> Result<(), FormatError> {
9322    let mut cursor = 0usize;
9323    while cursor < plaintext.len() {
9324        let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
9325            .map_err(|_| FormatError::InvalidZstdFrame)?;
9326        if frame_len == 0 {
9327            return Err(FormatError::InvalidZstdFrame);
9328        }
9329        let end = checked_add(cursor, frame_len, "zstd frame")?;
9330        validate_exact_zstd_frame(&plaintext[cursor..end])?;
9331        if let Some(dictionary) = dictionary {
9332            let mut decoder =
9333                zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
9334                    .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9335            read_zstd_frame_to_capped_output(
9336                &mut decoder,
9337                output,
9338                max_output_len,
9339                tar_stream_total_validator.as_deref_mut(),
9340            )?;
9341        } else {
9342            let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
9343                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9344            read_zstd_frame_to_capped_output(
9345                &mut decoder,
9346                output,
9347                max_output_len,
9348                tar_stream_total_validator.as_deref_mut(),
9349            )?;
9350        }
9351        cursor = end;
9352    }
9353    Ok(())
9354}
9355
9356fn read_zstd_frame_to_capped_output<R: Read>(
9357    decoder: &mut R,
9358    output: &mut Vec<u8>,
9359    max_output_len: usize,
9360    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9361) -> Result<(), FormatError> {
9362    let mut buf = [0u8; 64 * 1024];
9363    loop {
9364        let read = decoder
9365            .read(&mut buf)
9366            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9367        if read == 0 {
9368            return Ok(());
9369        }
9370        let next_len = output
9371            .len()
9372            .checked_add(read)
9373            .ok_or(FormatError::ReaderUnsupported(
9374                "sequential tar stream exceeds configured verification cap",
9375            ))?;
9376        if next_len > max_output_len {
9377            return Err(FormatError::ReaderUnsupported(
9378                "sequential tar stream exceeds configured verification cap",
9379            ));
9380        }
9381        output.extend_from_slice(&buf[..read]);
9382        if let Some(validator) = tar_stream_total_validator.as_mut() {
9383            validator.observe(output)?;
9384        }
9385    }
9386}
9387
9388fn load_archive_dictionary(
9389    blocks: &impl BlockProvider,
9390    subkeys: &Subkeys,
9391    volume_header: &VolumeHeader,
9392    crypto_header: &CryptoHeaderFixed,
9393    index_root: &IndexRoot,
9394) -> Result<Option<Vec<u8>>, FormatError> {
9395    if crypto_header.has_dictionary == 0 {
9396        return Ok(None);
9397    }
9398    let plaintext = load_metadata_object_from_parts(
9399        blocks,
9400        ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
9401        index_root.header.dictionary_decompressed_size,
9402    )?;
9403    Ok(Some(plaintext))
9404}
9405
9406#[derive(Clone, Copy)]
9407struct ObjectLoadContext<'a> {
9408    volume_header: &'a VolumeHeader,
9409    crypto_header: &'a CryptoHeaderFixed,
9410    extent: ObjectExtent,
9411    data_kind: BlockKind,
9412    parity_kind: BlockKind,
9413    key: &'a [u8; 32],
9414    nonce_seed: &'a [u8; 32],
9415    domain: &'a [u8],
9416    counter: u64,
9417    class_data_shard_max: u16,
9418    class_parity_shard_max: u16,
9419}
9420
9421impl<'a> ObjectLoadContext<'a> {
9422    fn index_root(
9423        volume_header: &'a VolumeHeader,
9424        crypto_header: &'a CryptoHeaderFixed,
9425        subkeys: &'a Subkeys,
9426        extent: ObjectExtent,
9427    ) -> Self {
9428        Self {
9429            volume_header,
9430            crypto_header,
9431            extent,
9432            data_kind: BlockKind::IndexRootData,
9433            parity_kind: BlockKind::IndexRootParity,
9434            key: &subkeys.index_root_key,
9435            nonce_seed: &subkeys.index_nonce_seed,
9436            domain: b"idxroot",
9437            counter: 0,
9438            class_data_shard_max: crypto_header.index_root_fec_data_shards,
9439            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9440        }
9441    }
9442
9443    fn index_shard(
9444        volume_header: &'a VolumeHeader,
9445        crypto_header: &'a CryptoHeaderFixed,
9446        subkeys: &'a Subkeys,
9447        entry: &ShardEntry,
9448    ) -> Self {
9449        Self {
9450            volume_header,
9451            crypto_header,
9452            extent: ObjectExtent {
9453                first_block_index: entry.first_block_index,
9454                data_block_count: entry.data_block_count,
9455                parity_block_count: entry.parity_block_count,
9456                encrypted_size: entry.encrypted_size,
9457            },
9458            data_kind: BlockKind::IndexShardData,
9459            parity_kind: BlockKind::IndexShardParity,
9460            key: &subkeys.index_shard_key,
9461            nonce_seed: &subkeys.index_nonce_seed,
9462            domain: b"idxshard",
9463            counter: entry.shard_index,
9464            class_data_shard_max: crypto_header.index_fec_data_shards,
9465            class_parity_shard_max: crypto_header.index_fec_parity_shards,
9466        }
9467    }
9468
9469    fn directory_hint(
9470        volume_header: &'a VolumeHeader,
9471        crypto_header: &'a CryptoHeaderFixed,
9472        subkeys: &'a Subkeys,
9473        entry: &DirectoryHintShardEntry,
9474    ) -> Self {
9475        Self {
9476            volume_header,
9477            crypto_header,
9478            extent: ObjectExtent {
9479                first_block_index: entry.first_block_index,
9480                data_block_count: entry.data_block_count,
9481                parity_block_count: entry.parity_block_count,
9482                encrypted_size: entry.encrypted_size,
9483            },
9484            data_kind: BlockKind::DirectoryHintData,
9485            parity_kind: BlockKind::DirectoryHintParity,
9486            key: &subkeys.dir_hint_key,
9487            nonce_seed: &subkeys.index_nonce_seed,
9488            domain: b"dirhint",
9489            counter: entry.hint_shard_index,
9490            class_data_shard_max: crypto_header.index_fec_data_shards,
9491            class_parity_shard_max: crypto_header.index_fec_parity_shards,
9492        }
9493    }
9494
9495    fn dictionary(
9496        volume_header: &'a VolumeHeader,
9497        crypto_header: &'a CryptoHeaderFixed,
9498        subkeys: &'a Subkeys,
9499        index_root: &IndexRoot,
9500    ) -> Result<Self, FormatError> {
9501        Ok(Self {
9502            volume_header,
9503            crypto_header,
9504            extent: dictionary_extent_from_index_root(index_root)?,
9505            data_kind: BlockKind::DictionaryData,
9506            parity_kind: BlockKind::DictionaryParity,
9507            key: &subkeys.dictionary_key,
9508            nonce_seed: &subkeys.index_nonce_seed,
9509            domain: b"dict",
9510            counter: 0,
9511            class_data_shard_max: crypto_header.index_root_fec_data_shards,
9512            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9513        })
9514    }
9515
9516    fn payload(
9517        volume_header: &'a VolumeHeader,
9518        crypto_header: &'a CryptoHeaderFixed,
9519        subkeys: &'a Subkeys,
9520        envelope: &EnvelopeEntry,
9521    ) -> Self {
9522        Self {
9523            volume_header,
9524            crypto_header,
9525            extent: ObjectExtent {
9526                first_block_index: envelope.first_block_index,
9527                data_block_count: envelope.data_block_count,
9528                parity_block_count: envelope.parity_block_count,
9529                encrypted_size: envelope.encrypted_size,
9530            },
9531            data_kind: BlockKind::PayloadData,
9532            parity_kind: BlockKind::PayloadParity,
9533            key: &subkeys.enc_key,
9534            nonce_seed: &subkeys.nonce_seed,
9535            domain: b"envelope",
9536            counter: envelope.envelope_index,
9537            class_data_shard_max: crypto_header.fec_data_shards,
9538            class_parity_shard_max: crypto_header.fec_parity_shards,
9539        }
9540    }
9541}
9542
9543fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
9544    if index_root.header.dictionary_data_block_count == 0
9545        || index_root.header.dictionary_encrypted_size == 0
9546        || index_root.header.dictionary_decompressed_size == 0
9547    {
9548        return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
9549    }
9550    Ok(ObjectExtent {
9551        first_block_index: index_root.header.dictionary_first_block,
9552        data_block_count: index_root.header.dictionary_data_block_count,
9553        parity_block_count: index_root.header.dictionary_parity_block_count,
9554        encrypted_size: index_root.header.dictionary_encrypted_size,
9555    })
9556}
9557
9558fn load_metadata_object_from_parts(
9559    blocks: &impl BlockProvider,
9560    context: ObjectLoadContext<'_>,
9561    decompressed_size: u32,
9562) -> Result<Vec<u8>, FormatError> {
9563    let compressed = load_decrypted_object_from_parts(blocks, context)?;
9564    decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
9565}
9566
9567fn load_decrypted_object_from_parts(
9568    blocks: &impl BlockProvider,
9569    context: ObjectLoadContext<'_>,
9570) -> Result<Vec<u8>, FormatError> {
9571    load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
9572}
9573
9574fn load_decrypted_object_from_parts_with_parity_policy(
9575    blocks: &impl BlockProvider,
9576    context: ObjectLoadContext<'_>,
9577    parity_policy: ParityReadPolicy,
9578) -> Result<Vec<u8>, FormatError> {
9579    let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
9580        blocks,
9581        context.crypto_header,
9582        context.extent,
9583        context.data_kind,
9584        context.parity_kind,
9585        context.class_data_shard_max,
9586        context.class_parity_shard_max,
9587        parity_policy,
9588    )?;
9589    let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
9590    for shard in repaired {
9591        encrypted.extend_from_slice(&shard);
9592    }
9593    if encrypted.len() != context.extent.encrypted_size as usize {
9594        return Err(FormatError::InvalidArchive(
9595            "object encrypted size does not match repaired shards",
9596        ));
9597    }
9598
9599    decrypt_padded_aead_object(
9600        AeadObjectContext {
9601            algo: context.crypto_header.aead_algo,
9602            key: context.key,
9603            nonce_seed: context.nonce_seed,
9604            domain: context.domain,
9605            archive_uuid: &context.volume_header.archive_uuid,
9606            session_id: &context.volume_header.session_id,
9607            counter: context.counter,
9608        },
9609        &encrypted,
9610    )
9611}
9612
9613fn load_repaired_object_data_shards_from_parts(
9614    blocks: &impl BlockProvider,
9615    crypto_header: &CryptoHeaderFixed,
9616    extent: ObjectExtent,
9617    data_kind: BlockKind,
9618    parity_kind: BlockKind,
9619    class_data_shard_max: u16,
9620    class_parity_shard_max: u16,
9621) -> Result<Vec<Vec<u8>>, FormatError> {
9622    load_repaired_object_data_shards_from_parts_with_parity_policy(
9623        blocks,
9624        crypto_header,
9625        extent,
9626        data_kind,
9627        parity_kind,
9628        class_data_shard_max,
9629        class_parity_shard_max,
9630        ParityReadPolicy::Always,
9631    )
9632}
9633
9634#[allow(clippy::too_many_arguments)]
9635fn load_repaired_object_data_shards_from_parts_with_parity_policy(
9636    blocks: &impl BlockProvider,
9637    crypto_header: &CryptoHeaderFixed,
9638    extent: ObjectExtent,
9639    data_kind: BlockKind,
9640    parity_kind: BlockKind,
9641    class_data_shard_max: u16,
9642    class_parity_shard_max: u16,
9643    parity_policy: ParityReadPolicy,
9644) -> Result<Vec<Vec<u8>>, FormatError> {
9645    validate_object_extent(
9646        extent,
9647        crypto_header,
9648        class_data_shard_max,
9649        class_parity_shard_max,
9650    )?;
9651    let block_size = crypto_header.block_size as usize;
9652    let data_count = extent.data_block_count as usize;
9653    let parity_count = extent.parity_block_count as usize;
9654    let mut data_shards = Vec::with_capacity(data_count);
9655    let mut parity_shards = Vec::with_capacity(parity_count);
9656
9657    for offset in 0..data_count {
9658        let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
9659        if let Some(record) = blocks.block(block_index)? {
9660            if record.kind != data_kind {
9661                return Err(FormatError::InvalidArchive(
9662                    "object data block has unexpected kind",
9663                ));
9664            }
9665            let should_be_last = offset + 1 == data_count;
9666            if record.is_last_data() != should_be_last {
9667                return Err(FormatError::InvalidArchive(
9668                    "object last-data flag is not on the final data block",
9669                ));
9670            }
9671            data_shards.push(Some(record.payload.clone()));
9672        } else {
9673            data_shards.push(None);
9674        }
9675    }
9676
9677    if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
9678        return repair_data_gf16(&data_shards, &[], block_size);
9679    }
9680
9681    for offset in 0..parity_count {
9682        let block_index = checked_u64_add(
9683            extent.first_block_index,
9684            data_count as u64 + offset as u64,
9685            "object",
9686        )?;
9687        if let Some(record) = blocks.block(block_index)? {
9688            if record.kind != parity_kind {
9689                return Err(FormatError::InvalidArchive(
9690                    "object parity block has unexpected kind",
9691                ));
9692            }
9693            if record.is_last_data() {
9694                return Err(FormatError::InvalidArchive(
9695                    "object parity block has last-data flag",
9696                ));
9697            }
9698            parity_shards.push(Some(record.payload.clone()));
9699        } else {
9700            parity_shards.push(None);
9701        }
9702    }
9703
9704    repair_data_gf16(&data_shards, &parity_shards, block_size)
9705}
9706
9707fn validate_object_extent(
9708    extent: ObjectExtent,
9709    crypto_header: &CryptoHeaderFixed,
9710    class_data_shard_max: u16,
9711    class_parity_shard_max: u16,
9712) -> Result<(), FormatError> {
9713    if extent.data_block_count == 0 || extent.encrypted_size == 0 {
9714        return Err(FormatError::InvalidArchive(
9715            "encrypted object has zero data blocks or size",
9716        ));
9717    }
9718    if extent.data_block_count > class_data_shard_max as u32 {
9719        return Err(FormatError::InvalidArchive(
9720            "encrypted object exceeds its class data-shard maximum",
9721        ));
9722    }
9723    if extent.parity_block_count > class_parity_shard_max as u32 {
9724        return Err(FormatError::InvalidArchive(
9725            "encrypted object exceeds its class parity-shard maximum",
9726        ));
9727    }
9728    let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
9729    if extent.parity_block_count != required_parity {
9730        return Err(FormatError::InvalidArchive(
9731            "encrypted object parity does not match v41 compute_parity",
9732        ));
9733    }
9734    let total = checked_u64_add(
9735        extent.data_block_count as u64,
9736        extent.parity_block_count as u64,
9737        "encrypted object shard count overflow",
9738    )?;
9739    if total > 65_535 {
9740        return Err(FormatError::FecTooManyShards(total as usize));
9741    }
9742    let expected = checked_u64_mul(
9743        extent.data_block_count as u64,
9744        crypto_header.block_size as u64,
9745        "encrypted object size overflow",
9746    )?;
9747    if expected != extent.encrypted_size as u64 {
9748        return Err(FormatError::InvalidArchive(
9749            "encrypted object size is not data_block_count * block_size",
9750        ));
9751    }
9752    if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
9753        return Err(FormatError::InvalidArchive(
9754            "encrypted object is too small for AEAD tag",
9755        ));
9756    }
9757    Ok(())
9758}
9759
9760pub(crate) fn required_object_parity(
9761    data_block_count: u64,
9762    crypto_header: &CryptoHeaderFixed,
9763) -> Result<u32, FormatError> {
9764    let min_parity =
9765        if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
9766            1
9767        } else {
9768            0
9769        };
9770    let mut parity = 0u64;
9771    for _ in 0..100 {
9772        let total = data_block_count
9773            .checked_add(parity)
9774            .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
9775        let by_volume = checked_u64_mul(
9776            crypto_header.volume_loss_tolerance as u64,
9777            ceil_div_u64(total, crypto_header.stripe_width as u64)?,
9778            "volume-loss parity overflow",
9779        )?;
9780        let by_bitrot = ceil_div_u64(
9781            checked_u64_mul(
9782                total,
9783                crypto_header.bit_rot_buffer_pct as u64,
9784                "bit-rot parity overflow",
9785            )?,
9786            100,
9787        )?;
9788        let next = by_volume
9789            .checked_add(by_bitrot)
9790            .ok_or(FormatError::InvalidArchive("parity overflow"))?
9791            .max(min_parity);
9792        if next == parity {
9793            return u32::try_from(next)
9794                .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
9795        }
9796        parity = next;
9797    }
9798    Err(FormatError::InvalidArchive(
9799        "parity calculation did not converge",
9800    ))
9801}
9802
9803fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
9804    if denominator == 0 {
9805        return Err(FormatError::InvalidArchive("division by zero"));
9806    }
9807    numerator
9808        .checked_add(denominator - 1)
9809        .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
9810        .map(|value| value / denominator)
9811}
9812
9813fn frame_range_for_file<'b>(
9814    shard: &'b IndexShard,
9815    file: &FileEntry,
9816) -> Result<&'b [FrameEntry], FormatError> {
9817    let start = shard
9818        .frames
9819        .binary_search_by_key(&file.first_frame_index, |entry| entry.frame_index)
9820        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))?;
9821    let count = usize::try_from(file.frame_count)
9822        .map_err(|_| FormatError::InvalidArchive("FileEntry frame count overflow"))?;
9823    let end = start.checked_add(count).ok_or(FormatError::InvalidArchive(
9824        "FileEntry frame range overflow",
9825    ))?;
9826    let frames = shard
9827        .frames
9828        .get(start..end)
9829        .ok_or(FormatError::InvalidArchive(
9830            "FileEntry references missing FrameEntry",
9831        ))?;
9832    for (offset, frame) in frames.iter().enumerate() {
9833        let expected = file.first_frame_index.checked_add(offset as u64).ok_or(
9834            FormatError::InvalidArchive("FileEntry frame range overflow"),
9835        )?;
9836        if frame.frame_index != expected {
9837            return Err(FormatError::InvalidArchive(
9838                "FileEntry references missing FrameEntry",
9839            ));
9840        }
9841    }
9842    Ok(frames)
9843}
9844
9845fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
9846    MetadataLimits {
9847        block_size: crypto_header.block_size,
9848        max_path_length: crypto_header.max_path_length,
9849        max_payload_data_shards: crypto_header.fec_data_shards,
9850        max_payload_parity_shards: crypto_header.fec_parity_shards,
9851        max_index_data_shards: crypto_header.index_fec_data_shards,
9852        max_index_parity_shards: crypto_header.index_fec_parity_shards,
9853        max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
9854        max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
9855        ..MetadataLimits::default()
9856    }
9857}
9858
9859fn verify_dense_keys<T>(
9860    entries: &BTreeMap<u64, T>,
9861    expected_count: u64,
9862    structure: &'static str,
9863) -> Result<(), FormatError> {
9864    if entries.len() as u64 != expected_count {
9865        return Err(FormatError::InvalidArchive(
9866            "decoded table count does not match IndexRoot",
9867        ));
9868    }
9869    for expected in 0..expected_count {
9870        if !entries.contains_key(&expected) {
9871            return Err(FormatError::InvalidMetadata {
9872                structure,
9873                reason: "global index coverage has a gap",
9874            });
9875        }
9876    }
9877    Ok(())
9878}
9879
9880fn validate_envelope_frame_coverage(
9881    frames: &BTreeMap<u64, FrameEntry>,
9882    envelopes: &BTreeMap<u64, EnvelopeEntry>,
9883) -> Result<(), FormatError> {
9884    let mut accounted_frames = BTreeSet::new();
9885    for envelope in envelopes.values() {
9886        let first = envelope.first_frame_index;
9887        let end =
9888            first
9889                .checked_add(envelope.frame_count as u64)
9890                .ok_or(FormatError::InvalidArchive(
9891                    "EnvelopeEntry frame range overflow",
9892                ))?;
9893        let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
9894        for frame_index in first..end {
9895            let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
9896                "EnvelopeEntry references missing FrameEntry",
9897            ))?;
9898            if frame.envelope_index != envelope.envelope_index {
9899                return Err(FormatError::InvalidArchive(
9900                    "FrameEntry envelope_index does not match containing EnvelopeEntry",
9901                ));
9902            }
9903            if !accounted_frames.insert(frame_index) {
9904                return Err(FormatError::InvalidArchive(
9905                    "FrameEntry is covered by multiple EnvelopeEntries",
9906                ));
9907            }
9908            let start = frame.offset_in_envelope as usize;
9909            let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
9910            if end > envelope.plaintext_size as usize {
9911                return Err(FormatError::InvalidArchive(
9912                    "FrameEntry exceeds EnvelopeEntry plaintext_size",
9913                ));
9914            }
9915            ranges.push((start, end));
9916        }
9917        validate_exact_coverage_ranges(
9918            &mut ranges,
9919            envelope.plaintext_size as usize,
9920            "EnvelopeEntry frame coverage has a gap or overlap",
9921        )?;
9922    }
9923
9924    for frame_index in frames.keys() {
9925        if !accounted_frames.contains(frame_index) {
9926            return Err(FormatError::InvalidArchive(
9927                "FrameEntry is not covered by any EnvelopeEntry",
9928            ));
9929        }
9930    }
9931    Ok(())
9932}
9933
9934fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
9935    let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
9936    for shard in shards {
9937        for (idx, file) in shard.files.iter().enumerate() {
9938            let path = shard
9939                .file_path(idx)
9940                .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
9941                .to_vec();
9942            let start = shard
9943                .tar_member_group_start(idx)
9944                .ok_or(FormatError::InvalidArchive(
9945                    "FileEntry tar member start is missing",
9946                ))?;
9947            let key = (file.path_hash, path, start);
9948            validate_global_file_table_key_step(previous.as_ref(), &key)?;
9949            previous = Some(key);
9950        }
9951    }
9952    Ok(())
9953}
9954
9955fn validate_global_file_table_key_step(
9956    previous: Option<&([u8; 8], Vec<u8>, u64)>,
9957    current: &([u8; 8], Vec<u8>, u64),
9958) -> Result<(), FormatError> {
9959    if let Some(previous) = previous {
9960        if previous >= current {
9961            return Err(FormatError::InvalidArchive(
9962                "global FileEntry rows are not sorted and unique",
9963            ));
9964        }
9965    }
9966    Ok(())
9967}
9968
9969fn validate_file_extent_coverage_ranges(
9970    extents: &[(u64, u64)],
9971    tar_len: u64,
9972) -> Result<(), FormatError> {
9973    let mut ranges = Vec::with_capacity(extents.len());
9974    for (start, len) in extents {
9975        let end = checked_u64_add(*start, *len, "FileEntry")?;
9976        if end > tar_len {
9977            return Err(FormatError::InvalidArchive(
9978                "FileEntry extent exceeds IndexRoot tar_total_size",
9979            ));
9980        }
9981        ranges.push((*start, end));
9982    }
9983    validate_exact_coverage_ranges_u64(
9984        &mut ranges,
9985        tar_len,
9986        "FileEntry extents do not cover tar stream exactly",
9987    )
9988}
9989
9990fn add_expected_directory_hint_rows(
9991    map: &mut DirectoryHintMap,
9992    shard_row_index: u32,
9993    path: &[u8],
9994    kind: TarEntryKind,
9995) {
9996    map.entry(Vec::new()).or_default().insert(shard_row_index);
9997    for (idx, byte) in path.iter().enumerate() {
9998        if *byte == b'/' {
9999            map.entry(path[..idx].to_vec())
10000                .or_default()
10001                .insert(shard_row_index);
10002        }
10003    }
10004    if kind == TarEntryKind::Directory {
10005        map.entry(path.to_vec())
10006            .or_default()
10007            .insert(shard_row_index);
10008    }
10009}
10010
10011fn validate_directory_hint_tables_against_expected(
10012    tables: &[DirectoryHintTable],
10013    expected: &DirectoryHintMap,
10014) -> Result<(), FormatError> {
10015    let mut actual = Vec::new();
10016    let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
10017
10018    for table in tables {
10019        for entry_index in 0..table.entries.len() {
10020            let path = table
10021                .entry_path(entry_index)
10022                .ok_or(FormatError::InvalidArchive(
10023                    "DirectoryHintEntry path is missing",
10024                ))?;
10025            let key = (hash_prefix(path), path.to_vec());
10026            if let Some(previous) = &previous_key {
10027                if previous >= &key {
10028                    return Err(FormatError::InvalidArchive(
10029                        "DirectoryHintEntry rows are not globally sorted",
10030                    ));
10031                }
10032            }
10033            previous_key = Some(key);
10034
10035            let rows =
10036                table
10037                    .shard_rows_for_entry(entry_index)
10038                    .ok_or(FormatError::InvalidArchive(
10039                        "DirectoryHintEntry shard rows are missing",
10040                    ))?;
10041            actual.push((path.to_vec(), rows.to_vec()));
10042        }
10043    }
10044
10045    if actual != sorted_directory_hint_rows(expected) {
10046        return Err(FormatError::InvalidArchive(
10047            "directory hint map does not match decoded files",
10048        ));
10049    }
10050    Ok(())
10051}
10052
10053fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
10054    let mut rows = map
10055        .iter()
10056        .map(|(path, shard_rows)| {
10057            (
10058                path.clone(),
10059                shard_rows.iter().copied().collect::<Vec<u32>>(),
10060            )
10061        })
10062        .collect::<Vec<_>>();
10063    rows.sort_by(|(left_path, _), (right_path, _)| {
10064        hash_prefix(left_path)
10065            .cmp(&hash_prefix(right_path))
10066            .then_with(|| left_path.cmp(right_path))
10067    });
10068    rows
10069}
10070
10071fn validate_exact_coverage_ranges(
10072    ranges: &mut [(usize, usize)],
10073    expected_end: usize,
10074    reason: &'static str,
10075) -> Result<(), FormatError> {
10076    ranges.sort_unstable();
10077    let mut cursor = 0usize;
10078    for (start, end) in ranges.iter().copied() {
10079        if start != cursor || end < start {
10080            return Err(FormatError::InvalidArchive(reason));
10081        }
10082        cursor = end;
10083    }
10084    if cursor != expected_end {
10085        return Err(FormatError::InvalidArchive(reason));
10086    }
10087    Ok(())
10088}
10089
10090fn validate_exact_coverage_ranges_u64(
10091    ranges: &mut [(u64, u64)],
10092    expected_end: u64,
10093    reason: &'static str,
10094) -> Result<(), FormatError> {
10095    ranges.sort_unstable();
10096    let mut cursor = 0u64;
10097    for (start, end) in ranges.iter().copied() {
10098        if start != cursor || end < start {
10099            return Err(FormatError::InvalidArchive(reason));
10100        }
10101        cursor = end;
10102    }
10103    if cursor != expected_end {
10104        return Err(FormatError::InvalidArchive(reason));
10105    }
10106    Ok(())
10107}
10108
10109fn object_block_range(
10110    first_block_index: u64,
10111    data_block_count: u32,
10112    parity_block_count: u32,
10113    structure: &'static str,
10114) -> Result<(u64, u64), FormatError> {
10115    let total = data_block_count as u64 + parity_block_count as u64;
10116    if total == 0 {
10117        return Err(FormatError::InvalidArchive(structure));
10118    }
10119    let end = checked_u64_add(first_block_index, total, structure)?;
10120    Ok((first_block_index, end))
10121}
10122
10123fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
10124    ranges.sort_unstable();
10125    for pair in ranges.windows(2) {
10126        if pair[0].1 > pair[1].0 {
10127            return Err(FormatError::InvalidArchive(
10128                "encrypted object block ranges overlap",
10129            ));
10130        }
10131    }
10132    Ok(())
10133}
10134
10135pub(crate) fn observed_archive_size(
10136    sizes: impl IntoIterator<Item = u64>,
10137) -> Result<u64, FormatError> {
10138    sizes.into_iter().try_fold(0u64, |sum, size| {
10139        sum.checked_add(size).ok_or(FormatError::InvalidArchive(
10140            "observed archive size overflow",
10141        ))
10142    })
10143}
10144
10145pub(crate) fn total_extraction_size_cap(
10146    options: ReaderOptions,
10147    observed_archive_bytes: u64,
10148) -> u64 {
10149    options
10150        .max_total_extraction_size
10151        .min(observed_archive_bytes.saturating_mul(10))
10152}
10153
10154fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
10155    std::str::from_utf8(bytes)
10156        .map(|path| path.to_owned())
10157        .map_err(|_| FormatError::UnsafeArchivePath)
10158}
10159
10160fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
10161    let mut bytes = [0u8; 104];
10162    bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
10163    bytes[36..40].fill(0);
10164    bytes
10165}
10166
10167fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
10168    let digest = Sha256::digest(bytes);
10169    let mut out = [0u8; 32];
10170    out.copy_from_slice(&digest);
10171    out
10172}
10173
10174fn slice<'b>(
10175    bytes: &'b [u8],
10176    offset: usize,
10177    len: usize,
10178    structure: &'static str,
10179) -> Result<&'b [u8], FormatError> {
10180    let end = checked_add(offset, len, structure)?;
10181    bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10182        structure,
10183        expected: end,
10184        actual: bytes.len(),
10185    })
10186}
10187
10188fn read_at_vec(
10189    reader: &dyn ArchiveReadAt,
10190    offset: u64,
10191    len: usize,
10192    structure: &'static str,
10193) -> Result<Vec<u8>, FormatError> {
10194    let expected_end = offset
10195        .checked_add(len as u64)
10196        .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
10197    let observed_len = reader.len()?;
10198    if expected_end > observed_len {
10199        return Err(FormatError::InvalidLength {
10200            structure,
10201            expected: to_usize(expected_end, structure)?,
10202            actual: to_usize(observed_len, structure)?,
10203        });
10204    }
10205    let mut out = vec![0u8; len];
10206    reader.read_exact_at(offset, &mut out)?;
10207    Ok(out)
10208}
10209
10210fn read_at_vec_unchecked(
10211    reader: &dyn ArchiveReadAt,
10212    offset: u64,
10213    len: usize,
10214) -> Result<Vec<u8>, FormatError> {
10215    let mut out = vec![0u8; len];
10216    reader.read_exact_at(offset, &mut out)?;
10217    Ok(out)
10218}
10219
10220fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
10221where
10222    T: Sync,
10223    U: Send,
10224    F: Fn(&T) -> Result<U, FormatError> + Sync,
10225{
10226    if jobs <= 1 || items.len() <= 1 {
10227        return items.iter().map(f).collect();
10228    }
10229    let worker_count = jobs.min(items.len());
10230    let chunk_size = items.len().div_ceil(worker_count);
10231    let mut out = Vec::with_capacity(items.len());
10232    thread::scope(|scope| {
10233        let handles = items
10234            .chunks(chunk_size)
10235            .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
10236            .collect::<Vec<_>>();
10237        for handle in handles {
10238            let mut chunk = handle
10239                .join()
10240                .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
10241            out.append(&mut chunk);
10242        }
10243        Ok(out)
10244    })
10245}
10246
10247fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
10248    lhs.checked_add(rhs)
10249        .ok_or(FormatError::InvalidArchive(structure))
10250}
10251
10252fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
10253    lhs.checked_add(rhs)
10254        .ok_or(FormatError::InvalidArchive(structure))
10255}
10256
10257fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
10258    usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
10259}
10260
10261#[cfg(test)]
10262mod tests {
10263    use std::fs;
10264
10265    use super::*;
10266    use crate::compression::compress_zstd_frame;
10267    use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
10268    use crate::fec::encode_parity_gf16;
10269    use crate::format::{
10270        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
10271        CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION, READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10272        VOLUME_FORMAT_REV, VOLUME_FORMAT_REV_44,
10273    };
10274    use crate::metadata::{
10275        DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
10276        ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
10277    };
10278    use crate::non_seekable_reader::{
10279        extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
10280        verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
10281        verify_non_seekable_stream_with_recipient_wrap_resolver_options, NonSeekableReaderOptions,
10282        SequentialRootAuthStatus,
10283    };
10284    use crate::raw_stream_profile::{
10285        serialize_raw_stream_content_model_extension, RAW_STREAM_UNSUPPORTED_MESSAGE,
10286    };
10287    use crate::wire::RecipientRecordV1;
10288    use crate::writer::{
10289        write_archive, write_archive_unencrypted, write_archive_with_dictionary,
10290        write_archive_with_kdf, write_archive_with_recipient_wrap_records,
10291        write_archive_with_root_auth, write_archive_with_root_auth_and_recipient_wrap_records,
10292        RegularFile, RootAuthSigningRequest, RootAuthWriterConfig, WriterOptions,
10293    };
10294
10295    fn master_key() -> MasterKey {
10296        MasterKey::from_raw_key(&[0x42; 32]).unwrap()
10297    }
10298
10299    fn recipient_wrap_test_record() -> RecipientRecordV1 {
10300        RecipientRecordV1 {
10301            record_length: 0,
10302            profile_id: 1,
10303            recipient_identity_type: 2,
10304            flags: 0,
10305            recipient_identity_length: 0,
10306            profile_payload_length: 0,
10307            recipient_identity_digest: [0u8; 32],
10308            recipient_identity_bytes: b"recipient-a".to_vec(),
10309            profile_payload_bytes: b"profile-payload".to_vec(),
10310        }
10311    }
10312
10313    fn recipient_wrap_layout(volume: &[u8]) -> (usize, usize, usize, usize, u32) {
10314        let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10315        let crypto_start = header.crypto_header_offset as usize;
10316        let crypto_len = header.crypto_header_length as usize;
10317        let crypto_end = crypto_start + crypto_len;
10318        let crypto = CryptoHeader::parse(
10319            &volume[crypto_start..crypto_end],
10320            header.crypto_header_length,
10321        )
10322        .unwrap();
10323        let KdfParams::RecipientWrap {
10324            key_wrap_table_length,
10325            ..
10326        } = crypto.kdf_params
10327        else {
10328            panic!("expected RecipientWrap KdfParams");
10329        };
10330        (
10331            crypto_start,
10332            crypto_len,
10333            crypto_end,
10334            key_wrap_table_length as usize,
10335            key_wrap_table_length,
10336        )
10337    }
10338
10339    fn rewrite_recipient_wrap_kdf_digest(crypto_bytes: &mut [u8], digest: [u8; 32]) {
10340        let digest_start = CRYPTO_HEADER_FIXED_LEN + 14;
10341        crypto_bytes[digest_start..digest_start + 32].copy_from_slice(&digest);
10342    }
10343
10344    fn mutate_top_level_recipient_wrap_public_profile(volume: &mut [u8]) {
10345        let (crypto_start, crypto_len, table_start, table_len, table_len_u32) =
10346            recipient_wrap_layout(volume);
10347        let table_end = table_start + table_len;
10348        volume[table_end - 1] ^= 0x5a;
10349        let digest = compute_key_wrap_table_digest(table_len_u32, &volume[table_start..table_end]);
10350        rewrite_recipient_wrap_kdf_digest(
10351            &mut volume[crypto_start..crypto_start + crypto_len],
10352            digest,
10353        );
10354    }
10355
10356    fn mutate_cmra_recipient_wrap_public_profile(volume: &mut [u8]) {
10357        rewrite_public_cmra_image(volume, |image| {
10358            let table_region = image
10359                .regions
10360                .iter_mut()
10361                .find(|region| region.region_type == 6)
10362                .unwrap();
10363            *table_region.bytes.last_mut().unwrap() ^= 0x5a;
10364            let digest =
10365                compute_key_wrap_table_digest(table_region.bytes.len() as u32, &table_region.bytes);
10366            let crypto_region = image
10367                .regions
10368                .iter_mut()
10369                .find(|region| region.region_type == 2)
10370                .unwrap();
10371            rewrite_recipient_wrap_kdf_digest(&mut crypto_region.bytes, digest);
10372        });
10373    }
10374
10375    fn add_raw_stream_profile_to_physical_crypto_header(volume: &mut Vec<u8>) {
10376        let mut header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10377        let crypto_start = header.crypto_header_offset as usize;
10378        let crypto_len = header.crypto_header_length as usize;
10379        let hmac_start = crypto_start + crypto_len - CRYPTO_HEADER_HMAC_LEN;
10380        let terminator_start = hmac_start - CRYPTO_EXTENSION_HEADER_LEN;
10381        let extension = serialize_raw_stream_content_model_extension();
10382        let new_crypto_len = header.crypto_header_length + extension.len() as u32;
10383
10384        volume.splice(terminator_start..terminator_start, extension);
10385        header.crypto_header_length = new_crypto_len;
10386        volume[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10387        volume[crypto_start + 4..crypto_start + 8].copy_from_slice(&new_crypto_len.to_le_bytes());
10388    }
10389
10390    fn recompute_physical_crypto_header_hmac(volume: &mut [u8], master_key: &MasterKey) {
10391        let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10392        let crypto_start = header.crypto_header_offset as usize;
10393        let crypto_end = crypto_start + header.crypto_header_length as usize;
10394        let hmac_start = crypto_end - CRYPTO_HEADER_HMAC_LEN;
10395        let subkeys =
10396            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
10397        let hmac = compute_hmac(
10398            HmacDomain::CryptoHeader,
10399            &subkeys.mac_key,
10400            &header.archive_uuid,
10401            &header.session_id,
10402            &volume[crypto_start..hmac_start],
10403        );
10404        volume[hmac_start..crypto_end].copy_from_slice(&hmac);
10405    }
10406
10407    #[test]
10408    fn reader_defaults_use_available_parallelism_jobs() {
10409        let options = ReaderOptions::default();
10410
10411        assert_eq!(options.jobs, default_jobs());
10412        assert!(options.jobs >= 1);
10413    }
10414
10415    #[test]
10416    fn reader_options_reject_zero_jobs() {
10417        let err = OpenedArchive::open_with_options(
10418            &[],
10419            &master_key(),
10420            ReaderOptions {
10421                jobs: 0,
10422                ..ReaderOptions::default()
10423            },
10424        )
10425        .unwrap_err();
10426
10427        assert_eq!(
10428            err,
10429            FormatError::ReaderUnsupported("jobs must be at least 1")
10430        );
10431    }
10432
10433    const TEST_ROOT_AUTH_ID: u16 = 0xe001;
10434    const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
10435
10436    fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
10437        RootAuthWriterConfig {
10438            authenticator_id: TEST_ROOT_AUTH_ID,
10439            signer_identity_type: 0,
10440            signer_identity: &[],
10441            authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
10442        }
10443    }
10444
10445    fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
10446        request.archive_root.to_vec()
10447    }
10448
10449    fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
10450        footer.authenticator_id == TEST_ROOT_AUTH_ID
10451            && footer.signer_identity_type == 0
10452            && footer.signer_identity_bytes.is_empty()
10453            && footer.authenticator_value.as_slice() == archive_root
10454    }
10455
10456    fn dictionary() -> &'static [u8] {
10457        b"dir/dict.txt common words common words common words dictionary payload"
10458    }
10459
10460    #[derive(Clone)]
10461    struct CountingReadAt {
10462        bytes: std::sync::Arc<Vec<u8>>,
10463        reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
10464        denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
10465    }
10466
10467    impl CountingReadAt {
10468        fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
10469            Self {
10470                bytes: std::sync::Arc::new(bytes),
10471                reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
10472                denied_ranges: std::sync::Arc::new(denied_ranges),
10473            }
10474        }
10475
10476        fn reads(&self) -> Vec<(u64, u64)> {
10477            self.reads.lock().unwrap().clone()
10478        }
10479    }
10480
10481    impl ArchiveReadAt for CountingReadAt {
10482        fn len(&self) -> Result<u64, FormatError> {
10483            u64::try_from(self.bytes.as_ref().len())
10484                .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
10485        }
10486
10487        fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
10488            let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
10489            self.reads.lock().unwrap().push((offset, end));
10490            if self
10491                .denied_ranges
10492                .iter()
10493                .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
10494            {
10495                return Err(FormatError::InvalidArchive("denied test read"));
10496            }
10497            let start = to_usize(offset, "archive")?;
10498            let end_usize = checked_add(start, buf.len(), "archive")?;
10499            let source = self
10500                .bytes
10501                .get(start..end_usize)
10502                .ok_or(FormatError::InvalidLength {
10503                    structure: "archive",
10504                    expected: end_usize,
10505                    actual: self.bytes.as_ref().len(),
10506                })?;
10507            buf.copy_from_slice(source);
10508            Ok(())
10509        }
10510    }
10511
10512    fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
10513        left_start < right_end && right_start < left_end
10514    }
10515
10516    fn single_stream_options() -> WriterOptions {
10517        WriterOptions {
10518            stripe_width: 1,
10519            volume_loss_tolerance: 0,
10520            ..WriterOptions::default()
10521        }
10522    }
10523
10524    struct ChunkedReader {
10525        bytes: Vec<u8>,
10526        cursor: usize,
10527        max_chunk: usize,
10528    }
10529
10530    impl ChunkedReader {
10531        fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
10532            Self {
10533                bytes,
10534                cursor: 0,
10535                max_chunk,
10536            }
10537        }
10538    }
10539
10540    impl std::io::Read for ChunkedReader {
10541        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
10542            if self.cursor >= self.bytes.len() {
10543                return Ok(0);
10544            }
10545            let available = self.bytes.len() - self.cursor;
10546            let len = available.min(buf.len()).min(self.max_chunk);
10547            buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
10548            self.cursor += len;
10549            Ok(len)
10550        }
10551    }
10552
10553    #[test]
10554    fn global_file_table_key_step_rejects_distinct_path_regression() {
10555        let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
10556        let current = ([1u8; 8], b"a.txt".to_vec(), 0);
10557
10558        assert_eq!(
10559            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
10560            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10561        );
10562    }
10563
10564    #[test]
10565    fn global_file_table_key_step_rejects_duplicate_full_key() {
10566        let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
10567        let current = ([1u8; 8], b"a.txt".to_vec(), 7);
10568
10569        assert_eq!(
10570            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
10571            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10572        );
10573    }
10574
10575    fn small_block_recovery_options() -> WriterOptions {
10576        WriterOptions {
10577            block_size: 4096,
10578            chunk_size: 32 * 1024,
10579            envelope_target_size: 32 * 1024,
10580            stripe_width: 1,
10581            volume_loss_tolerance: 0,
10582            bit_rot_buffer_pct: 1,
10583            fec_data_shards: 16,
10584            fec_parity_shards: 1,
10585            index_fec_data_shards: 4,
10586            index_fec_parity_shards: 1,
10587            index_root_fec_data_shards: 16,
10588            index_root_fec_parity_shards: 1,
10589            ..WriterOptions::default()
10590        }
10591    }
10592
10593    fn parity_rich_recovery_options() -> WriterOptions {
10594        WriterOptions {
10595            block_size: 4096,
10596            chunk_size: 32 * 1024,
10597            envelope_target_size: 32 * 1024,
10598            stripe_width: 1,
10599            volume_loss_tolerance: 0,
10600            bit_rot_buffer_pct: 40,
10601            fec_data_shards: 16,
10602            fec_parity_shards: 16,
10603            index_fec_data_shards: 4,
10604            index_fec_parity_shards: 4,
10605            index_root_fec_data_shards: 16,
10606            index_root_fec_parity_shards: 16,
10607            ..WriterOptions::default()
10608        }
10609    }
10610
10611    fn pseudo_random_bytes(len: usize) -> Vec<u8> {
10612        let mut state = 0x1234_5678u32;
10613        (0..len)
10614            .map(|_| {
10615                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10616                (state >> 24) as u8
10617            })
10618            .collect()
10619    }
10620
10621    #[test]
10622    fn opens_lists_verifies_and_extracts_one_file_archive() {
10623        let archive = write_archive(
10624            &[RegularFile::new("dir/hello.txt", b"hello m7")],
10625            &master_key(),
10626            single_stream_options(),
10627        )
10628        .unwrap();
10629        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10630
10631        assert_eq!(
10632            opened.list_files().unwrap(),
10633            vec![ArchiveEntry {
10634                path: "dir/hello.txt".to_string(),
10635                file_data_size: 8,
10636                kind: TarEntryKind::Regular,
10637                mode: 0o644,
10638                mtime: 0,
10639                diagnostics: Vec::new(),
10640            }]
10641        );
10642        opened.verify().unwrap();
10643        assert_eq!(
10644            opened.extract_file("dir/hello.txt").unwrap(),
10645            Some(b"hello m7".to_vec())
10646        );
10647        assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
10648    }
10649
10650    #[test]
10651    fn root_auth_archive_round_trips_and_verifies_with_callback() {
10652        let archive = write_archive_with_root_auth(
10653            &[RegularFile::new("signed.txt", b"root-auth payload")],
10654            &master_key(),
10655            single_stream_options(),
10656            RootAuthWriterConfig {
10657                authenticator_id: 0x7777,
10658                signer_identity_type: 1,
10659                signer_identity: b"test signer",
10660                authenticator_value_length: 32,
10661            },
10662            |request| Ok(request.archive_root.to_vec()),
10663        )
10664        .unwrap();
10665
10666        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10667        opened.verify().unwrap();
10668        let verified = opened
10669            .verify_root_auth_with(|footer, archive_root| {
10670                Ok(footer.authenticator_value == archive_root.as_slice())
10671            })
10672            .unwrap();
10673
10674        assert_eq!(verified.authenticator_id, 0x7777);
10675        assert_eq!(verified.signer_identity_type, 1);
10676        assert_eq!(verified.signer_identity_bytes, b"test signer");
10677        assert_eq!(
10678            verified.archive_root,
10679            opened.root_auth_footer.as_ref().unwrap().archive_root
10680        );
10681        assert_eq!(
10682            verified.diagnostics,
10683            vec![
10684                RootAuthDiagnostic::RootAuthContentVerified,
10685                RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
10686                RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
10687                RootAuthDiagnostic::RecoveryMarginUnchecked,
10688            ]
10689        );
10690    }
10691
10692    #[test]
10693    fn root_auth_rejects_fast_content_verification_token() {
10694        let archive = write_archive_with_root_auth(
10695            &[RegularFile::new("signed.txt", b"root-auth payload")],
10696            &master_key(),
10697            single_stream_options(),
10698            RootAuthWriterConfig {
10699                authenticator_id: 0x7777,
10700                signer_identity_type: 1,
10701                signer_identity: b"test signer",
10702                authenticator_value_length: 32,
10703            },
10704            |request| Ok(request.archive_root.to_vec()),
10705        )
10706        .unwrap();
10707
10708        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10709        let content_verification = opened.verify_content_fast().unwrap();
10710        assert_eq!(
10711            opened
10712                .verify_root_auth_with_verified_content(&content_verification, |_, _| Ok(true))
10713                .unwrap_err(),
10714            FormatError::ReaderUnsupported(
10715                "RootAuth verification requires full archive content verification"
10716            )
10717        );
10718    }
10719
10720    #[test]
10721    fn root_auth_verification_requires_authenticator_success() {
10722        let archive = write_archive_with_root_auth(
10723            &[RegularFile::new("signed.txt", b"root-auth payload")],
10724            &master_key(),
10725            single_stream_options(),
10726            RootAuthWriterConfig {
10727                authenticator_id: 9,
10728                signer_identity_type: 1,
10729                signer_identity: b"test signer",
10730                authenticator_value_length: 32,
10731            },
10732            |request| Ok(request.archive_root.to_vec()),
10733        )
10734        .unwrap();
10735        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10736
10737        assert_eq!(
10738            opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
10739            FormatError::InvalidArchive("root-auth authenticator verification failed")
10740        );
10741    }
10742
10743    #[test]
10744    fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
10745        let archive = write_archive_with_root_auth(
10746            &[RegularFile::new("public.txt", b"public commitment")],
10747            &master_key(),
10748            single_stream_options(),
10749            RootAuthWriterConfig {
10750                authenticator_id: 0x2222,
10751                signer_identity_type: 1,
10752                signer_identity: b"public verifier",
10753                authenticator_value_length: 32,
10754            },
10755            |request| Ok(request.archive_root.to_vec()),
10756        )
10757        .unwrap();
10758
10759        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
10760            Ok(footer.authenticator_value == archive_root.as_slice())
10761        })
10762        .unwrap();
10763
10764        assert_eq!(verified.authenticator_id, 0x2222);
10765        assert_eq!(verified.signer_identity_bytes, b"public verifier");
10766        assert!(verified.total_data_block_count > 0);
10767    }
10768
10769    #[test]
10770    fn public_no_key_verifier_not_invoked_for_future_revision() {
10771        let archive = write_archive_with_root_auth(
10772            &[RegularFile::new("public.txt", b"public callback")],
10773            &master_key(),
10774            single_stream_options(),
10775            RootAuthWriterConfig {
10776                authenticator_id: 0x2222,
10777                signer_identity_type: 1,
10778                signer_identity: b"public verifier",
10779                authenticator_value_length: 32,
10780            },
10781            |request| Ok(request.archive_root.to_vec()),
10782        )
10783        .unwrap();
10784        let mut bytes = archive.bytes;
10785        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10786        header.volume_format_rev = 45;
10787        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10788
10789        let mut called = false;
10790        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10791            called = true;
10792            Ok(true)
10793        })
10794        .unwrap_err();
10795
10796        assert!(!called);
10797        assert_eq!(
10798            err,
10799            FormatError::UnsupportedVolumeFormatRevision {
10800                format_version: FORMAT_VERSION,
10801                volume_format_rev: 45,
10802                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10803            }
10804        );
10805    }
10806
10807    #[test]
10808    fn public_no_key_rejects_public_header_revision_mismatch() {
10809        let archive = write_archive_with_root_auth(
10810            &[RegularFile::new("public.txt", b"public v44 only")],
10811            &master_key(),
10812            single_stream_options(),
10813            RootAuthWriterConfig {
10814                authenticator_id: 0x2222,
10815                signer_identity_type: 1,
10816                signer_identity: b"public verifier",
10817                authenticator_value_length: 32,
10818            },
10819            |request| Ok(request.archive_root.to_vec()),
10820        )
10821        .unwrap();
10822        let mut bytes = archive.bytes;
10823        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10824        header.volume_format_rev = 43;
10825        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10826
10827        let mut called = false;
10828        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10829            called = true;
10830            Ok(true)
10831        })
10832        .unwrap_err();
10833
10834        assert!(!called);
10835        assert_eq!(
10836            err,
10837            FormatError::UnsupportedVolumeFormatRevision {
10838                format_version: FORMAT_VERSION,
10839                volume_format_rev: 43,
10840                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10841            }
10842        );
10843    }
10844
10845    #[test]
10846    fn public_no_key_rejects_recovered_footer_revision_mismatch() {
10847        let archive = write_archive_with_root_auth(
10848            &[RegularFile::new("public.txt", b"public footer mismatch")],
10849            &master_key(),
10850            single_stream_options(),
10851            RootAuthWriterConfig {
10852                authenticator_id: 0x2222,
10853                signer_identity_type: 1,
10854                signer_identity: b"public verifier",
10855                authenticator_value_length: 32,
10856            },
10857            |request| Ok(request.archive_root.to_vec()),
10858        )
10859        .unwrap();
10860        let mut bytes = archive.bytes;
10861        rewrite_public_cmra_image(&mut bytes, |image| {
10862            let root_auth_region = image
10863                .regions
10864                .iter_mut()
10865                .find(|region| region.region_type == 4)
10866                .unwrap();
10867            rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
10868        });
10869
10870        let mut called = false;
10871        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10872            called = true;
10873            Ok(true)
10874        })
10875        .unwrap_err();
10876
10877        assert!(!called);
10878        assert_eq!(
10879            err,
10880            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10881        );
10882    }
10883
10884    #[test]
10885    fn public_no_key_rejects_recovered_image_with_unknown_layout_flags() {
10886        let archive = write_archive_with_root_auth(
10887            &[RegularFile::new("public.txt", b"public image mismatch")],
10888            &master_key(),
10889            single_stream_options(),
10890            RootAuthWriterConfig {
10891                authenticator_id: 0x2222,
10892                signer_identity_type: 1,
10893                signer_identity: b"public verifier",
10894                authenticator_value_length: 32,
10895            },
10896            |request| Ok(request.archive_root.to_vec()),
10897        )
10898        .unwrap();
10899        let bytes = rewrite_cmra_image_variable_len(
10900            &archive.bytes,
10901            CmraRecoveryMode::PublicNoKey,
10902            |image| {
10903                image.layout_flags |= 0x8000_0000;
10904            },
10905        );
10906
10907        let mut called = false;
10908        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10909            called = true;
10910            Ok(true)
10911        })
10912        .unwrap_err();
10913
10914        assert!(!called);
10915        assert_eq!(
10916            err,
10917            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10918        );
10919    }
10920
10921    #[test]
10922    fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
10923        let archive = write_archive_with_root_auth(
10924            &[RegularFile::new(
10925                "public-fields.txt",
10926                b"public source authority",
10927            )],
10928            &master_key(),
10929            single_stream_options(),
10930            RootAuthWriterConfig {
10931                authenticator_id: 0x2222,
10932                signer_identity_type: 1,
10933                signer_identity: b"public verifier",
10934                authenticator_value_length: 32,
10935            },
10936            |request| Ok(request.archive_root.to_vec()),
10937        )
10938        .unwrap();
10939        let mut bytes = archive.bytes.clone();
10940
10941        rewrite_public_cmra_image(&mut bytes, |image| {
10942            let manifest_region = image
10943                .regions
10944                .iter_mut()
10945                .find(|region| region.region_type == 3)
10946                .unwrap();
10947            manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
10948
10949            let trailer_region = image
10950                .regions
10951                .iter_mut()
10952                .find(|region| region.region_type == 5)
10953                .unwrap();
10954            let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
10955            trailer.block_count += 7;
10956            trailer_region.bytes = trailer.to_bytes().to_vec();
10957        });
10958
10959        public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
10960            Ok(footer.authenticator_value == archive_root.as_slice())
10961        })
10962        .unwrap();
10963    }
10964
10965    #[test]
10966    fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
10967        let archive = write_archive_with_root_auth(
10968            &[RegularFile::new(
10969                "public-crypto.txt",
10970                b"cross-volume public profile",
10971            )],
10972            &master_key(),
10973            WriterOptions {
10974                stripe_width: 2,
10975                volume_loss_tolerance: 0,
10976                ..WriterOptions::default()
10977            },
10978            RootAuthWriterConfig {
10979                authenticator_id: 0x3333,
10980                signer_identity_type: 1,
10981                signer_identity: b"public verifier",
10982                authenticator_value_length: 32,
10983            },
10984            |request| Ok(request.archive_root.to_vec()),
10985        )
10986        .unwrap();
10987        let mut volumes = archive.volumes.clone();
10988        let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
10989        let crypto_offset = volume_header.crypto_header_offset as usize;
10990        let expected_volume_size = 123_456_789u64;
10991        volumes[1][crypto_offset + 52..crypto_offset + 60]
10992            .copy_from_slice(&expected_volume_size.to_le_bytes());
10993        rewrite_public_cmra_image(&mut volumes[1], |image| {
10994            let crypto_region = image
10995                .regions
10996                .iter_mut()
10997                .find(|region| region.region_type == 2)
10998                .unwrap();
10999            crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
11000        });
11001
11002        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11003        public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
11004            Ok(footer.authenticator_value == archive_root.as_slice())
11005        })
11006        .unwrap();
11007    }
11008
11009    #[test]
11010    fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
11011        let archive = write_archive_with_root_auth(
11012            &[RegularFile::new("cmra-header.txt", b"header fallback")],
11013            &master_key(),
11014            single_stream_options(),
11015            RootAuthWriterConfig {
11016                authenticator_id: 0x4444,
11017                signer_identity_type: 1,
11018                signer_identity: b"public verifier",
11019                authenticator_value_length: 32,
11020            },
11021            |request| Ok(request.archive_root.to_vec()),
11022        )
11023        .unwrap();
11024        let final_locator = final_recovery_locator(&archive.bytes);
11025
11026        let mut bad_crc = archive.bytes.clone();
11027        let crc_offset =
11028            final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
11029        bad_crc[crc_offset] ^= 0x55;
11030        public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
11031            Ok(footer.authenticator_value == archive_root.as_slice())
11032        })
11033        .unwrap();
11034
11035        let mut bad_magic = archive.bytes.clone();
11036        bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
11037        public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
11038            Ok(footer.authenticator_value == archive_root.as_slice())
11039        })
11040        .unwrap();
11041
11042        let mut bad_hint = archive.bytes.clone();
11043        bad_hint[crc_offset] ^= 0xAA;
11044        for offset in [
11045            bad_hint.len() - LOCATOR_PAIR_LEN,
11046            bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
11047        ] {
11048            let mut locator = CriticalRecoveryLocator::parse(
11049                &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11050            )
11051            .unwrap();
11052            locator.volume_index_hint += 1;
11053            bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
11054                .copy_from_slice(&locator.to_bytes());
11055        }
11056        assert_eq!(
11057            public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
11058            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11059        );
11060    }
11061
11062    #[test]
11063    fn recovers_physical_volume_header_magic_from_cmra_authority() {
11064        let payload = b"front header authority".to_vec();
11065        let archive = write_archive(
11066            &[RegularFile::new("volume-header.txt", &payload)],
11067            &master_key(),
11068            small_block_recovery_options(),
11069        )
11070        .unwrap();
11071
11072        let mut corrupted = archive.bytes;
11073        corrupted[0] ^= 0x55;
11074
11075        let opened = open_archive(&corrupted, &master_key()).unwrap();
11076        assert_eq!(
11077            opened.extract_file("volume-header.txt").unwrap(),
11078            Some(payload)
11079        );
11080        opened.verify().unwrap();
11081    }
11082
11083    #[test]
11084    fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11085        let payload = b"crc-valid wrong volume index".to_vec();
11086        let mut options = small_block_recovery_options();
11087        options.stripe_width = 2;
11088        options.volume_loss_tolerance = 0;
11089        let archive = write_archive(
11090            &[RegularFile::new("volume-index.txt", &payload)],
11091            &master_key(),
11092            options,
11093        )
11094        .unwrap();
11095
11096        let mut corrupted = archive.volumes[0].clone();
11097        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11098        assert_eq!(header.volume_index, 0);
11099        header.volume_index = 1;
11100        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11101        assert_eq!(
11102            VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
11103                .unwrap()
11104                .volume_index,
11105            1
11106        );
11107
11108        let opened = open_archive_volumes(
11109            &[corrupted.as_slice(), archive.volumes[1].as_slice()],
11110            &master_key(),
11111        )
11112        .unwrap();
11113        assert_eq!(opened.volume_header.volume_index, 0);
11114        assert_eq!(
11115            opened.extract_file("volume-index.txt").unwrap(),
11116            Some(payload)
11117        );
11118        opened.verify().unwrap();
11119    }
11120
11121    #[test]
11122    fn recovers_physical_crypto_header_magic_from_cmra_authority() {
11123        let payload = b"crypto header authority".to_vec();
11124        let archive = write_archive(
11125            &[RegularFile::new("crypto-header.txt", &payload)],
11126            &master_key(),
11127            small_block_recovery_options(),
11128        )
11129        .unwrap();
11130        let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
11131            .unwrap()
11132            .crypto_header_offset;
11133
11134        let mut corrupted = archive.bytes;
11135        corrupted[crypto_offset as usize] ^= 0x55;
11136
11137        let opened = open_archive(&corrupted, &master_key()).unwrap();
11138        assert_eq!(
11139            opened.extract_file("crypto-header.txt").unwrap(),
11140            Some(payload)
11141        );
11142        opened.verify().unwrap();
11143    }
11144
11145    #[test]
11146    fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
11147        let payload = b"read-at header authority".to_vec();
11148        let archive = write_archive(
11149            &[RegularFile::new("read-at-header.txt", &payload)],
11150            &master_key(),
11151            small_block_recovery_options(),
11152        )
11153        .unwrap();
11154
11155        let mut corrupted = archive.bytes;
11156        corrupted[0] ^= 0x55;
11157
11158        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
11159        assert_eq!(
11160            opened.extract_file("read-at-header.txt").unwrap(),
11161            Some(payload)
11162        );
11163        opened.verify().unwrap();
11164    }
11165
11166    #[test]
11167    fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11168        let payload = b"read-at crc-valid wrong volume index".to_vec();
11169        let mut options = small_block_recovery_options();
11170        options.stripe_width = 2;
11171        options.volume_loss_tolerance = 0;
11172        let archive = write_archive(
11173            &[RegularFile::new("read-at-volume-index.txt", &payload)],
11174            &master_key(),
11175            options,
11176        )
11177        .unwrap();
11178
11179        let mut corrupted = archive.volumes[0].clone();
11180        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11181        assert_eq!(header.volume_index, 0);
11182        header.volume_index = 1;
11183        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11184
11185        let opened = open_seekable_archive_volumes(
11186            vec![corrupted, archive.volumes[1].clone()],
11187            &master_key(),
11188        )
11189        .unwrap();
11190        assert_eq!(opened.volume_header.volume_index, 0);
11191        assert_eq!(
11192            opened.extract_file("read-at-volume-index.txt").unwrap(),
11193            Some(payload)
11194        );
11195        opened.verify().unwrap();
11196    }
11197
11198    #[test]
11199    fn recovers_cmra_header_magic_from_locator_tuple() {
11200        let payload = b"cmra header authority".to_vec();
11201        let archive = write_archive(
11202            &[RegularFile::new("cmra-header-magic.txt", &payload)],
11203            &master_key(),
11204            small_block_recovery_options(),
11205        )
11206        .unwrap();
11207        let locator = final_recovery_locator(&archive.bytes);
11208
11209        let mut corrupted = archive.bytes;
11210        corrupted[locator.cmra_offset as usize] ^= 0x55;
11211
11212        let opened = open_archive(&corrupted, &master_key()).unwrap();
11213        assert_eq!(
11214            opened.extract_file("cmra-header-magic.txt").unwrap(),
11215            Some(payload)
11216        );
11217        opened.verify().unwrap();
11218    }
11219
11220    #[test]
11221    fn recovers_cmra_shard_magic_as_erasure() {
11222        let payload = b"cmra shard authority".to_vec();
11223        let archive = write_archive(
11224            &[RegularFile::new("cmra-shard-magic.txt", &payload)],
11225            &master_key(),
11226            small_block_recovery_options(),
11227        )
11228        .unwrap();
11229        let locator = final_recovery_locator(&archive.bytes);
11230        let first_shard_offset =
11231            locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11232
11233        let mut corrupted = archive.bytes;
11234        corrupted[first_shard_offset] ^= 0x55;
11235
11236        let opened = open_archive(&corrupted, &master_key()).unwrap();
11237        assert_eq!(
11238            opened.extract_file("cmra-shard-magic.txt").unwrap(),
11239            Some(payload)
11240        );
11241        opened.verify().unwrap();
11242    }
11243
11244    #[test]
11245    fn key_holding_rejects_recovered_image_with_unknown_layout_flags() {
11246        let archive = write_archive(
11247            &[RegularFile::new("cmra-image-revision.txt", b"payload")],
11248            &master_key(),
11249            small_block_recovery_options(),
11250        )
11251        .unwrap();
11252        let mut mutated = rewrite_cmra_image_variable_len(
11253            &archive.bytes,
11254            CmraRecoveryMode::KeyHolding,
11255            |image| {
11256                image.layout_flags |= 0x8000_0000;
11257            },
11258        );
11259        mutated[0] ^= 0x55;
11260
11261        assert!(open_archive(&mutated, &master_key()).is_err());
11262    }
11263
11264    #[test]
11265    fn key_holding_rejects_recovered_footer_revision_mismatch() {
11266        let archive = write_archive_with_root_auth(
11267            &[RegularFile::new("cmra-footer-revision.txt", b"payload")],
11268            &master_key(),
11269            single_stream_options(),
11270            test_root_auth_config(),
11271            |request| Ok(test_root_auth_value(request)),
11272        )
11273        .unwrap();
11274        let mut mutated = archive.bytes;
11275        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11276            let root_auth_region = image
11277                .regions
11278                .iter_mut()
11279                .find(|region| region.region_type == 4)
11280                .unwrap();
11281            rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
11282        });
11283        mutated[0] ^= 0x55;
11284
11285        assert!(open_archive(&mutated, &master_key()).is_err());
11286    }
11287
11288    #[test]
11289    fn key_holding_rejects_locator_image_revision_mismatch() {
11290        let archive = write_archive(
11291            &[RegularFile::new("cmra-locator-revision.txt", b"payload")],
11292            &master_key(),
11293            small_block_recovery_options(),
11294        )
11295        .unwrap();
11296        let mut mutated = archive.bytes;
11297        let locator = final_recovery_locator(&mutated);
11298        let mirror_offset = mutated.len() - LOCATOR_PAIR_LEN;
11299        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11300        for offset in [mirror_offset, final_offset] {
11301            rewrite_recovery_locator(&mut mutated, offset, |locator| {
11302                locator.volume_format_rev = 43;
11303            });
11304        }
11305        mutated[0] ^= 0x55;
11306        mutated[locator.cmra_offset as usize] ^= 0x55;
11307
11308        assert!(open_archive(&mutated, &master_key()).is_err());
11309    }
11310
11311    #[test]
11312    fn image_identity_allows_matching_current_revision() {
11313        let archive = write_archive(
11314            &[RegularFile::new("matching-v44.txt", b"payload")],
11315            &master_key(),
11316            single_stream_options(),
11317        )
11318        .unwrap();
11319        let locator = final_recovery_locator(&archive.bytes);
11320        let recovered = recover_cmra(
11321            &archive.bytes,
11322            locator.cmra_offset,
11323            Some(CmraDecoderTuple::from(locator)),
11324            CmraRecoveryMode::KeyHolding,
11325        )
11326        .unwrap();
11327        let image = recovered.image;
11328        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11329        let crypto_start = header.crypto_header_offset as usize;
11330        let crypto_end = crypto_start + header.crypto_header_length as usize;
11331        let crypto = CryptoHeader::parse(
11332            &archive.bytes[crypto_start..crypto_end],
11333            header.crypto_header_length,
11334        )
11335        .unwrap();
11336
11337        validate_image_identity(&image, &header, &crypto.fixed).unwrap();
11338    }
11339
11340    #[test]
11341    fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
11342        let archive = write_archive(
11343            &[RegularFile::new(
11344                "cmra-floor.txt",
11345                b"authenticated CMRA floor",
11346            )],
11347            &master_key(),
11348            single_stream_options(),
11349        )
11350        .unwrap();
11351        let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
11352        let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11353        let locator = CriticalRecoveryLocator::parse(
11354            &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11355        )
11356        .unwrap();
11357        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11358        let crypto_start = volume_header.crypto_header_offset as usize;
11359        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11360        let crypto_header = CryptoHeader::parse(
11361            &malformed[crypto_start..crypto_end],
11362            volume_header.crypto_header_length,
11363        )
11364        .unwrap();
11365        let subkeys = Subkeys::derive(
11366            &master_key(),
11367            &volume_header.archive_uuid,
11368            &volume_header.session_id,
11369        )
11370        .unwrap();
11371
11372        assert_eq!(
11373            parse_locator_cmra_candidate(
11374                &malformed,
11375                final_offset,
11376                locator,
11377                KeyHoldingTerminalContext {
11378                    subkeys: &subkeys,
11379                    volume_header: &volume_header,
11380                    crypto_header: &crypto_header.fixed,
11381                    crypto_header_bytes: &malformed[crypto_start..crypto_end],
11382                },
11383            )
11384            .unwrap_err(),
11385            FormatError::InvalidArchive(
11386                "CMRA parity shard count is below authenticated bit-rot lower bound"
11387            )
11388        );
11389        assert!(open_archive(&malformed, &master_key()).is_err());
11390    }
11391
11392    #[test]
11393    fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
11394        let archive = write_archive(
11395            &[RegularFile::new(
11396                "locator-order.txt",
11397                b"locator tuple first",
11398            )],
11399            &master_key(),
11400            single_stream_options(),
11401        )
11402        .unwrap();
11403        let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11404        let mut locator = final_recovery_locator(&archive.bytes);
11405        locator.cmra_shard_size = 513;
11406        locator.body_bytes_before_cmra = locator.cmra_offset + 1;
11407        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11408        let crypto_start = volume_header.crypto_header_offset as usize;
11409        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11410        let crypto_header = CryptoHeader::parse(
11411            &archive.bytes[crypto_start..crypto_end],
11412            volume_header.crypto_header_length,
11413        )
11414        .unwrap();
11415        let subkeys = Subkeys::derive(
11416            &master_key(),
11417            &volume_header.archive_uuid,
11418            &volume_header.session_id,
11419        )
11420        .unwrap();
11421
11422        assert_eq!(
11423            parse_locator_cmra_candidate(
11424                &archive.bytes,
11425                final_offset,
11426                locator,
11427                KeyHoldingTerminalContext {
11428                    subkeys: &subkeys,
11429                    volume_header: &volume_header,
11430                    crypto_header: &crypto_header.fixed,
11431                    crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
11432                },
11433            )
11434            .unwrap_err(),
11435            FormatError::InvalidArchive("CMRA shard_size is invalid")
11436        );
11437    }
11438
11439    #[test]
11440    fn sequential_extract_rejects_bytes_after_terminal_locator() {
11441        let archive = write_archive(
11442            &[RegularFile::new("seq.txt", b"sequential EOF")],
11443            &master_key(),
11444            single_stream_options(),
11445        )
11446        .unwrap();
11447        let mut appended = archive.bytes.clone();
11448        appended.extend_from_slice(&[0xAA; 32]);
11449
11450        assert_eq!(
11451            sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
11452            FormatError::InvalidArchive("sequential terminal does not end at EOF")
11453        );
11454    }
11455
11456    #[test]
11457    fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
11458        let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
11459        let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
11460
11461        assert_eq!(
11462            validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
11463            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
11464        );
11465    }
11466
11467    #[test]
11468    fn root_auth_verifies_key_holding_and_public_no_key_modes() {
11469        let archive = write_archive_with_root_auth(
11470            &[RegularFile::new("signed.txt", b"ed25519 payload")],
11471            &master_key(),
11472            single_stream_options(),
11473            test_root_auth_config(),
11474            |request| Ok(test_root_auth_value(request)),
11475        )
11476        .unwrap();
11477
11478        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11479        let root_auth = opened
11480            .verify_root_auth_with(|footer, archive_root| {
11481                Ok(test_root_auth_verifies(footer, archive_root))
11482            })
11483            .unwrap();
11484        assert_eq!(
11485            root_auth.archive_root,
11486            opened.root_auth_footer.as_ref().unwrap().archive_root
11487        );
11488
11489        let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
11490            Ok(test_root_auth_verifies(footer, archive_root))
11491        })
11492        .unwrap();
11493        assert_eq!(public.archive_root, root_auth.archive_root);
11494        assert_eq!(
11495            public.diagnostics,
11496            vec![
11497                PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
11498                PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
11499                PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
11500            ]
11501        );
11502    }
11503
11504    #[test]
11505    fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
11506        let options = WriterOptions {
11507            block_size: 4096,
11508            chunk_size: 16 * 1024,
11509            envelope_target_size: 16 * 1024,
11510            stripe_width: 2,
11511            volume_loss_tolerance: 1,
11512            bit_rot_buffer_pct: 0,
11513            fec_data_shards: 16,
11514            fec_parity_shards: 1,
11515            index_fec_data_shards: 4,
11516            index_fec_parity_shards: 1,
11517            index_root_fec_data_shards: 16,
11518            index_root_fec_parity_shards: 1,
11519            ..WriterOptions::default()
11520        };
11521        let archive = write_archive_with_root_auth(
11522            &[RegularFile::new("missing-volume.txt", b"recover me")],
11523            &master_key(),
11524            options,
11525            test_root_auth_config(),
11526            |request| Ok(test_root_auth_value(request)),
11527        )
11528        .unwrap();
11529
11530        let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
11531        let root_auth = opened
11532            .verify_root_auth_with(|footer, archive_root| {
11533                Ok(test_root_auth_verifies(footer, archive_root))
11534            })
11535            .unwrap();
11536        assert!(root_auth
11537            .diagnostics
11538            .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
11539    }
11540
11541    #[test]
11542    fn public_no_key_rejects_unsigned_archives() {
11543        let archive = write_archive(
11544            &[RegularFile::new("plain.txt", b"unsigned")],
11545            &master_key(),
11546            single_stream_options(),
11547        )
11548        .unwrap();
11549
11550        assert_eq!(
11551            public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
11552            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11553        );
11554    }
11555
11556    #[test]
11557    fn unsigned_archive_reports_root_auth_absent() {
11558        let archive = write_archive(
11559            &[RegularFile::new("plain.txt", b"unsigned")],
11560            &master_key(),
11561            single_stream_options(),
11562        )
11563        .unwrap();
11564        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11565
11566        assert_eq!(
11567            opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
11568            FormatError::ReaderUnsupported("root-auth footer is absent")
11569        );
11570    }
11571
11572    #[test]
11573    fn safe_extract_writes_regular_file_under_root() {
11574        let archive = write_archive(
11575            &[RegularFile::new("dir/hello.txt", b"safe m8")],
11576            &master_key(),
11577            single_stream_options(),
11578        )
11579        .unwrap();
11580        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11581        let tmp = tempfile::tempdir().unwrap();
11582
11583        opened
11584            .extract_file_to(
11585                "dir/hello.txt",
11586                tmp.path(),
11587                SafeExtractionOptions::default(),
11588            )
11589            .unwrap()
11590            .unwrap();
11591
11592        assert_eq!(
11593            std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
11594            b"safe m8"
11595        );
11596    }
11597
11598    #[test]
11599    fn seekable_extract_all_to_streams_unique_archive() {
11600        let archive = write_archive(
11601            &[
11602                RegularFile::new("alpha.txt", b"alpha"),
11603                RegularFile::new("dir/beta.txt", b"beta"),
11604            ],
11605            &master_key(),
11606            single_stream_options(),
11607        )
11608        .unwrap();
11609        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11610        let tmp = tempfile::tempdir().unwrap();
11611
11612        let diagnostics = opened
11613            .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11614            .unwrap();
11615
11616        assert_eq!(diagnostics.len(), 2);
11617        assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
11618        assert_eq!(
11619            fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
11620            b"beta"
11621        );
11622    }
11623
11624    #[test]
11625    fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
11626        let archive = write_archive(
11627            &[
11628                RegularFile::new("same.txt", b"old"),
11629                RegularFile::new("same.txt", b"new"),
11630            ],
11631            &master_key(),
11632            single_stream_options(),
11633        )
11634        .unwrap();
11635        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11636        let tmp = tempfile::tempdir().unwrap();
11637
11638        assert_eq!(
11639            opened
11640                .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11641                .unwrap_err(),
11642            FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
11643        );
11644    }
11645
11646    #[test]
11647    fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
11648        let archive = write_archive(
11649            &[
11650                RegularFile::new("same.txt", b"old"),
11651                RegularFile::new("same.txt", b"new"),
11652            ],
11653            &master_key(),
11654            single_stream_options(),
11655        )
11656        .unwrap();
11657        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11658        let tmp = tempfile::tempdir().unwrap();
11659
11660        let diagnostics = opened
11661            .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
11662            .unwrap();
11663
11664        assert_eq!(diagnostics.len(), 1);
11665        assert_eq!(diagnostics[0].0, "same.txt");
11666        assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
11667    }
11668
11669    #[test]
11670    fn safe_extract_rejects_overwriting_existing_file_by_default() {
11671        let archive = write_archive(
11672            &[RegularFile::new("hello.txt", b"new")],
11673            &master_key(),
11674            single_stream_options(),
11675        )
11676        .unwrap();
11677        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11678        let tmp = tempfile::tempdir().unwrap();
11679        std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
11680
11681        assert_eq!(
11682            opened
11683                .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
11684                .unwrap_err(),
11685            FormatError::UnsafeOverwrite
11686        );
11687        assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
11688    }
11689
11690    #[test]
11691    fn opens_and_verifies_empty_archive() {
11692        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11693        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11694
11695        assert!(opened.list_files().unwrap().is_empty());
11696        opened.verify().unwrap();
11697    }
11698
11699    #[test]
11700    fn default_reader_options_allow_v36_trailing_garbage_scan() {
11701        let archive = write_archive(
11702            &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
11703            &master_key(),
11704            single_stream_options(),
11705        )
11706        .unwrap();
11707        let mut with_trailing_garbage = archive.bytes.clone();
11708        with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
11709
11710        let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
11711        assert_eq!(
11712            opened.extract_file("garbage-tolerant.txt").unwrap(),
11713            Some(b"still intact".to_vec())
11714        );
11715    }
11716
11717    #[test]
11718    fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
11719        assert_eq!(
11720            open_archive(
11721                &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
11722                &master_key()
11723            )
11724            .unwrap_err(),
11725            FormatError::InvalidLength {
11726                structure: "archive",
11727                expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11728                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
11729            }
11730        );
11731
11732        let mut header = test_volume_header();
11733        header.crypto_header_length = 512;
11734        let mut unavailable_crypto = header.to_bytes().to_vec();
11735        unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
11736
11737        assert_eq!(
11738            open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
11739            FormatError::InvalidLength {
11740                structure: "CryptoHeader",
11741                expected: VOLUME_HEADER_LEN + 512,
11742                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11743            }
11744        );
11745    }
11746
11747    #[test]
11748    fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
11749        let archive = write_archive(
11750            &[RegularFile::new("offset.txt", b"offset")],
11751            &master_key(),
11752            small_block_recovery_options(),
11753        )
11754        .unwrap();
11755        let mut mutated = archive.bytes;
11756        let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11757        header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
11758        mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11759
11760        let opened = open_archive(&mutated, &master_key()).unwrap();
11761        assert_eq!(
11762            opened.volume_header.crypto_header_offset,
11763            VOLUME_HEADER_LEN as u32
11764        );
11765        assert_eq!(
11766            opened.extract_file("offset.txt").unwrap(),
11767            Some(b"offset".to_vec())
11768        );
11769        opened.verify().unwrap();
11770    }
11771
11772    #[test]
11773    fn rejects_wrong_key_before_metadata_release() {
11774        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11775        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11776
11777        assert_eq!(
11778            open_archive(&archive.bytes, &wrong).unwrap_err(),
11779            FormatError::HmacMismatch {
11780                structure: "CryptoHeader"
11781            }
11782        );
11783    }
11784
11785    #[test]
11786    fn ordinary_encrypted_writers_emit_v44_archives() {
11787        let raw_key_archive = write_archive(
11788            &[RegularFile::new("raw.txt", b"raw key payload")],
11789            &master_key(),
11790            single_stream_options(),
11791        )
11792        .unwrap();
11793        let raw_header = VolumeHeader::parse(&raw_key_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11794        assert_eq!(raw_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11795        let raw_opened = open_archive(&raw_key_archive.bytes, &master_key()).unwrap();
11796        assert_eq!(
11797            raw_opened.volume_header.volume_format_rev,
11798            VOLUME_FORMAT_REV_44
11799        );
11800        assert_eq!(
11801            raw_opened.extract_file("raw.txt").unwrap(),
11802            Some(b"raw key payload".to_vec())
11803        );
11804
11805        let passphrase_kdf = KdfParams::Argon2id {
11806            t_cost: 1,
11807            m_cost_kib: 8,
11808            parallelism: 1,
11809            salt: b"0123456789abcdef".to_vec(),
11810        };
11811        let passphrase_archive = write_archive_with_kdf(
11812            &[RegularFile::new("pass.txt", b"passphrase payload")],
11813            &master_key(),
11814            single_stream_options(),
11815            &passphrase_kdf,
11816        )
11817        .unwrap();
11818        let passphrase_header =
11819            VolumeHeader::parse(&passphrase_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11820        assert_eq!(passphrase_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11821        let passphrase_opened = open_archive(&passphrase_archive.bytes, &master_key()).unwrap();
11822        assert_eq!(
11823            passphrase_opened.volume_header.volume_format_rev,
11824            VOLUME_FORMAT_REV_44
11825        );
11826        assert_eq!(
11827            passphrase_opened.extract_file("pass.txt").unwrap(),
11828            Some(b"passphrase payload".to_vec())
11829        );
11830    }
11831
11832    #[test]
11833    fn rejects_future_volume_format_revision_before_key_mismatch() {
11834        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11835        let mut bytes = archive.bytes;
11836        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
11837        header.volume_format_rev = 45;
11838        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11839        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11840
11841        assert_eq!(
11842            open_archive(&bytes, &wrong).unwrap_err(),
11843            FormatError::UnsupportedVolumeFormatRevision {
11844                format_version: FORMAT_VERSION,
11845                volume_format_rev: 45,
11846                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11847            }
11848        );
11849    }
11850
11851    #[test]
11852    fn open_archive_unencrypted_accepts_v44_profile() {
11853        let archive = write_archive_unencrypted(
11854            &[RegularFile::new("payload.txt", b"smoke-v44-unencrypted")],
11855            WriterOptions {
11856                aead_algo: AeadAlgo::None,
11857                ..single_stream_options()
11858            },
11859        )
11860        .unwrap();
11861
11862        let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11863        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11864
11865        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11866        assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11867        assert_eq!(
11868            opened.extract_file("payload.txt").unwrap(),
11869            Some(b"smoke-v44-unencrypted".to_vec())
11870        );
11871        opened.verify().unwrap();
11872    }
11873
11874    #[test]
11875    fn root_auth_unencrypted_v44_round_trips_with_recomputed_archive_root() {
11876        let archive = write_archive_with_root_auth(
11877            &[RegularFile::new(
11878                "signed-v44.txt",
11879                b"root-auth v44 plaintext",
11880            )],
11881            &master_key(),
11882            WriterOptions {
11883                aead_algo: AeadAlgo::None,
11884                ..single_stream_options()
11885            },
11886            test_root_auth_config(),
11887            |request| Ok(test_root_auth_value(request)),
11888        )
11889        .unwrap();
11890        let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11891        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11892
11893        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11894        assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11895        assert_eq!(
11896            opened.extract_file("signed-v44.txt").unwrap(),
11897            Some(b"root-auth v44 plaintext".to_vec())
11898        );
11899
11900        let verified = opened
11901            .verify_root_auth_with(|footer, archive_root| {
11902                Ok(test_root_auth_verifies(footer, archive_root))
11903            })
11904            .unwrap();
11905
11906        assert_eq!(verified.format_version, FORMAT_VERSION);
11907        assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
11908        assert_eq!(
11909            verified.archive_root,
11910            opened.root_auth_footer.as_ref().unwrap().archive_root
11911        );
11912    }
11913
11914    #[test]
11915    fn recipientwrap_open_accepts_candidate_after_header_hmac() {
11916        let master = master_key();
11917        let archive = write_archive_with_recipient_wrap_records(
11918            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11919            &master,
11920            single_stream_options(),
11921            vec![recipient_wrap_test_record()],
11922        )
11923        .unwrap();
11924
11925        let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
11926            assert_eq!(
11927                context.archive_identity.volume_format_rev,
11928                VOLUME_FORMAT_REV_44
11929            );
11930            assert_eq!(context.record.profile_id, 1);
11931            Ok(vec![master.0])
11932        })
11933        .unwrap();
11934
11935        assert_eq!(
11936            opened.extract_file("wrapped.txt").unwrap(),
11937            Some(b"recipient payload".to_vec())
11938        );
11939        opened.verify().unwrap();
11940    }
11941
11942    #[test]
11943    fn recipientwrap_seekable_open_uses_lazy_block_source() {
11944        let master = master_key();
11945        let archive = write_archive_with_recipient_wrap_records(
11946            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11947            &master,
11948            single_stream_options(),
11949            vec![recipient_wrap_test_record()],
11950        )
11951        .unwrap();
11952
11953        let opened = open_seekable_archive_with_recipient_wrap_resolver_options(
11954            CountingReadAt::new(archive.bytes, vec![]),
11955            |context| {
11956                assert_eq!(
11957                    context.archive_identity.volume_format_rev,
11958                    VOLUME_FORMAT_REV_44
11959                );
11960                assert_eq!(context.record.profile_id, 1);
11961                Ok(vec![master.0])
11962            },
11963            ReaderOptions::default(),
11964        )
11965        .unwrap();
11966
11967        assert!(opened.blocks.is_empty());
11968        assert!(opened.lazy_blocks.is_some());
11969        assert_eq!(
11970            opened.extract_file("wrapped.txt").unwrap(),
11971            Some(b"recipient payload".to_vec())
11972        );
11973        opened.verify().unwrap();
11974    }
11975
11976    #[test]
11977    fn recipientwrap_seekable_volume_set_opens_with_resolver() {
11978        let master = master_key();
11979        let archive = write_archive_with_recipient_wrap_records(
11980            &[RegularFile::new("wrapped.txt", b"recipient payload")],
11981            &master,
11982            WriterOptions {
11983                stripe_width: 2,
11984                volume_loss_tolerance: 0,
11985                ..WriterOptions::default()
11986            },
11987            vec![recipient_wrap_test_record()],
11988        )
11989        .unwrap();
11990        assert_eq!(archive.volumes.len(), 2);
11991
11992        let opened = open_seekable_archive_volumes_with_recipient_wrap_resolver_options(
11993            archive.volumes,
11994            |context| {
11995                assert_eq!(
11996                    context.archive_identity.volume_format_rev,
11997                    VOLUME_FORMAT_REV_44
11998                );
11999                assert_eq!(context.record.profile_id, 1);
12000                Ok(vec![master.0])
12001            },
12002            ReaderOptions::default(),
12003        )
12004        .unwrap();
12005
12006        assert_eq!(opened.observed_volume_count, 2);
12007        assert!(opened.blocks.is_empty());
12008        assert!(opened.lazy_blocks.is_some());
12009        assert_eq!(
12010            opened.extract_file("wrapped.txt").unwrap(),
12011            Some(b"recipient payload".to_vec())
12012        );
12013        opened.verify().unwrap();
12014    }
12015
12016    #[test]
12017    fn recipientwrap_open_tries_subsequent_records_after_failed_candidate() {
12018        let master = master_key();
12019        let mut first_record = recipient_wrap_test_record();
12020        first_record.recipient_identity_bytes = b"first-candidate".to_vec();
12021        let mut second_record = recipient_wrap_test_record();
12022        second_record.recipient_identity_bytes = b"second-candidate".to_vec();
12023
12024        let archive = write_archive_with_recipient_wrap_records(
12025            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12026            &master,
12027            single_stream_options(),
12028            vec![first_record, second_record],
12029        )
12030        .unwrap();
12031
12032        let mut attempts = Vec::new();
12033        let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
12034            attempts.push(context.record.recipient_identity_bytes.clone());
12035            if context.record.recipient_identity_bytes.as_slice() == b"second-candidate" {
12036                Ok(vec![master.0])
12037            } else {
12038                Ok(vec![[0x99u8; 32]])
12039            }
12040        })
12041        .unwrap();
12042
12043        assert_eq!(
12044            attempts,
12045            vec![b"first-candidate".to_vec(), b"second-candidate".to_vec(),]
12046        );
12047        assert_eq!(
12048            opened.extract_file("wrapped.txt").unwrap(),
12049            Some(b"recipient payload".to_vec())
12050        );
12051        opened.verify().unwrap();
12052    }
12053
12054    #[test]
12055    fn recipientwrap_startup_rejects_malformed_record_length() {
12056        let master = master_key();
12057        let options = WriterOptions {
12058            bit_rot_buffer_pct: 0,
12059            ..single_stream_options()
12060        };
12061        let archive = write_archive_with_recipient_wrap_records(
12062            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12063            &master,
12064            options,
12065            vec![recipient_wrap_test_record()],
12066        )
12067        .unwrap();
12068        let mut bytes = archive.bytes;
12069        let (_, _, table_start, _table_len, _) = recipient_wrap_layout(&bytes);
12070        bytes[table_start + 96..table_start + 100].copy_from_slice(&1u32.to_le_bytes());
12071
12072        assert_eq!(
12073            open_archive_with_recipient_wrap_resolver(&bytes, |_| { Ok(vec![master.0]) })
12074                .unwrap_err(),
12075            FormatError::InvalidArchive("RecipientRecordV1 record_length is too small")
12076        );
12077    }
12078
12079    #[test]
12080    fn recipientwrap_future_revision_rejects_before_resolver_callback() {
12081        let archive = write_archive_with_recipient_wrap_records(
12082            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12083            &master_key(),
12084            single_stream_options(),
12085            vec![recipient_wrap_test_record()],
12086        )
12087        .unwrap();
12088        let mut bytes = archive.bytes;
12089        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12090        header.volume_format_rev = VOLUME_FORMAT_REV_44 + 1;
12091        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12092
12093        let mut called = false;
12094        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12095            called = true;
12096            Ok(vec![master_key().0])
12097        })
12098        .unwrap_err();
12099
12100        assert!(!called);
12101        assert_eq!(
12102            err,
12103            FormatError::UnsupportedVolumeFormatRevision {
12104                format_version: FORMAT_VERSION,
12105                volume_format_rev: VOLUME_FORMAT_REV_44 + 1,
12106                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12107            }
12108        );
12109    }
12110
12111    #[test]
12112    fn recipientwrap_stripe_width_mismatch_rejects_before_resolver_callback() {
12113        let archive = write_archive_with_recipient_wrap_records(
12114            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12115            &master_key(),
12116            single_stream_options(),
12117            vec![recipient_wrap_test_record()],
12118        )
12119        .unwrap();
12120        let mut bytes = archive.bytes;
12121        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12122        header.stripe_width += 1;
12123        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12124
12125        let mut called = false;
12126        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12127            called = true;
12128            Ok(vec![master_key().0])
12129        })
12130        .unwrap_err();
12131
12132        assert!(!called);
12133        assert_eq!(
12134            err,
12135            FormatError::InvalidArchive("VolumeHeader and CryptoHeader stripe_width differ")
12136        );
12137    }
12138
12139    #[test]
12140    fn recipientwrap_defers_raw_stream_profile_rejection_until_after_resolver_callback() {
12141        let master = master_key();
12142        let archive = write_archive_with_recipient_wrap_records(
12143            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12144            &master,
12145            single_stream_options(),
12146            vec![recipient_wrap_test_record()],
12147        )
12148        .unwrap();
12149        let mut bytes = archive.bytes;
12150        add_raw_stream_profile_to_physical_crypto_header(&mut bytes);
12151        recompute_physical_crypto_header_hmac(&mut bytes, &master);
12152
12153        let mut called = false;
12154        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12155            called = true;
12156            Ok(vec![master.0])
12157        })
12158        .unwrap_err();
12159
12160        assert!(called);
12161        assert_eq!(
12162            err,
12163            FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE)
12164        );
12165    }
12166
12167    #[test]
12168    fn recipientwrap_recovers_physical_key_wrap_table_from_cmra_authority() {
12169        let master = master_key();
12170        let archive = write_archive_with_recipient_wrap_records(
12171            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12172            &master,
12173            single_stream_options(),
12174            vec![recipient_wrap_test_record()],
12175        )
12176        .unwrap();
12177        let mut bytes = archive.bytes;
12178        let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12179        let crypto_start = header.crypto_header_offset as usize;
12180        let crypto_end = crypto_start + header.crypto_header_length as usize;
12181        let crypto_header = CryptoHeader::parse(
12182            &bytes[crypto_start..crypto_end],
12183            header.crypto_header_length,
12184        )
12185        .unwrap();
12186        let KdfParams::RecipientWrap {
12187            key_wrap_table_length,
12188            ..
12189        } = crypto_header.kdf_params
12190        else {
12191            panic!("expected RecipientWrap KdfParams");
12192        };
12193        let table_start = crypto_end;
12194        let table_end = table_start + key_wrap_table_length as usize;
12195        bytes[table_end - 1] ^= 0x01;
12196
12197        let mut called = false;
12198        let opened = open_archive_with_recipient_wrap_resolver(&bytes, |context| {
12199            called = true;
12200            assert_eq!(
12201                context.archive_identity.volume_format_rev,
12202                VOLUME_FORMAT_REV_44
12203            );
12204            assert_eq!(context.record.profile_id, 1);
12205            Ok(vec![master.0])
12206        })
12207        .unwrap();
12208
12209        assert!(called);
12210        assert_eq!(
12211            opened.extract_file("wrapped.txt").unwrap(),
12212            Some(b"recipient payload".to_vec())
12213        );
12214        opened.verify().unwrap();
12215    }
12216
12217    #[test]
12218    fn recipientwrap_open_rejects_wrong_candidate_header_hmac() {
12219        let archive = write_archive_with_recipient_wrap_records(
12220            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12221            &master_key(),
12222            single_stream_options(),
12223            vec![recipient_wrap_test_record()],
12224        )
12225        .unwrap();
12226        let wrong_candidate = [0x99u8; 32];
12227
12228        assert_eq!(
12229            open_archive_with_recipient_wrap_resolver(&archive.bytes, |_| {
12230                Ok(vec![wrong_candidate])
12231            })
12232            .unwrap_err(),
12233            FormatError::KeyMaterialMismatch
12234        );
12235    }
12236
12237    #[test]
12238    fn recipientwrap_open_recovers_tampered_physical_crypto_header_hmac_from_cmra() {
12239        let master = master_key();
12240        let archive = write_archive_with_recipient_wrap_records(
12241            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12242            &master,
12243            single_stream_options(),
12244            vec![recipient_wrap_test_record()],
12245        )
12246        .unwrap();
12247        let mut bytes = archive.bytes.clone();
12248        let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12249        let hmac_end = header.crypto_header_offset as usize + header.crypto_header_length as usize;
12250        bytes[hmac_end - 1] ^= 0x55;
12251
12252        let opened =
12253            open_archive_with_recipient_wrap_resolver(&bytes, |_| Ok(vec![master.0])).unwrap();
12254
12255        assert_eq!(
12256            opened.extract_file("wrapped.txt").unwrap(),
12257            Some(b"recipient payload".to_vec())
12258        );
12259        assert_eq!(
12260            opened.crypto_header_bytes,
12261            archive.bytes[header.crypto_header_offset as usize..hmac_end]
12262        );
12263        opened.verify().unwrap();
12264    }
12265
12266    #[test]
12267    fn recipientwrap_archive_does_not_fall_back_to_raw_master_key_open() {
12268        let master = master_key();
12269        let archive = write_archive_with_recipient_wrap_records(
12270            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12271            &master,
12272            single_stream_options(),
12273            vec![recipient_wrap_test_record()],
12274        )
12275        .unwrap();
12276
12277        assert_eq!(
12278            open_archive(&archive.bytes, &master).unwrap_err(),
12279            FormatError::KeyMaterialMismatch
12280        );
12281    }
12282
12283    #[test]
12284    fn recipientwrap_seekable_archive_does_not_fall_back_to_raw_master_key_open() {
12285        let master = master_key();
12286        let archive = write_archive_with_recipient_wrap_records(
12287            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12288            &master,
12289            single_stream_options(),
12290            vec![recipient_wrap_test_record()],
12291        )
12292        .unwrap();
12293
12294        assert_eq!(
12295            open_seekable_archive(archive.bytes, &master).unwrap_err(),
12296            FormatError::KeyMaterialMismatch
12297        );
12298    }
12299
12300    #[test]
12301    fn public_no_key_verifies_signed_recipientwrap_block_commitment() {
12302        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12303            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12304            &master_key(),
12305            single_stream_options(),
12306            vec![recipient_wrap_test_record()],
12307            test_root_auth_config(),
12308            |request| Ok(test_root_auth_value(request)),
12309        )
12310        .unwrap();
12311
12312        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
12313            Ok(test_root_auth_verifies(footer, archive_root))
12314        })
12315        .unwrap();
12316
12317        assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
12318        assert_eq!(verified.total_data_block_count, 3);
12319    }
12320
12321    #[test]
12322    fn public_no_key_rejects_recipientwrap_startup_and_cmra_kdf_mismatch() {
12323        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12324            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12325            &master_key(),
12326            single_stream_options(),
12327            vec![recipient_wrap_test_record()],
12328            test_root_auth_config(),
12329            |request| Ok(test_root_auth_value(request)),
12330        )
12331        .unwrap();
12332        let mut bytes = archive.bytes;
12333        mutate_top_level_recipient_wrap_public_profile(&mut bytes);
12334
12335        let err = public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
12336            Ok(test_root_auth_verifies(footer, archive_root))
12337        })
12338        .unwrap_err();
12339
12340        assert_eq!(
12341            err,
12342            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
12343        );
12344    }
12345
12346    #[test]
12347    fn public_no_key_rejects_recipientwrap_kdf_profile_mismatch_across_volumes() {
12348        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12349            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12350            &master_key(),
12351            WriterOptions {
12352                stripe_width: 2,
12353                volume_loss_tolerance: 0,
12354                ..WriterOptions::default()
12355            },
12356            vec![recipient_wrap_test_record()],
12357            test_root_auth_config(),
12358            |request| Ok(test_root_auth_value(request)),
12359        )
12360        .unwrap();
12361        let mut volumes = archive.volumes;
12362        mutate_top_level_recipient_wrap_public_profile(&mut volumes[1]);
12363        mutate_cmra_recipient_wrap_public_profile(&mut volumes[1]);
12364
12365        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12366        let err = public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
12367            Ok(test_root_auth_verifies(footer, archive_root))
12368        })
12369        .unwrap_err();
12370
12371        assert_eq!(
12372            err,
12373            FormatError::InvalidArchive("public no-key volume global metadata differs")
12374        );
12375    }
12376
12377    #[test]
12378    fn write_archive_defaults_to_current_revision() {
12379        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12380        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12381
12382        assert_eq!(header.format_version, FORMAT_VERSION);
12383        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV);
12384    }
12385
12386    #[test]
12387    fn non_seekable_stream_rejects_future_volume_format_revision() {
12388        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12389        let mut bytes = archive.bytes;
12390        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12391        header.volume_format_rev = 45;
12392        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12393
12394        assert_eq!(
12395            verify_non_seekable_stream(std::io::Cursor::new(bytes), &master_key()).unwrap_err(),
12396            FormatError::UnsupportedVolumeFormatRevision {
12397                format_version: FORMAT_VERSION,
12398                volume_format_rev: 45,
12399                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12400            }
12401        );
12402    }
12403
12404    #[test]
12405    fn open_seekable_archive_rejects_future_volume_format_revision() {
12406        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12407        let mut bytes = archive.bytes;
12408        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12409        header.volume_format_rev = 45;
12410        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12411
12412        assert_eq!(
12413            open_seekable_archive(CountingReadAt::new(bytes, vec![]), &master_key()).unwrap_err(),
12414            FormatError::UnsupportedVolumeFormatRevision {
12415                format_version: FORMAT_VERSION,
12416                volume_format_rev: 45,
12417                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12418            }
12419        );
12420    }
12421
12422    #[test]
12423    fn rejects_payload_tamper_even_with_recomputed_block_crc() {
12424        let mut archive = write_archive(
12425            &[RegularFile::new("file.txt", b"authenticated")],
12426            &master_key(),
12427            single_stream_options(),
12428        )
12429        .unwrap()
12430        .bytes;
12431        let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
12432        let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
12433        let crypto = CryptoHeader::parse(
12434            &archive[VOLUME_HEADER_LEN..crypto_end],
12435            volume.crypto_header_length,
12436        )
12437        .unwrap();
12438        let block_size = crypto.fixed.block_size as usize;
12439        archive[crypto_end + 16] ^= 1;
12440        let crc_offset = crypto_end + 16 + block_size;
12441        let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
12442        archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12443
12444        let opened = open_archive(&archive, &master_key()).unwrap();
12445        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12446    }
12447
12448    #[test]
12449    fn list_and_extract_use_final_view_for_duplicate_paths() {
12450        let archive = write_archive(
12451            &[
12452                RegularFile {
12453                    mtime: 1_700_000_000,
12454                    ..RegularFile::new("same.txt", b"old")
12455                },
12456                RegularFile {
12457                    mtime: 1_700_000_100,
12458                    ..RegularFile::new("same.txt", b"newer")
12459                },
12460            ],
12461            &master_key(),
12462            single_stream_options(),
12463        )
12464        .unwrap();
12465        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12466
12467        let listed = opened.list_index_entries().unwrap();
12468        assert_eq!(listed.len(), 1);
12469        assert_eq!(listed[0].path, "same.txt");
12470        assert_eq!(listed[0].name, "same.txt");
12471        assert_eq!(listed[0].file_data_size, 5);
12472        assert_eq!(listed[0].kind, TarEntryKind::Regular);
12473        assert_eq!(listed[0].mode, 0o644);
12474        assert_eq!(listed[0].mtime, 1_700_000_100);
12475        assert_eq!(listed[0].frame_count, 1);
12476        assert!(listed[0].layout.compressed_size > 0);
12477        let looked_up = opened.lookup_index_entry("same.txt").unwrap().unwrap();
12478        assert_eq!(looked_up.path, "same.txt");
12479        assert_eq!(looked_up.file_data_size, 5);
12480        assert_eq!(looked_up.kind, TarEntryKind::Regular);
12481        assert_eq!(looked_up.mode, 0o644);
12482        assert_eq!(looked_up.mtime, 1_700_000_100);
12483        assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
12484        assert_eq!(
12485            opened.list_files().unwrap(),
12486            vec![ArchiveEntry {
12487                path: "same.txt".to_string(),
12488                file_data_size: 5,
12489                kind: TarEntryKind::Regular,
12490                mode: 0o644,
12491                mtime: 1_700_000_100,
12492                diagnostics: Vec::new(),
12493            }]
12494        );
12495        assert_eq!(
12496            opened.extract_file("same.txt").unwrap(),
12497            Some(b"newer".to_vec())
12498        );
12499        opened.verify().unwrap();
12500    }
12501
12502    #[test]
12503    fn index_entries_do_not_decrypt_payload_envelopes() {
12504        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12505        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12506
12507        let listed = opened.list_index_entries().unwrap();
12508        assert_eq!(listed.len(), 2);
12509        assert_eq!(listed[0].path, "broken.txt");
12510        assert_eq!(listed[0].file_data_size, b"broken payload\n".len() as u64);
12511        assert_eq!(listed[0].kind, TarEntryKind::Regular);
12512        assert_eq!(listed[0].mode, 0o644);
12513        assert_eq!(listed[0].mtime, 0);
12514        assert_eq!(listed[1].path, "healthy.txt");
12515        assert_eq!(listed[1].file_data_size, b"healthy payload\n".len() as u64);
12516        assert_eq!(listed[1].kind, TarEntryKind::Regular);
12517        assert_eq!(listed[1].mode, 0o644);
12518        assert_eq!(listed[1].mtime, 0);
12519        let looked_up = opened.lookup_index_entry("broken.txt").unwrap().unwrap();
12520        assert_eq!(looked_up.path, "broken.txt");
12521        assert_eq!(looked_up.file_data_size, b"broken payload\n".len() as u64);
12522        assert_eq!(looked_up.kind, TarEntryKind::Regular);
12523        assert_eq!(looked_up.mode, 0o644);
12524        assert_eq!(looked_up.mtime, 0);
12525        assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
12526    }
12527
12528    #[test]
12529    fn index_entry_layout_stats_match_frame_and_envelope_tables() {
12530        let payload = pseudo_random_bytes(12 * 1024);
12531        let archive = write_archive(
12532            &[RegularFile::new("chunked.bin", &payload)],
12533            &master_key(),
12534            WriterOptions {
12535                block_size: 4096,
12536                chunk_size: 1024,
12537                envelope_target_size: 2048,
12538                stripe_width: 1,
12539                volume_loss_tolerance: 0,
12540                bit_rot_buffer_pct: 0,
12541                fec_data_shards: 16,
12542                fec_parity_shards: 0,
12543                ..WriterOptions::default()
12544            },
12545        )
12546        .unwrap();
12547        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12548        let entry = opened.list_index_entries().unwrap().remove(0);
12549        let located = opened
12550            .locate_index_file(b"chunked.bin")
12551            .unwrap()
12552            .expect("indexed file exists");
12553        let file = &located.shard.files[located.file_index];
12554        let frames = frame_range_for_file(&located.shard, file).unwrap();
12555        assert!(frames.len() > 1);
12556
12557        let expected_compressed_size = frames
12558            .iter()
12559            .map(|frame| frame.compressed_size as u64)
12560            .sum::<u64>();
12561        let expected_decompressed_frame_size = frames
12562            .iter()
12563            .map(|frame| frame.decompressed_size as u64)
12564            .sum::<u64>();
12565        let envelope_indexes = frames
12566            .iter()
12567            .map(|frame| frame.envelope_index)
12568            .collect::<BTreeSet<_>>();
12569        assert!(envelope_indexes.len() > 1);
12570
12571        let envelopes = envelope_indexes
12572            .iter()
12573            .map(|index| envelope_by_index(&located.shard, *index).unwrap())
12574            .collect::<Vec<_>>();
12575        let expected_first_payload_block_index = envelopes
12576            .iter()
12577            .map(|envelope| envelope.first_block_index)
12578            .min();
12579        let expected_payload_data_block_count = envelopes
12580            .iter()
12581            .map(|envelope| envelope.data_block_count as u64)
12582            .sum::<u64>();
12583        let expected_payload_parity_block_count = envelopes
12584            .iter()
12585            .map(|envelope| envelope.parity_block_count as u64)
12586            .sum::<u64>();
12587        let expected_payload_encrypted_size = envelopes
12588            .iter()
12589            .map(|envelope| envelope.encrypted_size as u64)
12590            .sum::<u64>();
12591
12592        assert_eq!(entry.path, "chunked.bin");
12593        assert_eq!(entry.name, "chunked.bin");
12594        assert_eq!(entry.file_data_size, payload.len() as u64);
12595        assert_eq!(entry.kind, TarEntryKind::Regular);
12596        assert_eq!(entry.mode, 0o644);
12597        assert_eq!(entry.tar_member_group_size, file.tar_member_group_size);
12598        assert_eq!(entry.first_frame_index, file.first_frame_index);
12599        assert_eq!(entry.frame_count, file.frame_count);
12600        assert_eq!(
12601            entry.offset_in_first_frame_plaintext,
12602            file.offset_in_first_frame_plaintext
12603        );
12604        assert_eq!(entry.layout.compressed_size, expected_compressed_size);
12605        assert_eq!(
12606            entry.layout.decompressed_frame_size,
12607            expected_decompressed_frame_size
12608        );
12609        assert_eq!(entry.layout.envelope_count as usize, envelope_indexes.len());
12610        assert_eq!(
12611            entry.layout.first_envelope_index,
12612            envelope_indexes.iter().next().copied()
12613        );
12614        assert_eq!(
12615            entry.layout.last_envelope_index,
12616            envelope_indexes.iter().next_back().copied()
12617        );
12618        assert_eq!(
12619            entry.layout.first_payload_block_index,
12620            expected_first_payload_block_index
12621        );
12622        assert_eq!(
12623            entry.layout.payload_data_block_count,
12624            expected_payload_data_block_count
12625        );
12626        assert_eq!(
12627            entry.layout.payload_parity_block_count,
12628            expected_payload_parity_block_count
12629        );
12630        assert_eq!(
12631            entry.layout.payload_encrypted_size,
12632            expected_payload_encrypted_size
12633        );
12634    }
12635
12636    #[test]
12637    fn extract_file_does_not_decrypt_unselected_payload_envelope() {
12638        // This fixture corrupts only the unselected envelope, proving selected
12639        // extraction does not decrypt unrelated payload envelopes.
12640        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12641        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12642
12643        assert_eq!(
12644            opened.extract_file("healthy.txt").unwrap(),
12645            Some(b"healthy payload\n".to_vec())
12646        );
12647        assert_eq!(
12648            opened.extract_file("broken.txt").unwrap_err(),
12649            FormatError::AeadFailure
12650        );
12651        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12652    }
12653
12654    #[test]
12655    fn seekable_extract_does_not_read_unselected_payload_envelope() {
12656        let healthy = pseudo_random_bytes(64 * 1024);
12657        let broken = pseudo_random_bytes(64 * 1024);
12658        let options = WriterOptions {
12659            block_size: 4096,
12660            chunk_size: 4096,
12661            envelope_target_size: 8192,
12662            stripe_width: 1,
12663            volume_loss_tolerance: 0,
12664            bit_rot_buffer_pct: 0,
12665            fec_data_shards: 4,
12666            fec_parity_shards: 0,
12667            index_fec_data_shards: 4,
12668            index_fec_parity_shards: 0,
12669            index_root_fec_data_shards: 4,
12670            index_root_fec_parity_shards: 0,
12671            ..WriterOptions::default()
12672        };
12673        let archive = write_archive(
12674            &[
12675                RegularFile::new("healthy.bin", &healthy),
12676                RegularFile::new("broken.bin", &broken),
12677            ],
12678            &master_key(),
12679            options,
12680        )
12681        .unwrap();
12682        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12683        let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
12684        let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
12685        let denied_block_indices = broken_envelopes
12686            .iter()
12687            .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
12688            .flat_map(|envelope| {
12689                let block_count =
12690                    envelope.data_block_count as u64 + envelope.parity_block_count as u64;
12691                envelope.first_block_index..envelope.first_block_index + block_count
12692            })
12693            .collect::<BTreeSet<_>>();
12694        assert!(
12695            !denied_block_indices.is_empty(),
12696            "fixture must place broken.bin in at least one unshared envelope"
12697        );
12698        let denied_ranges = block_record_slots(&archive.bytes)
12699            .into_iter()
12700            .filter_map(|(offset, len, record)| {
12701                denied_block_indices
12702                    .contains(&record.block_index)
12703                    .then_some((offset as u64, (offset + len) as u64))
12704            })
12705            .collect::<Vec<_>>();
12706        assert!(!denied_ranges.is_empty());
12707
12708        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12709        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12710
12711        assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
12712        for (read_start, read_end) in reader.reads() {
12713            assert!(
12714                denied_ranges
12715                    .iter()
12716                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12717                "targeted extract read an unrelated payload BlockRecord range"
12718            );
12719        }
12720        assert_eq!(
12721            opened.extract_file("broken.bin").unwrap_err(),
12722            FormatError::InvalidArchive("denied test read")
12723        );
12724    }
12725
12726    #[test]
12727    fn extract_file_to_writer_streams_before_reading_later_envelopes() {
12728        struct FailOnFirstWrite;
12729
12730        impl std::io::Write for FailOnFirstWrite {
12731            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
12732                Err(std::io::Error::other("sink stopped"))
12733            }
12734
12735            fn flush(&mut self) -> std::io::Result<()> {
12736                Ok(())
12737            }
12738        }
12739
12740        let payload = pseudo_random_bytes(128 * 1024);
12741        let options = WriterOptions {
12742            block_size: 4096,
12743            chunk_size: 4096,
12744            envelope_target_size: 8192,
12745            stripe_width: 1,
12746            volume_loss_tolerance: 0,
12747            bit_rot_buffer_pct: 0,
12748            fec_data_shards: 4,
12749            fec_parity_shards: 0,
12750            index_fec_data_shards: 4,
12751            index_fec_parity_shards: 0,
12752            index_root_fec_data_shards: 4,
12753            index_root_fec_parity_shards: 0,
12754            ..WriterOptions::default()
12755        };
12756        let archive = write_archive(
12757            &[RegularFile::new("large.bin", &payload)],
12758            &master_key(),
12759            options,
12760        )
12761        .unwrap();
12762        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12763        let envelopes = envelope_entries_for_path(&eager, "large.bin");
12764        let first_envelope = envelopes
12765            .first()
12766            .expect("large fixture should have at least one envelope")
12767            .envelope_index;
12768        let later_envelope_blocks = envelopes
12769            .iter()
12770            .filter(|entry| entry.envelope_index != first_envelope)
12771            .flat_map(|entry| {
12772                let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
12773                entry.first_block_index..entry.first_block_index + block_count
12774            })
12775            .collect::<BTreeSet<_>>();
12776        assert!(
12777            !later_envelope_blocks.is_empty(),
12778            "fixture must span more than one payload envelope"
12779        );
12780        let denied_ranges = block_record_slots(&archive.bytes)
12781            .into_iter()
12782            .filter_map(|(offset, len, record)| {
12783                later_envelope_blocks
12784                    .contains(&record.block_index)
12785                    .then_some((offset as u64, (offset + len) as u64))
12786            })
12787            .collect::<Vec<_>>();
12788        assert!(!denied_ranges.is_empty());
12789
12790        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12791        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12792        let mut writer = FailOnFirstWrite;
12793
12794        let err = opened
12795            .extract_file_to_writer("large.bin", &mut writer)
12796            .unwrap_err();
12797        assert_eq!(err.to_string(), "extraction output write failed");
12798        for (read_start, read_end) in reader.reads() {
12799            assert!(
12800                denied_ranges
12801                    .iter()
12802                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12803                "streaming writer read a later payload envelope before surfacing writer failure"
12804            );
12805        }
12806    }
12807
12808    #[test]
12809    fn extract_file_to_writer_writes_bounded_chunks() {
12810        struct ChunkRecorder {
12811            total: usize,
12812            max_write: usize,
12813            writes: usize,
12814        }
12815
12816        impl std::io::Write for ChunkRecorder {
12817            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12818                self.total += buf.len();
12819                self.max_write = self.max_write.max(buf.len());
12820                self.writes += 1;
12821                Ok(buf.len())
12822            }
12823
12824            fn flush(&mut self) -> std::io::Result<()> {
12825                Ok(())
12826            }
12827        }
12828
12829        let payload = pseudo_random_bytes(128 * 1024);
12830        let options = WriterOptions {
12831            block_size: 4096,
12832            chunk_size: 4096,
12833            envelope_target_size: 8192,
12834            stripe_width: 1,
12835            volume_loss_tolerance: 0,
12836            bit_rot_buffer_pct: 0,
12837            fec_data_shards: 4,
12838            fec_parity_shards: 0,
12839            index_fec_data_shards: 4,
12840            index_fec_parity_shards: 0,
12841            index_root_fec_data_shards: 4,
12842            index_root_fec_parity_shards: 0,
12843            ..WriterOptions::default()
12844        };
12845        let archive = write_archive(
12846            &[RegularFile::new("large.bin", &payload)],
12847            &master_key(),
12848            options,
12849        )
12850        .unwrap();
12851        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12852        let mut writer = ChunkRecorder {
12853            total: 0,
12854            max_write: 0,
12855            writes: 0,
12856        };
12857
12858        opened
12859            .extract_file_to_writer("large.bin", &mut writer)
12860            .unwrap()
12861            .unwrap();
12862
12863        assert_eq!(writer.total, payload.len());
12864        assert!(writer.writes > 1);
12865        assert!(
12866            writer.max_write <= options.chunk_size as usize,
12867            "writer saw a {} byte chunk, larger than the {} byte frame target",
12868            writer.max_write,
12869            options.chunk_size
12870        );
12871    }
12872
12873    #[test]
12874    fn extract_file_to_writer_with_progress_reports_payload_bytes() {
12875        struct ChunkRecorder {
12876            total: usize,
12877        }
12878
12879        impl std::io::Write for ChunkRecorder {
12880            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12881                self.total += buf.len();
12882                Ok(buf.len())
12883            }
12884
12885            fn flush(&mut self) -> std::io::Result<()> {
12886                Ok(())
12887            }
12888        }
12889
12890        let payload = pseudo_random_bytes(128 * 1024);
12891        let options = WriterOptions {
12892            block_size: 4096,
12893            chunk_size: 4096,
12894            envelope_target_size: 8192,
12895            stripe_width: 1,
12896            volume_loss_tolerance: 0,
12897            bit_rot_buffer_pct: 0,
12898            fec_data_shards: 4,
12899            fec_parity_shards: 0,
12900            index_fec_data_shards: 4,
12901            index_fec_parity_shards: 0,
12902            index_root_fec_data_shards: 4,
12903            index_root_fec_parity_shards: 0,
12904            ..WriterOptions::default()
12905        };
12906        let archive = write_archive(
12907            &[RegularFile::new("large.bin", &payload)],
12908            &master_key(),
12909            options,
12910        )
12911        .unwrap();
12912        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12913        let mut writer = ChunkRecorder { total: 0 };
12914        let mut progress_events = Vec::new();
12915        let mut progress = |archive_path: &str, bytes: u64| {
12916            progress_events.push((archive_path.to_owned(), bytes));
12917        };
12918
12919        opened
12920            .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
12921            .unwrap()
12922            .unwrap();
12923
12924        let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
12925        assert_eq!(writer.total, payload.len());
12926        assert_eq!(reported_bytes, payload.len() as u64);
12927        assert!(progress_events.len() > 1);
12928        assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
12929    }
12930
12931    #[test]
12932    fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
12933        let payload = pseudo_random_bytes(128 * 1024);
12934        let options = WriterOptions {
12935            block_size: 4096,
12936            chunk_size: 4096,
12937            envelope_target_size: 8192,
12938            stripe_width: 1,
12939            volume_loss_tolerance: 0,
12940            bit_rot_buffer_pct: 0,
12941            fec_data_shards: 4,
12942            fec_parity_shards: 0,
12943            index_fec_data_shards: 4,
12944            index_fec_parity_shards: 0,
12945            index_root_fec_data_shards: 4,
12946            index_root_fec_parity_shards: 0,
12947            ..WriterOptions::default()
12948        };
12949        let archive = write_archive(
12950            &[RegularFile::new("large.bin", &payload)],
12951            &master_key(),
12952            options,
12953        )
12954        .unwrap();
12955        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12956        let envelopes = envelope_entries_for_path(&eager, "large.bin");
12957        let last_envelope = envelopes
12958            .last()
12959            .expect("large fixture should have at least one envelope");
12960        assert_ne!(
12961            envelopes.first().unwrap().envelope_index,
12962            last_envelope.envelope_index,
12963            "fixture must span more than one payload envelope"
12964        );
12965        let corrupt_slot = block_record_slots(&archive.bytes)
12966            .into_iter()
12967            .enumerate()
12968            .find_map(|(slot, (_, _, record))| {
12969                (record.block_index == last_envelope.first_block_index).then_some(slot)
12970            })
12971            .unwrap();
12972        let mut corrupted = archive.bytes;
12973        corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
12974        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
12975        let tmp = tempfile::tempdir().unwrap();
12976
12977        assert!(matches!(
12978            opened
12979                .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
12980                .unwrap_err(),
12981            FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
12982        ));
12983        assert!(!tmp.path().join("large.bin").exists());
12984        assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
12985    }
12986
12987    #[test]
12988    fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
12989        let archive = write_archive(
12990            &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
12991            &master_key(),
12992            single_stream_options(),
12993        )
12994        .unwrap();
12995        let opened = open_archive_with_bootstrap_sidecar(
12996            &archive.bytes,
12997            &archive.bootstrap_sidecar,
12998            &master_key(),
12999        )
13000        .unwrap();
13001
13002        assert_eq!(
13003            opened.list_files().unwrap(),
13004            vec![ArchiveEntry {
13005                path: "dir/sidecar.txt".to_string(),
13006                file_data_size: 13,
13007                kind: TarEntryKind::Regular,
13008                mode: 0o644,
13009                mtime: 0,
13010                diagnostics: Vec::new(),
13011            }]
13012        );
13013        assert_eq!(
13014            opened.extract_file("dir/sidecar.txt").unwrap(),
13015            Some(b"hello sidecar".to_vec())
13016        );
13017        opened.verify().unwrap();
13018    }
13019
13020    #[test]
13021    fn fast_verify_plaintext_zero_recovery_defers_payload_semantics() {
13022        let options = WriterOptions {
13023            aead_algo: AeadAlgo::None,
13024            volume_loss_tolerance: 0,
13025            bit_rot_buffer_pct: 0,
13026            ..single_stream_options()
13027        };
13028        let archive = write_archive_unencrypted(
13029            &[RegularFile::new(
13030                "payload.txt",
13031                b"payload bytes large enough to produce a zstd frame",
13032            )],
13033            options,
13034        )
13035        .unwrap();
13036        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13037        let tables = opened.load_payload_index_tables().unwrap();
13038        let first_envelope = tables.envelopes.values().next().unwrap();
13039        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
13040        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
13041        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13042        let payload_offset = crypto_end + first_envelope.first_block_index as usize * record_len;
13043
13044        let mut tampered = archive.bytes.clone();
13045        tampered[payload_offset + 16] ^= 0x01;
13046        let crc_offset = payload_offset + 16 + opened.crypto_header.block_size as usize;
13047        let crc = crc32c::crc32c(&tampered[payload_offset..crc_offset]);
13048        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
13049
13050        let tampered_opened = open_archive(&tampered, &master_key()).unwrap();
13051        assert!(tampered_opened.fast_verify_defers_payload_semantics());
13052        tampered_opened.verify_content_fast().unwrap();
13053        assert!(tampered_opened.verify_content().is_err());
13054    }
13055
13056    #[test]
13057    fn fast_verify_root_auth_archive_requires_full_root_auth_scan() {
13058        let archive = write_archive_with_root_auth(
13059            &[RegularFile::new("signed.txt", b"root-auth payload")],
13060            &master_key(),
13061            single_stream_options(),
13062            RootAuthWriterConfig {
13063                authenticator_id: 0x7777,
13064                signer_identity_type: 1,
13065                signer_identity: b"test signer",
13066                authenticator_value_length: 32,
13067            },
13068            |request| Ok(request.archive_root.to_vec()),
13069        )
13070        .unwrap();
13071
13072        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13073        assert!(!opened.fast_verify_defers_payload_semantics());
13074        assert!(matches!(
13075            opened.verify_content_fast().unwrap().mode,
13076            ContentVerificationMode::Fast
13077        ));
13078    }
13079
13080    #[test]
13081    fn fast_verify_dictionary_archive_does_not_defer_payload_semantics() {
13082        let archive = write_archive_with_dictionary(
13083            &[RegularFile::new("dict.txt", b"dictionary payload")],
13084            &master_key(),
13085            single_stream_options(),
13086            dictionary(),
13087        )
13088        .unwrap();
13089
13090        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13091        assert!(!opened.fast_verify_defers_payload_semantics());
13092    }
13093
13094    #[test]
13095    fn fast_verify_encrypted_archive_does_not_defer_payload_semantics() {
13096        let options = WriterOptions {
13097            aead_algo: AeadAlgo::AesGcmSiv256,
13098            volume_loss_tolerance: 0,
13099            bit_rot_buffer_pct: 0,
13100            ..single_stream_options()
13101        };
13102        let archive = write_archive(
13103            &[RegularFile::new("payload.txt", b"encrypted payload")],
13104            &master_key(),
13105            options,
13106        )
13107        .unwrap();
13108
13109        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13110        assert!(!opened.fast_verify_defers_payload_semantics());
13111    }
13112
13113    #[test]
13114    fn fast_verify_repair_archive_does_not_defer_payload_semantics() {
13115        let options = WriterOptions {
13116            fec_parity_shards: 2,
13117            volume_loss_tolerance: 0,
13118            bit_rot_buffer_pct: 0,
13119            ..single_stream_options()
13120        };
13121        let archive = write_archive(
13122            &[RegularFile::new("payload.txt", b"payload for repair")],
13123            &master_key(),
13124            options,
13125        )
13126        .unwrap();
13127
13128        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13129        assert!(!opened.fast_verify_defers_payload_semantics());
13130    }
13131
13132    #[test]
13133    fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
13134        let archive = write_archive_with_dictionary(
13135            &[RegularFile::new(
13136                "dir/dict.txt",
13137                b"common words common words dictionary payload",
13138            )],
13139            &master_key(),
13140            single_stream_options(),
13141            dictionary(),
13142        )
13143        .unwrap();
13144        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13145
13146        assert_eq!(opened.crypto_header.has_dictionary, 1);
13147        assert!(opened.index_root.header.dictionary_data_block_count > 0);
13148        assert_eq!(
13149            opened.list_files().unwrap(),
13150            vec![ArchiveEntry {
13151                path: "dir/dict.txt".to_string(),
13152                file_data_size: 44,
13153                kind: TarEntryKind::Regular,
13154                mode: 0o644,
13155                mtime: 0,
13156                diagnostics: Vec::new(),
13157            }]
13158        );
13159        assert_eq!(
13160            opened.extract_file("dir/dict.txt").unwrap(),
13161            Some(b"common words common words dictionary payload".to_vec())
13162        );
13163        opened.verify().unwrap();
13164    }
13165
13166    #[test]
13167    fn dictionary_object_tamper_fails_before_payload_decompression() {
13168        let archive = write_archive_with_dictionary(
13169            &[RegularFile::new(
13170                "dir/dict.txt",
13171                b"common words common words dictionary payload",
13172            )],
13173            &master_key(),
13174            single_stream_options(),
13175            dictionary(),
13176        )
13177        .unwrap();
13178        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13179        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
13180        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
13181        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13182        let dictionary_offset =
13183            crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
13184
13185        let mut tampered = archive.bytes.clone();
13186        tampered[dictionary_offset + 16] ^= 0x01;
13187        let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
13188        let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
13189        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
13190
13191        assert_eq!(
13192            open_archive(&tampered, &master_key()).unwrap_err(),
13193            FormatError::AeadFailure
13194        );
13195    }
13196
13197    #[test]
13198    fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
13199        let archive = write_archive_with_dictionary(
13200            &[RegularFile::new(
13201                "dict-sidecar.txt",
13202                b"common words common words sidecar payload",
13203            )],
13204            &master_key(),
13205            single_stream_options(),
13206            dictionary(),
13207        )
13208        .unwrap();
13209        let opened = open_non_seekable_archive(
13210            &archive.bytes,
13211            &master_key(),
13212            Some(&archive.bootstrap_sidecar),
13213        )
13214        .unwrap();
13215
13216        assert_eq!(
13217            opened.extract_file("dict-sidecar.txt").unwrap(),
13218            Some(b"common words common words sidecar payload".to_vec())
13219        );
13220        opened.verify().unwrap();
13221    }
13222
13223    #[test]
13224    fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
13225        let archive = write_archive(
13226            &[RegularFile::new(
13227                "sidecar-terminal.txt",
13228                b"sidecar authority",
13229            )],
13230            &master_key(),
13231            single_stream_options(),
13232        )
13233        .unwrap();
13234        let mut corrupted = archive.bytes.clone();
13235        corrupt_v41_terminal_recovery(&mut corrupted);
13236        assert!(open_archive(&corrupted, &master_key()).is_err());
13237
13238        let opened =
13239            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
13240                .unwrap();
13241
13242        assert!(opened.volume_trailer.is_none());
13243        assert_eq!(
13244            opened.extract_file("sidecar-terminal.txt").unwrap(),
13245            Some(b"sidecar authority".to_vec())
13246        );
13247        opened.verify().unwrap();
13248    }
13249
13250    #[test]
13251    fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
13252        let archive = write_archive_with_dictionary(
13253            &[RegularFile::new(
13254                "dict-no-terminal.txt",
13255                b"common words common words without terminal",
13256            )],
13257            &master_key(),
13258            single_stream_options(),
13259            dictionary(),
13260        )
13261        .unwrap();
13262        let terminal_offset = terminal_material_offset(&archive.bytes);
13263        let truncated = archive.bytes[..terminal_offset].to_vec();
13264        assert!(open_archive(&truncated, &master_key()).is_err());
13265
13266        let opened =
13267            open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
13268                .unwrap();
13269
13270        assert!(opened.volume_trailer.is_none());
13271        assert_eq!(
13272            opened.extract_file("dict-no-terminal.txt").unwrap(),
13273            Some(b"common words common words without terminal".to_vec())
13274        );
13275        opened.verify().unwrap();
13276    }
13277
13278    #[test]
13279    fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
13280        let archive = write_archive(
13281            &[RegularFile::new(
13282                "sidecar-erasure.txt",
13283                b"repair through sidecar",
13284            )],
13285            &master_key(),
13286            single_stream_options(),
13287        )
13288        .unwrap();
13289        let mut corrupted = archive.bytes.clone();
13290        corrupt_first_block_record_payload(&mut corrupted);
13291
13292        let opened = open_archive_with_bootstrap_sidecar(
13293            &corrupted,
13294            &archive.bootstrap_sidecar,
13295            &master_key(),
13296        )
13297        .unwrap();
13298        assert_eq!(
13299            opened.extract_file("sidecar-erasure.txt").unwrap(),
13300            Some(b"repair through sidecar".to_vec())
13301        );
13302    }
13303
13304    #[test]
13305    fn extraction_rejects_logical_payload_above_total_size_cap() {
13306        let archive = write_archive(
13307            &[RegularFile::new("cap.txt", b"payload")],
13308            &master_key(),
13309            single_stream_options(),
13310        )
13311        .unwrap();
13312        let options = ReaderOptions {
13313            max_total_extraction_size: 3,
13314            ..ReaderOptions::default()
13315        };
13316        let opened =
13317            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13318
13319        assert_eq!(
13320            opened.extract_file("cap.txt").unwrap_err(),
13321            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13322        );
13323    }
13324
13325    #[test]
13326    fn verify_does_not_apply_extraction_payload_cap() {
13327        let archive = write_archive(
13328            &[RegularFile::new("verify-cap.txt", b"payload")],
13329            &master_key(),
13330            single_stream_options(),
13331        )
13332        .unwrap();
13333        let options = ReaderOptions {
13334            max_total_extraction_size: 3,
13335            ..ReaderOptions::default()
13336        };
13337        let opened =
13338            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13339
13340        opened.verify().unwrap();
13341        assert_eq!(
13342            opened.extract_file("verify-cap.txt").unwrap_err(),
13343            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13344        );
13345    }
13346
13347    #[test]
13348    fn verify_streams_past_legacy_in_memory_tar_cap() {
13349        let data = vec![0x5a; 4096];
13350        let archive = write_archive(
13351            &[RegularFile::new("verify-large.txt", &data)],
13352            &master_key(),
13353            single_stream_options(),
13354        )
13355        .unwrap();
13356        let options = ReaderOptions {
13357            max_verify_tar_size: 1,
13358            ..ReaderOptions::default()
13359        };
13360        let opened =
13361            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13362
13363        opened.verify().unwrap();
13364    }
13365
13366    #[test]
13367    fn dictionary_sidecar_requires_dictionary_record_section() {
13368        let archive = write_archive_with_dictionary(
13369            &[RegularFile::new("dict-missing.txt", b"common words")],
13370            &master_key(),
13371            single_stream_options(),
13372            dictionary(),
13373        )
13374        .unwrap();
13375        let header = BootstrapSidecarHeader::parse(
13376            &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
13377        )
13378        .unwrap();
13379        let mut missing_dictionary =
13380            archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
13381        rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
13382            header.flags &= !0x04;
13383            header.dictionary_records_offset = 0;
13384            header.dictionary_records_length = 0;
13385        });
13386
13387        assert_eq!(
13388            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
13389                .unwrap_err(),
13390            FormatError::ReaderUnsupported("dictionary bootstrap required")
13391        );
13392    }
13393
13394    #[test]
13395    fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
13396        let archive = write_archive_with_dictionary(
13397            &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
13398            &master_key(),
13399            single_stream_options(),
13400            dictionary(),
13401        )
13402        .unwrap();
13403
13404        let mut wrong_kind = archive.bootstrap_sidecar.clone();
13405        mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
13406            record.kind = BlockKind::IndexRootData;
13407        });
13408        assert_eq!(
13409            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
13410                .unwrap_err(),
13411            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
13412        );
13413
13414        let mut wrong_last = archive.bootstrap_sidecar.clone();
13415        mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
13416            record.flags = 0;
13417        });
13418        assert_eq!(
13419            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
13420                .unwrap_err(),
13421            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
13422        );
13423    }
13424
13425    #[test]
13426    fn non_seekable_random_access_requires_sidecar() {
13427        let archive = write_archive(
13428            &[RegularFile::new("file.txt", b"payload")],
13429            &master_key(),
13430            single_stream_options(),
13431        )
13432        .unwrap();
13433
13434        assert_eq!(
13435            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13436            FormatError::ReaderUnsupported(
13437                "non-seekable random access requires a bootstrap sidecar"
13438            )
13439        );
13440        assert!(open_non_seekable_archive(
13441            &archive.bytes,
13442            &master_key(),
13443            Some(&archive.bootstrap_sidecar)
13444        )
13445        .is_ok());
13446    }
13447
13448    #[test]
13449    fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
13450        let archive = write_archive(
13451            &[RegularFile::new("sparse.txt", b"sparse sidecar")],
13452            &master_key(),
13453            single_stream_options(),
13454        )
13455        .unwrap();
13456        let index_root_only = sparse_bootstrap_sidecar(
13457            &archive.bootstrap_sidecar,
13458            &master_key(),
13459            false,
13460            true,
13461            false,
13462        );
13463
13464        assert_eq!(
13465            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
13466                .unwrap_err(),
13467            FormatError::ReaderUnsupported(
13468                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13469            )
13470        );
13471    }
13472
13473    #[test]
13474    fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
13475        let archive = write_archive(
13476            &[RegularFile::new("sparse-index.txt", b"recover index root")],
13477            &master_key(),
13478            single_stream_options(),
13479        )
13480        .unwrap();
13481        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13482        let mut corrupted = archive.bytes.clone();
13483        corrupt_object_extent_records(
13484            &mut corrupted,
13485            index_root_extent_from_manifest(&opened.manifest_footer),
13486        );
13487        assert!(open_archive(&corrupted, &master_key()).is_err());
13488
13489        let index_root_only = sparse_bootstrap_sidecar(
13490            &archive.bootstrap_sidecar,
13491            &master_key(),
13492            false,
13493            true,
13494            false,
13495        );
13496        let recovered =
13497            open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
13498                .unwrap();
13499
13500        assert_eq!(
13501            recovered.extract_file("sparse-index.txt").unwrap(),
13502            Some(b"recover index root".to_vec())
13503        );
13504        recovered.verify().unwrap();
13505    }
13506
13507    #[test]
13508    fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
13509        let archive = write_archive_with_dictionary(
13510            &[RegularFile::new(
13511                "sparse-dict.txt",
13512                b"common words common words sparse dictionary",
13513            )],
13514            &master_key(),
13515            single_stream_options(),
13516            dictionary(),
13517        )
13518        .unwrap();
13519        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13520        let mut corrupted = archive.bytes.clone();
13521        corrupt_object_extent_records(
13522            &mut corrupted,
13523            dictionary_extent_from_index_root(&opened.index_root).unwrap(),
13524        );
13525        assert!(open_archive(&corrupted, &master_key()).is_err());
13526
13527        let dictionary_only = sparse_bootstrap_sidecar(
13528            &archive.bootstrap_sidecar,
13529            &master_key(),
13530            false,
13531            false,
13532            true,
13533        );
13534        assert_eq!(
13535            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
13536                .unwrap_err(),
13537            FormatError::ReaderUnsupported(
13538                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13539            )
13540        );
13541
13542        let recovered =
13543            open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
13544                .unwrap();
13545        assert_eq!(
13546            recovered.extract_file("sparse-dict.txt").unwrap(),
13547            Some(b"common words common words sparse dictionary".to_vec())
13548        );
13549        recovered.verify().unwrap();
13550    }
13551
13552    #[test]
13553    fn sequential_extracts_dictionary_free_tar_stream() {
13554        let archive = write_archive(
13555            &[RegularFile::new("seq.txt", b"streaming")],
13556            &master_key(),
13557            single_stream_options(),
13558        )
13559        .unwrap();
13560
13561        let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
13562        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13563        assert_eq!(member.path, b"seq.txt");
13564        assert_eq!(member.data, b"streaming");
13565    }
13566
13567    #[test]
13568    fn sequential_rejects_logical_payload_above_total_size_cap() {
13569        let archive = write_archive(
13570            &[RegularFile::new("seq-cap.txt", b"payload")],
13571            &master_key(),
13572            single_stream_options(),
13573        )
13574        .unwrap();
13575        let options = ReaderOptions {
13576            max_total_extraction_size: 3,
13577            ..ReaderOptions::default()
13578        };
13579
13580        assert_eq!(
13581            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13582                .unwrap_err(),
13583            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13584        );
13585    }
13586
13587    #[test]
13588    fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
13589        let archive = write_archive(
13590            &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
13591            &master_key(),
13592            single_stream_options(),
13593        )
13594        .unwrap();
13595        let options = ReaderOptions {
13596            max_verify_tar_size: 512,
13597            ..ReaderOptions::default()
13598        };
13599
13600        assert_eq!(
13601            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13602                .unwrap_err(),
13603            FormatError::ReaderUnsupported(
13604                "sequential tar stream exceeds configured verification cap"
13605            )
13606        );
13607    }
13608
13609    #[test]
13610    fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
13611        let archive = write_archive(
13612            &[RegularFile::new("seq-erasure.txt", b"stream repair")],
13613            &master_key(),
13614            single_stream_options(),
13615        )
13616        .unwrap();
13617        let mut corrupted = archive.bytes;
13618        corrupt_first_block_record_payload(&mut corrupted);
13619
13620        let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
13621        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13622        assert_eq!(member.path, b"seq-erasure.txt");
13623        assert_eq!(member.data, b"stream repair");
13624    }
13625
13626    #[test]
13627    fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
13628        let archive = write_archive(
13629            &[RegularFile::new("seq-no-parity.txt", b"no repair")],
13630            &master_key(),
13631            WriterOptions {
13632                bit_rot_buffer_pct: 0,
13633                fec_parity_shards: 0,
13634                index_fec_parity_shards: 0,
13635                index_root_fec_parity_shards: 0,
13636                ..single_stream_options()
13637            },
13638        )
13639        .unwrap();
13640        let mut corrupted = archive.bytes;
13641        corrupt_first_block_record_payload(&mut corrupted);
13642
13643        assert_eq!(
13644            sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
13645            FormatError::BadCrc {
13646                structure: "BlockRecord"
13647            }
13648        );
13649    }
13650
13651    #[test]
13652    fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
13653        let archive = write_archive(
13654            &[RegularFile::new(
13655                "seq.txt",
13656                b"payload must not be returned after terminal auth failure",
13657            )],
13658            &master_key(),
13659            single_stream_options(),
13660        )
13661        .unwrap();
13662        let mut corrupted = archive.bytes;
13663        corrupt_v41_terminal_recovery(&mut corrupted);
13664
13665        match sequential_extract_tar_stream(&corrupted, &master_key()) {
13666            Ok(bytes) => panic!(
13667                "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
13668                bytes.len()
13669            ),
13670            Err(err) => assert_eq!(
13671                err,
13672                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13673            ),
13674        }
13675    }
13676
13677    #[test]
13678    fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
13679        let archive = write_archive_with_dictionary(
13680            &[RegularFile::new(
13681                "seq-dict.txt",
13682                b"common words common words dictionary payload",
13683            )],
13684            &master_key(),
13685            single_stream_options(),
13686            b"common words dictionary",
13687        )
13688        .unwrap();
13689
13690        match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
13691            Ok(bytes) => panic!(
13692                "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
13693                bytes.len()
13694            ),
13695            Err(err) => assert_eq!(
13696                err,
13697                FormatError::ReaderUnsupported(
13698                    "dictionary bootstrap required for non-seekable sequential extraction"
13699                )
13700            ),
13701        }
13702    }
13703
13704    #[test]
13705    fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
13706        let archive = write_archive_with_dictionary(
13707            &[RegularFile::new(
13708                "seq-dict-open.txt",
13709                b"common words common words bootstrap required",
13710            )],
13711            &master_key(),
13712            single_stream_options(),
13713            b"common words bootstrap",
13714        )
13715        .unwrap();
13716
13717        assert_eq!(
13718            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13719            FormatError::ReaderUnsupported(
13720                "non-seekable random access requires a bootstrap sidecar"
13721            )
13722        );
13723    }
13724
13725    #[test]
13726    fn sequential_zstd_stream_rejects_skippable_frame_segments() {
13727        let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
13728        let mut output = Vec::new();
13729
13730        assert_eq!(
13731            decode_concatenated_zstd_frames_with_cap(
13732                &skippable,
13733                None,
13734                &mut output,
13735                usize::MAX,
13736                None,
13737            )
13738            .unwrap_err(),
13739            FormatError::NotStandardZstdFrame
13740        );
13741        assert!(output.is_empty());
13742    }
13743
13744    #[test]
13745    fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
13746        let archive = write_archive(
13747            &[RegularFile::new("live.txt", b"stream verify")],
13748            &master_key(),
13749            single_stream_options(),
13750        )
13751        .unwrap();
13752
13753        let report =
13754            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13755
13756        assert_eq!(report.file_count, 1);
13757        assert_eq!(report.total_volumes, 1);
13758        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13759        assert!(report.payload_block_count > 0);
13760    }
13761
13762    #[test]
13763    fn live_non_seekable_verify_stream_accepts_recipientwrap_archive() {
13764        let master = master_key();
13765        let archive = write_archive_with_recipient_wrap_records(
13766            &[RegularFile::new(
13767                "wrapped-live.txt",
13768                b"stream recipient verify",
13769            )],
13770            &master,
13771            single_stream_options(),
13772            vec![recipient_wrap_test_record()],
13773        )
13774        .unwrap();
13775
13776        let mut called = false;
13777        let report = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13778            std::io::Cursor::new(archive.bytes),
13779            |context| {
13780                called = true;
13781                assert_eq!(
13782                    context.archive_identity.volume_format_rev,
13783                    VOLUME_FORMAT_REV_44
13784                );
13785                assert_eq!(context.record.profile_id, 1);
13786                Ok(vec![master.0])
13787            },
13788            NonSeekableReaderOptions::default(),
13789        )
13790        .unwrap();
13791
13792        assert!(called);
13793        assert_eq!(report.file_count, 1);
13794        assert_eq!(report.total_volumes, 1);
13795        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13796    }
13797
13798    #[test]
13799    fn live_non_seekable_recipientwrap_resolver_rejects_unencrypted_archive() {
13800        let archive = write_archive_unencrypted(
13801            &[RegularFile::new("plain-live.txt", b"plaintext payload")],
13802            single_stream_options(),
13803        )
13804        .unwrap();
13805
13806        let mut called = false;
13807        let err = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13808            std::io::Cursor::new(archive.bytes),
13809            |_| {
13810                called = true;
13811                Ok(vec![master_key().0])
13812            },
13813            NonSeekableReaderOptions::default(),
13814        )
13815        .unwrap_err();
13816
13817        assert!(!called);
13818        assert_eq!(err, FormatError::KeyMaterialMismatch);
13819    }
13820
13821    #[test]
13822    fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
13823        let archive = write_archive(
13824            &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
13825            &master_key(),
13826            single_stream_options(),
13827        )
13828        .unwrap();
13829
13830        let report =
13831            verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
13832                .unwrap();
13833
13834        assert_eq!(report.file_count, 1);
13835        assert_eq!(report.tar_total_size % 512, 0);
13836    }
13837
13838    #[test]
13839    fn live_non_seekable_verify_stream_accepts_empty_archive() {
13840        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13841
13842        let report =
13843            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13844
13845        assert_eq!(report.file_count, 0);
13846        assert_eq!(report.payload_block_count, 0);
13847        assert_eq!(report.tar_total_size, 0);
13848    }
13849
13850    #[test]
13851    fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
13852        let archive = write_archive_with_dictionary(
13853            &[RegularFile::new(
13854                "live-dict.txt",
13855                b"common words common words dictionary payload",
13856            )],
13857            &master_key(),
13858            single_stream_options(),
13859            b"common words dictionary",
13860        )
13861        .unwrap();
13862
13863        assert_eq!(
13864            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
13865                .unwrap_err(),
13866            FormatError::ReaderUnsupported(
13867                "dictionary bootstrap required for non-seekable sequential verification"
13868            )
13869        );
13870    }
13871
13872    #[test]
13873    fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
13874        let archive = write_archive_with_dictionary(
13875            &[RegularFile::new(
13876                "live-dict-sidecar.txt",
13877                b"common words common words dictionary payload",
13878            )],
13879            &master_key(),
13880            single_stream_options(),
13881            b"common words dictionary",
13882        )
13883        .unwrap();
13884
13885        let report = verify_non_seekable_stream_with_bootstrap_sidecar(
13886            std::io::Cursor::new(archive.bytes),
13887            &archive.bootstrap_sidecar,
13888            &master_key(),
13889            NonSeekableReaderOptions::default(),
13890        )
13891        .unwrap();
13892
13893        assert_eq!(report.file_count, 1);
13894        assert_eq!(report.total_volumes, 1);
13895    }
13896
13897    #[test]
13898    fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
13899        let archive = write_archive(
13900            &[RegularFile::new("tail-cap.txt", b"payload")],
13901            &master_key(),
13902            single_stream_options(),
13903        )
13904        .unwrap();
13905        let options = NonSeekableReaderOptions {
13906            max_terminal_tail_size: 8,
13907            ..NonSeekableReaderOptions::default()
13908        };
13909
13910        assert_eq!(
13911            verify_non_seekable_stream_with_options(
13912                std::io::Cursor::new(archive.bytes),
13913                &master_key(),
13914                options
13915            )
13916            .unwrap_err(),
13917            FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
13918        );
13919    }
13920
13921    #[test]
13922    fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
13923        let archive = write_archive(
13924            &[RegularFile::new("metadata-cap.txt", b"payload")],
13925            &master_key(),
13926            single_stream_options(),
13927        )
13928        .unwrap();
13929        let options = NonSeekableReaderOptions {
13930            max_retained_metadata_bytes: 1,
13931            ..NonSeekableReaderOptions::default()
13932        };
13933
13934        assert_eq!(
13935            verify_non_seekable_stream_with_options(
13936                std::io::Cursor::new(archive.bytes),
13937                &master_key(),
13938                options
13939            )
13940            .unwrap_err(),
13941            FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
13942        );
13943    }
13944
13945    #[test]
13946    fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
13947        let archive = write_archive(
13948            &[RegularFile::new("metadata-erasure.txt", b"payload")],
13949            &master_key(),
13950            single_stream_options(),
13951        )
13952        .unwrap();
13953        let mut corrupted = archive.bytes;
13954        let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
13955        corrupt_block_record_payload_at_slot(&mut corrupted, slot);
13956
13957        let report =
13958            verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
13959
13960        assert_eq!(report.file_count, 1);
13961    }
13962
13963    #[test]
13964    fn live_non_seekable_verify_rejects_member_count_above_cap() {
13965        let archive = write_archive(
13966            &[RegularFile::new("member-cap.txt", b"payload")],
13967            &master_key(),
13968            single_stream_options(),
13969        )
13970        .unwrap();
13971        let options = NonSeekableReaderOptions {
13972            max_streamed_member_count: 0,
13973            ..NonSeekableReaderOptions::default()
13974        };
13975
13976        assert_eq!(
13977            verify_non_seekable_stream_with_options(
13978                std::io::Cursor::new(archive.bytes),
13979                &master_key(),
13980                options
13981            )
13982            .unwrap_err(),
13983            FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
13984        );
13985    }
13986
13987    #[test]
13988    fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
13989        let archive = write_archive(
13990            &[RegularFile::new("live-total-cap.txt", b"payload")],
13991            &master_key(),
13992            single_stream_options(),
13993        )
13994        .unwrap();
13995        let mut options = NonSeekableReaderOptions::default();
13996        options.reader.max_total_extraction_size = 3;
13997
13998        assert_eq!(
13999            verify_non_seekable_stream_with_options(
14000                std::io::Cursor::new(archive.bytes),
14001                &master_key(),
14002                options
14003            )
14004            .unwrap_err(),
14005            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14006        );
14007    }
14008
14009    #[test]
14010    fn live_non_seekable_verify_reports_root_auth_wire_only() {
14011        let archive = write_archive_with_root_auth(
14012            &[RegularFile::new("signed-live.txt", b"root-auth stream")],
14013            &master_key(),
14014            single_stream_options(),
14015            test_root_auth_config(),
14016            |request| Ok(test_root_auth_value(request)),
14017        )
14018        .unwrap();
14019
14020        let report =
14021            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
14022
14023        assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
14024    }
14025
14026    #[test]
14027    fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
14028        let archive = write_archive(
14029            &[
14030                RegularFile::new("alpha.txt", b"alpha"),
14031                RegularFile::new("nested/beta.txt", b"beta"),
14032            ],
14033            &master_key(),
14034            single_stream_options(),
14035        )
14036        .unwrap();
14037        let tmp = tempfile::tempdir().unwrap();
14038        let out = tmp.path().join("out");
14039
14040        let report = extract_non_seekable_stream_to_dir(
14041            std::io::Cursor::new(archive.bytes),
14042            &master_key(),
14043            &out,
14044            NonSeekableReaderOptions::default(),
14045            SafeExtractionOptions::default(),
14046        )
14047        .unwrap();
14048
14049        assert_eq!(report.verification.file_count, 2);
14050        assert_eq!(report.extracted_member_count, 2);
14051        assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
14052        assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
14053    }
14054
14055    #[test]
14056    fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
14057        let archive = write_archive(
14058            &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
14059            &master_key(),
14060            single_stream_options(),
14061        )
14062        .unwrap();
14063        let tmp = tempfile::tempdir().unwrap();
14064        let out = tmp.path().join("out");
14065
14066        extract_non_seekable_stream_to_dir(
14067            ChunkedReader::new(archive.bytes, 1),
14068            &master_key(),
14069            &out,
14070            NonSeekableReaderOptions::default(),
14071            SafeExtractionOptions::default(),
14072        )
14073        .unwrap();
14074
14075        assert_eq!(
14076            fs::read(out.join("tiny-extract.txt")).unwrap(),
14077            b"chunked extraction"
14078        );
14079    }
14080
14081    #[test]
14082    fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
14083        let archive = write_archive(
14084            &[RegularFile::new("late-fail.txt", b"must remain staged")],
14085            &master_key(),
14086            single_stream_options(),
14087        )
14088        .unwrap();
14089        let mut corrupted = archive.bytes;
14090        corrupt_v41_terminal_recovery(&mut corrupted);
14091        let tmp = tempfile::tempdir().unwrap();
14092        let out = tmp.path().join("out");
14093
14094        match extract_non_seekable_stream_to_dir(
14095            std::io::Cursor::new(corrupted),
14096            &master_key(),
14097            &out,
14098            NonSeekableReaderOptions::default(),
14099            SafeExtractionOptions::default(),
14100        )
14101        .unwrap_err()
14102        {
14103            ExtractError::Format(err) => assert_eq!(
14104                err,
14105                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
14106            ),
14107            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
14108        }
14109        assert!(!out.exists());
14110    }
14111
14112    #[test]
14113    fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
14114        let archive = write_archive(
14115            &[RegularFile::new("same.txt", b"new")],
14116            &master_key(),
14117            single_stream_options(),
14118        )
14119        .unwrap();
14120        let tmp = tempfile::tempdir().unwrap();
14121        let out = tmp.path().join("out");
14122        fs::create_dir(&out).unwrap();
14123        fs::write(out.join("same.txt"), b"old").unwrap();
14124
14125        match extract_non_seekable_stream_to_dir(
14126            std::io::Cursor::new(archive.bytes.clone()),
14127            &master_key(),
14128            &out,
14129            NonSeekableReaderOptions::default(),
14130            SafeExtractionOptions::default(),
14131        )
14132        .unwrap_err()
14133        {
14134            ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
14135            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
14136        }
14137        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
14138
14139        extract_non_seekable_stream_to_dir(
14140            std::io::Cursor::new(archive.bytes),
14141            &master_key(),
14142            &out,
14143            NonSeekableReaderOptions::default(),
14144            SafeExtractionOptions {
14145                overwrite_existing: true,
14146            },
14147        )
14148        .unwrap();
14149        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
14150    }
14151
14152    #[test]
14153    fn live_non_seekable_list_stream_matches_seekable_final_view() {
14154        let archive = write_archive(
14155            &[
14156                RegularFile::new("a.txt", b"a"),
14157                RegularFile::new("b.txt", b"bb"),
14158            ],
14159            &master_key(),
14160            single_stream_options(),
14161        )
14162        .unwrap();
14163        let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
14164        let expected = seekable.list_files().unwrap();
14165
14166        let report = list_non_seekable_stream(
14167            std::io::Cursor::new(archive.bytes),
14168            &master_key(),
14169            NonSeekableReaderOptions::default(),
14170        )
14171        .unwrap();
14172
14173        assert_eq!(report.verification.file_count, 2);
14174        assert_eq!(report.entries, expected);
14175    }
14176
14177    #[test]
14178    fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
14179        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14180        let mut bad_flags = archive.bootstrap_sidecar.clone();
14181        rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
14182            header.flags |= 0x08;
14183        });
14184        assert_eq!(
14185            open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
14186                .unwrap_err(),
14187            FormatError::UnknownBootstrapSidecarFlags(0x0b)
14188        );
14189
14190        let mut trailing = archive.bootstrap_sidecar.clone();
14191        trailing.push(0);
14192        assert_eq!(
14193            open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
14194                .unwrap_err(),
14195            FormatError::NonCanonicalBootstrapSidecarLayout
14196        );
14197    }
14198
14199    #[test]
14200    fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
14201        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14202        let mut wrong_volume = archive.bootstrap_sidecar.clone();
14203        mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
14204            footer.volume_index = 1;
14205        });
14206        assert_eq!(
14207            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
14208                .unwrap_err(),
14209            FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
14210        );
14211
14212        let mut non_authoritative = archive.bootstrap_sidecar.clone();
14213        mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
14214            footer.is_authoritative = 0;
14215        });
14216        assert_eq!(
14217            open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
14218                .unwrap_err(),
14219            FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
14220        );
14221    }
14222
14223    #[test]
14224    fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
14225        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14226        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14227        let crypto_start = volume_header.crypto_header_offset as usize;
14228        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14229        let crypto_header = CryptoHeader::parse(
14230            &archive.bytes[crypto_start..crypto_end],
14231            volume_header.crypto_header_length,
14232        )
14233        .unwrap();
14234        let subkeys = Subkeys::derive(
14235            &master_key(),
14236            &volume_header.archive_uuid,
14237            &volume_header.session_id,
14238        )
14239        .unwrap();
14240        let mut opened_header = volume_header;
14241        opened_header.volume_index = 1;
14242
14243        let parsed = parse_bootstrap_sidecar(
14244            &archive.bootstrap_sidecar,
14245            &opened_header,
14246            &crypto_header.fixed,
14247            &subkeys,
14248        )
14249        .unwrap();
14250
14251        assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
14252    }
14253
14254    #[test]
14255    fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
14256        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14257        let mut conflicting = archive.bootstrap_sidecar.clone();
14258        mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
14259            footer.index_root_first_block += 1;
14260        });
14261
14262        assert_eq!(
14263            open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
14264                .unwrap_err(),
14265            FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
14266        );
14267    }
14268
14269    #[test]
14270    fn sidecar_size_cap_counts_only_present_sparse_sections() {
14271        let mut crypto_header = test_crypto_header();
14272        crypto_header.has_dictionary = 1;
14273        crypto_header.index_root_fec_data_shards = 1;
14274        crypto_header.index_root_fec_parity_shards = 0;
14275        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14276        let header = BootstrapSidecarHeader {
14277            archive_uuid: [0x31; 16],
14278            session_id: [0x42; 16],
14279            flags: 0x04,
14280            manifest_footer_offset: 0,
14281            manifest_footer_length: 0,
14282            index_root_records_offset: 0,
14283            index_root_records_length: 0,
14284            dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14285            dictionary_records_length: record_len,
14286            sidecar_hmac: [0u8; 32],
14287            header_crc32c: 0,
14288        };
14289
14290        validate_sidecar_size_cap(
14291            &header,
14292            &crypto_header,
14293            BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
14294        )
14295        .unwrap();
14296        assert_eq!(
14297            validate_sidecar_size_cap(
14298                &header,
14299                &crypto_header,
14300                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
14301            )
14302            .unwrap_err(),
14303            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14304        );
14305    }
14306
14307    #[test]
14308    fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
14309        let mut crypto_header = test_crypto_header();
14310        crypto_header.index_root_fec_data_shards = 1;
14311        crypto_header.index_root_fec_parity_shards = 0;
14312        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14313        let header = BootstrapSidecarHeader {
14314            archive_uuid: [0x31; 16],
14315            session_id: [0x42; 16],
14316            flags: 0x02,
14317            manifest_footer_offset: 0,
14318            manifest_footer_length: 0,
14319            index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14320            index_root_records_length: record_len * 2,
14321            dictionary_records_offset: 0,
14322            dictionary_records_length: 0,
14323            sidecar_hmac: [0u8; 32],
14324            header_crc32c: 0,
14325        };
14326
14327        assert_eq!(
14328            validate_sidecar_size_cap(
14329                &header,
14330                &crypto_header,
14331                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
14332            )
14333            .unwrap_err(),
14334            FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
14335        );
14336    }
14337
14338    #[test]
14339    fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
14340        let mut crypto_header = test_crypto_header();
14341        crypto_header.block_size = u32::MAX;
14342        crypto_header.index_root_fec_data_shards = u16::MAX;
14343        crypto_header.index_root_fec_parity_shards = u16::MAX;
14344        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14345        let max_records = crypto_header.index_root_fec_data_shards as u64
14346            + crypto_header.index_root_fec_parity_shards as u64;
14347        let max_section_len = max_records * record_len;
14348        let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
14349            + MANIFEST_FOOTER_LEN as u64
14350            + max_section_len
14351            + max_section_len;
14352        let header = BootstrapSidecarHeader {
14353            archive_uuid: [0x31; 16],
14354            session_id: [0x42; 16],
14355            flags: 0x01 | 0x02 | 0x04,
14356            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14357            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14358            index_root_records_offset: 0,
14359            index_root_records_length: max_section_len,
14360            dictionary_records_offset: 0,
14361            dictionary_records_length: max_section_len,
14362            sidecar_hmac: [0u8; 32],
14363            header_crc32c: 0,
14364        };
14365
14366        validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
14367        assert_eq!(
14368            validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
14369            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14370        );
14371    }
14372
14373    #[test]
14374    fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
14375        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14376        let mut with_dictionary = archive.bootstrap_sidecar.clone();
14377        let header =
14378            BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
14379                .unwrap();
14380        let record_len = sidecar_record_len(&with_dictionary);
14381        let first_record = header.index_root_records_offset as usize;
14382        let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
14383        let dictionary_offset = with_dictionary.len() as u64;
14384        with_dictionary.extend_from_slice(&copied_record);
14385        rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
14386            header.flags |= 0x04;
14387            header.dictionary_records_offset = dictionary_offset;
14388            header.dictionary_records_length = record_len as u64;
14389        });
14390
14391        assert_eq!(
14392            open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
14393                .unwrap_err(),
14394            FormatError::InvalidArchive(
14395                "bootstrap sidecar has dictionary records while has_dictionary is false"
14396            )
14397        );
14398    }
14399
14400    #[test]
14401    fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
14402        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14403        let mut missing = archive.bootstrap_sidecar.clone();
14404        let record_len = sidecar_record_len(&missing);
14405        let new_len = missing.len() - record_len;
14406        missing.truncate(new_len);
14407        rewrite_sidecar_header(&mut missing, &master_key(), |header| {
14408            header.index_root_records_length -= record_len as u64;
14409        });
14410        assert_eq!(
14411            open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
14412                .unwrap_err(),
14413            FormatError::InvalidArchive(
14414                "sidecar BlockRecord section does not match declared extent"
14415            )
14416        );
14417
14418        let mut duplicate = archive.bootstrap_sidecar.clone();
14419        mutate_sidecar_index_record(&mut duplicate, 1, |record| {
14420            record.block_index -= 1;
14421        });
14422        assert_eq!(
14423            open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
14424                .unwrap_err(),
14425            FormatError::InvalidArchive(
14426                "sidecar BlockRecord section has missing or duplicate blocks"
14427            )
14428        );
14429
14430        let mut misordered = archive.bootstrap_sidecar.clone();
14431        swap_sidecar_index_records(&mut misordered, 0, 1);
14432        assert_eq!(
14433            open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
14434                .unwrap_err(),
14435            FormatError::InvalidArchive(
14436                "sidecar BlockRecord section has missing or duplicate blocks"
14437            )
14438        );
14439
14440        let mut wrong_kind = archive.bootstrap_sidecar.clone();
14441        mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
14442            record.kind = BlockKind::PayloadData;
14443        });
14444        assert_eq!(
14445            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
14446                .unwrap_err(),
14447            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
14448        );
14449
14450        let mut wrong_last = archive.bootstrap_sidecar.clone();
14451        mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
14452            record.flags = 0;
14453        });
14454        assert_eq!(
14455            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
14456                .unwrap_err(),
14457            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
14458        );
14459    }
14460
14461    #[test]
14462    fn verify_helper_rejects_envelope_frame_coverage_gap() {
14463        let frames = BTreeMap::from([(
14464            0,
14465            FrameEntry {
14466                frame_index: 0,
14467                envelope_index: 0,
14468                offset_in_envelope: 0,
14469                compressed_size: 10,
14470                decompressed_size: 512,
14471                flags: 0,
14472                tar_stream_offset: 0,
14473            },
14474        )]);
14475        let envelopes = BTreeMap::from([(
14476            0,
14477            EnvelopeEntry {
14478                envelope_index: 0,
14479                first_block_index: 0,
14480                data_block_count: 1,
14481                parity_block_count: 1,
14482                encrypted_size: 4096,
14483                plaintext_size: 11,
14484                first_frame_index: 0,
14485                frame_count: 1,
14486            },
14487        )]);
14488
14489        assert_eq!(
14490            validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
14491            FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
14492        );
14493    }
14494
14495    #[test]
14496    fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
14497        assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
14498        assert_eq!(
14499            validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
14500            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14501        );
14502        assert_eq!(
14503            validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
14504            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14505        );
14506    }
14507
14508    #[test]
14509    fn verify_rejects_authenticated_content_hash_mismatch() {
14510        let options = WriterOptions {
14511            index_root_fec_parity_shards: 0,
14512            ..single_stream_options()
14513        };
14514        let archive = write_archive(
14515            &[RegularFile::new("content-hash.txt", b"hash covered")],
14516            &master_key(),
14517            options,
14518        )
14519        .unwrap();
14520        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14521
14522        let mut root = opened.index_root.clone();
14523        root.header.content_sha256 = [0xa5; 32];
14524        let root_plaintext = root.to_bytes();
14525        IndexRoot::parse(
14526            &root_plaintext,
14527            false,
14528            metadata_limits(&opened.crypto_header),
14529        )
14530        .unwrap();
14531        assert_eq!(
14532            root_plaintext.len() as u32,
14533            opened.manifest_footer.index_root_decompressed_size
14534        );
14535
14536        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14537        let mut next_block_index = opened.manifest_footer.index_root_first_block;
14538        let replacement = encrypt_test_object(
14539            &compressed_root,
14540            &opened.subkeys.index_root_key,
14541            &opened.subkeys.index_nonce_seed,
14542            b"idxroot",
14543            0,
14544            BlockKind::IndexRootData,
14545            &mut next_block_index,
14546            &opened.crypto_header,
14547            &opened.volume_header,
14548        );
14549        assert_eq!(
14550            replacement.extent.data_block_count,
14551            opened.manifest_footer.index_root_data_block_count
14552        );
14553        assert_eq!(
14554            replacement.extent.encrypted_size,
14555            opened.manifest_footer.index_root_encrypted_size
14556        );
14557
14558        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14559        let crypto_end = volume_header.crypto_header_offset as usize
14560            + volume_header.crypto_header_length as usize;
14561        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14562        let mut malformed = archive.bytes.clone();
14563        for record in replacement.records {
14564            let offset = crypto_end + record.block_index as usize * record_len;
14565            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14566        }
14567
14568        let reopened = open_archive(&malformed, &master_key()).unwrap();
14569        assert_eq!(
14570            reopened.verify().unwrap_err(),
14571            FormatError::InvalidArchive(
14572                "IndexRoot content_sha256 does not match decoded tar stream"
14573            )
14574        );
14575    }
14576
14577    #[test]
14578    fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
14579        let (mut path_mismatch, _) = multi_envelope_reader_fixture();
14580        rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
14581            path[0] = b'x';
14582        });
14583        assert_eq!(
14584            path_mismatch.verify().unwrap_err(),
14585            FormatError::InvalidArchive("tar member path does not match FileEntry path")
14586        );
14587
14588        let (mut size_mismatch, _) = multi_envelope_reader_fixture();
14589        rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
14590            file.file_data_size += 1;
14591        });
14592        assert_eq!(
14593            size_mismatch.verify().unwrap_err(),
14594            FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
14595        );
14596    }
14597
14598    #[test]
14599    fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
14600        let (mut opened, _) = multi_envelope_reader_fixture();
14601        let locating = opened.index_root.shards[0].clone();
14602        let mut duplicate = opened.load_index_shard(&locating).unwrap();
14603        duplicate.header.shard_index = 1;
14604        duplicate.frames[0].flags ^= 0x0000_0001;
14605        let duplicate_plaintext = duplicate.to_bytes();
14606        let mut next_block_index = opened
14607            .blocks
14608            .keys()
14609            .last()
14610            .copied()
14611            .map(|index| index + 1)
14612            .unwrap_or(0);
14613        let duplicate_object = encrypt_test_object(
14614            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14615            &opened.subkeys.index_shard_key,
14616            &opened.subkeys.index_nonce_seed,
14617            b"idxshard",
14618            1,
14619            BlockKind::IndexShardData,
14620            &mut next_block_index,
14621            &opened.crypto_header,
14622            &opened.volume_header,
14623        );
14624        insert_records(&mut opened.blocks, &duplicate_object.records);
14625        opened.index_root.shards.push(ShardEntry {
14626            shard_index: 1,
14627            first_block_index: duplicate_object.extent.first_block_index,
14628            data_block_count: duplicate_object.extent.data_block_count,
14629            parity_block_count: 0,
14630            encrypted_size: duplicate_object.extent.encrypted_size,
14631            decompressed_size: duplicate_plaintext.len() as u32,
14632            file_count: locating.file_count,
14633            first_path_hash: locating.first_path_hash,
14634            last_path_hash: locating.last_path_hash,
14635        });
14636        opened.index_root.header.file_count += locating.file_count as u64;
14637
14638        assert_eq!(
14639            opened.verify().unwrap_err(),
14640            FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
14641        );
14642    }
14643
14644    #[test]
14645    fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
14646        let (mut opened, _) = multi_envelope_reader_fixture();
14647        let locating = opened.index_root.shards[0].clone();
14648        let mut duplicate = opened.load_index_shard(&locating).unwrap();
14649        duplicate.header.shard_index = 1;
14650        duplicate.envelopes[0].first_block_index += 1;
14651        let duplicate_plaintext = duplicate.to_bytes();
14652        let mut next_block_index = opened
14653            .blocks
14654            .keys()
14655            .last()
14656            .copied()
14657            .map(|index| index + 1)
14658            .unwrap_or(0);
14659        let duplicate_object = encrypt_test_object(
14660            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14661            &opened.subkeys.index_shard_key,
14662            &opened.subkeys.index_nonce_seed,
14663            b"idxshard",
14664            1,
14665            BlockKind::IndexShardData,
14666            &mut next_block_index,
14667            &opened.crypto_header,
14668            &opened.volume_header,
14669        );
14670        insert_records(&mut opened.blocks, &duplicate_object.records);
14671        opened.index_root.shards.push(ShardEntry {
14672            shard_index: 1,
14673            first_block_index: duplicate_object.extent.first_block_index,
14674            data_block_count: duplicate_object.extent.data_block_count,
14675            parity_block_count: 0,
14676            encrypted_size: duplicate_object.extent.encrypted_size,
14677            decompressed_size: duplicate_plaintext.len() as u32,
14678            file_count: locating.file_count,
14679            first_path_hash: locating.first_path_hash,
14680            last_path_hash: locating.last_path_hash,
14681        });
14682        opened.index_root.header.file_count += locating.file_count as u64;
14683
14684        assert_eq!(
14685            opened.verify().unwrap_err(),
14686            FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
14687        );
14688    }
14689
14690    #[test]
14691    fn verify_rejects_non_contiguous_global_envelope_indexes() {
14692        let (mut opened, _) = multi_envelope_reader_fixture();
14693        replace_first_index_shard(&mut opened, |shard| {
14694            let frame = shard
14695                .frames
14696                .iter_mut()
14697                .find(|entry| entry.frame_index == 1)
14698                .unwrap();
14699            frame.envelope_index = 2;
14700
14701            let envelope = shard
14702                .envelopes
14703                .iter_mut()
14704                .find(|entry| entry.envelope_index == 1)
14705                .unwrap();
14706            envelope.envelope_index = 2;
14707        });
14708
14709        assert_eq!(
14710            opened.verify().unwrap_err(),
14711            FormatError::InvalidMetadata {
14712                structure: "EnvelopeEntry",
14713                reason: "global index coverage has a gap",
14714            }
14715        );
14716    }
14717
14718    #[test]
14719    fn verify_rejects_payload_object_extent_overlap() {
14720        let (mut opened, _) = multi_envelope_reader_fixture();
14721        replace_first_index_shard(&mut opened, |shard| {
14722            let first_block_index = shard.envelopes[0].first_block_index;
14723            shard.envelopes[1].first_block_index = first_block_index;
14724        });
14725
14726        assert_eq!(
14727            opened.verify().unwrap_err(),
14728            FormatError::InvalidArchive("encrypted object block ranges overlap")
14729        );
14730    }
14731
14732    #[test]
14733    fn verify_accepts_cross_shard_shared_envelope_frame_union() {
14734        let volume_header = test_volume_header();
14735        let crypto_header = test_crypto_header();
14736        let subkeys = Subkeys::derive(
14737            &master_key(),
14738            &volume_header.archive_uuid,
14739            &volume_header.session_id,
14740        )
14741        .unwrap();
14742        let mut next_block_index = 0u64;
14743        let mut blocks = BTreeMap::new();
14744
14745        let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
14746        let beta = test_member(b"beta.txt", b"beta cross shard\n");
14747        let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
14748        let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
14749        let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
14750        let envelope_plaintext =
14751            [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
14752        let payload = encrypt_test_object(
14753            &envelope_plaintext,
14754            &subkeys.enc_key,
14755            &subkeys.nonce_seed,
14756            b"envelope",
14757            0,
14758            BlockKind::PayloadData,
14759            &mut next_block_index,
14760            &crypto_header,
14761            &volume_header,
14762        );
14763        insert_records(&mut blocks, &payload.records);
14764
14765        let envelope = EnvelopeEntry {
14766            envelope_index: 0,
14767            first_block_index: payload.extent.first_block_index,
14768            data_block_count: payload.extent.data_block_count,
14769            parity_block_count: 0,
14770            encrypted_size: payload.extent.encrypted_size,
14771            plaintext_size: envelope_plaintext.len() as u32,
14772            first_frame_index: 0,
14773            frame_count: 2,
14774        };
14775        let frame0 = FrameEntry {
14776            frame_index: 0,
14777            envelope_index: 0,
14778            offset_in_envelope: 0,
14779            compressed_size: frame0_plaintext.len() as u32,
14780            decompressed_size: alpha.len() as u32,
14781            flags: 0x0000_0003,
14782            tar_stream_offset: 0,
14783        };
14784        let frame1 = FrameEntry {
14785            frame_index: 1,
14786            envelope_index: 0,
14787            offset_in_envelope: frame0_plaintext.len() as u32,
14788            compressed_size: frame1_plaintext.len() as u32,
14789            decompressed_size: beta.len() as u32,
14790            flags: 0x0000_0003,
14791            tar_stream_offset: alpha.len() as u64,
14792        };
14793
14794        let (shard0_plaintext, first0, last0) = build_test_index_shard(
14795            &[TestFileMeta {
14796                path: b"alpha.txt".to_vec(),
14797                frame_index: 0,
14798                tar_stream_offset: 0,
14799                member_group_size: alpha.len() as u64,
14800                file_data_size: b"alpha cross shard\n".len() as u64,
14801            }],
14802            &[frame0],
14803            std::slice::from_ref(&envelope),
14804        );
14805        let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
14806            &[TestFileMeta {
14807                path: b"beta.txt".to_vec(),
14808                frame_index: 1,
14809                tar_stream_offset: alpha.len() as u64,
14810                member_group_size: beta.len() as u64,
14811                file_data_size: b"beta cross shard\n".len() as u64,
14812            }],
14813            &[frame1],
14814            std::slice::from_ref(&envelope),
14815        );
14816        shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
14817
14818        let shard0 = encrypt_test_object(
14819            &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
14820            &subkeys.index_shard_key,
14821            &subkeys.index_nonce_seed,
14822            b"idxshard",
14823            0,
14824            BlockKind::IndexShardData,
14825            &mut next_block_index,
14826            &crypto_header,
14827            &volume_header,
14828        );
14829        let shard1 = encrypt_test_object(
14830            &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
14831            &subkeys.index_shard_key,
14832            &subkeys.index_nonce_seed,
14833            b"idxshard",
14834            1,
14835            BlockKind::IndexShardData,
14836            &mut next_block_index,
14837            &crypto_header,
14838            &volume_header,
14839        );
14840        insert_records(&mut blocks, &shard0.records);
14841        insert_records(&mut blocks, &shard1.records);
14842
14843        let index_root = IndexRoot {
14844            header: IndexRootHeader {
14845                frame_count: 2,
14846                envelope_count: 1,
14847                file_count: 2,
14848                payload_block_count: payload.extent.data_block_count as u64,
14849                tar_total_size: tar_stream.len() as u64,
14850                content_sha256: sha256_bytes(&tar_stream),
14851                ..IndexRootHeader::empty()
14852            },
14853            shards: vec![
14854                ShardEntry {
14855                    shard_index: 0,
14856                    first_block_index: shard0.extent.first_block_index,
14857                    data_block_count: shard0.extent.data_block_count,
14858                    parity_block_count: 0,
14859                    encrypted_size: shard0.extent.encrypted_size,
14860                    decompressed_size: shard0_plaintext.len() as u32,
14861                    file_count: 1,
14862                    first_path_hash: first0,
14863                    last_path_hash: last0,
14864                },
14865                ShardEntry {
14866                    shard_index: 1,
14867                    first_block_index: shard1.extent.first_block_index,
14868                    data_block_count: shard1.extent.data_block_count,
14869                    parity_block_count: 0,
14870                    encrypted_size: shard1.extent.encrypted_size,
14871                    decompressed_size: shard1_plaintext.len() as u32,
14872                    file_count: 1,
14873                    first_path_hash: first1,
14874                    last_path_hash: last1,
14875                },
14876            ],
14877            directory_hint_shards: Vec::new(),
14878        };
14879
14880        let index_root_plaintext = index_root.to_bytes();
14881        let index_root_object = encrypt_test_object(
14882            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14883            &subkeys.index_root_key,
14884            &subkeys.index_nonce_seed,
14885            b"idxroot",
14886            0,
14887            BlockKind::IndexRootData,
14888            &mut next_block_index,
14889            &crypto_header,
14890            &volume_header,
14891        );
14892        insert_records(&mut blocks, &index_root_object.records);
14893
14894        let archive_uuid = volume_header.archive_uuid;
14895        let session_id = volume_header.session_id;
14896        let opened = OpenedArchive {
14897            options: ReaderOptions::default(),
14898            observed_archive_bytes: 1_000_000,
14899            observed_volume_count: 1,
14900            subkeys,
14901            blocks,
14902            lazy_blocks: None,
14903            crypto_header_bytes: Vec::new(),
14904            volume_header,
14905            crypto_header,
14906            manifest_footer: ManifestFooter {
14907                archive_uuid,
14908                session_id,
14909                volume_index: 0,
14910                is_authoritative: 1,
14911                total_volumes: 1,
14912                index_root_first_block: index_root_object.extent.first_block_index,
14913                index_root_data_block_count: index_root_object.extent.data_block_count,
14914                index_root_parity_block_count: 0,
14915                index_root_encrypted_size: index_root_object.extent.encrypted_size,
14916                index_root_decompressed_size: index_root_plaintext.len() as u32,
14917                manifest_hmac: [0u8; 32],
14918            },
14919            volume_trailer: Some(VolumeTrailer {
14920                archive_uuid,
14921                session_id,
14922                volume_index: 0,
14923                block_count: next_block_index,
14924                bytes_written: 0,
14925                manifest_footer_offset: 0,
14926                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14927                closed_at_ns: 0,
14928                root_auth_footer_offset: 0,
14929                root_auth_footer_length: 0,
14930                root_auth_flags: 0,
14931                trailer_hmac: [0u8; 32],
14932            }),
14933            root_auth_footer: None,
14934            index_root,
14935            payload_dictionary: None,
14936        };
14937
14938        opened.verify().unwrap();
14939    }
14940
14941    #[test]
14942    fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
14943        let options = WriterOptions {
14944            index_root_fec_parity_shards: 0,
14945            ..single_stream_options()
14946        };
14947        let archive = write_archive(
14948            &[RegularFile::new("only.txt", b"only payload")],
14949            &master_key(),
14950            options,
14951        )
14952        .unwrap();
14953        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14954        assert!(opened.index_root.directory_hint_shards.is_empty());
14955
14956        let mut root = opened.index_root.clone();
14957        root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
14958        root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
14959        let root_plaintext = root.to_bytes();
14960        IndexRoot::parse(
14961            &root_plaintext,
14962            false,
14963            metadata_limits(&opened.crypto_header),
14964        )
14965        .unwrap();
14966        assert_eq!(
14967            root_plaintext.len() as u32,
14968            opened.manifest_footer.index_root_decompressed_size
14969        );
14970
14971        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14972        let mut next_block_index = opened.manifest_footer.index_root_first_block;
14973        let replacement = encrypt_test_object(
14974            &compressed_root,
14975            &opened.subkeys.index_root_key,
14976            &opened.subkeys.index_nonce_seed,
14977            b"idxroot",
14978            0,
14979            BlockKind::IndexRootData,
14980            &mut next_block_index,
14981            &opened.crypto_header,
14982            &opened.volume_header,
14983        );
14984        assert_eq!(
14985            replacement.extent.first_block_index,
14986            opened.manifest_footer.index_root_first_block
14987        );
14988        assert_eq!(
14989            replacement.extent.data_block_count,
14990            opened.manifest_footer.index_root_data_block_count
14991        );
14992        assert_eq!(
14993            replacement.extent.encrypted_size,
14994            opened.manifest_footer.index_root_encrypted_size
14995        );
14996
14997        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14998        let crypto_end = volume_header.crypto_header_offset as usize
14999            + volume_header.crypto_header_length as usize;
15000        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
15001        let mut malformed = archive.bytes.clone();
15002        for record in replacement.records {
15003            let offset = crypto_end + record.block_index as usize * record_len;
15004            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
15005        }
15006
15007        let reopened = open_archive(&malformed, &master_key()).unwrap();
15008        assert_eq!(
15009            reopened.index_root.header.file_count,
15010            DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
15011        );
15012        assert!(reopened.index_root.directory_hint_shards.is_empty());
15013
15014        assert_eq!(
15015            reopened.verify().unwrap_err(),
15016            FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
15017        );
15018    }
15019
15020    #[test]
15021    fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
15022        let mut map = DirectoryHintMap::new();
15023        add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
15024        add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
15025
15026        assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
15027        assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
15028        assert_eq!(
15029            map.get(b"foo/bar".as_slice()),
15030            Some(&BTreeSet::from([2, 4]))
15031        );
15032        assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
15033        assert!(!map.contains_key(b"foobar".as_slice()));
15034    }
15035
15036    #[test]
15037    fn directory_hint_validation_requires_exact_global_map() {
15038        let mut expected = DirectoryHintMap::new();
15039        add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
15040        add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
15041        let rows = sorted_directory_hint_rows(&expected);
15042        let table = directory_hint_table_from_rows(7, &rows, 2);
15043
15044        validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
15045            .unwrap();
15046
15047        let mut missing_root = expected.clone();
15048        missing_root.remove(&Vec::new());
15049        let missing_root_rows = sorted_directory_hint_rows(&missing_root);
15050        let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
15051        assert_eq!(
15052            validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
15053                .unwrap_err(),
15054            FormatError::InvalidArchive("directory hint map does not match decoded files")
15055        );
15056
15057        let mut expected_missing_directory_entry = expected.clone();
15058        expected_missing_directory_entry
15059            .get_mut(b"foo".as_slice())
15060            .unwrap()
15061            .remove(&1);
15062        assert_eq!(
15063            validate_directory_hint_tables_against_expected(
15064                std::slice::from_ref(&table),
15065                &expected_missing_directory_entry,
15066            )
15067            .unwrap_err(),
15068            FormatError::InvalidArchive("directory hint map does not match decoded files")
15069        );
15070
15071        let mut extra = expected.clone();
15072        extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
15073        let extra_rows = sorted_directory_hint_rows(&extra);
15074        let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
15075        assert_eq!(
15076            validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
15077            FormatError::InvalidArchive("directory hint map does not match decoded files")
15078        );
15079    }
15080
15081    #[test]
15082    fn directory_hint_validation_rejects_global_order_mismatch() {
15083        let mut expected = DirectoryHintMap::new();
15084        expected.insert(Vec::new(), BTreeSet::from([0]));
15085        expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
15086        let rows = sorted_directory_hint_rows(&expected);
15087        let first = directory_hint_table_from_rows(8, &rows[..1], 1);
15088        let second = directory_hint_table_from_rows(9, &rows[1..], 1);
15089
15090        assert_eq!(
15091            validate_directory_hint_tables_against_expected(&[second, first], &expected)
15092                .unwrap_err(),
15093            FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
15094        );
15095    }
15096
15097    #[test]
15098    fn object_extent_rejects_parity_above_class_cap() {
15099        let crypto_header = CryptoHeaderFixed {
15100            length: 0,
15101            compression_algo: CompressionAlgo::ZstdFramed,
15102            aead_algo: AeadAlgo::AesGcmSiv256,
15103            fec_algo: FecAlgo::ReedSolomonGF16,
15104            kdf_algo: KdfAlgo::Raw,
15105            chunk_size: 1024,
15106            envelope_target_size: 4096,
15107            block_size: 4096,
15108            fec_data_shards: 1,
15109            fec_parity_shards: 1,
15110            index_fec_data_shards: 1,
15111            index_fec_parity_shards: 1,
15112            index_root_fec_data_shards: 1,
15113            index_root_fec_parity_shards: 1,
15114            stripe_width: 1,
15115            volume_loss_tolerance: 0,
15116            bit_rot_buffer_pct: 0,
15117            has_dictionary: 0,
15118            max_path_length: 4096,
15119            expected_volume_size: 0,
15120        };
15121        let extent = ObjectExtent {
15122            first_block_index: 0,
15123            data_block_count: 1,
15124            parity_block_count: 2,
15125            encrypted_size: 4096,
15126        };
15127
15128        assert_eq!(
15129            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
15130            FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
15131        );
15132    }
15133
15134    #[test]
15135    fn object_extent_rejects_parity_below_recoverability_requirement() {
15136        let crypto_header = CryptoHeaderFixed {
15137            length: 0,
15138            compression_algo: CompressionAlgo::ZstdFramed,
15139            aead_algo: AeadAlgo::AesGcmSiv256,
15140            fec_algo: FecAlgo::ReedSolomonGF16,
15141            kdf_algo: KdfAlgo::Raw,
15142            chunk_size: 1024,
15143            envelope_target_size: 4096,
15144            block_size: 4096,
15145            fec_data_shards: 1,
15146            fec_parity_shards: 1,
15147            index_fec_data_shards: 1,
15148            index_fec_parity_shards: 1,
15149            index_root_fec_data_shards: 1,
15150            index_root_fec_parity_shards: 1,
15151            stripe_width: 2,
15152            volume_loss_tolerance: 1,
15153            bit_rot_buffer_pct: 0,
15154            has_dictionary: 0,
15155            max_path_length: 4096,
15156            expected_volume_size: 0,
15157        };
15158        let extent = ObjectExtent {
15159            first_block_index: 0,
15160            data_block_count: 1,
15161            parity_block_count: 0,
15162            encrypted_size: 4096,
15163        };
15164
15165        assert_eq!(
15166            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
15167            FormatError::InvalidArchive(
15168                "encrypted object parity does not match v41 compute_parity"
15169            )
15170        );
15171    }
15172
15173    #[test]
15174    fn encrypted_object_extent_matrix_rejects_overlaps() {
15175        let (opened, _) = multi_envelope_reader_fixture();
15176        let loaded_shard = opened
15177            .load_index_shard(&opened.index_root.shards[0])
15178            .unwrap();
15179        let base_envelopes = loaded_shard
15180            .envelopes
15181            .iter()
15182            .map(|entry| (entry.envelope_index, entry.clone()))
15183            .collect::<BTreeMap<_, _>>();
15184        let payload_start = loaded_shard.envelopes[0].first_block_index;
15185        let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
15186
15187        let mut payload_overlap = base_envelopes.clone();
15188        payload_overlap
15189            .get_mut(&loaded_shard.envelopes[1].envelope_index)
15190            .unwrap()
15191            .first_block_index = payload_start;
15192        assert_eq!(
15193            opened
15194                .validate_encrypted_object_block_ranges(&payload_overlap)
15195                .unwrap_err(),
15196            overlap
15197        );
15198
15199        let mut shard_overlap = opened.clone();
15200        let shard = shard_overlap.index_root.shards[0].clone();
15201        shard_overlap.index_root.shards.push(ShardEntry {
15202            shard_index: 1,
15203            ..shard
15204        });
15205        assert_eq!(
15206            shard_overlap
15207                .validate_encrypted_object_block_ranges(&base_envelopes)
15208                .unwrap_err(),
15209            overlap
15210        );
15211
15212        let mut dictionary_overlap = opened.clone();
15213        dictionary_overlap.crypto_header.has_dictionary = 1;
15214        dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
15215        dictionary_overlap
15216            .index_root
15217            .header
15218            .dictionary_data_block_count = 1;
15219        dictionary_overlap
15220            .index_root
15221            .header
15222            .dictionary_parity_block_count = 0;
15223        dictionary_overlap
15224            .index_root
15225            .header
15226            .dictionary_encrypted_size = 4096;
15227        dictionary_overlap
15228            .index_root
15229            .header
15230            .dictionary_decompressed_size = 128;
15231        assert_eq!(
15232            dictionary_overlap
15233                .validate_encrypted_object_block_ranges(&base_envelopes)
15234                .unwrap_err(),
15235            overlap
15236        );
15237
15238        let mut hint_overlap = opened.clone();
15239        hint_overlap
15240            .index_root
15241            .directory_hint_shards
15242            .push(DirectoryHintShardEntry {
15243                hint_shard_index: 0,
15244                first_dir_hash: [0; 8],
15245                last_dir_hash: [0; 8],
15246                first_block_index: payload_start,
15247                data_block_count: 1,
15248                parity_block_count: 0,
15249                encrypted_size: 4096,
15250                decompressed_size: 128,
15251                entry_count: 1,
15252            });
15253        assert_eq!(
15254            hint_overlap
15255                .validate_encrypted_object_block_ranges(&base_envelopes)
15256                .unwrap_err(),
15257            overlap
15258        );
15259    }
15260
15261    #[test]
15262    fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
15263        let volume_header = test_volume_header();
15264        let crypto_header = test_crypto_header();
15265        let subkeys = Subkeys::derive(
15266            &master_key(),
15267            &volume_header.archive_uuid,
15268            &volume_header.session_id,
15269        )
15270        .unwrap();
15271        let mut next_block_index = 0u64;
15272
15273        let index_root_payload = b"index root metadata object";
15274        let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
15275        assert_metadata_object_from_compressed(
15276            &{
15277                let mut bytes = index_root_compressed.clone();
15278                bytes.push(0);
15279                bytes
15280            },
15281            index_root_payload.len(),
15282            &subkeys,
15283            &volume_header,
15284            &crypto_header,
15285            &subkeys.index_root_key,
15286            &subkeys.index_nonce_seed,
15287            b"idxroot",
15288            0,
15289            BlockKind::IndexRootData,
15290            BlockKind::IndexRootParity,
15291            crypto_header.index_root_fec_data_shards,
15292            crypto_header.index_root_fec_parity_shards,
15293            &mut next_block_index,
15294            FormatError::TrailingBytesAfterZstdFrame,
15295        );
15296        assert_metadata_object_from_compressed(
15297            &index_root_compressed,
15298            index_root_payload.len() + 1,
15299            &subkeys,
15300            &volume_header,
15301            &crypto_header,
15302            &subkeys.index_root_key,
15303            &subkeys.index_nonce_seed,
15304            b"idxroot",
15305            0,
15306            BlockKind::IndexRootData,
15307            BlockKind::IndexRootParity,
15308            crypto_header.index_root_fec_data_shards,
15309            crypto_header.index_root_fec_parity_shards,
15310            &mut next_block_index,
15311            FormatError::ZstdDecompressedSizeMismatch {
15312                expected: index_root_payload.len() + 1,
15313                actual: index_root_payload.len(),
15314            },
15315        );
15316
15317        let index_shard_payload = b"index shard metadata object";
15318        let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
15319        assert_metadata_object_from_compressed(
15320            &{
15321                let mut bytes = index_shard_compressed.clone();
15322                bytes.push(0);
15323                bytes
15324            },
15325            index_shard_payload.len(),
15326            &subkeys,
15327            &volume_header,
15328            &crypto_header,
15329            &subkeys.index_shard_key,
15330            &subkeys.index_nonce_seed,
15331            b"idxshard",
15332            1,
15333            BlockKind::IndexShardData,
15334            BlockKind::IndexShardParity,
15335            crypto_header.index_fec_data_shards,
15336            crypto_header.index_fec_parity_shards,
15337            &mut next_block_index,
15338            FormatError::TrailingBytesAfterZstdFrame,
15339        );
15340        assert_metadata_object_from_compressed(
15341            &index_shard_compressed,
15342            index_shard_payload.len() + 1,
15343            &subkeys,
15344            &volume_header,
15345            &crypto_header,
15346            &subkeys.index_shard_key,
15347            &subkeys.index_nonce_seed,
15348            b"idxshard",
15349            1,
15350            BlockKind::IndexShardData,
15351            BlockKind::IndexShardParity,
15352            crypto_header.index_fec_data_shards,
15353            crypto_header.index_fec_parity_shards,
15354            &mut next_block_index,
15355            FormatError::ZstdDecompressedSizeMismatch {
15356                expected: index_shard_payload.len() + 1,
15357                actual: index_shard_payload.len(),
15358            },
15359        );
15360
15361        let directory_hint_payload = b"directory hint metadata object";
15362        let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
15363        assert_metadata_object_from_compressed(
15364            &{
15365                let mut bytes = directory_hint_compressed.clone();
15366                bytes.push(0);
15367                bytes
15368            },
15369            directory_hint_payload.len(),
15370            &subkeys,
15371            &volume_header,
15372            &crypto_header,
15373            &subkeys.dir_hint_key,
15374            &subkeys.index_nonce_seed,
15375            b"dirhint",
15376            0,
15377            BlockKind::DirectoryHintData,
15378            BlockKind::DirectoryHintParity,
15379            crypto_header.index_fec_data_shards,
15380            crypto_header.index_fec_parity_shards,
15381            &mut next_block_index,
15382            FormatError::TrailingBytesAfterZstdFrame,
15383        );
15384        assert_metadata_object_from_compressed(
15385            &directory_hint_compressed,
15386            directory_hint_payload.len() + 1,
15387            &subkeys,
15388            &volume_header,
15389            &crypto_header,
15390            &subkeys.dir_hint_key,
15391            &subkeys.index_nonce_seed,
15392            b"dirhint",
15393            0,
15394            BlockKind::DirectoryHintData,
15395            BlockKind::DirectoryHintParity,
15396            crypto_header.index_fec_data_shards,
15397            crypto_header.index_fec_parity_shards,
15398            &mut next_block_index,
15399            FormatError::ZstdDecompressedSizeMismatch {
15400                expected: directory_hint_payload.len() + 1,
15401                actual: directory_hint_payload.len(),
15402            },
15403        );
15404
15405        let dictionary_payload = b"dictionary metadata object";
15406        let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
15407        assert_metadata_object_from_compressed(
15408            &{
15409                let mut bytes = dictionary_compressed.clone();
15410                bytes.push(0);
15411                bytes
15412            },
15413            dictionary_payload.len(),
15414            &subkeys,
15415            &volume_header,
15416            &crypto_header,
15417            &subkeys.dictionary_key,
15418            &subkeys.index_nonce_seed,
15419            b"dict",
15420            0,
15421            BlockKind::DictionaryData,
15422            BlockKind::DictionaryParity,
15423            crypto_header.index_root_fec_data_shards,
15424            crypto_header.index_root_fec_parity_shards,
15425            &mut next_block_index,
15426            FormatError::TrailingBytesAfterZstdFrame,
15427        );
15428        assert_metadata_object_from_compressed(
15429            &dictionary_compressed,
15430            dictionary_payload.len() + 1,
15431            &subkeys,
15432            &volume_header,
15433            &crypto_header,
15434            &subkeys.dictionary_key,
15435            &subkeys.index_nonce_seed,
15436            b"dict",
15437            0,
15438            BlockKind::DictionaryData,
15439            BlockKind::DictionaryParity,
15440            crypto_header.index_root_fec_data_shards,
15441            crypto_header.index_root_fec_parity_shards,
15442            &mut next_block_index,
15443            FormatError::ZstdDecompressedSizeMismatch {
15444                expected: dictionary_payload.len() + 1,
15445                actual: dictionary_payload.len(),
15446            },
15447        );
15448    }
15449
15450    #[test]
15451    fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
15452        let volume_header = test_volume_header();
15453        let crypto_header = test_crypto_header();
15454        let subkeys = Subkeys::derive(
15455            &master_key(),
15456            &volume_header.archive_uuid,
15457            &volume_header.session_id,
15458        )
15459        .unwrap();
15460        let mut next_block_index = 0u64;
15461
15462        let index_root_payload = b"index root metadata object";
15463        let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
15464            index_root_payload,
15465            &subkeys,
15466            &volume_header,
15467            &crypto_header,
15468            &subkeys.index_root_key,
15469            &subkeys.index_nonce_seed,
15470            b"idxroot",
15471            0,
15472            BlockKind::IndexRootData,
15473            &mut next_block_index,
15474        );
15475        let mut index_root_extent = index_root_extent;
15476        index_root_extent.encrypted_size = index_root_extent
15477            .encrypted_size
15478            .saturating_add(crypto_header.block_size);
15479        assert_eq!(
15480            load_metadata_object_from_parts(
15481                &index_root_records,
15482                ObjectLoadContext::index_root(
15483                    &volume_header,
15484                    &crypto_header,
15485                    &subkeys,
15486                    index_root_extent,
15487                ),
15488                index_root_payload.len() as u32,
15489            )
15490            .unwrap_err(),
15491            FormatError::InvalidArchive(
15492                "encrypted object size is not data_block_count * block_size"
15493            )
15494        );
15495
15496        let index_shard_payload = b"index shard metadata object";
15497        let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
15498            index_shard_payload,
15499            &subkeys,
15500            &volume_header,
15501            &crypto_header,
15502            &subkeys.index_shard_key,
15503            &subkeys.index_nonce_seed,
15504            b"idxshard",
15505            1,
15506            BlockKind::IndexShardData,
15507            &mut next_block_index,
15508        );
15509        let mut index_shard_extent = index_shard_extent;
15510        index_shard_extent.encrypted_size = index_shard_extent
15511            .encrypted_size
15512            .saturating_add(crypto_header.block_size);
15513        assert_eq!(
15514            load_metadata_object_from_parts(
15515                &index_shard_records,
15516                ObjectLoadContext {
15517                    volume_header: &volume_header,
15518                    crypto_header: &crypto_header,
15519                    extent: index_shard_extent,
15520                    data_kind: BlockKind::IndexShardData,
15521                    parity_kind: BlockKind::IndexShardParity,
15522                    key: &subkeys.index_shard_key,
15523                    nonce_seed: &subkeys.index_nonce_seed,
15524                    domain: b"idxshard",
15525                    counter: 1,
15526                    class_data_shard_max: crypto_header.index_fec_data_shards,
15527                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
15528                },
15529                index_shard_payload.len() as u32,
15530            )
15531            .unwrap_err(),
15532            FormatError::InvalidArchive(
15533                "encrypted object size is not data_block_count * block_size"
15534            )
15535        );
15536
15537        let directory_hint_payload = b"directory hint metadata object";
15538        let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
15539            directory_hint_payload,
15540            &subkeys,
15541            &volume_header,
15542            &crypto_header,
15543            &subkeys.dir_hint_key,
15544            &subkeys.index_nonce_seed,
15545            b"dirhint",
15546            0,
15547            BlockKind::DirectoryHintData,
15548            &mut next_block_index,
15549        );
15550        let mut directory_hint_extent = directory_hint_extent;
15551        directory_hint_extent.encrypted_size = directory_hint_extent
15552            .encrypted_size
15553            .saturating_add(crypto_header.block_size);
15554        assert_eq!(
15555            load_metadata_object_from_parts(
15556                &directory_hint_records,
15557                ObjectLoadContext {
15558                    volume_header: &volume_header,
15559                    crypto_header: &crypto_header,
15560                    extent: directory_hint_extent,
15561                    data_kind: BlockKind::DirectoryHintData,
15562                    parity_kind: BlockKind::DirectoryHintParity,
15563                    key: &subkeys.dir_hint_key,
15564                    nonce_seed: &subkeys.index_nonce_seed,
15565                    domain: b"dirhint",
15566                    counter: 0,
15567                    class_data_shard_max: crypto_header.index_fec_data_shards,
15568                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
15569                },
15570                directory_hint_payload.len() as u32,
15571            )
15572            .unwrap_err(),
15573            FormatError::InvalidArchive(
15574                "encrypted object size is not data_block_count * block_size"
15575            )
15576        );
15577
15578        let dictionary_payload = b"dictionary metadata object";
15579        let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
15580            dictionary_payload,
15581            &subkeys,
15582            &volume_header,
15583            &crypto_header,
15584            &subkeys.dictionary_key,
15585            &subkeys.index_nonce_seed,
15586            b"dict",
15587            0,
15588            BlockKind::DictionaryData,
15589            &mut next_block_index,
15590        );
15591        let mut dictionary_extent = dictionary_extent;
15592        dictionary_extent.encrypted_size = dictionary_extent
15593            .encrypted_size
15594            .saturating_add(crypto_header.block_size);
15595        assert_eq!(
15596            load_metadata_object_from_parts(
15597                &dictionary_records,
15598                ObjectLoadContext {
15599                    volume_header: &volume_header,
15600                    crypto_header: &crypto_header,
15601                    extent: dictionary_extent,
15602                    data_kind: BlockKind::DictionaryData,
15603                    parity_kind: BlockKind::DictionaryParity,
15604                    key: &subkeys.dictionary_key,
15605                    nonce_seed: &subkeys.index_nonce_seed,
15606                    domain: b"dict",
15607                    counter: 0,
15608                    class_data_shard_max: crypto_header.index_root_fec_data_shards,
15609                    class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
15610                },
15611                dictionary_payload.len() as u32,
15612            )
15613            .unwrap_err(),
15614            FormatError::InvalidArchive(
15615                "encrypted object size is not data_block_count * block_size"
15616            )
15617        );
15618    }
15619
15620    #[test]
15621    fn opens_complete_multi_volume_archive() {
15622        let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
15623        let archive = write_archive(
15624            &files,
15625            &master_key(),
15626            WriterOptions {
15627                stripe_width: 2,
15628                volume_loss_tolerance: 1,
15629                ..single_stream_options()
15630            },
15631        )
15632        .unwrap();
15633        assert_eq!(archive.volumes.len(), 2);
15634
15635        let volume_refs = archive
15636            .volumes
15637            .iter()
15638            .map(Vec::as_slice)
15639            .collect::<Vec<_>>();
15640        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15641
15642        assert_eq!(opened.volume_header.stripe_width, 2);
15643        assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
15644        assert_eq!(
15645            opened.extract_file("alpha.txt").unwrap(),
15646            Some(b"hello from volume stripes".to_vec())
15647        );
15648        opened.verify().unwrap();
15649    }
15650
15651    #[test]
15652    fn recovers_from_one_missing_volume_when_parity_allows() {
15653        let files = [RegularFile::new("alpha.txt", b"recover me")];
15654        let archive = write_archive(
15655            &files,
15656            &master_key(),
15657            WriterOptions {
15658                stripe_width: 2,
15659                volume_loss_tolerance: 1,
15660                ..single_stream_options()
15661            },
15662        )
15663        .unwrap();
15664
15665        let recovered =
15666            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
15667        assert_eq!(
15668            recovered.extract_file("alpha.txt").unwrap(),
15669            Some(b"recover me".to_vec())
15670        );
15671        recovered.verify().unwrap();
15672    }
15673
15674    #[test]
15675    fn recovers_from_crc_corrupted_block_when_parity_allows() {
15676        let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
15677        let archive = write_archive(
15678            &files,
15679            &master_key(),
15680            WriterOptions {
15681                stripe_width: 2,
15682                volume_loss_tolerance: 1,
15683                ..single_stream_options()
15684            },
15685        )
15686        .unwrap();
15687        let mut volumes = archive.volumes.clone();
15688        corrupt_first_block_record_payload(&mut volumes[0]);
15689
15690        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15691        let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15692
15693        assert_eq!(
15694            recovered.extract_file("alpha.txt").unwrap(),
15695            Some(b"repair corrupt block".to_vec())
15696        );
15697        recovered.verify().unwrap();
15698    }
15699
15700    #[test]
15701    fn rejects_multi_volume_count_mismatch_without_tolerance() {
15702        let files = [RegularFile::new("alpha.txt", b"count check")];
15703        let archive = write_archive(
15704            &files,
15705            &master_key(),
15706            WriterOptions {
15707                stripe_width: 3,
15708                volume_loss_tolerance: 0,
15709                ..single_stream_options()
15710            },
15711        )
15712        .unwrap();
15713
15714        assert_eq!(
15715            open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
15716            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15717        );
15718    }
15719
15720    #[test]
15721    fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
15722        let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
15723        let archive = write_archive(
15724            &files,
15725            &master_key(),
15726            WriterOptions {
15727                stripe_width: 2,
15728                volume_loss_tolerance: 1,
15729                ..single_stream_options()
15730            },
15731        )
15732        .unwrap();
15733
15734        let mut bad_first = archive.volumes[0].clone();
15735        rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
15736            footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
15737        });
15738
15739        open_archive_volumes(
15740            &[bad_first.as_slice(), archive.volumes[1].as_slice()],
15741            &master_key(),
15742        )
15743        .unwrap();
15744    }
15745
15746    #[test]
15747    fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
15748        let files = [RegularFile::new("alpha.txt", b"repair meta root")];
15749        let archive = write_archive(
15750            &files,
15751            &master_key(),
15752            WriterOptions {
15753                stripe_width: 2,
15754                volume_loss_tolerance: 1,
15755                ..single_stream_options()
15756            },
15757        )
15758        .unwrap();
15759        let mut volumes = archive.volumes.clone();
15760
15761        let mut corrupted = false;
15762        for volume in &mut volumes {
15763            if let Some(slot) =
15764                block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
15765            {
15766                corrupt_block_record_payload_at_slot(volume, *slot);
15767                corrupted = true;
15768                break;
15769            }
15770        }
15771        assert!(corrupted, "expected an IndexRootData record");
15772
15773        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15774        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15775        assert_eq!(
15776            opened.extract_file("alpha.txt").unwrap(),
15777            Some(b"repair meta root".to_vec())
15778        );
15779        opened.verify().unwrap();
15780    }
15781
15782    #[test]
15783    fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
15784        let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
15785        let archive = write_archive(
15786            &files,
15787            &master_key(),
15788            WriterOptions {
15789                stripe_width: 2,
15790                volume_loss_tolerance: 1,
15791                ..single_stream_options()
15792            },
15793        )
15794        .unwrap();
15795        let mut volumes = archive.volumes.clone();
15796
15797        let mut corrupted = false;
15798        for volume in &mut volumes {
15799            if let Some(slot) =
15800                block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
15801            {
15802                corrupt_block_record_payload_at_slot(volume, *slot);
15803                corrupted = true;
15804                break;
15805            }
15806        }
15807        assert!(corrupted, "expected an IndexShardData record");
15808
15809        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15810        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15811        assert_eq!(
15812            opened.extract_file("alpha.txt").unwrap(),
15813            Some(b"repair meta shard".to_vec())
15814        );
15815        opened.verify().unwrap();
15816    }
15817
15818    #[test]
15819    fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
15820        let files = [RegularFile::new(
15821            "alpha.txt",
15822            b"bitrot parity is not volume loss",
15823        )];
15824        let archive = write_archive(
15825            &files,
15826            &master_key(),
15827            WriterOptions {
15828                stripe_width: 2,
15829                volume_loss_tolerance: 0,
15830                bit_rot_buffer_pct: 1,
15831                ..single_stream_options()
15832            },
15833        )
15834        .unwrap();
15835
15836        assert_eq!(
15837            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
15838            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15839        );
15840    }
15841
15842    #[test]
15843    fn repairs_crc_erasure_only_within_parity_budget() {
15844        let payload = pseudo_random_bytes(12_000);
15845        let archive = write_archive(
15846            &[RegularFile::new("rot.bin", &payload)],
15847            &master_key(),
15848            small_block_recovery_options(),
15849        )
15850        .unwrap();
15851        let payload_slots = first_payload_data_run_slots(&archive.bytes);
15852        assert!(
15853            payload_slots.len() >= 2,
15854            "fixture must contain a multi-block payload object"
15855        );
15856
15857        let mut one_erasure = archive.bytes.clone();
15858        corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
15859        let repaired = open_archive(&one_erasure, &master_key()).unwrap();
15860        assert_eq!(
15861            repaired.extract_file("rot.bin").unwrap(),
15862            Some(payload.clone())
15863        );
15864
15865        let mut two_erasures = archive.bytes.clone();
15866        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
15867        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
15868        let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
15869        assert_eq!(
15870            unrepaired.extract_file("rot.bin").unwrap_err(),
15871            FormatError::FecTooFewAvailableShards
15872        );
15873    }
15874
15875    #[test]
15876    fn verify_rejects_missing_required_object_block_extent() {
15877        let (mut opened, missing_block) = multi_envelope_reader_fixture();
15878        assert!(opened.blocks.remove(&missing_block).is_some());
15879
15880        assert_eq!(
15881            opened.verify().unwrap_err(),
15882            FormatError::FecTooFewAvailableShards
15883        );
15884    }
15885
15886    #[test]
15887    fn parity_crc_erasure_does_not_hide_authenticated_data() {
15888        let payload = pseudo_random_bytes(12_000);
15889        let archive = write_archive(
15890            &[RegularFile::new("parity-erasure.bin", &payload)],
15891            &master_key(),
15892            parity_rich_recovery_options(),
15893        )
15894        .unwrap();
15895        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15896        let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
15897        assert!(
15898            parity_slots.len() >= 2,
15899            "fixture must contain redundant parity shards"
15900        );
15901        let mut corrupted = archive.bytes;
15902        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15903        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
15904
15905        let opened = open_archive(&corrupted, &master_key()).unwrap();
15906        assert_eq!(
15907            opened.extract_file("parity-erasure.bin").unwrap(),
15908            Some(payload)
15909        );
15910        opened.verify().unwrap();
15911    }
15912
15913    #[test]
15914    fn repair_patches_restore_crc_erased_payload_block() {
15915        let payload = pseudo_random_bytes(12_000);
15916        let archive = write_archive(
15917            &[RegularFile::new("rot.bin", &payload)],
15918            &master_key(),
15919            small_block_recovery_options(),
15920        )
15921        .unwrap();
15922        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15923        let mut corrupted = archive.bytes.clone();
15924        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15925
15926        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15927        opened.verify().unwrap();
15928        let patches = opened.repair_patches().unwrap();
15929        assert_eq!(patches.len(), 1);
15930        apply_repair_patches(&mut corrupted, &patches);
15931
15932        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15933        repaired.verify().unwrap();
15934        assert!(repaired.repair_patches().unwrap().is_empty());
15935    }
15936
15937    #[test]
15938    fn repair_patches_restore_crc_erased_payload_parity_block() {
15939        let payload = pseudo_random_bytes(12_000);
15940        let archive = write_archive(
15941            &[RegularFile::new("parity-erasure.bin", &payload)],
15942            &master_key(),
15943            parity_rich_recovery_options(),
15944        )
15945        .unwrap();
15946        let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
15947        let mut corrupted = archive.bytes.clone();
15948        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
15949
15950        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15951        opened.verify().unwrap();
15952        let patches = opened.repair_patches().unwrap();
15953        assert_eq!(patches.len(), 1);
15954        apply_repair_patches(&mut corrupted, &patches);
15955
15956        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15957        repaired.verify().unwrap();
15958        assert!(repaired.repair_patches().unwrap().is_empty());
15959    }
15960
15961    #[test]
15962    fn recovers_physical_odd_block_size_from_cmra_authority() {
15963        let archive = write_archive(
15964            &[RegularFile::new("odd-block.txt", b"payload")],
15965            &master_key(),
15966            small_block_recovery_options(),
15967        )
15968        .unwrap();
15969        let mut malformed = archive.bytes;
15970        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
15971        let block_size_offset = volume_header.crypto_header_offset as usize + 24;
15972        malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
15973
15974        let opened = open_archive(&malformed, &master_key()).unwrap();
15975        assert_ne!(opened.crypto_header.block_size, 4097);
15976        assert_eq!(
15977            opened.extract_file("odd-block.txt").unwrap(),
15978            Some(b"payload".to_vec())
15979        );
15980        opened.verify().unwrap();
15981    }
15982
15983    #[test]
15984    fn repairs_structurally_malformed_payload_block_slots() {
15985        let payload = pseudo_random_bytes(12_000);
15986        let archive = write_archive(
15987            &[RegularFile::new("structural-block.bin", &payload)],
15988            &master_key(),
15989            small_block_recovery_options(),
15990        )
15991        .unwrap();
15992        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15993
15994        let mut bad_magic = archive.bytes.clone();
15995        corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
15996        assert_eq!(
15997            open_archive(&bad_magic, &master_key())
15998                .unwrap()
15999                .extract_file("structural-block.bin")
16000                .unwrap(),
16001            Some(payload.clone())
16002        );
16003
16004        let mut bad_reserved = archive.bytes;
16005        corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
16006        assert_eq!(
16007            open_archive(&bad_reserved, &master_key())
16008                .unwrap()
16009                .extract_file("structural-block.bin")
16010                .unwrap(),
16011            Some(payload)
16012        );
16013    }
16014
16015    #[test]
16016    fn repair_patches_restore_structurally_malformed_payload_block_slot() {
16017        let payload = pseudo_random_bytes(12_000);
16018        let archive = write_archive(
16019            &[RegularFile::new("structural-patch.bin", &payload)],
16020            &master_key(),
16021            small_block_recovery_options(),
16022        )
16023        .unwrap();
16024        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
16025        let mut corrupted = archive.bytes.clone();
16026        corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
16027
16028        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
16029        opened.verify().unwrap();
16030        assert_eq!(
16031            opened.extract_file("structural-patch.bin").unwrap(),
16032            Some(payload)
16033        );
16034        let patches = opened.repair_patches().unwrap();
16035        assert_eq!(patches.len(), 1);
16036        apply_repair_patches(&mut corrupted, &patches);
16037
16038        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
16039        repaired.verify().unwrap();
16040        assert!(repaired.repair_patches().unwrap().is_empty());
16041    }
16042
16043    #[test]
16044    fn repairs_structurally_malformed_index_root_block_slot() {
16045        let archive = write_archive(
16046            &[RegularFile::new(
16047                "structural-index-root.txt",
16048                b"metadata repair",
16049            )],
16050            &master_key(),
16051            small_block_recovery_options(),
16052        )
16053        .unwrap();
16054        let index_root_slot =
16055            first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
16056        let mut corrupted = archive.bytes;
16057        corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
16058
16059        let opened = open_archive(&corrupted, &master_key()).unwrap();
16060        assert_eq!(
16061            opened.extract_file("structural-index-root.txt").unwrap(),
16062            Some(b"metadata repair".to_vec())
16063        );
16064        opened.verify().unwrap();
16065    }
16066
16067    #[test]
16068    fn rejects_parity_block_with_last_data_flag() {
16069        let archive = write_archive(
16070            &[RegularFile::new("parity-flag.txt", b"payload")],
16071            &master_key(),
16072            small_block_recovery_options(),
16073        )
16074        .unwrap();
16075        let parity_slot =
16076            first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
16077        let mut malformed = archive.bytes;
16078        mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
16079            record.flags = 0x01;
16080        });
16081
16082        assert_eq!(
16083            open_archive(&malformed, &master_key()).unwrap_err(),
16084            FormatError::ParityBlockHasLastDataFlag
16085        );
16086    }
16087
16088    #[test]
16089    fn rejects_missing_and_duplicate_payload_last_data_flags() {
16090        let payload = pseudo_random_bytes(12_000);
16091        let archive = write_archive(
16092            &[RegularFile::new("flags.bin", &payload)],
16093            &master_key(),
16094            small_block_recovery_options(),
16095        )
16096        .unwrap();
16097        let payload_slots = first_payload_data_run_slots(&archive.bytes);
16098        assert!(
16099            payload_slots.len() >= 2,
16100            "fixture must contain a multi-block payload object"
16101        );
16102
16103        let mut duplicate_last = archive.bytes.clone();
16104        mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
16105            record.flags = 0x01;
16106        });
16107        let opened = open_archive(&duplicate_last, &master_key()).unwrap();
16108        assert_eq!(
16109            opened.extract_file("flags.bin").unwrap_err(),
16110            FormatError::InvalidArchive("object last-data flag is not on the final data block")
16111        );
16112
16113        let mut missing_last = archive.bytes;
16114        mutate_block_record_at_slot(
16115            &mut missing_last,
16116            *payload_slots.last().unwrap(),
16117            |record| {
16118                record.flags = 0;
16119            },
16120        );
16121        let opened = open_archive(&missing_last, &master_key()).unwrap();
16122        assert_eq!(
16123            opened.extract_file("flags.bin").unwrap_err(),
16124            FormatError::InvalidArchive("object last-data flag is not on the final data block")
16125        );
16126    }
16127
16128    #[test]
16129    fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
16130        let files = [RegularFile::new(
16131            "footer-copy.txt",
16132            b"survives one bad footer",
16133        )];
16134        let archive = write_archive(
16135            &files,
16136            &master_key(),
16137            WriterOptions {
16138                stripe_width: 2,
16139                volume_loss_tolerance: 1,
16140                ..single_stream_options()
16141            },
16142        )
16143        .unwrap();
16144        let mut volumes = archive.volumes.clone();
16145        corrupt_manifest_footer_hmac(&mut volumes[0]);
16146
16147        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16148        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16149        assert_eq!(opened.manifest_footer.volume_index, 0);
16150        assert_eq!(opened.volume_header.volume_index, 0);
16151        assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
16152        assert_eq!(
16153            opened.extract_file("footer-copy.txt").unwrap(),
16154            Some(b"survives one bad footer".to_vec())
16155        );
16156        opened.verify().unwrap();
16157    }
16158
16159    #[test]
16160    fn manifest_footer_corruption_requires_trusted_sidecar() {
16161        let archive = write_archive(
16162            &[RegularFile::new("footer.txt", b"sidecar authority")],
16163            &master_key(),
16164            single_stream_options(),
16165        )
16166        .unwrap();
16167        let manifest_offset = terminal_material_offset(&archive.bytes);
16168        let mut corrupted = archive.bytes.clone();
16169        corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
16170        corrupt_v41_terminal_recovery(&mut corrupted);
16171
16172        assert!(open_archive(&corrupted, &master_key()).is_err());
16173
16174        let opened =
16175            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
16176                .unwrap();
16177        assert!(opened.volume_trailer.is_none());
16178        assert_eq!(
16179            opened.extract_file("footer.txt").unwrap(),
16180            Some(b"sidecar authority".to_vec())
16181        );
16182        opened.verify().unwrap();
16183    }
16184
16185    #[test]
16186    fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
16187        let archive = write_archive(
16188            &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
16189            &master_key(),
16190            single_stream_options(),
16191        )
16192        .unwrap();
16193        let strict_options = ReaderOptions {
16194            max_trailing_garbage_scan: 0,
16195            ..ReaderOptions::default()
16196        };
16197
16198        let manifest_offset = terminal_material_offset(&archive.bytes);
16199        for offset in [
16200            manifest_offset + 71,
16201            manifest_offset + MANIFEST_HMAC_COVERED_LEN,
16202        ] {
16203            let mut corrupted = archive.bytes.clone();
16204            corrupted[offset] ^= 0x01;
16205            open_archive(&corrupted, &master_key()).unwrap();
16206        }
16207
16208        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16209        for offset in [
16210            trailer_offset + 75,
16211            trailer_offset + TRAILER_HMAC_COVERED_LEN,
16212        ] {
16213            let mut corrupted = archive.bytes.clone();
16214            corrupted[offset] ^= 0x01;
16215            OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
16216        }
16217
16218        let mut covered_sidecar = archive.bootstrap_sidecar.clone();
16219        let mut header =
16220            BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16221                .unwrap();
16222        header.manifest_footer_offset += 1;
16223        covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16224        assert_eq!(
16225            open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
16226                .unwrap_err(),
16227            FormatError::HmacMismatch {
16228                structure: "BootstrapSidecarHeader"
16229            }
16230        );
16231
16232        let mut tag_sidecar = archive.bootstrap_sidecar.clone();
16233        let mut header =
16234            BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
16235        header.sidecar_hmac[0] ^= 1;
16236        tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16237        assert_eq!(
16238            open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
16239                .unwrap_err(),
16240            FormatError::HmacMismatch {
16241                structure: "BootstrapSidecarHeader"
16242            }
16243        );
16244
16245        let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
16246        let header =
16247            BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16248                .unwrap();
16249        let mut header_bytes = header.to_bytes();
16250        header_bytes[124] ^= 0x01;
16251        let crc = crc32c::crc32c(&header_bytes[..124]);
16252        header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
16253        non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
16254        let opened = open_archive_with_bootstrap_sidecar(
16255            &archive.bytes,
16256            &non_covered_sidecar,
16257            &master_key(),
16258        )
16259        .unwrap();
16260        assert_eq!(
16261            opened.extract_file("hmac-boundary.txt").unwrap(),
16262            Some(b"boundary bytes".to_vec())
16263        );
16264    }
16265
16266    #[test]
16267    fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
16268        let archive = write_archive(
16269            &[RegularFile::new("volume-index.txt", b"identity")],
16270            &master_key(),
16271            single_stream_options(),
16272        )
16273        .unwrap();
16274
16275        let mut bad_trailer = archive.bytes.clone();
16276        rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
16277            trailer.volume_index = 1;
16278        });
16279        open_archive(&bad_trailer, &master_key()).unwrap();
16280
16281        let mut bad_manifest = archive.bytes;
16282        rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
16283            footer.volume_index = 1;
16284        });
16285        open_archive(&bad_manifest, &master_key()).unwrap();
16286    }
16287
16288    #[test]
16289    fn rejects_same_key_header_terminal_material_splice() {
16290        let first = write_archive(
16291            &[RegularFile::new("splice.txt", b"same shape")],
16292            &master_key(),
16293            single_stream_options(),
16294        )
16295        .unwrap();
16296        let second = write_archive(
16297            &[RegularFile::new("splice.txt", b"same shape")],
16298            &master_key(),
16299            single_stream_options(),
16300        )
16301        .unwrap();
16302        assert_ne!(first.archive_uuid, second.archive_uuid);
16303        assert_eq!(
16304            terminal_material_offset(&first.bytes),
16305            terminal_material_offset(&second.bytes)
16306        );
16307        assert_eq!(first.bytes.len(), second.bytes.len());
16308
16309        let terminal_offset = terminal_material_offset(&first.bytes);
16310        let mut spliced = first.bytes.clone();
16311        spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
16312
16313        assert_eq!(
16314            open_archive(&spliced, &master_key()).unwrap_err(),
16315            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16316        );
16317    }
16318
16319    #[test]
16320    fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
16321        let kdf_params = crate::crypto::KdfParams::Argon2id {
16322            t_cost: 1,
16323            m_cost_kib: 8,
16324            parallelism: 1,
16325            salt: b"0123456789abcdef".to_vec(),
16326        };
16327        let archive = write_archive_with_kdf(
16328            &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
16329            &master_key(),
16330            single_stream_options(),
16331            &kdf_params,
16332        )
16333        .unwrap();
16334        let mut mutated = archive.bytes.clone();
16335        let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
16336        let subkeys = Subkeys::derive(
16337            &master_key(),
16338            &volume_header.archive_uuid,
16339            &volume_header.session_id,
16340        )
16341        .unwrap();
16342
16343        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
16344            let crypto_region = image
16345                .regions
16346                .iter_mut()
16347                .find(|region| region.region_type == 2)
16348                .unwrap();
16349            let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
16350            let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
16351            crypto_region.bytes[salt_start] ^= 0x01;
16352            let hmac = compute_hmac(
16353                HmacDomain::CryptoHeader,
16354                &subkeys.mac_key,
16355                &volume_header.archive_uuid,
16356                &volume_header.session_id,
16357                &crypto_region.bytes[..hmac_offset],
16358            );
16359            crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
16360        });
16361
16362        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16363        let locator = final_recovery_locator(&mutated);
16364        let crypto_start = volume_header.crypto_header_offset as usize;
16365        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16366        let parsed_crypto = CryptoHeader::parse(
16367            &mutated[crypto_start..crypto_end],
16368            volume_header.crypto_header_length,
16369        )
16370        .unwrap();
16371        assert_eq!(
16372            parse_locator_cmra_candidate(
16373                &mutated,
16374                final_offset,
16375                locator,
16376                KeyHoldingTerminalContext {
16377                    subkeys: &subkeys,
16378                    volume_header: &volume_header,
16379                    crypto_header: &parsed_crypto.fixed,
16380                    crypto_header_bytes: &mutated[crypto_start..crypto_end],
16381                },
16382            )
16383            .unwrap_err(),
16384            FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
16385        );
16386        assert!(open_archive(&mutated, &master_key()).is_err());
16387    }
16388
16389    #[test]
16390    fn recovers_physical_crypto_header_splice_from_cmra_authority() {
16391        let base = WriterOptions {
16392            archive_uuid: Some([0x11; 16]),
16393            session_id: Some([0x22; 16]),
16394            ..small_block_recovery_options()
16395        };
16396        let same_archive = WriterOptions {
16397            archive_uuid: Some([0x11; 16]),
16398            session_id: Some([0x33; 16]),
16399            ..small_block_recovery_options()
16400        };
16401
16402        let first = write_archive(
16403            &[RegularFile::new("splice.txt", b"same shape")],
16404            &master_key(),
16405            base,
16406        )
16407        .unwrap();
16408        let second = write_archive(
16409            &[RegularFile::new("splice.txt", b"same shape")],
16410            &master_key(),
16411            same_archive,
16412        )
16413        .unwrap();
16414
16415        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16416        let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
16417        let crypto_start = volume_header.crypto_header_offset as usize;
16418        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16419        let second_crypto_end = second_volume_header.crypto_header_offset as usize
16420            + second_volume_header.crypto_header_length as usize;
16421        assert_eq!(crypto_end, second_crypto_end);
16422
16423        let mut spliced = first.bytes.clone();
16424        spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
16425
16426        let opened = open_archive(&spliced, &master_key()).unwrap();
16427        assert_eq!(
16428            opened.crypto_header_bytes,
16429            first.bytes[crypto_start..crypto_end].to_vec()
16430        );
16431        assert_eq!(
16432            opened.extract_file("splice.txt").unwrap(),
16433            Some(b"same shape".to_vec())
16434        );
16435        opened.verify().unwrap();
16436    }
16437
16438    #[test]
16439    fn rejects_same_key_object_splice_with_session_mismatch() {
16440        let first = write_archive(
16441            &[RegularFile::new("splice.txt", b"same shape")],
16442            &master_key(),
16443            WriterOptions {
16444                archive_uuid: Some([0x11; 16]),
16445                session_id: Some([0x22; 16]),
16446                ..single_stream_options()
16447            },
16448        )
16449        .unwrap();
16450        let second = write_archive(
16451            &[RegularFile::new("splice.txt", b"same shape")],
16452            &master_key(),
16453            WriterOptions {
16454                archive_uuid: Some([0x11; 16]),
16455                session_id: Some([0x33; 16]),
16456                ..single_stream_options()
16457            },
16458        )
16459        .unwrap();
16460
16461        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16462        let crypto_end = volume_header.crypto_header_offset as usize
16463            + volume_header.crypto_header_length as usize;
16464        let terminal_offset = terminal_material_offset(&first.bytes);
16465        let second_terminal_offset = terminal_material_offset(&second.bytes);
16466        assert_eq!(terminal_offset, second_terminal_offset);
16467
16468        let mut spliced = first.bytes.clone();
16469        spliced[crypto_end..terminal_offset]
16470            .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
16471
16472        assert_eq!(
16473            open_archive(&spliced, &master_key()).unwrap_err(),
16474            FormatError::AeadFailure
16475        );
16476    }
16477
16478    #[test]
16479    fn rejects_authenticated_trailer_pointer_and_count_mutations() {
16480        let archive = write_archive(
16481            &[RegularFile::new(
16482                "trailer-range.txt",
16483                b"authenticated ranges",
16484            )],
16485            &master_key(),
16486            single_stream_options(),
16487        )
16488        .unwrap();
16489        let strict_options = ReaderOptions {
16490            max_trailing_garbage_scan: 0,
16491            ..ReaderOptions::default()
16492        };
16493        let bytes = archive.bytes;
16494        let manifest_offset = terminal_material_offset(&bytes);
16495        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16496
16497        let mut wrong_footer_length = bytes.clone();
16498        rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
16499            trailer.manifest_footer_length = 42;
16500        });
16501        OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
16502            .unwrap();
16503
16504        for (label, offset) in [
16505            (
16506                "offset before trailer by 1",
16507                manifest_offset.saturating_sub(1),
16508            ),
16509            ("offset after trailer", manifest_offset + 1),
16510            ("offset at stream start", 0),
16511            ("offset at trailer", trailer_offset),
16512            ("offset beyond trailer", trailer_offset + 4),
16513        ] {
16514            let mut wrong_footer_offset = bytes.clone();
16515            rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16516                trailer.manifest_footer_offset = offset as u64;
16517            });
16518            open_archive(&wrong_footer_offset, &master_key())
16519                .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
16520        }
16521
16522        let mut wrong_bytes_written = bytes.clone();
16523        rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
16524            trailer.bytes_written += 1;
16525        });
16526        open_archive(&wrong_bytes_written, &master_key()).unwrap();
16527
16528        let mut wrong_block_count = bytes.clone();
16529        rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
16530            trailer.block_count += 1;
16531        });
16532        open_archive(&wrong_block_count, &master_key()).unwrap();
16533
16534        let mut wrong_footer_offset = bytes.clone();
16535        rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16536            trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
16537        });
16538        open_archive(&wrong_footer_offset, &master_key()).unwrap();
16539    }
16540
16541    #[test]
16542    fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
16543        let archive = write_archive(
16544            &[RegularFile::new(
16545                "trailer-trailing-scan.txt",
16546                b"trailer scan boundaries",
16547            )],
16548            &master_key(),
16549            single_stream_options(),
16550        )
16551        .unwrap();
16552        let options = ReaderOptions {
16553            max_trailing_garbage_scan: 8,
16554            ..ReaderOptions::default()
16555        };
16556
16557        let mut within_scan = archive.bytes.clone();
16558        within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
16559        let opened =
16560            OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
16561        assert_eq!(
16562            opened.extract_file("trailer-trailing-scan.txt").unwrap(),
16563            Some(b"trailer scan boundaries".to_vec())
16564        );
16565
16566        let mut beyond_scan = archive.bytes.clone();
16567        beyond_scan.resize(
16568            beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
16569            0xAA,
16570        );
16571        assert_eq!(
16572            OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
16573            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16574        );
16575    }
16576
16577    #[test]
16578    fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
16579        let archive = write_archive(
16580            &[RegularFile::new("index-root-size.txt", b"extent size")],
16581            &master_key(),
16582            single_stream_options(),
16583        )
16584        .unwrap();
16585        let mut malformed = archive.bytes;
16586        let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
16587            .expect("archive should contain IndexRootData");
16588        mutate_block_record_at_slot(&mut malformed, slot, |record| {
16589            record.payload[0] ^= 0x55;
16590        });
16591
16592        assert_eq!(
16593            open_archive(&malformed, &master_key()).unwrap_err(),
16594            FormatError::AeadFailure
16595        );
16596    }
16597
16598    #[test]
16599    fn rejects_block_record_at_wrong_stripe_position() {
16600        let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
16601        let archive = write_archive(
16602            &files,
16603            &master_key(),
16604            WriterOptions {
16605                stripe_width: 2,
16606                volume_loss_tolerance: 1,
16607                ..single_stream_options()
16608            },
16609        )
16610        .unwrap();
16611        let mut volumes = archive.volumes.clone();
16612        mutate_first_block_record(&mut volumes[0], |record| {
16613            record.block_index += 2;
16614        });
16615
16616        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16617        assert_eq!(
16618            open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
16619            FormatError::InvalidArchive("BlockRecord index does not match volume position")
16620        );
16621    }
16622
16623    #[test]
16624    fn rejects_decreasing_block_record_index_in_required_region() {
16625        let archive = write_archive(
16626            &[RegularFile::new("alpha.txt", b"decreasing block index")],
16627            &master_key(),
16628            single_stream_options(),
16629        )
16630        .unwrap();
16631        assert!(block_record_slots(&archive.bytes).len() >= 2);
16632
16633        let mut malformed = archive.bytes;
16634        mutate_block_record_at_slot(&mut malformed, 1, |record| {
16635            record.block_index = 0;
16636        });
16637
16638        assert_eq!(
16639            open_archive(&malformed, &master_key()).unwrap_err(),
16640            FormatError::InvalidArchive("BlockRecord index does not match volume position")
16641        );
16642    }
16643
16644    #[test]
16645    fn rejects_duplicate_authenticated_volume_indexes() {
16646        let files = [RegularFile::new("alpha.txt", b"duplicates")];
16647        let archive = write_archive(
16648            &files,
16649            &master_key(),
16650            WriterOptions {
16651                stripe_width: 2,
16652                volume_loss_tolerance: 1,
16653                ..single_stream_options()
16654            },
16655        )
16656        .unwrap();
16657
16658        assert_eq!(
16659            open_archive_volumes(
16660                &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
16661                &master_key()
16662            )
16663            .unwrap_err(),
16664            FormatError::InvalidArchive("duplicate authenticated volume index")
16665        );
16666    }
16667
16668    #[test]
16669    fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
16670        let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
16671        let archive = write_archive(
16672            &files,
16673            &master_key(),
16674            WriterOptions {
16675                stripe_width: 2,
16676                volume_loss_tolerance: 1,
16677                ..single_stream_options()
16678            },
16679        )
16680        .unwrap();
16681        let mut conflicting = archive.volumes[0].clone();
16682        corrupt_first_block_record_payload(&mut conflicting);
16683
16684        assert_eq!(
16685            open_archive_volumes(
16686                &[archive.volumes[0].as_slice(), conflicting.as_slice()],
16687                &master_key()
16688            )
16689            .unwrap_err(),
16690            FormatError::InvalidArchive("duplicate authenticated volume index")
16691        );
16692    }
16693
16694    fn directory_hint_table_from_rows(
16695        hint_shard_index: u64,
16696        rows: &[(Vec<u8>, Vec<u32>)],
16697        shard_count: u32,
16698    ) -> DirectoryHintTable {
16699        let mut entries = Vec::new();
16700        let mut shard_row_indexes = Vec::new();
16701        let mut string_pool = Vec::new();
16702
16703        for (path, rows) in rows {
16704            let path_offset = if path.is_empty() {
16705                0
16706            } else {
16707                let offset = string_pool.len() as u64;
16708                string_pool.extend_from_slice(path);
16709                offset
16710            };
16711            let shard_list_start_index = shard_row_indexes.len() as u32;
16712            shard_row_indexes.extend_from_slice(rows);
16713            entries.push(DirectoryHintEntry {
16714                dir_hash: hash_prefix(path),
16715                path_offset,
16716                path_length: path.len() as u32,
16717                shard_list_start_index,
16718                shard_count: rows.len() as u32,
16719            });
16720        }
16721
16722        let table_bytes =
16723            directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
16724        let locating = DirectoryHintShardEntry {
16725            hint_shard_index,
16726            first_dir_hash: hash_prefix(&rows.first().unwrap().0),
16727            last_dir_hash: hash_prefix(&rows.last().unwrap().0),
16728            first_block_index: 0,
16729            data_block_count: 1,
16730            parity_block_count: 0,
16731            encrypted_size: 4096,
16732            decompressed_size: table_bytes.len() as u32,
16733            entry_count: rows.len() as u64,
16734        };
16735        DirectoryHintTable::parse(
16736            &table_bytes,
16737            &locating,
16738            shard_count,
16739            MetadataLimits::default(),
16740        )
16741        .unwrap()
16742    }
16743
16744    fn directory_hint_table_bytes(
16745        hint_shard_index: u64,
16746        entries: Vec<DirectoryHintEntry>,
16747        shard_row_indexes: Vec<u32>,
16748        string_pool: Vec<u8>,
16749    ) -> Vec<u8> {
16750        let header_len = DirectoryHintTableHeader {
16751            version: 1,
16752            hint_shard_index,
16753            entry_count: 0,
16754            entry_table_offset: 0,
16755            shard_list_offset: 0,
16756            string_pool_offset: 0,
16757            string_pool_size: 0,
16758        }
16759        .to_bytes()
16760        .len();
16761        let entry_len = entries
16762            .first()
16763            .map(|entry| entry.to_bytes().len())
16764            .unwrap_or(0);
16765        let shard_list_offset = if entries.is_empty() {
16766            0
16767        } else {
16768            header_len + entries.len() * entry_len
16769        };
16770        let string_pool_offset = if string_pool.is_empty() {
16771            0
16772        } else {
16773            shard_list_offset + shard_row_indexes.len() * 4
16774        };
16775
16776        let header = DirectoryHintTableHeader {
16777            version: 1,
16778            hint_shard_index,
16779            entry_count: entries.len() as u64,
16780            entry_table_offset: if entries.is_empty() {
16781                0
16782            } else {
16783                header_len as u64
16784            },
16785            shard_list_offset: shard_list_offset as u64,
16786            string_pool_offset: string_pool_offset as u64,
16787            string_pool_size: string_pool.len() as u64,
16788        };
16789
16790        let mut out = Vec::new();
16791        out.extend_from_slice(&header.to_bytes());
16792        for entry in entries {
16793            out.extend_from_slice(&entry.to_bytes());
16794        }
16795        for row in shard_row_indexes {
16796            out.extend_from_slice(&row.to_le_bytes());
16797        }
16798        out.extend_from_slice(&string_pool);
16799        out
16800    }
16801
16802    fn corrupt_first_block_record_payload(volume: &mut [u8]) {
16803        let (record_offset, _) = first_block_record(volume);
16804        volume[record_offset + 16] ^= 0x55;
16805    }
16806
16807    fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
16808        let (record_offset, _) = block_record_at_slot(volume, slot);
16809        volume[record_offset + 16] ^= 0x55;
16810    }
16811
16812    fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
16813        for patch in patches {
16814            let offset = patch.record_offset as usize;
16815            let end = offset + patch.record_bytes.len();
16816            volume[offset..end].copy_from_slice(&patch.record_bytes);
16817        }
16818    }
16819
16820    fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
16821        let (record_offset, _) = block_record_at_slot(volume, slot);
16822        volume[record_offset] ^= 0x55;
16823    }
16824
16825    fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
16826        let (record_offset, _) = block_record_at_slot(volume, slot);
16827        volume[record_offset + 14] = 0x01;
16828    }
16829
16830    fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
16831        let manifest_offset = terminal_material_offset(volume);
16832        volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
16833    }
16834
16835    fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
16836        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16837        CriticalRecoveryLocator::parse(
16838            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
16839        )
16840        .unwrap()
16841    }
16842
16843    fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
16844        let locator = final_recovery_locator(volume);
16845        let tuple = CmraDecoderTuple::from(locator);
16846        assert!(parity_shard_count < tuple.parity_shard_count);
16847        let cmra_offset = locator.cmra_offset as usize;
16848        let shard_size = tuple.shard_size as usize;
16849        let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
16850        let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
16851        let mut header = CriticalMetadataRecoveryHeader::parse(
16852            &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
16853        )
16854        .unwrap();
16855        header.parity_shard_count = parity_shard_count;
16856
16857        let mut cmra =
16858            Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
16859        cmra.extend_from_slice(&header.to_bytes());
16860        let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
16861        for row in 0..kept_rows {
16862            let start = rows_start + row * row_len;
16863            cmra.extend_from_slice(&volume[start..start + row_len]);
16864        }
16865
16866        let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
16867        out.extend_from_slice(&volume[..cmra_offset]);
16868        out.extend_from_slice(&cmra);
16869        let mut mirror = locator;
16870        mirror.locator_sequence = 1;
16871        mirror.cmra_length = cmra.len() as u32;
16872        mirror.cmra_parity_shard_count = parity_shard_count;
16873        out.extend_from_slice(&mirror.to_bytes());
16874        let final_locator = CriticalRecoveryLocator {
16875            volume_format_rev: locator.volume_format_rev,
16876            locator_sequence: 0,
16877            ..mirror
16878        };
16879        out.extend_from_slice(&final_locator.to_bytes());
16880        out
16881    }
16882
16883    fn rewrite_public_cmra_image(
16884        volume: &mut [u8],
16885        mutate: impl FnOnce(&mut CriticalMetadataImage),
16886    ) {
16887        rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
16888    }
16889
16890    fn rewrite_root_auth_footer_revision_bytes(bytes: &mut [u8], revision: u16) {
16891        bytes[72..74].copy_from_slice(&revision.to_le_bytes());
16892        let crc_offset = bytes.len() - 4;
16893        let crc = crc32c::crc32c(&bytes[..crc_offset]);
16894        bytes[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
16895    }
16896
16897    fn rewrite_cmra_image(
16898        volume: &mut [u8],
16899        mode: CmraRecoveryMode,
16900        mutate: impl FnOnce(&mut CriticalMetadataImage),
16901    ) {
16902        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16903        let locator = final_recovery_locator(volume);
16904        let tuple = CmraDecoderTuple::from(locator);
16905        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16906        let mut image = recovered.image;
16907        mutate(&mut image);
16908        refresh_critical_image_region_digests(&mut image);
16909        let image_bytes = image.to_bytes().unwrap();
16910        assert_eq!(image_bytes.len(), tuple.image_length as usize);
16911
16912        let shard_size = tuple.shard_size as usize;
16913        let data_shard_count = tuple.data_shard_count as usize;
16914        let parity_shard_count = tuple.parity_shard_count as usize;
16915        assert!(image_bytes.len() <= data_shard_count * shard_size);
16916
16917        let mut data_shards = Vec::with_capacity(data_shard_count);
16918        for idx in 0..data_shard_count {
16919            let start = idx * shard_size;
16920            let end = (start + shard_size).min(image_bytes.len());
16921            let mut payload = vec![0u8; shard_size];
16922            if start < image_bytes.len() {
16923                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
16924            }
16925            data_shards.push(payload);
16926        }
16927        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
16928        let image_sha256 = sha256_bytes(&image_bytes);
16929
16930        let header = CriticalMetadataRecoveryHeader {
16931            shard_size: tuple.shard_size,
16932            data_shard_count: tuple.data_shard_count,
16933            parity_shard_count: tuple.parity_shard_count,
16934            image_length: tuple.image_length,
16935            archive_uuid_hint: locator.archive_uuid_hint,
16936            session_id_hint: locator.session_id_hint,
16937            volume_index_hint: locator.volume_index_hint,
16938            image_sha256,
16939            header_crc32c: 0,
16940        };
16941        let mut cmra = Vec::new();
16942        cmra.extend_from_slice(&header.to_bytes());
16943        for (idx, payload) in data_shards.into_iter().enumerate() {
16944            let payload_len = if idx + 1 == data_shard_count {
16945                image_bytes.len() - idx * shard_size
16946            } else {
16947                shard_size
16948            };
16949            cmra.extend_from_slice(
16950                &CriticalMetadataRecoveryShard {
16951                    shard_index: idx as u16,
16952                    shard_role: 0,
16953                    shard_payload_length: payload_len as u32,
16954                    payload,
16955                    shard_crc32c: 0,
16956                }
16957                .to_bytes(shard_size)
16958                .unwrap(),
16959            );
16960        }
16961        for (idx, payload) in parity_shards.into_iter().enumerate() {
16962            cmra.extend_from_slice(
16963                &CriticalMetadataRecoveryShard {
16964                    shard_index: (data_shard_count + idx) as u16,
16965                    shard_role: 1,
16966                    shard_payload_length: shard_size as u32,
16967                    payload,
16968                    shard_crc32c: 0,
16969                }
16970                .to_bytes(shard_size)
16971                .unwrap(),
16972            );
16973        }
16974        assert_eq!(cmra.len() as u64, recovered.cmra_length);
16975        let cmra_offset = locator.cmra_offset as usize;
16976        volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
16977
16978        rewrite_locator_image_sha(volume, final_offset, image_sha256);
16979        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
16980        rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
16981    }
16982
16983    fn rewrite_cmra_image_variable_len(
16984        volume: &[u8],
16985        mode: CmraRecoveryMode,
16986        mutate: impl FnOnce(&mut CriticalMetadataImage),
16987    ) -> Vec<u8> {
16988        let locator = final_recovery_locator(volume);
16989        let tuple = CmraDecoderTuple::from(locator);
16990        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16991        let mut image = recovered.image;
16992        mutate(&mut image);
16993        refresh_critical_image_region_digests(&mut image);
16994        let image_bytes = image.to_bytes().unwrap();
16995
16996        let shard_size = tuple.shard_size as usize;
16997        let data_shard_count = image_bytes.len().div_ceil(shard_size);
16998        let parity_shard_count = tuple.parity_shard_count as usize;
16999        assert!(data_shard_count > 0);
17000        assert!(image_bytes.len() <= data_shard_count * shard_size);
17001
17002        let mut data_shards = Vec::with_capacity(data_shard_count);
17003        for idx in 0..data_shard_count {
17004            let start = idx * shard_size;
17005            let end = (start + shard_size).min(image_bytes.len());
17006            let mut payload = vec![0u8; shard_size];
17007            if start < image_bytes.len() {
17008                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
17009            }
17010            data_shards.push(payload);
17011        }
17012        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
17013        let image_sha256 = sha256_bytes(&image_bytes);
17014        let data_shard_count_u16 = u16::try_from(data_shard_count).unwrap();
17015        let image_length_u32 = u32::try_from(image_bytes.len()).unwrap();
17016
17017        let header = CriticalMetadataRecoveryHeader {
17018            shard_size: tuple.shard_size,
17019            data_shard_count: data_shard_count_u16,
17020            parity_shard_count: tuple.parity_shard_count,
17021            image_length: image_length_u32,
17022            archive_uuid_hint: locator.archive_uuid_hint,
17023            session_id_hint: locator.session_id_hint,
17024            volume_index_hint: locator.volume_index_hint,
17025            image_sha256,
17026            header_crc32c: 0,
17027        };
17028        let mut cmra = Vec::new();
17029        cmra.extend_from_slice(&header.to_bytes());
17030        for (idx, payload) in data_shards.into_iter().enumerate() {
17031            let payload_len = if idx + 1 == data_shard_count {
17032                image_bytes.len() - idx * shard_size
17033            } else {
17034                shard_size
17035            };
17036            cmra.extend_from_slice(
17037                &CriticalMetadataRecoveryShard {
17038                    shard_index: idx as u16,
17039                    shard_role: 0,
17040                    shard_payload_length: payload_len as u32,
17041                    payload,
17042                    shard_crc32c: 0,
17043                }
17044                .to_bytes(shard_size)
17045                .unwrap(),
17046            );
17047        }
17048        for (idx, payload) in parity_shards.into_iter().enumerate() {
17049            cmra.extend_from_slice(
17050                &CriticalMetadataRecoveryShard {
17051                    shard_index: (data_shard_count + idx) as u16,
17052                    shard_role: 1,
17053                    shard_payload_length: shard_size as u32,
17054                    payload,
17055                    shard_crc32c: 0,
17056                }
17057                .to_bytes(shard_size)
17058                .unwrap(),
17059            );
17060        }
17061
17062        let locator_base = CriticalRecoveryLocator {
17063            volume_format_rev: image.volume_format_rev,
17064            cmra_offset: locator.cmra_offset,
17065            cmra_length: cmra.len() as u32,
17066            volume_trailer_offset: locator.volume_trailer_offset,
17067            body_bytes_before_cmra: locator.body_bytes_before_cmra,
17068            archive_uuid_hint: locator.archive_uuid_hint,
17069            session_id_hint: locator.session_id_hint,
17070            volume_index_hint: locator.volume_index_hint,
17071            locator_sequence: 1,
17072            cmra_shard_size: tuple.shard_size,
17073            cmra_data_shard_count: data_shard_count_u16,
17074            cmra_parity_shard_count: tuple.parity_shard_count,
17075            cmra_image_length: image_length_u32,
17076            cmra_image_sha256: image_sha256,
17077            locator_crc32c: 0,
17078        };
17079
17080        let cmra_offset = locator.cmra_offset as usize;
17081        let mut out = Vec::new();
17082        out.extend_from_slice(&volume[..cmra_offset]);
17083        out.extend_from_slice(&cmra);
17084        out.extend_from_slice(&locator_base.to_bytes());
17085        out.extend_from_slice(
17086            &CriticalRecoveryLocator {
17087                locator_sequence: 0,
17088                ..locator_base
17089            }
17090            .to_bytes(),
17091        );
17092        out
17093    }
17094
17095    fn rewrite_recovery_locator(
17096        volume: &mut [u8],
17097        offset: usize,
17098        mutate: impl FnOnce(&mut CriticalRecoveryLocator),
17099    ) {
17100        let mut locator =
17101            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
17102                .unwrap();
17103        mutate(&mut locator);
17104        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
17105    }
17106
17107    fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
17108        image.volume_header_sha256 = sha256_bytes(
17109            &image
17110                .regions
17111                .iter()
17112                .find(|region| region.region_type == 1)
17113                .unwrap()
17114                .bytes,
17115        );
17116        image.crypto_header_sha256 = sha256_bytes(
17117            &image
17118                .regions
17119                .iter()
17120                .find(|region| region.region_type == 2)
17121                .unwrap()
17122                .bytes,
17123        );
17124        image.key_wrap_table_sha256 = image
17125            .regions
17126            .iter()
17127            .find(|region| region.region_type == 6)
17128            .map(|region| sha256_bytes(&region.bytes))
17129            .unwrap_or([0u8; 32]);
17130        image.manifest_footer_sha256 = sha256_bytes(
17131            &image
17132                .regions
17133                .iter()
17134                .find(|region| region.region_type == 3)
17135                .unwrap()
17136                .bytes,
17137        );
17138        image.root_auth_footer_sha256 = image
17139            .regions
17140            .iter()
17141            .find(|region| region.region_type == 4)
17142            .map(|region| sha256_bytes(&region.bytes))
17143            .unwrap_or([0u8; 32]);
17144        image.volume_trailer_sha256 = sha256_bytes(
17145            &image
17146                .regions
17147                .iter()
17148                .find(|region| region.region_type == 5)
17149                .unwrap()
17150                .bytes,
17151        );
17152    }
17153
17154    fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
17155        let mut locator =
17156            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
17157                .unwrap();
17158        locator.cmra_image_sha256 = image_sha256;
17159        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
17160    }
17161
17162    fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
17163        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17164        let final_locator = CriticalRecoveryLocator::parse(
17165            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
17166        )
17167        .unwrap();
17168        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
17169        volume[final_locator.cmra_offset as usize] ^= 0x55;
17170        volume[mirror_offset] ^= 0x55;
17171        volume[final_offset] ^= 0x55;
17172    }
17173
17174    fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
17175        let (record_offset, record_len) = first_block_record(volume);
17176        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17177        let mut record = BlockRecord::parse(
17178            &volume[record_offset..record_offset + record_len],
17179            block_size,
17180        )
17181        .unwrap();
17182        mutate(&mut record);
17183        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
17184    }
17185
17186    fn mutate_block_record_at_slot(
17187        volume: &mut [u8],
17188        slot: usize,
17189        mutate: impl FnOnce(&mut BlockRecord),
17190    ) {
17191        let (record_offset, record_len) = block_record_at_slot(volume, slot);
17192        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17193        let mut record = BlockRecord::parse(
17194            &volume[record_offset..record_offset + record_len],
17195            block_size,
17196        )
17197        .unwrap();
17198        mutate(&mut record);
17199        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
17200    }
17201
17202    fn first_block_record(volume: &[u8]) -> (usize, usize) {
17203        block_record_at_slot(volume, 0)
17204    }
17205
17206    fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
17207        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17208        let crypto_start = volume_header.crypto_header_offset as usize;
17209        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17210        let crypto_header = CryptoHeader::parse(
17211            &volume[crypto_start..crypto_end],
17212            volume_header.crypto_header_length,
17213        )
17214        .unwrap();
17215        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17216        let record_offset = crypto_end + slot * record_len;
17217        assert!(volume.len() >= record_offset + record_len);
17218        (record_offset, record_len)
17219    }
17220
17221    fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
17222        block_record_slots(volume)
17223            .into_iter()
17224            .enumerate()
17225            .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17226    }
17227
17228    fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
17229        block_record_slots(volume)
17230            .into_iter()
17231            .enumerate()
17232            .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17233            .collect()
17234    }
17235
17236    fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
17237        let mut slots = Vec::new();
17238        for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
17239            if record.kind == BlockKind::PayloadData {
17240                slots.push(slot);
17241            } else if !slots.is_empty() {
17242                break;
17243            }
17244        }
17245        slots
17246    }
17247
17248    fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
17249        envelope_entries_for_path(opened, path)
17250            .into_iter()
17251            .map(|entry| entry.envelope_index)
17252            .collect()
17253    }
17254
17255    fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
17256        let normalized =
17257            normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
17258        let located = opened.locate_index_file(&normalized).unwrap().unwrap();
17259        let file = &located.shard.files[located.file_index];
17260        frame_range_for_file(&located.shard, file)
17261            .unwrap()
17262            .iter()
17263            .map(|frame| {
17264                located
17265                    .shard
17266                    .envelopes
17267                    .iter()
17268                    .find(|entry| entry.envelope_index == frame.envelope_index)
17269                    .unwrap()
17270                    .clone()
17271            })
17272            .collect()
17273    }
17274
17275    fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
17276        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17277        let crypto_start = volume_header.crypto_header_offset as usize;
17278        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17279        let crypto_header = CryptoHeader::parse(
17280            &volume[crypto_start..crypto_end],
17281            volume_header.crypto_header_length,
17282        )
17283        .unwrap();
17284        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17285        let manifest_offset = terminal_material_offset(volume);
17286        assert_eq!((manifest_offset - crypto_end) % record_len, 0);
17287        let record_count = (manifest_offset - crypto_end) / record_len;
17288        (0..record_count)
17289            .map(|slot| {
17290                let offset = crypto_end + slot * record_len;
17291                let record = BlockRecord::parse(
17292                    &volume[offset..offset + record_len],
17293                    record_len - BLOCK_RECORD_FRAMING_LEN,
17294                )
17295                .unwrap();
17296                (offset, record_len, record)
17297            })
17298            .collect()
17299    }
17300
17301    fn rewrite_manifest_footer(
17302        volume: &mut [u8],
17303        master_key: &MasterKey,
17304        mutate: impl FnOnce(&mut ManifestFooter),
17305    ) {
17306        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17307        let offset = terminal_material_offset(volume);
17308        let mut footer =
17309            ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17310        mutate(&mut footer);
17311        footer.manifest_hmac = [0u8; 32];
17312        let mut footer_bytes = footer.to_bytes();
17313        let subkeys = Subkeys::derive(
17314            master_key,
17315            &volume_header.archive_uuid,
17316            &volume_header.session_id,
17317        )
17318        .unwrap();
17319        footer.manifest_hmac = compute_hmac(
17320            HmacDomain::ManifestFooter,
17321            &subkeys.mac_key,
17322            &volume_header.archive_uuid,
17323            &volume_header.session_id,
17324            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17325        );
17326        footer_bytes = footer.to_bytes();
17327        volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17328    }
17329
17330    fn rewrite_volume_trailer(
17331        volume: &mut [u8],
17332        master_key: &MasterKey,
17333        mutate: impl FnOnce(&mut VolumeTrailer),
17334    ) {
17335        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17336        let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
17337        let mut trailer =
17338            VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
17339        mutate(&mut trailer);
17340        trailer.trailer_hmac = [0u8; 32];
17341        let mut trailer_bytes = trailer.to_bytes();
17342        let subkeys = Subkeys::derive(
17343            master_key,
17344            &volume_header.archive_uuid,
17345            &volume_header.session_id,
17346        )
17347        .unwrap();
17348        trailer.trailer_hmac = compute_hmac(
17349            HmacDomain::VolumeTrailer,
17350            &subkeys.mac_key,
17351            &volume_header.archive_uuid,
17352            &volume_header.session_id,
17353            &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
17354        );
17355        trailer_bytes = trailer.to_bytes();
17356        volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
17357    }
17358
17359    fn rewrite_sidecar_header(
17360        sidecar: &mut [u8],
17361        master_key: &MasterKey,
17362        mutate: impl FnOnce(&mut BootstrapSidecarHeader),
17363    ) {
17364        let mut header =
17365            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17366        mutate(&mut header);
17367        write_signed_sidecar_header(sidecar, master_key, &mut header);
17368    }
17369
17370    fn write_signed_sidecar_header(
17371        sidecar: &mut [u8],
17372        master_key: &MasterKey,
17373        header: &mut BootstrapSidecarHeader,
17374    ) {
17375        header.sidecar_hmac = [0u8; 32];
17376        let mut header_bytes = header.to_bytes();
17377        let subkeys =
17378            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
17379        header.sidecar_hmac = compute_hmac(
17380            HmacDomain::BootstrapSidecar,
17381            &subkeys.mac_key,
17382            &header.archive_uuid,
17383            &header.session_id,
17384            &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
17385        );
17386        header_bytes = header.to_bytes();
17387        sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
17388    }
17389
17390    fn sparse_bootstrap_sidecar(
17391        source: &[u8],
17392        master_key: &MasterKey,
17393        include_manifest: bool,
17394        include_index_root: bool,
17395        include_dictionary: bool,
17396    ) -> Vec<u8> {
17397        let source_header =
17398            BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17399        let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
17400        let mut header = BootstrapSidecarHeader {
17401            archive_uuid: source_header.archive_uuid,
17402            session_id: source_header.session_id,
17403            flags: 0,
17404            manifest_footer_offset: 0,
17405            manifest_footer_length: 0,
17406            index_root_records_offset: 0,
17407            index_root_records_length: 0,
17408            dictionary_records_offset: 0,
17409            dictionary_records_length: 0,
17410            sidecar_hmac: [0u8; 32],
17411            header_crc32c: 0,
17412        };
17413
17414        if include_manifest {
17415            assert!(source_header.has_manifest_footer());
17416            let (offset, length) = append_sidecar_section(
17417                source,
17418                &mut sidecar,
17419                source_header.manifest_footer_offset,
17420                source_header.manifest_footer_length as u64,
17421            );
17422            header.flags |= 0x01;
17423            header.manifest_footer_offset = offset;
17424            header.manifest_footer_length = length as u32;
17425        }
17426        if include_index_root {
17427            assert!(source_header.has_index_root_records());
17428            let (offset, length) = append_sidecar_section(
17429                source,
17430                &mut sidecar,
17431                source_header.index_root_records_offset,
17432                source_header.index_root_records_length,
17433            );
17434            header.flags |= 0x02;
17435            header.index_root_records_offset = offset;
17436            header.index_root_records_length = length;
17437        }
17438        if include_dictionary {
17439            assert!(source_header.has_dictionary_records());
17440            let (offset, length) = append_sidecar_section(
17441                source,
17442                &mut sidecar,
17443                source_header.dictionary_records_offset,
17444                source_header.dictionary_records_length,
17445            );
17446            header.flags |= 0x04;
17447            header.dictionary_records_offset = offset;
17448            header.dictionary_records_length = length;
17449        }
17450
17451        write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
17452        sidecar
17453    }
17454
17455    fn append_sidecar_section(
17456        source: &[u8],
17457        sidecar: &mut Vec<u8>,
17458        source_offset: u64,
17459        length: u64,
17460    ) -> (u64, u64) {
17461        let source_offset = source_offset as usize;
17462        let length = length as usize;
17463        let offset = sidecar.len() as u64;
17464        sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
17465        (offset, length as u64)
17466    }
17467
17468    fn mutate_sidecar_manifest(
17469        sidecar: &mut [u8],
17470        master_key: &MasterKey,
17471        mutate: impl FnOnce(&mut ManifestFooter),
17472    ) {
17473        let header =
17474            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17475        let offset = header.manifest_footer_offset as usize;
17476        let mut footer =
17477            ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17478        mutate(&mut footer);
17479        footer.manifest_hmac = [0u8; 32];
17480        let mut footer_bytes = footer.to_bytes();
17481        let subkeys =
17482            Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
17483        footer.manifest_hmac = compute_hmac(
17484            HmacDomain::ManifestFooter,
17485            &subkeys.mac_key,
17486            &footer.archive_uuid,
17487            &footer.session_id,
17488            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17489        );
17490        footer_bytes = footer.to_bytes();
17491        sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17492    }
17493
17494    fn mutate_sidecar_index_record(
17495        sidecar: &mut [u8],
17496        record_index: usize,
17497        mutate: impl FnOnce(&mut BlockRecord),
17498    ) {
17499        let header =
17500            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17501        let record_len = sidecar_record_len(sidecar);
17502        let offset = header.index_root_records_offset as usize + record_index * record_len;
17503        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17504        let mut record =
17505            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17506        mutate(&mut record);
17507        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17508    }
17509
17510    fn mutate_sidecar_dictionary_record(
17511        sidecar: &mut [u8],
17512        record_index: usize,
17513        mutate: impl FnOnce(&mut BlockRecord),
17514    ) {
17515        let header =
17516            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17517        let record_len = sidecar_record_len(sidecar);
17518        let offset = header.dictionary_records_offset as usize + record_index * record_len;
17519        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17520        let mut record =
17521            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17522        mutate(&mut record);
17523        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17524    }
17525
17526    fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
17527        let header =
17528            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17529        let record_len = sidecar_record_len(sidecar);
17530        let left_offset = header.index_root_records_offset as usize + left * record_len;
17531        let right_offset = header.index_root_records_offset as usize + right * record_len;
17532        for idx in 0..record_len {
17533            sidecar.swap(left_offset + idx, right_offset + idx);
17534        }
17535    }
17536
17537    fn sidecar_record_len(sidecar: &[u8]) -> usize {
17538        let header =
17539            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17540        let footer_offset = header.manifest_footer_offset as usize;
17541        let footer =
17542            ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
17543                .unwrap();
17544        let index_record_count = footer.index_root_data_block_count as usize
17545            + footer.index_root_parity_block_count as usize;
17546        header.index_root_records_length as usize / index_record_count
17547    }
17548
17549    fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
17550        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17551        assert_eq!(volume_header.volume_index, 0);
17552        assert_eq!(volume_header.stripe_width, 1);
17553        let crypto_start = volume_header.crypto_header_offset as usize;
17554        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17555        let crypto_header = CryptoHeader::parse(
17556            &volume[crypto_start..crypto_end],
17557            volume_header.crypto_header_length,
17558        )
17559        .unwrap();
17560        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17561        let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
17562        for offset in 0..record_count {
17563            let block_index = extent.first_block_index + offset;
17564            let record_offset = crypto_end + block_index as usize * record_len;
17565            volume[record_offset + 16] ^= 0x55;
17566        }
17567    }
17568
17569    fn terminal_material_offset(volume: &[u8]) -> usize {
17570        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17571        let crypto_start = volume_header.crypto_header_offset as usize;
17572        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17573        let crypto_header = CryptoHeader::parse(
17574            &volume[crypto_start..crypto_end],
17575            volume_header.crypto_header_length,
17576        )
17577        .unwrap();
17578        let (_, offset, _) = parse_stream_block_prefix(
17579            volume,
17580            crypto_end,
17581            crypto_header.fixed.block_size as usize,
17582            &volume_header,
17583        )
17584        .unwrap();
17585        offset
17586    }
17587
17588    #[derive(Debug)]
17589    struct TestObject {
17590        extent: ObjectExtent,
17591        records: Vec<BlockRecord>,
17592    }
17593
17594    #[derive(Debug)]
17595    struct TestFileMeta {
17596        path: Vec<u8>,
17597        frame_index: u64,
17598        tar_stream_offset: u64,
17599        member_group_size: u64,
17600        file_data_size: u64,
17601    }
17602
17603    fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
17604        let volume_header = test_volume_header();
17605        let crypto_header = test_crypto_header();
17606        let subkeys = Subkeys::derive(
17607            &master_key(),
17608            &volume_header.archive_uuid,
17609            &volume_header.session_id,
17610        )
17611        .unwrap();
17612        let mut next_block_index = 0u64;
17613        let mut blocks = BTreeMap::new();
17614
17615        let healthy = test_member(b"healthy.txt", b"healthy payload\n");
17616        let broken = test_member(b"broken.txt", b"broken payload\n");
17617        let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
17618
17619        let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
17620        let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
17621
17622        let healthy_payload = encrypt_test_object(
17623            &healthy_frame,
17624            &subkeys.enc_key,
17625            &subkeys.nonce_seed,
17626            b"envelope",
17627            0,
17628            BlockKind::PayloadData,
17629            &mut next_block_index,
17630            &crypto_header,
17631            &volume_header,
17632        );
17633        let broken_payload = encrypt_test_object(
17634            &broken_frame,
17635            &subkeys.enc_key,
17636            &subkeys.nonce_seed,
17637            b"envelope",
17638            1,
17639            BlockKind::PayloadData,
17640            &mut next_block_index,
17641            &crypto_header,
17642            &volume_header,
17643        );
17644        let broken_payload_block = broken_payload.extent.first_block_index;
17645        insert_records(&mut blocks, &healthy_payload.records);
17646        insert_records(&mut blocks, &broken_payload.records);
17647
17648        let frames = vec![
17649            FrameEntry {
17650                frame_index: 0,
17651                envelope_index: 0,
17652                offset_in_envelope: 0,
17653                compressed_size: healthy_frame.len() as u32,
17654                decompressed_size: healthy.len() as u32,
17655                flags: 0x0000_0003,
17656                tar_stream_offset: 0,
17657            },
17658            FrameEntry {
17659                frame_index: 1,
17660                envelope_index: 1,
17661                offset_in_envelope: 0,
17662                compressed_size: broken_frame.len() as u32,
17663                decompressed_size: broken.len() as u32,
17664                flags: 0x0000_0003,
17665                tar_stream_offset: healthy.len() as u64,
17666            },
17667        ];
17668        let envelopes = vec![
17669            EnvelopeEntry {
17670                envelope_index: 0,
17671                first_block_index: healthy_payload.extent.first_block_index,
17672                data_block_count: healthy_payload.extent.data_block_count,
17673                parity_block_count: 0,
17674                encrypted_size: healthy_payload.extent.encrypted_size,
17675                plaintext_size: healthy_frame.len() as u32,
17676                first_frame_index: 0,
17677                frame_count: 1,
17678            },
17679            EnvelopeEntry {
17680                envelope_index: 1,
17681                first_block_index: broken_payload.extent.first_block_index,
17682                data_block_count: broken_payload.extent.data_block_count,
17683                parity_block_count: 0,
17684                encrypted_size: broken_payload.extent.encrypted_size,
17685                plaintext_size: broken_frame.len() as u32,
17686                first_frame_index: 1,
17687                frame_count: 1,
17688            },
17689        ];
17690        let files = vec![
17691            TestFileMeta {
17692                path: b"healthy.txt".to_vec(),
17693                frame_index: 0,
17694                tar_stream_offset: 0,
17695                member_group_size: healthy.len() as u64,
17696                file_data_size: b"healthy payload\n".len() as u64,
17697            },
17698            TestFileMeta {
17699                path: b"broken.txt".to_vec(),
17700                frame_index: 1,
17701                tar_stream_offset: healthy.len() as u64,
17702                member_group_size: broken.len() as u64,
17703                file_data_size: b"broken payload\n".len() as u64,
17704            },
17705        ];
17706
17707        let (index_shard_plaintext, first_path_hash, last_path_hash) =
17708            build_test_index_shard(&files, &frames, &envelopes);
17709        let index_shard = encrypt_test_object(
17710            &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
17711            &subkeys.index_shard_key,
17712            &subkeys.index_nonce_seed,
17713            b"idxshard",
17714            0,
17715            BlockKind::IndexShardData,
17716            &mut next_block_index,
17717            &crypto_header,
17718            &volume_header,
17719        );
17720        insert_records(&mut blocks, &index_shard.records);
17721
17722        let shard_entry = ShardEntry {
17723            shard_index: 0,
17724            first_block_index: index_shard.extent.first_block_index,
17725            data_block_count: index_shard.extent.data_block_count,
17726            parity_block_count: 0,
17727            encrypted_size: index_shard.extent.encrypted_size,
17728            decompressed_size: index_shard_plaintext.len() as u32,
17729            file_count: files.len() as u32,
17730            first_path_hash,
17731            last_path_hash,
17732        };
17733        let mut root_header = IndexRootHeader::empty();
17734        root_header.frame_count = frames.len() as u64;
17735        root_header.envelope_count = envelopes.len() as u64;
17736        root_header.file_count = files.len() as u64;
17737        root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
17738            + broken_payload.extent.data_block_count as u64;
17739        root_header.tar_total_size = tar_stream.len() as u64;
17740        root_header.content_sha256 = sha256_bytes(&tar_stream);
17741        let index_root = IndexRoot {
17742            header: root_header,
17743            shards: vec![shard_entry],
17744            directory_hint_shards: Vec::new(),
17745        };
17746
17747        let index_root_plaintext = index_root.to_bytes();
17748        let index_root_object = encrypt_test_object(
17749            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
17750            &subkeys.index_root_key,
17751            &subkeys.index_nonce_seed,
17752            b"idxroot",
17753            0,
17754            BlockKind::IndexRootData,
17755            &mut next_block_index,
17756            &crypto_header,
17757            &volume_header,
17758        );
17759        insert_records(&mut blocks, &index_root_object.records);
17760
17761        let archive_uuid = volume_header.archive_uuid;
17762        let session_id = volume_header.session_id;
17763        let opened = OpenedArchive {
17764            options: ReaderOptions::default(),
17765            observed_archive_bytes: 1_000_000,
17766            observed_volume_count: 1,
17767            subkeys,
17768            blocks,
17769            lazy_blocks: None,
17770            crypto_header_bytes: Vec::new(),
17771            volume_header,
17772            crypto_header,
17773            manifest_footer: ManifestFooter {
17774                archive_uuid,
17775                session_id,
17776                volume_index: 0,
17777                is_authoritative: 1,
17778                total_volumes: 1,
17779                index_root_first_block: index_root_object.extent.first_block_index,
17780                index_root_data_block_count: index_root_object.extent.data_block_count,
17781                index_root_parity_block_count: 0,
17782                index_root_encrypted_size: index_root_object.extent.encrypted_size,
17783                index_root_decompressed_size: index_root_plaintext.len() as u32,
17784                manifest_hmac: [0u8; 32],
17785            },
17786            volume_trailer: Some(VolumeTrailer {
17787                archive_uuid,
17788                session_id,
17789                volume_index: 0,
17790                block_count: next_block_index,
17791                bytes_written: 0,
17792                manifest_footer_offset: 0,
17793                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
17794                closed_at_ns: 0,
17795                root_auth_footer_offset: 0,
17796                root_auth_footer_length: 0,
17797                root_auth_flags: 0,
17798                trailer_hmac: [0u8; 32],
17799            }),
17800            root_auth_footer: None,
17801            index_root,
17802            payload_dictionary: None,
17803        };
17804        (opened, broken_payload_block)
17805    }
17806
17807    fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
17808        let locating = opened.index_root.shards[0].clone();
17809        let mut shard = opened.load_index_shard(&locating).unwrap();
17810        mutate(&mut shard);
17811        let plaintext = shard.to_bytes();
17812        let mut next_block_index = opened
17813            .blocks
17814            .keys()
17815            .last()
17816            .copied()
17817            .map(|index| index + 1)
17818            .unwrap_or(0);
17819        let replacement = encrypt_test_object(
17820            &compress_zstd_frame(&plaintext, 1).unwrap(),
17821            &opened.subkeys.index_shard_key,
17822            &opened.subkeys.index_nonce_seed,
17823            b"idxshard",
17824            locating.shard_index,
17825            BlockKind::IndexShardData,
17826            &mut next_block_index,
17827            &opened.crypto_header,
17828            &opened.volume_header,
17829        );
17830        insert_records(&mut opened.blocks, &replacement.records);
17831        opened.index_root.shards[0] = ShardEntry {
17832            shard_index: locating.shard_index,
17833            first_block_index: replacement.extent.first_block_index,
17834            data_block_count: replacement.extent.data_block_count,
17835            parity_block_count: 0,
17836            encrypted_size: replacement.extent.encrypted_size,
17837            decompressed_size: plaintext.len() as u32,
17838            file_count: shard.files.len() as u32,
17839            first_path_hash: shard.files.first().unwrap().path_hash,
17840            last_path_hash: shard.files.last().unwrap().path_hash,
17841        };
17842    }
17843
17844    fn rewrite_as_single_healthy_file(
17845        opened: &mut OpenedArchive,
17846        mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
17847    ) {
17848        let healthy_path = b"healthy.txt";
17849        let healthy_payload = b"healthy payload\n";
17850        let healthy_member = test_member(healthy_path, healthy_payload);
17851        replace_first_index_shard(opened, |shard| {
17852            let file_index = (0..shard.files.len())
17853                .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
17854                .unwrap();
17855            let mut file = shard.files[file_index].clone();
17856            let frame = shard
17857                .frames
17858                .iter()
17859                .find(|entry| entry.frame_index == 0)
17860                .unwrap()
17861                .clone();
17862            let envelope = shard
17863                .envelopes
17864                .iter()
17865                .find(|entry| entry.envelope_index == 0)
17866                .unwrap()
17867                .clone();
17868            let mut path = healthy_path.to_vec();
17869
17870            file.path_offset = 0;
17871            file.path_length = path.len() as u32;
17872            file.first_frame_index = 0;
17873            file.frame_count = 1;
17874            file.offset_in_first_frame_plaintext = 0;
17875            file.tar_member_group_size = healthy_member.len() as u64;
17876            file.file_data_size = healthy_payload.len() as u64;
17877            file.flags = 0;
17878            mutate(&mut file, &mut path);
17879            file.path_offset = 0;
17880            file.path_length = path.len() as u32;
17881            file.path_hash = hash_prefix(&path);
17882
17883            shard.files = vec![file];
17884            shard.frames = vec![frame];
17885            shard.envelopes = vec![envelope];
17886            shard.string_pool = path;
17887        });
17888
17889        opened.index_root.header.file_count = 1;
17890        opened.index_root.header.frame_count = 1;
17891        opened.index_root.header.envelope_count = 1;
17892        opened.index_root.header.payload_block_count = 1;
17893        opened.index_root.header.tar_total_size = healthy_member.len() as u64;
17894        opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
17895    }
17896
17897    fn test_volume_header() -> VolumeHeader {
17898        VolumeHeader {
17899            format_version: FORMAT_VERSION,
17900            volume_format_rev: VOLUME_FORMAT_REV,
17901            volume_index: 0,
17902            stripe_width: 1,
17903            archive_uuid: [0x31; 16],
17904            session_id: [0x42; 16],
17905            crypto_header_offset: VOLUME_HEADER_LEN as u32,
17906            crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
17907            header_crc32c: 0,
17908        }
17909    }
17910
17911    fn test_crypto_header() -> CryptoHeaderFixed {
17912        CryptoHeaderFixed {
17913            length: CRYPTO_HEADER_FIXED_LEN as u32,
17914            compression_algo: CompressionAlgo::ZstdFramed,
17915            aead_algo: AeadAlgo::AesGcmSiv256,
17916            fec_algo: FecAlgo::ReedSolomonGF16,
17917            kdf_algo: KdfAlgo::Raw,
17918            chunk_size: 4096,
17919            envelope_target_size: 8192,
17920            block_size: 4096,
17921            fec_data_shards: 4,
17922            fec_parity_shards: 0,
17923            index_fec_data_shards: 4,
17924            index_fec_parity_shards: 0,
17925            index_root_fec_data_shards: 4,
17926            index_root_fec_parity_shards: 0,
17927            stripe_width: 1,
17928            volume_loss_tolerance: 0,
17929            bit_rot_buffer_pct: 0,
17930            has_dictionary: 0,
17931            max_path_length: 4096,
17932            expected_volume_size: 0,
17933        }
17934    }
17935
17936    #[allow(clippy::too_many_arguments)]
17937    fn encrypt_test_object(
17938        plaintext: &[u8],
17939        key: &[u8; 32],
17940        nonce_seed: &[u8; 32],
17941        domain: &[u8],
17942        counter: u64,
17943        data_kind: BlockKind,
17944        next_block_index: &mut u64,
17945        crypto_header: &CryptoHeaderFixed,
17946        volume_header: &VolumeHeader,
17947    ) -> TestObject {
17948        let block_size = crypto_header.block_size as usize;
17949        let encrypted = encrypt_padded_aead_object(
17950            AeadObjectContext {
17951                algo: crypto_header.aead_algo,
17952                key,
17953                nonce_seed,
17954                domain,
17955                archive_uuid: &volume_header.archive_uuid,
17956                session_id: &volume_header.session_id,
17957                counter,
17958            },
17959            block_size,
17960            plaintext,
17961        )
17962        .unwrap();
17963        assert_eq!(encrypted.len() % block_size, 0);
17964
17965        let first_block_index = *next_block_index;
17966        let data_block_count = encrypted.len() / block_size;
17967        let records = encrypted
17968            .chunks(block_size)
17969            .enumerate()
17970            .map(|(index, payload)| BlockRecord {
17971                block_index: first_block_index + index as u64,
17972                kind: data_kind,
17973                flags: if index + 1 == data_block_count {
17974                    0x01
17975                } else {
17976                    0
17977                },
17978                payload: payload.to_vec(),
17979                record_crc32c: 0,
17980            })
17981            .collect::<Vec<_>>();
17982        *next_block_index += data_block_count as u64;
17983
17984        TestObject {
17985            extent: ObjectExtent {
17986                first_block_index,
17987                data_block_count: data_block_count as u32,
17988                parity_block_count: 0,
17989                encrypted_size: encrypted.len() as u32,
17990            },
17991            records,
17992        }
17993    }
17994
17995    fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
17996        for record in records {
17997            assert!(blocks.insert(record.block_index, record.clone()).is_none());
17998        }
17999    }
18000
18001    #[allow(clippy::too_many_arguments)]
18002    fn build_metadata_object_from_payload(
18003        payload: &[u8],
18004        _subkeys: &Subkeys,
18005        volume_header: &VolumeHeader,
18006        crypto_header: &CryptoHeaderFixed,
18007        key: &[u8; 32],
18008        nonce_seed: &[u8; 32],
18009        domain: &[u8],
18010        counter: u64,
18011        data_kind: BlockKind,
18012        next_block_index: &mut u64,
18013    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
18014        let compressed = compress_zstd_frame(payload, 1).unwrap();
18015        build_metadata_object_from_compressed(
18016            &compressed,
18017            key,
18018            nonce_seed,
18019            domain,
18020            counter,
18021            data_kind,
18022            next_block_index,
18023            crypto_header,
18024            volume_header,
18025        )
18026    }
18027
18028    #[allow(clippy::too_many_arguments)]
18029    fn build_metadata_object_from_compressed(
18030        compressed: &[u8],
18031        key: &[u8; 32],
18032        nonce_seed: &[u8; 32],
18033        domain: &[u8],
18034        counter: u64,
18035        data_kind: BlockKind,
18036        next_block_index: &mut u64,
18037        crypto_header: &CryptoHeaderFixed,
18038        volume_header: &VolumeHeader,
18039    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
18040        let object = encrypt_test_object(
18041            compressed,
18042            key,
18043            nonce_seed,
18044            domain,
18045            counter,
18046            data_kind,
18047            next_block_index,
18048            crypto_header,
18049            volume_header,
18050        );
18051
18052        let mut blocks = BTreeMap::new();
18053        for record in object.records {
18054            blocks.insert(record.block_index, record);
18055        }
18056        (object.extent, blocks)
18057    }
18058
18059    #[allow(clippy::too_many_arguments)]
18060    fn assert_metadata_object_from_compressed(
18061        compressed: &[u8],
18062        decompressed_size: usize,
18063        _subkeys: &Subkeys,
18064        volume_header: &VolumeHeader,
18065        crypto_header: &CryptoHeaderFixed,
18066        key: &[u8; 32],
18067        nonce_seed: &[u8; 32],
18068        domain: &[u8],
18069        counter: u64,
18070        data_kind: BlockKind,
18071        parity_kind: BlockKind,
18072        class_data_shards: u16,
18073        class_parity_shards: u16,
18074        next_block_index: &mut u64,
18075        expected: FormatError,
18076    ) {
18077        let (extent, blocks) = build_metadata_object_from_compressed(
18078            compressed,
18079            key,
18080            nonce_seed,
18081            domain,
18082            counter,
18083            data_kind,
18084            next_block_index,
18085            crypto_header,
18086            volume_header,
18087        );
18088        let error = load_metadata_object_from_parts(
18089            &blocks,
18090            ObjectLoadContext {
18091                volume_header,
18092                crypto_header,
18093                extent,
18094                data_kind,
18095                parity_kind,
18096                key,
18097                nonce_seed,
18098                domain,
18099                counter,
18100                class_data_shard_max: class_data_shards,
18101                class_parity_shard_max: class_parity_shards,
18102            },
18103            decompressed_size as u32,
18104        )
18105        .unwrap_err();
18106        assert_eq!(error, expected);
18107    }
18108
18109    fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
18110        let record = blocks.get_mut(&block_index).unwrap();
18111        assert_eq!(record.kind, BlockKind::PayloadData);
18112        record.payload[0] ^= 0x55;
18113    }
18114
18115    fn build_test_index_shard(
18116        files: &[TestFileMeta],
18117        frames: &[FrameEntry],
18118        envelopes: &[EnvelopeEntry],
18119    ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
18120        let mut sorted = files
18121            .iter()
18122            .map(|file| (hash_prefix(&file.path), file))
18123            .collect::<Vec<_>>();
18124        sorted.sort_by(|left, right| {
18125            (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
18126                right.0,
18127                right.1.path.as_slice(),
18128                right.1.tar_stream_offset,
18129            ))
18130        });
18131
18132        let mut string_pool = Vec::new();
18133        let mut file_entries = Vec::with_capacity(sorted.len());
18134        for (path_hash, file) in &sorted {
18135            let path_offset = string_pool.len() as u32;
18136            string_pool.extend_from_slice(&file.path);
18137            file_entries.push(FileEntry {
18138                path_hash: *path_hash,
18139                path_offset,
18140                path_length: file.path.len() as u32,
18141                first_frame_index: file.frame_index,
18142                frame_count: 1,
18143                offset_in_first_frame_plaintext: 0,
18144                tar_member_group_size: file.member_group_size,
18145                file_data_size: file.file_data_size,
18146                kind: crate::tar_model::TarEntryKind::Regular,
18147                mode: 0o644,
18148                mtime: 0,
18149                flags: 0,
18150            });
18151        }
18152
18153        let header = IndexShardHeader {
18154            version: 1,
18155            shard_index: 0,
18156            file_count: file_entries.len() as u32,
18157            frame_count: frames.len() as u32,
18158            envelope_count: envelopes.len() as u32,
18159            file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
18160            frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
18161                as u32,
18162            envelope_table_offset: (INDEX_SHARD_HEADER_LEN
18163                + file_entries.len() * FILE_ENTRY_LEN
18164                + frames.len() * FRAME_ENTRY_LEN) as u32,
18165            string_pool_offset: (INDEX_SHARD_HEADER_LEN
18166                + file_entries.len() * FILE_ENTRY_LEN
18167                + frames.len() * FRAME_ENTRY_LEN
18168                + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
18169            string_pool_size: string_pool.len() as u32,
18170        };
18171
18172        let mut bytes = Vec::new();
18173        bytes.extend_from_slice(&header.to_bytes());
18174        for entry in &file_entries {
18175            bytes.extend_from_slice(&entry.to_bytes());
18176        }
18177        for entry in frames {
18178            bytes.extend_from_slice(&entry.to_bytes());
18179        }
18180        for entry in envelopes {
18181            bytes.extend_from_slice(&entry.to_bytes());
18182        }
18183        bytes.extend_from_slice(&string_pool);
18184
18185        (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
18186    }
18187
18188    fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
18189        let mut out = Vec::new();
18190        out.extend_from_slice(&test_tar_header(path, data.len() as u64));
18191        out.extend_from_slice(data);
18192        out.resize(out.len() + padding_to_512(data.len()), 0);
18193        out
18194    }
18195
18196    fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
18197        let mut header = [0u8; 512];
18198        header[..path.len()].copy_from_slice(path);
18199        write_test_tar_octal(&mut header[100..108], 0o644);
18200        write_test_tar_octal(&mut header[108..116], 0);
18201        write_test_tar_octal(&mut header[116..124], 0);
18202        write_test_tar_octal(&mut header[124..136], size);
18203        write_test_tar_octal(&mut header[136..148], 0);
18204        header[148..156].fill(b' ');
18205        header[156] = b'0';
18206        header[257..263].copy_from_slice(b"ustar\0");
18207        header[263..265].copy_from_slice(b"00");
18208        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
18209        write_test_tar_checksum(&mut header[148..156], checksum);
18210        header
18211    }
18212
18213    fn write_test_tar_octal(field: &mut [u8], value: u64) {
18214        let digits = format!("{value:o}");
18215        field.fill(0);
18216        let start = field.len() - 1 - digits.len();
18217        field[..start].fill(b'0');
18218        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
18219    }
18220
18221    fn write_test_tar_checksum(field: &mut [u8], value: u64) {
18222        let digits = format!("{value:06o}");
18223        field[0..6].copy_from_slice(digits.as_bytes());
18224        field[6] = 0;
18225        field[7] = b' ';
18226    }
18227
18228    fn padding_to_512(len: usize) -> usize {
18229        let remainder = len % 512;
18230        if remainder == 0 {
18231            0
18232        } else {
18233            512 - remainder
18234        }
18235    }
18236}