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::entry_metadata::ArchiveTimestamp;
15use crate::fec::{encode_parity_gf16, repair_data_gf16};
16use crate::format::{
17    AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, VolumeFormatRevision,
18    BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
19    CRITICAL_METADATA_RECOVERY_HEADER_LEN, CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN,
20    CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_HEADER_HMAC_LEN, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN,
21    MANIFEST_FOOTER_LEN, MASTER_KEY_LEN, READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN,
22    READER_MAX_KEY_WRAP_TABLE_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN,
23    VOLUME_FORMAT_REV_45, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
24};
25use crate::metadata::{
26    hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
27    EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
28};
29use crate::non_seekable_reader::{
30    StreamedEnvelopeSummary, StreamedFrameSummary, StreamedPayloadSummary,
31};
32use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
33use crate::root_auth::{
34    archive_root_for_revision, critical_metadata_digest, data_block_merkle_root_for_revision,
35    fec_layout_digest_for_revision, index_digest_for_revision,
36    root_auth_descriptor_digest_for_revision, signer_identity_digest, ArchiveRootInputs,
37    CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
38};
39use crate::tar_model::{
40    metadata_verification_report, parse_tar_member_group, plan_owned_member_restore, restore_phase,
41    restore_regular_file_metadata_to_open_file, restore_streaming_tar_member_group,
42    stream_regular_tar_member_group_to_writer, validate_owned_restore_plan,
43    validate_tar_stream_total_extraction_size, MetadataDiagnostic, MetadataVerificationReport,
44    NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, StreamingMemberExpectation,
45    TarEntryKind, TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
46    TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
47};
48use crate::wire::{
49    compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
50    CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
51    CryptoHeader, CryptoHeaderFixed, ExtensionTlv, KeyWrapTableV1, ManifestFooter,
52    RootAuthFooterV1, VolumeHeader, VolumeTrailer,
53};
54
55const TRAILER_HMAC_COVERED_LEN: usize = 96;
56const MANIFEST_HMAC_COVERED_LEN: usize = 104;
57const SIDECAR_HMAC_COVERED_LEN: usize = 92;
58const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
59const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
60const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
61const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
62
63fn default_jobs() -> usize {
64    std::thread::available_parallelism()
65        .map(|jobs| jobs.get())
66        .unwrap_or(1)
67}
68
69pub trait ArchiveReadAt: Send + Sync + 'static {
70    fn len(&self) -> Result<u64, FormatError>;
71    fn is_empty(&self) -> Result<bool, FormatError> {
72        Ok(self.len()? == 0)
73    }
74    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
75}
76
77pub type RecipientWrapCandidateMasterKey = [u8; MASTER_KEY_LEN];
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct RecipientWrapArchiveIdentity {
81    pub archive_uuid: [u8; 16],
82    pub session_id: [u8; 16],
83    pub format_version: u16,
84    pub volume_format_rev: u16,
85}
86
87#[derive(Debug, Clone, Copy)]
88pub struct RecipientWrapRecordContext<'a> {
89    pub archive_identity: RecipientWrapArchiveIdentity,
90    pub record: &'a crate::wire::RecipientRecordV1,
91}
92
93impl ArchiveReadAt for File {
94    fn len(&self) -> Result<u64, FormatError> {
95        self.metadata()
96            .map(|metadata| metadata.len())
97            .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
98    }
99
100    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
101        file_read_exact_at(self, offset, buf)
102    }
103}
104
105#[cfg(unix)]
106fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
107    use std::os::unix::fs::FileExt;
108
109    file_read_exact_at_with(offset, buf, |chunk, offset| file.read_at(chunk, offset))
110}
111
112#[cfg(windows)]
113fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
114    use std::os::windows::fs::FileExt;
115
116    file_read_exact_at_with(offset, buf, |chunk, offset| file.seek_read(chunk, offset))
117}
118
119#[cfg(any(unix, windows))]
120fn file_read_exact_at_with<F>(
121    mut offset: u64,
122    mut buf: &mut [u8],
123    mut read_at: F,
124) -> Result<(), FormatError>
125where
126    F: FnMut(&mut [u8], u64) -> std::io::Result<usize>,
127{
128    while !buf.is_empty() {
129        let read =
130            read_at(buf, offset).map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
131        if read == 0 {
132            return Err(FormatError::InvalidArchive("archive read failed"));
133        }
134        offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
135        let rest = std::mem::take(&mut buf).split_at_mut(read).1;
136        buf = rest;
137    }
138    Ok(())
139}
140
141#[cfg(not(any(unix, windows)))]
142fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
143    let mut file = file
144        .try_clone()
145        .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
146    std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
147        .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
148    file.read_exact(buf)
149        .map_err(|_| FormatError::InvalidArchive("archive read failed"))
150}
151
152impl ArchiveReadAt for Vec<u8> {
153    fn len(&self) -> Result<u64, FormatError> {
154        u64::try_from(self.len())
155            .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
156    }
157
158    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
159        let offset = to_usize(offset, "archive")?;
160        let end = checked_add(offset, buf.len(), "archive")?;
161        let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
162            structure: "archive",
163            expected: end,
164            actual: self.len(),
165        })?;
166        buf.copy_from_slice(source);
167        Ok(())
168    }
169}
170
171impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
172    fn len(&self) -> Result<u64, FormatError> {
173        (**self).len()
174    }
175
176    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
177        (**self).read_exact_at(offset, buf)
178    }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct ReaderOptions {
183    pub max_trailing_garbage_scan: usize,
184    pub max_verify_tar_size: usize,
185    pub max_total_extraction_size: u64,
186    pub jobs: usize,
187}
188
189impl Default for ReaderOptions {
190    fn default() -> Self {
191        Self {
192            max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
193            max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
194            max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
195            jobs: default_jobs(),
196        }
197    }
198}
199
200pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
201    if options.jobs == 0 {
202        return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
203    }
204    Ok(())
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct ArchiveEntry {
209    pub path: String,
210    pub file_data_size: u64,
211    pub kind: TarEntryKind,
212    pub mode: u32,
213    pub mtime: ArchiveTimestamp,
214    pub diagnostics: Vec<MetadataDiagnostic>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct ArchiveIndexEntry {
219    pub path: String,
220    pub name: String,
221    pub file_data_size: u64,
222    pub flags: u32,
223    pub path_hash: [u8; 8],
224    pub tar_member_group_size: u64,
225    pub first_frame_index: u64,
226    pub frame_count: u32,
227    pub offset_in_first_frame_plaintext: u32,
228    pub layout: ArchiveIndexEntryLayout,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct ArchiveIndexEntryLayout {
233    pub compressed_size: u64,
234    pub decompressed_frame_size: u64,
235    pub envelope_count: u32,
236    pub first_envelope_index: Option<u64>,
237    pub last_envelope_index: Option<u64>,
238    pub first_payload_block_index: Option<u64>,
239    pub payload_data_block_count: u64,
240    pub payload_parity_block_count: u64,
241    pub payload_encrypted_size: u64,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct ExtractedArchiveMember {
246    pub path: String,
247    pub kind: TarEntryKind,
248    pub data: Vec<u8>,
249    pub link_target: Option<String>,
250    pub reparse_placeholder: bool,
251    pub diagnostics: Vec<MetadataDiagnostic>,
252}
253
254/// Receives logical regular-file bytes while the archive reader extracts data.
255///
256/// Callbacks report uncompressed member payload bytes after they are accepted by
257/// the destination writer. Each selected file is capped by its authenticated
258/// `file_data_size`.
259pub trait ArchiveExtractProgressSink {
260    /// Reports newly extracted payload bytes for one archive member.
261    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
262}
263
264impl<F> ArchiveExtractProgressSink for F
265where
266    F: FnMut(&str, u64),
267{
268    fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
269        self(archive_path, bytes);
270    }
271}
272
273#[derive(Debug, Clone)]
274pub struct OpenedArchive {
275    options: ReaderOptions,
276    observed_archive_bytes: u64,
277    observed_volume_count: u32,
278    subkeys: Subkeys,
279    blocks: BTreeMap<u64, BlockRecord>,
280    lazy_blocks: Option<Arc<SeekableBlockSource>>,
281    crypto_header_bytes: Vec<u8>,
282    pub volume_header: VolumeHeader,
283    pub crypto_header: CryptoHeaderFixed,
284    pub manifest_footer: ManifestFooter,
285    pub volume_trailer: Option<VolumeTrailer>,
286    pub root_auth_footer: Option<RootAuthFooterV1>,
287    pub index_root: IndexRoot,
288    payload_dictionary: Option<Vec<u8>>,
289}
290
291#[derive(Debug)]
292pub struct ArchiveContentVerification<'a> {
293    archive: &'a OpenedArchive,
294    mode: ContentVerificationMode,
295    metadata_report: Option<MetadataVerificationReport>,
296}
297
298impl ArchiveContentVerification<'_> {
299    /// Per-entry revision-45 capture, profile, restore-capability, and fidelity
300    /// results. Fast verification returns `None` when payload semantics were
301    /// deliberately deferred.
302    pub fn metadata_report(&self) -> Option<&MetadataVerificationReport> {
303        self.metadata_report.as_ref()
304    }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308enum ContentVerificationMode {
309    Full,
310    Fast,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct ArchiveRepairPatch {
315    pub volume_index: u32,
316    pub block_index: u64,
317    pub record_offset: u64,
318    pub record_bytes: Vec<u8>,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum RootAuthDiagnostic {
323    RootAuthContentVerified,
324    RootAuthDeferredFullArchiveScanRequired,
325    AuthenticatedMetadataNotRootSigned,
326    RecoveryMarginNotRootAuthenticated,
327    ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
328    RecoveryMarginChecked,
329    RecoveryMarginFailed,
330    RecoveryMarginUnchecked,
331}
332
333impl RootAuthDiagnostic {
334    pub const fn label(self) -> &'static str {
335        match self {
336            Self::RootAuthContentVerified => "root_auth_content_verified",
337            Self::RootAuthDeferredFullArchiveScanRequired => {
338                "root_auth_deferred_full_archive_scan_required"
339            }
340            Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
341            Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
342            Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
343                "replicated_global_copy_unchecked_due_to_volume_loss"
344            }
345            Self::RecoveryMarginChecked => "recovery_margin_checked",
346            Self::RecoveryMarginFailed => "recovery_margin_failed",
347            Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
348        }
349    }
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum PublicNoKeyDiagnostic {
354    PublicDataBlockCommitmentVerified,
355    PublicPhysicalCompletenessUnverified,
356    PublicRecoveryMarginUnchecked,
357}
358
359impl PublicNoKeyDiagnostic {
360    pub const fn label(self) -> &'static str {
361        match self {
362            Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
363            Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
364            Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
365        }
366    }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct RootAuthVerification {
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<RootAuthDiagnostic>,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct PublicNoKeyVerification {
383    pub format_version: u16,
384    pub volume_format_rev: u16,
385    pub archive_root: [u8; 32],
386    pub authenticator_id: u16,
387    pub signer_identity_type: u16,
388    pub signer_identity_bytes: Vec<u8>,
389    pub total_data_block_count: u64,
390    pub diagnostics: Vec<PublicNoKeyDiagnostic>,
391}
392
393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394struct RootAuthMaterial {
395    critical_metadata_digest: [u8; 32],
396    index_digest: [u8; 32],
397    fec_layout_digest: [u8; 32],
398    data_block_merkle_root: [u8; 32],
399    signer_identity_digest: [u8; 32],
400    archive_root: [u8; 32],
401    total_data_block_count: u64,
402}
403
404#[derive(Debug, Clone, Copy)]
405struct ObjectExtent {
406    first_block_index: u64,
407    data_block_count: u32,
408    parity_block_count: u32,
409    encrypted_size: u32,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413enum ParityReadPolicy {
414    Always,
415    RepairOnly,
416}
417
418pub(crate) struct StreamedArchiveOpenParts {
419    pub(crate) options: ReaderOptions,
420    pub(crate) observed_archive_bytes: u64,
421    pub(crate) subkeys: Subkeys,
422    pub(crate) blocks: BTreeMap<u64, BlockRecord>,
423    pub(crate) crypto_header_bytes: Vec<u8>,
424    pub(crate) volume_header: VolumeHeader,
425    pub(crate) crypto_header: CryptoHeaderFixed,
426    pub(crate) manifest_footer: ManifestFooter,
427    pub(crate) volume_trailer: VolumeTrailer,
428    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
429}
430
431#[derive(Clone, Copy)]
432struct WinningIndexEntry {
433    start: u64,
434    file_data_size: u64,
435    shard_index: usize,
436    file_index: usize,
437}
438
439struct LocatedIndexFile {
440    shard: IndexShard,
441    file_index: usize,
442    start: u64,
443}
444
445struct ExtractProgressWriter<'a, W> {
446    inner: &'a mut W,
447    archive_path: &'a str,
448    file_data_size: u64,
449    reported_bytes: u64,
450    progress: &'a mut dyn ArchiveExtractProgressSink,
451}
452
453impl<'a, W> ExtractProgressWriter<'a, W> {
454    fn new(
455        inner: &'a mut W,
456        archive_path: &'a str,
457        file_data_size: u64,
458        progress: &'a mut dyn ArchiveExtractProgressSink,
459    ) -> Self {
460        Self {
461            inner,
462            archive_path,
463            file_data_size,
464            reported_bytes: 0,
465            progress,
466        }
467    }
468
469    fn report(&mut self, bytes: u64) {
470        if bytes == 0 || self.file_data_size == 0 {
471            return;
472        }
473        let capped_next = self
474            .reported_bytes
475            .saturating_add(bytes)
476            .min(self.file_data_size);
477        let delta = capped_next.saturating_sub(self.reported_bytes);
478        if delta == 0 {
479            return;
480        }
481        self.reported_bytes = capped_next;
482        self.progress.file_bytes_extracted(self.archive_path, delta);
483    }
484}
485
486impl<W: Write> Write for ExtractProgressWriter<'_, W> {
487    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
488        let written = self.inner.write(buf)?;
489        self.report(written as u64);
490        Ok(written)
491    }
492
493    fn flush(&mut self) -> std::io::Result<()> {
494        self.inner.flush()
495    }
496}
497
498struct DecodedTarMemberGroupReader<'a> {
499    archive: &'a OpenedArchive,
500    shard: &'a IndexShard,
501    file: &'a FileEntry,
502    decompressor: zstd::bulk::Decompressor<'static>,
503    next_frame_offset: u64,
504    cached_envelope_index: Option<u64>,
505    cached_envelope_plaintext: Vec<u8>,
506    current_frame: Vec<u8>,
507    current_frame_offset: usize,
508    remaining_group_bytes: u64,
509}
510
511struct SeekableVolumeSource {
512    reader: Arc<dyn ArchiveReadAt>,
513    volume_index: u32,
514    block_records_start: u64,
515    block_count: u64,
516    record_len: u64,
517    block_size: usize,
518}
519
520impl std::fmt::Debug for SeekableVolumeSource {
521    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522        f.debug_struct("SeekableVolumeSource")
523            .field("volume_index", &self.volume_index)
524            .field("block_records_start", &self.block_records_start)
525            .field("block_count", &self.block_count)
526            .field("record_len", &self.record_len)
527            .field("block_size", &self.block_size)
528            .finish_non_exhaustive()
529    }
530}
531
532#[derive(Debug)]
533struct SeekableBlockSource {
534    stripe_width: u32,
535    volumes: Vec<Option<SeekableVolumeSource>>,
536}
537
538trait BlockProvider {
539    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
540}
541
542struct OpenedBlockProvider<'a> {
543    memory_blocks: &'a BTreeMap<u64, BlockRecord>,
544    lazy_blocks: Option<&'a SeekableBlockSource>,
545}
546
547impl SeekableBlockSource {
548    fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
549        if self.stripe_width == 0 {
550            return Err(FormatError::ZeroStripeWidth);
551        }
552        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
553            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
554        let Some(volume) = self
555            .volumes
556            .get(volume_index as usize)
557            .and_then(Option::as_ref)
558        else {
559            return Err(FormatError::InvalidArchive(
560                "repair output requires all archive volumes",
561            ));
562        };
563        let slot = block_index / self.stripe_width as u64;
564        if slot >= volume.block_count {
565            return Err(FormatError::InvalidArchive(
566                "BlockRecord global coverage has a gap",
567            ));
568        }
569        Ok((volume_index, volume.record_offset(slot)?))
570    }
571
572    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
573        if self.stripe_width == 0 {
574            return Err(FormatError::ZeroStripeWidth);
575        }
576        let volume_index = u32::try_from(block_index % self.stripe_width as u64)
577            .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
578        let Some(volume) = self
579            .volumes
580            .get(volume_index as usize)
581            .and_then(Option::as_ref)
582        else {
583            return Ok(None);
584        };
585        let slot = block_index / self.stripe_width as u64;
586        if slot >= volume.block_count {
587            return Ok(None);
588        }
589        match volume.read_slot(slot, block_index) {
590            Ok(record) => Ok(Some(record)),
591            Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
592            Err(err) => Err(err),
593        }
594    }
595
596    fn is_complete_volume_set(&self) -> bool {
597        self.volumes.iter().all(Option::is_some)
598    }
599
600    fn total_block_count(&self) -> Result<u64, FormatError> {
601        self.volumes
602            .iter()
603            .map(|volume| {
604                volume
605                    .as_ref()
606                    .map(|volume| volume.block_count)
607                    .ok_or(FormatError::InvalidArchive(
608                        "missing volume in complete set",
609                    ))
610            })
611            .try_fold(0u64, |sum, count| {
612                checked_u64_add(sum, count?, "BlockRecord count overflow")
613            })
614    }
615}
616
617impl SeekableVolumeSource {
618    fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
619        self.block_records_start
620            .checked_add(checked_u64_mul(
621                slot,
622                self.record_len,
623                "BlockRecord offset overflow",
624            )?)
625            .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
626    }
627
628    fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
629        let record_offset = self.record_offset(slot)?;
630        let raw = read_at_vec_unchecked(
631            self.reader.as_ref(),
632            record_offset,
633            usize::try_from(self.record_len)
634                .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
635        )?;
636        let record = BlockRecord::parse(&raw, self.block_size)?;
637        if record.block_index != expected_block_index {
638            return Err(FormatError::InvalidArchive(
639                "BlockRecord index does not match volume position",
640            ));
641        }
642        Ok(record)
643    }
644}
645
646impl BlockProvider for BTreeMap<u64, BlockRecord> {
647    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
648        Ok(self.get(&block_index).cloned())
649    }
650}
651
652impl BlockProvider for OpenedBlockProvider<'_> {
653    fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
654        if let Some(record) = self.memory_blocks.get(&block_index) {
655            return Ok(Some(record.clone()));
656        }
657        match self.lazy_blocks {
658            Some(source) => source.block(block_index),
659            None => Ok(None),
660        }
661    }
662}
663
664fn subkeys_for_open(
665    master_key: Option<&MasterKey>,
666    aead_algo: AeadAlgo,
667    archive_uuid: &[u8; 16],
668    session_id: &[u8; 16],
669) -> Result<Subkeys, FormatError> {
670    if aead_algo.is_encrypted() {
671        Subkeys::derive(
672            master_key.ok_or(FormatError::KeyMaterialMismatch)?,
673            archive_uuid,
674            session_id,
675        )
676    } else {
677        Ok(Subkeys::unencrypted_placeholder())
678    }
679}
680
681type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
682pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
683const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
684    "fast full extract requires unique archive paths";
685
686fn parse_volume_format_dispatch(
687    volume_header: &VolumeHeader,
688) -> Result<VolumeFormatRevision, FormatError> {
689    let revision = volume_header.parse_volume_format_revision()?;
690    match revision {
691        VolumeFormatRevision::V45 => Ok(revision),
692    }
693}
694
695#[derive(Debug)]
696struct PayloadIndexTables {
697    shards: Vec<IndexShard>,
698    file_count: u64,
699    frames: BTreeMap<u64, FrameEntry>,
700    envelopes: BTreeMap<u64, EnvelopeEntry>,
701}
702
703pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
704    OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
705}
706
707pub fn open_archive_with_recipient_wrap_resolver<F>(
708    bytes: &[u8],
709    resolver: F,
710) -> Result<OpenedArchive, FormatError>
711where
712    F: FnMut(
713        RecipientWrapRecordContext<'_>,
714    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
715{
716    OpenedArchive::open_with_recipient_wrap_resolver_options(
717        bytes,
718        resolver,
719        ReaderOptions::default(),
720    )
721}
722
723pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
724    require_unencrypted_volume_profile(bytes)?;
725    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
726    OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
727}
728
729pub fn open_archive_volumes(
730    volumes: &[&[u8]],
731    master_key: &MasterKey,
732) -> Result<OpenedArchive, FormatError> {
733    OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
734}
735
736pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
737    for volume in volumes {
738        require_unencrypted_volume_profile(volume)?;
739    }
740    let placeholder = MasterKey::from_raw_key(&[0; 32])?;
741    OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
742}
743
744pub fn open_archive_with_bootstrap_sidecar(
745    bytes: &[u8],
746    bootstrap_sidecar: &[u8],
747    master_key: &MasterKey,
748) -> Result<OpenedArchive, FormatError> {
749    OpenedArchive::open_with_bootstrap_sidecar_options(
750        bytes,
751        bootstrap_sidecar,
752        master_key,
753        ReaderOptions::default(),
754    )
755}
756
757fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
758    if bytes.len() < VOLUME_HEADER_LEN {
759        return Err(FormatError::InvalidLength {
760            structure: "archive",
761            expected: VOLUME_HEADER_LEN,
762            actual: bytes.len(),
763        });
764    }
765    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
766    parse_volume_format_dispatch(&volume_header)?;
767    let crypto_start = volume_header.crypto_header_offset as usize;
768    let crypto_len = volume_header.crypto_header_length as usize;
769    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
770    let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
771    if crypto_header.fixed.aead_algo == AeadAlgo::None
772        && crypto_header.fixed.kdf_algo == KdfAlgo::None
773    {
774        Ok(())
775    } else {
776        Err(FormatError::KeyMaterialMismatch)
777    }
778}
779
780pub fn open_seekable_archive<R: ArchiveReadAt>(
781    reader: R,
782    master_key: &MasterKey,
783) -> Result<OpenedArchive, FormatError> {
784    OpenedArchive::open_seekable_volumes_with_options(
785        vec![reader],
786        master_key,
787        ReaderOptions::default(),
788    )
789}
790
791pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
792    readers: Vec<R>,
793    master_key: &MasterKey,
794) -> Result<OpenedArchive, FormatError> {
795    OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
796}
797
798pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
799    reader: R,
800    bootstrap_sidecar: &[u8],
801    master_key: &MasterKey,
802) -> Result<OpenedArchive, FormatError> {
803    open_seekable_archive_with_bootstrap_sidecar_options(
804        reader,
805        bootstrap_sidecar,
806        master_key,
807        ReaderOptions::default(),
808    )
809}
810
811pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
812    reader: R,
813    bootstrap_sidecar: &[u8],
814    master_key: &MasterKey,
815    options: ReaderOptions,
816) -> Result<OpenedArchive, FormatError> {
817    OpenedArchive::open_seekable_volumes_with_options_for_mode(
818        vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
819        master_key,
820        options,
821        Some(bootstrap_sidecar),
822    )
823}
824
825pub fn open_seekable_archive_with_recipient_wrap_resolver_options<R, F>(
826    reader: R,
827    resolver: F,
828    options: ReaderOptions,
829) -> Result<OpenedArchive, FormatError>
830where
831    R: ArchiveReadAt,
832    F: FnMut(
833        RecipientWrapRecordContext<'_>,
834    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
835{
836    OpenedArchive::open_seekable_with_recipient_wrap_resolver_options(reader, resolver, options)
837}
838
839pub fn open_seekable_archive_volumes_with_recipient_wrap_resolver_options<R, F>(
840    readers: Vec<R>,
841    resolver: F,
842    options: ReaderOptions,
843) -> Result<OpenedArchive, FormatError>
844where
845    R: ArchiveReadAt,
846    F: FnMut(
847        RecipientWrapRecordContext<'_>,
848    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
849{
850    OpenedArchive::open_seekable_volumes_with_recipient_wrap_resolver_options(
851        readers, resolver, options,
852    )
853}
854
855pub fn open_non_seekable_archive(
856    bytes: &[u8],
857    master_key: &MasterKey,
858    bootstrap_sidecar: Option<&[u8]>,
859) -> Result<OpenedArchive, FormatError> {
860    match bootstrap_sidecar {
861        Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
862            bytes,
863            sidecar,
864            master_key,
865            ReaderOptions::default(),
866            BootstrapSidecarUse::NonSeekableRandomAccess,
867        ),
868        None => Err(FormatError::ReaderUnsupported(
869            "non-seekable random access requires a bootstrap sidecar",
870        )),
871    }
872}
873
874pub fn public_no_key_verify_archive_with<F>(
875    bytes: &[u8],
876    verifier: F,
877) -> Result<PublicNoKeyVerification, FormatError>
878where
879    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
880{
881    public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
882}
883
884pub fn public_no_key_verify_volumes_with<F>(
885    volumes: &[&[u8]],
886    verifier: F,
887) -> Result<PublicNoKeyVerification, FormatError>
888where
889    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
890{
891    public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
892}
893
894/// Decode a single-volume, dictionary-free non-seekable archive image into tar
895/// bytes after authenticating its terminal ManifestFooter and VolumeTrailer.
896///
897/// This is a whole-buffer helper, not a live provisional-output API.
898/// Callers receive no decoded bytes if terminal authentication fails.
899pub fn sequential_extract_tar_stream(
900    bytes: &[u8],
901    master_key: &MasterKey,
902) -> Result<Vec<u8>, FormatError> {
903    sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
904}
905
906impl OpenedArchive {
907    fn block_provider(&self) -> OpenedBlockProvider<'_> {
908        OpenedBlockProvider {
909            memory_blocks: &self.blocks,
910            lazy_blocks: self.lazy_blocks.as_deref(),
911        }
912    }
913
914    fn missing_volume_count(&self) -> u32 {
915        self.crypto_header
916            .stripe_width
917            .saturating_sub(self.observed_volume_count)
918    }
919
920    fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
921        let mut diagnostics = vec![
922            RootAuthDiagnostic::RootAuthContentVerified,
923            RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
924            RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
925        ];
926        if self.missing_volume_count() > 0 {
927            diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
928        }
929        diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
930        diagnostics
931    }
932
933    pub fn open_with_options(
934        bytes: &[u8],
935        master_key: &MasterKey,
936        options: ReaderOptions,
937    ) -> Result<Self, FormatError> {
938        Self::open_volumes_with_options(&[bytes], master_key, options)
939    }
940
941    pub fn open_with_recipient_wrap_resolver_options<F>(
942        bytes: &[u8],
943        mut resolver: F,
944        options: ReaderOptions,
945    ) -> Result<Self, FormatError>
946    where
947        F: FnMut(
948            RecipientWrapRecordContext<'_>,
949        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
950    {
951        validate_reader_options(options)?;
952        let observed_archive_bytes = observed_archive_size(std::iter::once(bytes.len() as u64))?;
953        let parsed =
954            parse_seekable_volume_with_recipient_wrap_resolver(bytes, &mut resolver, options)?;
955        let ParsedSeekableVolume {
956            volume_header,
957            crypto_header,
958            crypto_header_bytes,
959            key_wrap_table_bytes: _,
960            subkeys,
961            manifest_footer,
962            manifest_footer_error,
963            root_auth_footer,
964            root_auth_footer_bytes: _,
965            volume_trailer,
966            blocks,
967            erased_block_indices,
968        } = parsed;
969        let manifest_footer = match manifest_footer {
970            Some(footer) => footer,
971            None => {
972                return Err(manifest_footer_error.unwrap_or(FormatError::InvalidArchive(
973                    "no authenticated ManifestFooter found",
974                )));
975            }
976        };
977        let observed_volume_count = 1;
978        let missing_volume_count = crypto_header
979            .stripe_width
980            .checked_sub(observed_volume_count)
981            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
982        if missing_volume_count > crypto_header.volume_loss_tolerance as u32 {
983            return Err(FormatError::InvalidArchive(
984                "missing volume count exceeds volume_loss_tolerance",
985            ));
986        }
987        if missing_volume_count == 0 {
988            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
989        }
990
991        let limits = metadata_limits(&crypto_header);
992        let index_root_plaintext = load_metadata_object_from_parts(
993            &blocks,
994            ObjectLoadContext::index_root(
995                &volume_header,
996                &crypto_header,
997                &subkeys,
998                ObjectExtent {
999                    first_block_index: manifest_footer.index_root_first_block,
1000                    data_block_count: manifest_footer.index_root_data_block_count,
1001                    parity_block_count: manifest_footer.index_root_parity_block_count,
1002                    encrypted_size: manifest_footer.index_root_encrypted_size,
1003                },
1004            ),
1005            manifest_footer.index_root_decompressed_size,
1006        )?;
1007        let index_root = IndexRoot::parse(
1008            &index_root_plaintext,
1009            crypto_header.has_dictionary != 0,
1010            limits,
1011        )?;
1012        let payload_dictionary = load_archive_dictionary(
1013            &blocks,
1014            &subkeys,
1015            &volume_header,
1016            &crypto_header,
1017            &index_root,
1018        )?;
1019
1020        Ok(Self {
1021            options,
1022            observed_archive_bytes,
1023            observed_volume_count,
1024            subkeys,
1025            blocks,
1026            lazy_blocks: None,
1027            crypto_header_bytes,
1028            volume_header,
1029            crypto_header,
1030            manifest_footer,
1031            volume_trailer: Some(volume_trailer),
1032            root_auth_footer,
1033            index_root,
1034            payload_dictionary,
1035        })
1036    }
1037
1038    pub fn open_seekable_with_recipient_wrap_resolver_options<R, F>(
1039        reader: R,
1040        mut resolver: F,
1041        options: ReaderOptions,
1042    ) -> Result<Self, FormatError>
1043    where
1044        R: ArchiveReadAt,
1045        F: FnMut(
1046            RecipientWrapRecordContext<'_>,
1047        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1048    {
1049        validate_reader_options(options)?;
1050        let reader = Arc::new(reader) as Arc<dyn ArchiveReadAt>;
1051        let observed_len = reader.len()?;
1052        let observed_archive_bytes = observed_archive_size([observed_len])?;
1053        let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1054            reader.clone(),
1055            &mut resolver,
1056            options,
1057        )?;
1058        let manifest_footer = match parsed.manifest_footer.take() {
1059            Some(footer) => footer,
1060            None => {
1061                return Err(parsed.manifest_footer_error.take().unwrap_or(
1062                    FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1063                ));
1064            }
1065        };
1066        let observed_volume_count = 1;
1067        let missing_volume_count = parsed
1068            .crypto_header
1069            .stripe_width
1070            .checked_sub(observed_volume_count)
1071            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1072        if missing_volume_count > parsed.crypto_header.volume_loss_tolerance as u32 {
1073            return Err(FormatError::InvalidArchive(
1074                "missing volume count exceeds volume_loss_tolerance",
1075            ));
1076        }
1077
1078        let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1079        let mut lazy_volume_slots = Vec::new();
1080        lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1081        let slot = parsed.volume_header.volume_index as usize;
1082        if slot >= lazy_volume_slots.len() {
1083            return Err(FormatError::InvalidArchive(
1084                "authenticated volume index exceeds stripe_width",
1085            ));
1086        }
1087        lazy_volume_slots[slot] = Some(SeekableVolumeSource {
1088            reader: parsed.reader.clone(),
1089            volume_index: parsed.volume_header.volume_index,
1090            block_records_start: parsed.block_records_start,
1091            block_count: parsed.volume_trailer.block_count,
1092            record_len,
1093            block_size: parsed.crypto_header.block_size as usize,
1094        });
1095        let lazy_source = Arc::new(SeekableBlockSource {
1096            stripe_width: parsed.crypto_header.stripe_width,
1097            volumes: lazy_volume_slots,
1098        });
1099        let blocks = BTreeMap::new();
1100        let block_provider = OpenedBlockProvider {
1101            memory_blocks: &blocks,
1102            lazy_blocks: Some(lazy_source.as_ref()),
1103        };
1104        let limits = metadata_limits(&parsed.crypto_header);
1105        let index_root_plaintext = load_metadata_object_from_parts(
1106            &block_provider,
1107            ObjectLoadContext::index_root(
1108                &parsed.volume_header,
1109                &parsed.crypto_header,
1110                &parsed.subkeys,
1111                index_root_extent_from_manifest(&manifest_footer),
1112            ),
1113            manifest_footer.index_root_decompressed_size,
1114        )?;
1115        let index_root = IndexRoot::parse(
1116            &index_root_plaintext,
1117            parsed.crypto_header.has_dictionary != 0,
1118            limits,
1119        )?;
1120        let block_provider = OpenedBlockProvider {
1121            memory_blocks: &blocks,
1122            lazy_blocks: Some(lazy_source.as_ref()),
1123        };
1124        let payload_dictionary = load_archive_dictionary(
1125            &block_provider,
1126            &parsed.subkeys,
1127            &parsed.volume_header,
1128            &parsed.crypto_header,
1129            &index_root,
1130        )?;
1131
1132        Ok(Self {
1133            options,
1134            observed_archive_bytes,
1135            observed_volume_count,
1136            subkeys: parsed.subkeys,
1137            blocks,
1138            lazy_blocks: Some(lazy_source),
1139            crypto_header_bytes: parsed.crypto_header_bytes,
1140            volume_header: parsed.volume_header,
1141            crypto_header: parsed.crypto_header,
1142            manifest_footer,
1143            volume_trailer: Some(parsed.volume_trailer),
1144            root_auth_footer: parsed.root_auth_footer,
1145            index_root,
1146            payload_dictionary,
1147        })
1148    }
1149
1150    pub fn open_seekable_volumes_with_recipient_wrap_resolver_options<R, F>(
1151        readers: Vec<R>,
1152        mut resolver: F,
1153        options: ReaderOptions,
1154    ) -> Result<Self, FormatError>
1155    where
1156        R: ArchiveReadAt,
1157        F: FnMut(
1158            RecipientWrapRecordContext<'_>,
1159        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1160    {
1161        validate_reader_options(options)?;
1162        if readers.is_empty() {
1163            return Err(FormatError::InvalidArchive("no volumes supplied"));
1164        }
1165        let readers = readers
1166            .into_iter()
1167            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1168            .collect::<Vec<_>>();
1169        let observed_archive_bytes = observed_archive_size(
1170            readers
1171                .iter()
1172                .map(|reader| reader.len())
1173                .collect::<Result<Vec<_>, _>>()?,
1174        )?;
1175        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1176        let mut manifest_authority: Option<ManifestFooter> = None;
1177        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1178        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1179        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1180        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1181        let mut saw_root_auth_absent = false;
1182        let mut first_manifest_footer_error: Option<FormatError> = None;
1183        let mut seen_volume_indexes = BTreeSet::new();
1184        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1185
1186        for reader in readers {
1187            let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1188                reader,
1189                &mut resolver,
1190                options,
1191            )?;
1192            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1193                return Err(FormatError::InvalidArchive(
1194                    "duplicate authenticated volume index",
1195                ));
1196            }
1197
1198            if let Some(first) = &first {
1199                validate_volume_set_member_metadata(
1200                    &first.volume_header,
1201                    &first.crypto_header,
1202                    &first.crypto_header_bytes,
1203                    &parsed.volume_header,
1204                    &parsed.crypto_header,
1205                    &parsed.crypto_header_bytes,
1206                )?;
1207                validate_key_wrap_table_bytes_match(
1208                    &first.key_wrap_table_bytes,
1209                    &parsed.key_wrap_table_bytes,
1210                )?;
1211            } else {
1212                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1213            }
1214
1215            if let Some(footer) = &parsed.manifest_footer {
1216                if let Some(authority) = &manifest_authority {
1217                    if !manifest_bootstrap_fields_match(authority, footer) {
1218                        return Err(FormatError::InvalidArchive(
1219                            "ManifestFooter bootstrap fields differ",
1220                        ));
1221                    }
1222                } else {
1223                    manifest_authority = Some(footer.clone());
1224                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1225                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1226                }
1227            } else if first_manifest_footer_error.is_none() {
1228                first_manifest_footer_error = parsed.manifest_footer_error.take();
1229            }
1230
1231            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1232                (Some(footer), Some(bytes)) => {
1233                    if saw_root_auth_absent {
1234                        return Err(FormatError::InvalidArchive(
1235                            "root-auth footer presence differs across volumes",
1236                        ));
1237                    }
1238                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1239                        if authority_bytes != bytes {
1240                            return Err(FormatError::InvalidArchive(
1241                                "RootAuthFooter copies differ",
1242                            ));
1243                        }
1244                    } else {
1245                        root_auth_authority = Some(footer.clone());
1246                        root_auth_authority_bytes = Some(bytes.clone());
1247                    }
1248                }
1249                (None, None) => {
1250                    if root_auth_authority_bytes.is_some() {
1251                        return Err(FormatError::InvalidArchive(
1252                            "root-auth footer presence differs across volumes",
1253                        ));
1254                    }
1255                    saw_root_auth_absent = true;
1256                }
1257                _ => {
1258                    return Err(FormatError::InvalidArchive(
1259                        "root-auth footer terminal state is inconsistent",
1260                    ));
1261                }
1262            }
1263
1264            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1265            let source = SeekableVolumeSource {
1266                reader: parsed.reader.clone(),
1267                volume_index: parsed.volume_header.volume_index,
1268                block_records_start: parsed.block_records_start,
1269                block_count: parsed.volume_trailer.block_count,
1270                record_len,
1271                block_size: parsed.crypto_header.block_size as usize,
1272            };
1273            let slot = parsed.volume_header.volume_index as usize;
1274            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1275            {
1276                return Err(FormatError::InvalidArchive(
1277                    "duplicate authenticated volume index",
1278                ));
1279            }
1280
1281            if first.is_none() {
1282                first = Some(parsed);
1283            }
1284        }
1285
1286        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1287        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1288            Some(err) => err,
1289            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1290        })?;
1291        let authority_volume_header = manifest_authority_volume_header.ok_or(
1292            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1293        )?;
1294        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1295            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1296        )?;
1297        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1298            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1299        let missing_volume_count = first
1300            .crypto_header
1301            .stripe_width
1302            .checked_sub(observed_volume_count)
1303            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1304        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1305            return Err(FormatError::InvalidArchive(
1306                "missing volume count exceeds volume_loss_tolerance",
1307            ));
1308        }
1309
1310        let blocks = BTreeMap::new();
1311        let lazy_source = Arc::new(SeekableBlockSource {
1312            stripe_width: first.crypto_header.stripe_width,
1313            volumes: lazy_volume_slots,
1314        });
1315        let block_provider = OpenedBlockProvider {
1316            memory_blocks: &blocks,
1317            lazy_blocks: Some(lazy_source.as_ref()),
1318        };
1319        let limits = metadata_limits(&first.crypto_header);
1320        let index_root_plaintext = load_metadata_object_from_parts(
1321            &block_provider,
1322            ObjectLoadContext::index_root(
1323                &first.volume_header,
1324                &first.crypto_header,
1325                &first.subkeys,
1326                index_root_extent_from_manifest(&manifest_footer),
1327            ),
1328            manifest_footer.index_root_decompressed_size,
1329        )?;
1330        let index_root = IndexRoot::parse(
1331            &index_root_plaintext,
1332            first.crypto_header.has_dictionary != 0,
1333            limits,
1334        )?;
1335        let block_provider = OpenedBlockProvider {
1336            memory_blocks: &blocks,
1337            lazy_blocks: Some(lazy_source.as_ref()),
1338        };
1339        let payload_dictionary = load_archive_dictionary(
1340            &block_provider,
1341            &first.subkeys,
1342            &first.volume_header,
1343            &first.crypto_header,
1344            &index_root,
1345        )?;
1346
1347        Ok(Self {
1348            options,
1349            observed_archive_bytes,
1350            observed_volume_count,
1351            subkeys: first.subkeys,
1352            blocks,
1353            lazy_blocks: Some(lazy_source),
1354            crypto_header_bytes: first.crypto_header_bytes,
1355            volume_header: authority_volume_header,
1356            crypto_header: first.crypto_header,
1357            manifest_footer,
1358            volume_trailer: Some(authority_volume_trailer),
1359            root_auth_footer: root_auth_authority,
1360            index_root,
1361            payload_dictionary,
1362        })
1363    }
1364
1365    pub fn open_volumes_with_options(
1366        volumes: &[&[u8]],
1367        master_key: &MasterKey,
1368        options: ReaderOptions,
1369    ) -> Result<Self, FormatError> {
1370        validate_reader_options(options)?;
1371        if volumes.is_empty() {
1372            return Err(FormatError::InvalidArchive("no volumes supplied"));
1373        }
1374
1375        let observed_archive_bytes =
1376            observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
1377        let mut first: Option<ParsedSeekableVolume> = None;
1378        let mut manifest_authority: Option<ManifestFooter> = None;
1379        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1380        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1381        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1382        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1383        let mut saw_root_auth_absent = false;
1384        let mut first_manifest_footer_error: Option<FormatError> = None;
1385        let mut seen_volume_indexes = BTreeSet::new();
1386        let mut blocks = BTreeMap::new();
1387        let mut erased_block_indices = BTreeSet::new();
1388
1389        for volume_bytes in volumes {
1390            let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
1391            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1392                return Err(FormatError::InvalidArchive(
1393                    "duplicate authenticated volume index",
1394                ));
1395            }
1396
1397            if let Some(first) = &first {
1398                validate_volume_set_member(first, &parsed)?;
1399            }
1400
1401            if let Some(footer) = &parsed.manifest_footer {
1402                if let Some(authority) = &manifest_authority {
1403                    if !manifest_bootstrap_fields_match(authority, footer) {
1404                        return Err(FormatError::InvalidArchive(
1405                            "ManifestFooter bootstrap fields differ",
1406                        ));
1407                    }
1408                } else {
1409                    manifest_authority = Some(footer.clone());
1410                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1411                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1412                }
1413            } else if first_manifest_footer_error.is_none() {
1414                first_manifest_footer_error = parsed.manifest_footer_error.take();
1415            }
1416
1417            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1418                (Some(footer), Some(bytes)) => {
1419                    if saw_root_auth_absent {
1420                        return Err(FormatError::InvalidArchive(
1421                            "root-auth footer presence differs across volumes",
1422                        ));
1423                    }
1424                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1425                        if authority_bytes != bytes {
1426                            return Err(FormatError::InvalidArchive(
1427                                "RootAuthFooter copies differ",
1428                            ));
1429                        }
1430                    } else {
1431                        root_auth_authority = Some(footer.clone());
1432                        root_auth_authority_bytes = Some(bytes.clone());
1433                    }
1434                }
1435                (None, None) => {
1436                    if root_auth_authority_bytes.is_some() {
1437                        return Err(FormatError::InvalidArchive(
1438                            "root-auth footer presence differs across volumes",
1439                        ));
1440                    }
1441                    saw_root_auth_absent = true;
1442                }
1443                _ => {
1444                    return Err(FormatError::InvalidArchive(
1445                        "root-auth footer terminal state is inconsistent",
1446                    ));
1447                }
1448            }
1449
1450            for (block_index, record) in &parsed.blocks {
1451                if blocks.insert(*block_index, record.clone()).is_some() {
1452                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1453                }
1454            }
1455            for block_index in &parsed.erased_block_indices {
1456                erased_block_indices.insert(*block_index);
1457            }
1458
1459            if first.is_none() {
1460                first = Some(parsed);
1461            }
1462        }
1463
1464        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1465        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1466            Some(err) => err,
1467            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1468        })?;
1469        let authority_volume_header = manifest_authority_volume_header.ok_or(
1470            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1471        )?;
1472        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1473            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1474        )?;
1475        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1476            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1477        let missing_volume_count = first
1478            .crypto_header
1479            .stripe_width
1480            .checked_sub(observed_volume_count)
1481            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1482        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1483            return Err(FormatError::InvalidArchive(
1484                "missing volume count exceeds volume_loss_tolerance",
1485            ));
1486        }
1487        if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
1488            validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
1489        }
1490
1491        let limits = metadata_limits(&first.crypto_header);
1492        let index_root_plaintext = load_metadata_object_from_parts(
1493            &blocks,
1494            ObjectLoadContext::index_root(
1495                &first.volume_header,
1496                &first.crypto_header,
1497                &first.subkeys,
1498                ObjectExtent {
1499                    first_block_index: manifest_footer.index_root_first_block,
1500                    data_block_count: manifest_footer.index_root_data_block_count,
1501                    parity_block_count: manifest_footer.index_root_parity_block_count,
1502                    encrypted_size: manifest_footer.index_root_encrypted_size,
1503                },
1504            ),
1505            manifest_footer.index_root_decompressed_size,
1506        )?;
1507        let index_root = IndexRoot::parse(
1508            &index_root_plaintext,
1509            first.crypto_header.has_dictionary != 0,
1510            limits,
1511        )?;
1512        let payload_dictionary = load_archive_dictionary(
1513            &blocks,
1514            &first.subkeys,
1515            &first.volume_header,
1516            &first.crypto_header,
1517            &index_root,
1518        )?;
1519
1520        Ok(Self {
1521            options,
1522            observed_archive_bytes,
1523            observed_volume_count,
1524            subkeys: first.subkeys,
1525            blocks,
1526            lazy_blocks: None,
1527            crypto_header_bytes: first.crypto_header_bytes,
1528            volume_header: authority_volume_header,
1529            crypto_header: first.crypto_header,
1530            manifest_footer,
1531            volume_trailer: Some(authority_volume_trailer),
1532            root_auth_footer: root_auth_authority,
1533            index_root,
1534            payload_dictionary,
1535        })
1536    }
1537
1538    pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
1539        readers: Vec<R>,
1540        master_key: &MasterKey,
1541        options: ReaderOptions,
1542    ) -> Result<Self, FormatError> {
1543        let readers = readers
1544            .into_iter()
1545            .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1546            .collect::<Vec<_>>();
1547        Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1548    }
1549
1550    fn open_seekable_volumes_with_options_for_mode(
1551        readers: Vec<Arc<dyn ArchiveReadAt>>,
1552        master_key: &MasterKey,
1553        options: ReaderOptions,
1554        bootstrap_sidecar: Option<&[u8]>,
1555    ) -> Result<Self, FormatError> {
1556        validate_reader_options(options)?;
1557        if readers.is_empty() {
1558            return Err(FormatError::InvalidArchive("no volumes supplied"));
1559        }
1560        if bootstrap_sidecar.is_some() && readers.len() > 1 {
1561            return Err(FormatError::ReaderUnsupported(
1562                "multi-volume inputs with bootstrap sidecar are not supported",
1563            ));
1564        }
1565
1566        let observed_archive_bytes = observed_archive_size(
1567            readers
1568                .iter()
1569                .map(|reader| reader.len())
1570                .collect::<Result<Vec<_>, _>>()?
1571                .into_iter()
1572                .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1573        )?;
1574        let mut first: Option<ParsedSeekableReadAtVolume> = None;
1575        let mut manifest_authority: Option<ManifestFooter> = None;
1576        let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1577        let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1578        let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1579        let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1580        let mut saw_root_auth_absent = false;
1581        let mut first_manifest_footer_error: Option<FormatError> = None;
1582        let mut seen_volume_indexes = BTreeSet::new();
1583        let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1584
1585        for reader in readers {
1586            let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1587            if bootstrap_sidecar.is_some() {
1588                validate_bootstrap_single_volume_input(
1589                    &parsed.volume_header,
1590                    &parsed.crypto_header,
1591                )?;
1592            }
1593            if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1594                return Err(FormatError::InvalidArchive(
1595                    "duplicate authenticated volume index",
1596                ));
1597            }
1598
1599            if let Some(first) = &first {
1600                validate_volume_set_member_metadata(
1601                    &first.volume_header,
1602                    &first.crypto_header,
1603                    &first.crypto_header_bytes,
1604                    &parsed.volume_header,
1605                    &parsed.crypto_header,
1606                    &parsed.crypto_header_bytes,
1607                )?;
1608                validate_key_wrap_table_bytes_match(
1609                    &first.key_wrap_table_bytes,
1610                    &parsed.key_wrap_table_bytes,
1611                )?;
1612            } else {
1613                lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1614            }
1615
1616            if let Some(footer) = &parsed.manifest_footer {
1617                if let Some(authority) = &manifest_authority {
1618                    if !manifest_bootstrap_fields_match(authority, footer) {
1619                        return Err(FormatError::InvalidArchive(
1620                            "ManifestFooter bootstrap fields differ",
1621                        ));
1622                    }
1623                } else {
1624                    manifest_authority = Some(footer.clone());
1625                    manifest_authority_volume_header = Some(parsed.volume_header.clone());
1626                    manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1627                }
1628            } else if first_manifest_footer_error.is_none() {
1629                first_manifest_footer_error = parsed.manifest_footer_error.take();
1630            }
1631
1632            match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1633                (Some(footer), Some(bytes)) => {
1634                    if saw_root_auth_absent {
1635                        return Err(FormatError::InvalidArchive(
1636                            "root-auth footer presence differs across volumes",
1637                        ));
1638                    }
1639                    if let Some(authority_bytes) = &root_auth_authority_bytes {
1640                        if authority_bytes != bytes {
1641                            return Err(FormatError::InvalidArchive(
1642                                "RootAuthFooter copies differ",
1643                            ));
1644                        }
1645                    } else {
1646                        root_auth_authority = Some(footer.clone());
1647                        root_auth_authority_bytes = Some(bytes.clone());
1648                    }
1649                }
1650                (None, None) => {
1651                    if root_auth_authority_bytes.is_some() {
1652                        return Err(FormatError::InvalidArchive(
1653                            "root-auth footer presence differs across volumes",
1654                        ));
1655                    }
1656                    saw_root_auth_absent = true;
1657                }
1658                _ => {
1659                    return Err(FormatError::InvalidArchive(
1660                        "root-auth footer terminal state is inconsistent",
1661                    ));
1662                }
1663            }
1664
1665            let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1666            let source = SeekableVolumeSource {
1667                reader: parsed.reader.clone(),
1668                volume_index: parsed.volume_header.volume_index,
1669                block_records_start: parsed.block_records_start,
1670                block_count: parsed.volume_trailer.block_count,
1671                record_len,
1672                block_size: parsed.crypto_header.block_size as usize,
1673            };
1674            let slot = parsed.volume_header.volume_index as usize;
1675            if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1676            {
1677                return Err(FormatError::InvalidArchive(
1678                    "duplicate authenticated volume index",
1679                ));
1680            }
1681
1682            if first.is_none() {
1683                first = Some(parsed);
1684            }
1685        }
1686
1687        let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1688        let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1689            Some(err) => err,
1690            None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1691        })?;
1692        let authority_volume_header = manifest_authority_volume_header.ok_or(
1693            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1694        )?;
1695        let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1696            FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1697        )?;
1698        let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1699            .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1700        let missing_volume_count = first
1701            .crypto_header
1702            .stripe_width
1703            .checked_sub(observed_volume_count)
1704            .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1705        if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1706            return Err(FormatError::InvalidArchive(
1707                "missing volume count exceeds volume_loss_tolerance",
1708            ));
1709        }
1710
1711        let mut blocks = BTreeMap::new();
1712        let sidecar = if let Some(bytes) = bootstrap_sidecar {
1713            let sidecar = parse_bootstrap_sidecar(
1714                bytes,
1715                &first.volume_header,
1716                &first.crypto_header,
1717                &first.subkeys,
1718            )?;
1719            sidecar
1720                .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1721            if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1722                if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1723                    return Err(FormatError::InvalidArchive(
1724                        "bootstrap sidecar conflicts with terminal ManifestFooter",
1725                    ));
1726                }
1727            }
1728            Some((bytes, sidecar))
1729        } else {
1730            None
1731        };
1732
1733        if let Some((sidecar_bytes, sidecar)) = &sidecar {
1734            if let Some((offset, length)) = sidecar.index_root_records_section {
1735                let index_root_records = parse_sidecar_block_records(
1736                    sidecar_bytes,
1737                    first.crypto_header.block_size as usize,
1738                    SidecarBlockRecordsSection {
1739                        offset,
1740                        length,
1741                        extent: index_root_extent_from_manifest(&manifest_footer),
1742                        data_kind: BlockKind::IndexRootData,
1743                        parity_kind: BlockKind::IndexRootParity,
1744                        structure: "IndexRoot",
1745                    },
1746                )?;
1747                insert_sidecar_records(&mut blocks, index_root_records)?;
1748            }
1749        }
1750
1751        let lazy_source = Arc::new(SeekableBlockSource {
1752            stripe_width: first.crypto_header.stripe_width,
1753            volumes: lazy_volume_slots,
1754        });
1755        let block_provider = OpenedBlockProvider {
1756            memory_blocks: &blocks,
1757            lazy_blocks: Some(lazy_source.as_ref()),
1758        };
1759        let limits = metadata_limits(&first.crypto_header);
1760        let index_root_plaintext = load_metadata_object_from_parts(
1761            &block_provider,
1762            ObjectLoadContext::index_root(
1763                &first.volume_header,
1764                &first.crypto_header,
1765                &first.subkeys,
1766                index_root_extent_from_manifest(&manifest_footer),
1767            ),
1768            manifest_footer.index_root_decompressed_size,
1769        )?;
1770        let index_root = IndexRoot::parse(
1771            &index_root_plaintext,
1772            first.crypto_header.has_dictionary != 0,
1773            limits,
1774        )?;
1775        if first.crypto_header.has_dictionary != 0 {
1776            if let Some((sidecar_bytes, sidecar)) = &sidecar {
1777                if let Some((offset, length)) = sidecar.dictionary_records_section {
1778                    let dictionary_records = parse_sidecar_block_records(
1779                        sidecar_bytes,
1780                        first.crypto_header.block_size as usize,
1781                        SidecarBlockRecordsSection {
1782                            offset,
1783                            length,
1784                            extent: dictionary_extent_from_index_root(&index_root)?,
1785                            data_kind: BlockKind::DictionaryData,
1786                            parity_kind: BlockKind::DictionaryParity,
1787                            structure: "Dictionary",
1788                        },
1789                    )?;
1790                    insert_sidecar_records(&mut blocks, dictionary_records)?;
1791                }
1792            }
1793        }
1794        let block_provider = OpenedBlockProvider {
1795            memory_blocks: &blocks,
1796            lazy_blocks: Some(lazy_source.as_ref()),
1797        };
1798        let payload_dictionary = load_archive_dictionary(
1799            &block_provider,
1800            &first.subkeys,
1801            &first.volume_header,
1802            &first.crypto_header,
1803            &index_root,
1804        )?;
1805
1806        Ok(Self {
1807            options,
1808            observed_archive_bytes,
1809            observed_volume_count,
1810            subkeys: first.subkeys,
1811            blocks,
1812            lazy_blocks: Some(lazy_source),
1813            crypto_header_bytes: first.crypto_header_bytes,
1814            volume_header: authority_volume_header,
1815            crypto_header: first.crypto_header,
1816            manifest_footer,
1817            volume_trailer: Some(authority_volume_trailer),
1818            root_auth_footer: root_auth_authority,
1819            index_root,
1820            payload_dictionary,
1821        })
1822    }
1823
1824    pub fn open_with_bootstrap_sidecar_options(
1825        bytes: &[u8],
1826        bootstrap_sidecar: &[u8],
1827        master_key: &MasterKey,
1828        options: ReaderOptions,
1829    ) -> Result<Self, FormatError> {
1830        Self::open_with_bootstrap_sidecar_options_for_mode(
1831            bytes,
1832            bootstrap_sidecar,
1833            master_key,
1834            options,
1835            BootstrapSidecarUse::SeekableAssist,
1836        )
1837    }
1838
1839    fn open_with_bootstrap_sidecar_options_for_mode(
1840        bytes: &[u8],
1841        bootstrap_sidecar: &[u8],
1842        master_key: &MasterKey,
1843        options: ReaderOptions,
1844        sidecar_use: BootstrapSidecarUse,
1845    ) -> Result<Self, FormatError> {
1846        let observed_archive_bytes =
1847            observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1848        if bytes.len() < VOLUME_HEADER_LEN {
1849            return Err(FormatError::InvalidLength {
1850                structure: "archive",
1851                expected: VOLUME_HEADER_LEN,
1852                actual: bytes.len(),
1853            });
1854        }
1855
1856        let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1857        parse_volume_format_dispatch(&volume_header)?;
1858        let crypto_start = volume_header.crypto_header_offset as usize;
1859        let crypto_len = volume_header.crypto_header_length as usize;
1860        let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1861        let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1862        let subkeys = subkeys_for_open(
1863            Some(master_key),
1864            parsed_crypto.fixed.aead_algo,
1865            &volume_header.archive_uuid,
1866            &volume_header.session_id,
1867        )?;
1868        verify_integrity_tag(
1869            HmacDomain::CryptoHeader,
1870            parsed_crypto.fixed.aead_algo,
1871            volume_header.volume_format_rev,
1872            Some(&subkeys.mac_key),
1873            &volume_header.archive_uuid,
1874            &volume_header.session_id,
1875            parsed_crypto.hmac_covered_bytes,
1876            &parsed_crypto.header_hmac,
1877        )?;
1878        parsed_crypto.validate_extension_semantics()?;
1879        reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1880        validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1881        validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1882
1883        let sidecar = parse_bootstrap_sidecar(
1884            bootstrap_sidecar,
1885            &volume_header,
1886            &parsed_crypto.fixed,
1887            &subkeys,
1888        )?;
1889        sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1890        let block_records_start = startup_block_records_start(
1891            &volume_header,
1892            &parsed_crypto.kdf_params,
1893            |start, length| {
1894                let start = to_usize(start, "KeyWrapTableV1")?;
1895                Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
1896            },
1897        )?;
1898
1899        let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1900            bytes,
1901            to_usize(block_records_start, "BlockRecord")?,
1902            parsed_crypto.fixed.block_size as usize,
1903            &volume_header,
1904        )?;
1905        let terminal_material = match sidecar_use {
1906            BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1907                bytes,
1908                terminal_offset,
1909                observed_block_count,
1910                KeyHoldingTerminalContext {
1911                    subkeys: &subkeys,
1912                    volume_header: &volume_header,
1913                    crypto_header: &parsed_crypto.fixed,
1914                    crypto_header_bytes: crypto_bytes,
1915                },
1916                options,
1917            )?),
1918            BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1919                bytes,
1920                terminal_offset,
1921                observed_block_count,
1922                KeyHoldingTerminalContext {
1923                    subkeys: &subkeys,
1924                    volume_header: &volume_header,
1925                    crypto_header: &parsed_crypto.fixed,
1926                    crypto_header_bytes: crypto_bytes,
1927                },
1928                options,
1929            )
1930            .ok(),
1931        };
1932        let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1933        let manifest_authority = match sidecar_use {
1934            BootstrapSidecarUse::SeekableAssist => {
1935                let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1936                    "terminal ManifestFooter/VolumeTrailer is required",
1937                ))?;
1938                if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1939                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1940                        return Err(FormatError::InvalidArchive(
1941                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1942                        ));
1943                    }
1944                }
1945                terminal_manifest.clone()
1946            }
1947            BootstrapSidecarUse::NonSeekableRandomAccess => {
1948                let sidecar_manifest = sidecar
1949                    .manifest_footer
1950                    .as_ref()
1951                    .ok_or(FormatError::ReaderUnsupported(
1952                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1953                ))?;
1954                if let Some(terminal_manifest) = terminal_manifest {
1955                    if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1956                        return Err(FormatError::InvalidArchive(
1957                            "bootstrap sidecar conflicts with terminal ManifestFooter",
1958                        ));
1959                    }
1960                }
1961                sidecar_manifest.clone()
1962            }
1963        };
1964        manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1965
1966        if let Some((offset, length)) = sidecar.index_root_records_section {
1967            let index_root_records = parse_sidecar_block_records(
1968                bootstrap_sidecar,
1969                parsed_crypto.fixed.block_size as usize,
1970                SidecarBlockRecordsSection {
1971                    offset,
1972                    length,
1973                    extent: index_root_extent_from_manifest(&manifest_authority),
1974                    data_kind: BlockKind::IndexRootData,
1975                    parity_kind: BlockKind::IndexRootParity,
1976                    structure: "IndexRoot",
1977                },
1978            )?;
1979            insert_sidecar_records(&mut blocks, index_root_records)?;
1980        }
1981
1982        let limits = metadata_limits(&parsed_crypto.fixed);
1983        let index_root_plaintext = load_metadata_object_from_parts(
1984            &blocks,
1985            ObjectLoadContext::index_root(
1986                &volume_header,
1987                &parsed_crypto.fixed,
1988                &subkeys,
1989                index_root_extent_from_manifest(&manifest_authority),
1990            ),
1991            manifest_authority.index_root_decompressed_size,
1992        )?;
1993        let index_root = IndexRoot::parse(
1994            &index_root_plaintext,
1995            parsed_crypto.fixed.has_dictionary != 0,
1996            limits,
1997        )?;
1998        if parsed_crypto.fixed.has_dictionary != 0 {
1999            if let Some((offset, length)) = sidecar.dictionary_records_section {
2000                let dictionary_records = parse_sidecar_block_records(
2001                    bootstrap_sidecar,
2002                    parsed_crypto.fixed.block_size as usize,
2003                    SidecarBlockRecordsSection {
2004                        offset,
2005                        length,
2006                        extent: dictionary_extent_from_index_root(&index_root)?,
2007                        data_kind: BlockKind::DictionaryData,
2008                        parity_kind: BlockKind::DictionaryParity,
2009                        structure: "dictionary",
2010                    },
2011                )?;
2012                insert_sidecar_records(&mut blocks, dictionary_records)?;
2013            }
2014        }
2015        let payload_dictionary = load_archive_dictionary(
2016            &blocks,
2017            &subkeys,
2018            &volume_header,
2019            &parsed_crypto.fixed,
2020            &index_root,
2021        )?;
2022
2023        Ok(Self {
2024            options,
2025            observed_archive_bytes,
2026            observed_volume_count: 1,
2027            subkeys,
2028            blocks,
2029            lazy_blocks: None,
2030            crypto_header_bytes: crypto_bytes.to_vec(),
2031            volume_header,
2032            crypto_header: parsed_crypto.fixed,
2033            manifest_footer: manifest_authority,
2034            volume_trailer: terminal_material
2035                .as_ref()
2036                .map(|(_, trailer, _)| trailer.clone()),
2037            root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
2038            index_root,
2039            payload_dictionary,
2040        })
2041    }
2042
2043    /// Return path and payload-size entries from encrypted index metadata only.
2044    ///
2045    /// Unlike [`Self::list_files`], this does not decode tar member groups, so
2046    /// it does not read or decrypt payload envelopes after the index shards are
2047    /// available.
2048    pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
2049        let shards = self.load_all_index_shards()?;
2050        final_index_entry_winners(&shards)?
2051            .into_iter()
2052            .map(|(path, winner)| {
2053                archive_index_entry_from_loaded_file_with_path(
2054                    path,
2055                    &shards[winner.shard_index],
2056                    winner.file_index,
2057                )
2058            })
2059            .collect()
2060    }
2061
2062    /// Look up one archive path using encrypted index metadata only.
2063    pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
2064        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2065        self.locate_index_file(&normalized)?
2066            .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
2067            .transpose()
2068    }
2069
2070    pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
2071        let shards = self.load_all_index_shards()?;
2072        final_index_entry_winners(&shards)?
2073            .into_iter()
2074            .map(|(path, winner)| {
2075                let shard = &shards[winner.shard_index];
2076                let member =
2077                    self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
2078                let mtime = member
2079                    .v45_metadata
2080                    .as_ref()
2081                    .ok_or(FormatError::InvalidArchive(
2082                        "revision-45 member metadata is missing",
2083                    ))?
2084                    .portable_mirror
2085                    .mtime;
2086                Ok(ArchiveEntry {
2087                    path,
2088                    file_data_size: winner.file_data_size,
2089                    kind: member.kind,
2090                    mode: member.mode,
2091                    mtime: ArchiveTimestamp::new(mtime.0, mtime.1),
2092                    diagnostics: member.diagnostics,
2093                })
2094            })
2095            .collect()
2096    }
2097
2098    /// Validate metadata restoration for the final archive entries without
2099    /// creating destination paths.
2100    pub fn plan_metadata_restore(
2101        &self,
2102        options: SafeExtractionOptions,
2103    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2104        let shards = self.load_all_index_shards()?;
2105        let mut planned = Vec::new();
2106        for (path, winner) in final_index_entry_winners(&shards)? {
2107            let member = self.decode_loaded_owned_tar_member(
2108                &shards[winner.shard_index],
2109                winner.file_index,
2110                false,
2111            )?;
2112            planned.push((path, member));
2113        }
2114        let members: Vec<_> = planned.iter().map(|(_, member)| member).collect();
2115        validate_owned_restore_plan(&members, options)?;
2116        planned
2117            .into_iter()
2118            .map(|(path, member)| Ok((path, plan_owned_member_restore(&member, options)?)))
2119            .collect()
2120    }
2121
2122    /// Return only the regular-file payload bytes for `path`.
2123    ///
2124    /// This is a payload-only convenience for callers that do not need tar
2125    /// metadata fidelity diagnostics. Use [`Self::extract_file_with_diagnostics`]
2126    /// or [`Self::extract_member`] when revision-45 metadata fidelity
2127    /// diagnostics must be reported to users.
2128    pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
2129        self.extract_member(path)?
2130            .map(|member| {
2131                if member.kind != TarEntryKind::Regular || member.reparse_placeholder {
2132                    return Err(FormatError::ReaderUnsupported(
2133                        "extract_file returns only regular file payloads",
2134                    ));
2135                }
2136                Ok(member.data)
2137            })
2138            .transpose()
2139    }
2140
2141    /// Return regular-file payload bytes together with parsed tar metadata
2142    /// diagnostics for `path`.
2143    pub fn extract_file_with_diagnostics(
2144        &self,
2145        path: &str,
2146    ) -> Result<Option<ExtractedRegularFile>, FormatError> {
2147        self.extract_member(path)?
2148            .map(|member| {
2149                if member.kind != TarEntryKind::Regular || member.reparse_placeholder {
2150                    return Err(FormatError::ReaderUnsupported(
2151                        "extract_file_with_diagnostics returns only regular file payloads",
2152                    ));
2153                }
2154                Ok((member.data, member.diagnostics))
2155            })
2156            .transpose()
2157    }
2158
2159    /// Stream regular-file payload bytes for `path` into `writer`.
2160    ///
2161    /// This keeps extraction memory bounded by the selected payload envelope,
2162    /// one decompressed frame, and small tar metadata buffers. It returns the
2163    /// same metadata diagnostics as [`Self::extract_file_with_diagnostics`].
2164    pub fn extract_file_to_writer<W: Write>(
2165        &self,
2166        path: &str,
2167        writer: &mut W,
2168    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2169        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2170        self.locate_index_file(&normalized)?
2171            .map(|located| {
2172                self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
2173            })
2174            .transpose()
2175    }
2176
2177    /// Stream regular-file payload bytes for `path` into `writer` while
2178    /// reporting extracted logical payload bytes.
2179    pub fn extract_file_to_writer_with_progress<W: Write>(
2180        &self,
2181        path: &str,
2182        writer: &mut W,
2183        progress: &mut dyn ArchiveExtractProgressSink,
2184    ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2185        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2186        self.locate_index_file(&normalized)?
2187            .map(|located| {
2188                self.stream_loaded_file_to_writer_with_progress(
2189                    &located.shard,
2190                    located.file_index,
2191                    writer,
2192                    progress,
2193                )
2194            })
2195            .transpose()
2196    }
2197
2198    /// Apply the selected restore policy's regular-file metadata to an already
2199    /// materialized output file without rewriting its payload.
2200    pub fn restore_file_metadata_to_open_file(
2201        &self,
2202        path: &str,
2203        file: &File,
2204        options: SafeExtractionOptions,
2205    ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2206        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2207        let normalized_path = std::str::from_utf8(&normalized)
2208            .map_err(|_| FormatError::UnsafeArchivePath)?
2209            .to_owned();
2210        let shards = self.load_all_index_shards()?;
2211        let winners = final_index_entry_winners(&shards)?;
2212        let Some(requested) = winners.get(&normalized_path).copied() else {
2213            return Ok(None);
2214        };
2215        let member = self.decode_loaded_owned_tar_member(
2216            &shards[requested.shard_index],
2217            requested.file_index,
2218            false,
2219        )?;
2220        restore_regular_file_metadata_to_open_file(file, &member, options).map(Some)
2221    }
2222
2223    pub fn extract_member(
2224        &self,
2225        path: &str,
2226    ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
2227        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2228        self.locate_index_file(&normalized)?
2229            .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
2230            .transpose()
2231    }
2232
2233    pub fn extract_file_to(
2234        &self,
2235        path: &str,
2236        root: &std::path::Path,
2237        options: SafeExtractionOptions,
2238    ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2239        let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2240        let normalized_path = std::str::from_utf8(&normalized)
2241            .map_err(|_| FormatError::UnsafeArchivePath)?
2242            .to_owned();
2243        let shards = self.load_all_index_shards()?;
2244        let winners = final_index_entry_winners(&shards)?;
2245        let Some(requested) = winners.get(&normalized_path).copied() else {
2246            return Ok(None);
2247        };
2248        let requested_member = self.decode_loaded_owned_tar_member(
2249            &shards[requested.shard_index],
2250            requested.file_index,
2251            false,
2252        )?;
2253        let mut entries = Vec::with_capacity(2);
2254        if requested_member.kind == TarEntryKind::Hardlink {
2255            let target = requested_member
2256                .link_target
2257                .as_deref()
2258                .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
2259            let target = std::str::from_utf8(target)
2260                .map_err(|_| FormatError::UnsafeArchivePath)?
2261                .to_owned();
2262            let target_entry = winners
2263                .get(&target)
2264                .copied()
2265                .ok_or(FormatError::InvalidArchive(
2266                    "hardlink target is absent from the final index",
2267                ))?;
2268            entries.push((target, target_entry));
2269        }
2270        entries.push((normalized_path.clone(), requested));
2271        let restored = self.extract_winning_index_entries_to(&shards, entries, root, options, 1)?;
2272        restored
2273            .into_iter()
2274            .find_map(|(entry_path, diagnostics)| {
2275                (entry_path == normalized_path).then_some(diagnostics)
2276            })
2277            .map(Some)
2278            .ok_or(FormatError::InvalidArchive(
2279                "selected restore result is missing",
2280            ))
2281    }
2282
2283    pub fn extract_indexed_files_to(
2284        &self,
2285        root: &std::path::Path,
2286        options: SafeExtractionOptions,
2287        jobs: usize,
2288    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2289        if jobs == 0 {
2290            return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2291        }
2292
2293        let shards = self.load_all_index_shards()?;
2294        let mut entries = final_index_entry_winners(&shards)?
2295            .into_iter()
2296            .collect::<Vec<_>>();
2297        entries.sort_by_key(|(_, entry)| entry.start);
2298        self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2299    }
2300
2301    /// Restore a selected final-view path set after preflighting it as one graph.
2302    ///
2303    /// Canonical hardlink targets are included as restore dependencies. No
2304    /// selected destination is written until every selected member and added
2305    /// dependency passes metadata-policy and path-graph validation.
2306    pub fn extract_selected_files_to(
2307        &self,
2308        paths: &[String],
2309        root: &std::path::Path,
2310        options: SafeExtractionOptions,
2311        jobs: usize,
2312    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2313        if jobs == 0 {
2314            return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2315        }
2316
2317        let shards = self.load_all_index_shards()?;
2318        let winners = final_index_entry_winners(&shards)?;
2319        let mut selected = BTreeMap::new();
2320        for path in paths {
2321            let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2322            let normalized = std::str::from_utf8(&normalized)
2323                .map_err(|_| FormatError::UnsafeArchivePath)?
2324                .to_owned();
2325            let entry = winners
2326                .get(&normalized)
2327                .copied()
2328                .ok_or(FormatError::ReaderUnsupported(
2329                    "selected archive path is absent from the final index",
2330                ))?;
2331            selected.insert(normalized, entry);
2332        }
2333
2334        let requested = selected
2335            .iter()
2336            .map(|(path, entry)| (path.clone(), *entry))
2337            .collect::<Vec<_>>();
2338        for (_, entry) in requested {
2339            let member = self.decode_loaded_owned_tar_member(
2340                &shards[entry.shard_index],
2341                entry.file_index,
2342                false,
2343            )?;
2344            if member.kind != TarEntryKind::Hardlink {
2345                continue;
2346            }
2347            let target = member
2348                .link_target
2349                .as_deref()
2350                .ok_or(FormatError::InvalidArchive("hardlink target is missing"))?;
2351            let target = std::str::from_utf8(target)
2352                .map_err(|_| FormatError::UnsafeArchivePath)?
2353                .to_owned();
2354            let target_entry = winners
2355                .get(&target)
2356                .copied()
2357                .ok_or(FormatError::InvalidArchive(
2358                    "hardlink target is absent from the final index",
2359                ))?;
2360            selected.entry(target).or_insert(target_entry);
2361        }
2362
2363        let mut entries = selected.into_iter().collect::<Vec<_>>();
2364        entries.sort_by_key(|(_, entry)| entry.start);
2365        self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2366    }
2367
2368    pub fn verify(&self) -> Result<(), FormatError> {
2369        self.verify_content().map(|_| ())
2370    }
2371
2372    pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2373        self.verify_content_with_parity_policy(
2374            ParityReadPolicy::Always,
2375            ContentVerificationMode::Full,
2376        )
2377    }
2378
2379    pub fn verify_content_fast(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2380        if self.fast_verify_defers_payload_semantics() {
2381            self.verify_payload_record_integrity_only()?;
2382            return Ok(ArchiveContentVerification {
2383                archive: self,
2384                mode: ContentVerificationMode::Fast,
2385                metadata_report: None,
2386            });
2387        }
2388        self.verify_content_with_parity_policy(
2389            ParityReadPolicy::RepairOnly,
2390            ContentVerificationMode::Fast,
2391        )
2392    }
2393
2394    pub fn fast_verify_defers_payload_semantics(&self) -> bool {
2395        self.root_auth_footer.is_none()
2396            && self.crypto_header.has_dictionary == 0
2397            && !self.crypto_header.aead_algo.is_encrypted()
2398            && self.crypto_header.fec_parity_shards == 0
2399            && self.crypto_header.index_fec_parity_shards == 0
2400            && self.crypto_header.index_root_fec_parity_shards == 0
2401            && self.manifest_footer.index_root_parity_block_count == 0
2402    }
2403
2404    fn verify_payload_record_integrity_only(&self) -> Result<(), FormatError> {
2405        let tables = self.load_payload_index_tables()?;
2406        let block_provider = self.block_provider();
2407        let block_size = self.crypto_header.block_size as u64;
2408        for envelope in tables.envelopes.values() {
2409            if envelope.parity_block_count != 0 {
2410                return Err(FormatError::InvalidArchive(
2411                    "fast payload record scan requires zero parity",
2412                ));
2413            }
2414            let expected_encrypted_size = checked_u64_mul(
2415                envelope.data_block_count as u64,
2416                block_size,
2417                "payload envelope encrypted size",
2418            )?;
2419            if envelope.encrypted_size as u64 != expected_encrypted_size {
2420                return Err(FormatError::InvalidArchive(
2421                    "payload envelope encrypted_size mismatch",
2422                ));
2423            }
2424            for offset in 0..envelope.data_block_count {
2425                let block_index =
2426                    checked_u64_add(envelope.first_block_index, offset as u64, "payload")?;
2427                let record = block_provider
2428                    .block(block_index)?
2429                    .ok_or(FormatError::InvalidArchive("payload data block is missing"))?;
2430                if record.kind != BlockKind::PayloadData {
2431                    return Err(FormatError::InvalidArchive(
2432                        "payload data block has unexpected kind",
2433                    ));
2434                }
2435                let should_be_last = offset + 1 == envelope.data_block_count;
2436                if record.is_last_data() != should_be_last {
2437                    return Err(FormatError::InvalidArchive(
2438                        "payload last-data flag is not on the final data block",
2439                    ));
2440                }
2441            }
2442        }
2443        Ok(())
2444    }
2445
2446    fn verify_content_with_parity_policy(
2447        &self,
2448        parity_policy: ParityReadPolicy,
2449        mode: ContentVerificationMode,
2450    ) -> Result<ArchiveContentVerification<'_>, FormatError> {
2451        let tables = self.load_payload_index_tables()?;
2452        let streamed = self.scan_seekable_payload(
2453            &tables,
2454            u64::MAX,
2455            NoopTarStreamObserver,
2456            true,
2457            parity_policy,
2458        )?;
2459        self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
2460        let metadata_report = metadata_verification_report(&streamed.tar.members)?;
2461        Ok(ArchiveContentVerification {
2462            archive: self,
2463            mode,
2464            metadata_report: Some(metadata_report),
2465        })
2466    }
2467
2468    pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
2469        let lazy_source = self
2470            .lazy_blocks
2471            .as_ref()
2472            .ok_or(FormatError::ReaderUnsupported(
2473                "repair output requires seekable archive input",
2474            ))?;
2475        if !lazy_source.is_complete_volume_set() {
2476            return Err(FormatError::ReaderUnsupported(
2477                "repair output requires all archive volumes",
2478            ));
2479        }
2480
2481        let shards = self.load_all_index_shards()?;
2482        let rows = self.root_auth_fec_layout_rows(&shards)?;
2483        let block_provider = self.block_provider();
2484        let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
2485        for row in rows.into_iter().filter(|row| row.present) {
2486            self.collect_repair_patches_for_object(
2487                &block_provider,
2488                lazy_source,
2489                row,
2490                &mut patches,
2491            )?;
2492        }
2493        Ok(patches.into_values().collect())
2494    }
2495
2496    pub fn extract_all_to(
2497        &self,
2498        root: &std::path::Path,
2499        options: SafeExtractionOptions,
2500    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2501        let tables = self.load_payload_index_tables()?;
2502        if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
2503            return Err(FormatError::ReaderUnsupported(
2504                FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
2505            ));
2506        }
2507
2508        let dry_run = self.scan_seekable_payload(
2509            &tables,
2510            total_extraction_size_cap(self.options, self.observed_archive_bytes),
2511            NoopTarStreamObserver,
2512            false,
2513            ParityReadPolicy::RepairOnly,
2514        )?;
2515        self.validate_streamed_payload_summary(&tables, &dry_run, true, false)?;
2516
2517        let observer = TarStreamFilesystemRestoreObserver::new(root, options);
2518        let streamed = self.scan_seekable_payload(
2519            &tables,
2520            total_extraction_size_cap(self.options, self.observed_archive_bytes),
2521            observer,
2522            false,
2523            ParityReadPolicy::RepairOnly,
2524        )?;
2525        streamed
2526            .tar
2527            .members
2528            .into_iter()
2529            .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
2530            .collect()
2531    }
2532
2533    fn collect_repair_patches_for_object(
2534        &self,
2535        blocks: &impl BlockProvider,
2536        source: &SeekableBlockSource,
2537        row: FecLayoutObjectRow,
2538        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2539    ) -> Result<(), FormatError> {
2540        let (data_kind, parity_kind, data_max, parity_max) =
2541            self.fec_object_class_shape(row.object_class)?;
2542        let extent = ObjectExtent {
2543            first_block_index: row.first_block_index,
2544            data_block_count: row.data_block_count,
2545            parity_block_count: row.parity_block_count,
2546            encrypted_size: row.encrypted_size,
2547        };
2548        validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
2549
2550        let block_size = self.crypto_header.block_size as usize;
2551        let data_count = extent.data_block_count as usize;
2552        let parity_count = extent.parity_block_count as usize;
2553        let mut data_shards = Vec::with_capacity(data_count);
2554        let mut parity_shards = Vec::with_capacity(parity_count);
2555
2556        for offset in 0..data_count {
2557            let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2558            match blocks.block(block_index)? {
2559                Some(record) => {
2560                    if record.kind != data_kind {
2561                        return Err(FormatError::InvalidArchive(
2562                            "object data block has unexpected kind",
2563                        ));
2564                    }
2565                    let should_be_last = offset + 1 == data_count;
2566                    if record.is_last_data() != should_be_last {
2567                        return Err(FormatError::InvalidArchive(
2568                            "object last-data flag is not on the final data block",
2569                        ));
2570                    }
2571                    data_shards.push(Some(record.payload.clone()));
2572                }
2573                None => data_shards.push(None),
2574            }
2575        }
2576
2577        for offset in 0..parity_count {
2578            let block_index = checked_u64_add(
2579                extent.first_block_index,
2580                data_count as u64 + offset as u64,
2581                "object",
2582            )?;
2583            match blocks.block(block_index)? {
2584                Some(record) => {
2585                    if record.kind != parity_kind {
2586                        return Err(FormatError::InvalidArchive(
2587                            "object parity block has unexpected kind",
2588                        ));
2589                    }
2590                    if record.is_last_data() {
2591                        return Err(FormatError::InvalidArchive(
2592                            "object parity block has last-data flag",
2593                        ));
2594                    }
2595                    parity_shards.push(Some(record.payload.clone()));
2596                }
2597                None => parity_shards.push(None),
2598            }
2599        }
2600
2601        let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2602        for (offset, payload) in repaired_data.iter().enumerate() {
2603            if data_shards[offset].is_none() {
2604                let block_index =
2605                    checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2606                let flags = if offset + 1 == data_count { 0x01 } else { 0 };
2607                self.insert_repair_patch(
2608                    patches,
2609                    source,
2610                    block_index,
2611                    data_kind,
2612                    flags,
2613                    payload.clone(),
2614                )?;
2615            }
2616        }
2617
2618        if parity_count > 0 {
2619            let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
2620            for (offset, payload) in repaired_parity.into_iter().enumerate() {
2621                if parity_shards[offset].as_ref() != Some(&payload) {
2622                    let block_index = checked_u64_add(
2623                        extent.first_block_index,
2624                        data_count as u64 + offset as u64,
2625                        "object",
2626                    )?;
2627                    self.insert_repair_patch(
2628                        patches,
2629                        source,
2630                        block_index,
2631                        parity_kind,
2632                        0,
2633                        payload,
2634                    )?;
2635                }
2636            }
2637        }
2638
2639        Ok(())
2640    }
2641
2642    fn insert_repair_patch(
2643        &self,
2644        patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2645        source: &SeekableBlockSource,
2646        block_index: u64,
2647        kind: BlockKind,
2648        flags: u8,
2649        payload: Vec<u8>,
2650    ) -> Result<(), FormatError> {
2651        let (volume_index, record_offset) = source.record_location(block_index)?;
2652        let record = BlockRecord {
2653            block_index,
2654            kind,
2655            flags,
2656            payload,
2657            record_crc32c: 0,
2658        };
2659        let patch = ArchiveRepairPatch {
2660            volume_index,
2661            block_index,
2662            record_offset,
2663            record_bytes: record.to_bytes(),
2664        };
2665        if let Some(existing) = patches.insert(block_index, patch.clone()) {
2666            if existing != patch {
2667                return Err(FormatError::InvalidArchive(
2668                    "conflicting repair patch for BlockRecord",
2669                ));
2670            }
2671        }
2672        Ok(())
2673    }
2674
2675    fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
2676        if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
2677            && self.index_root.directory_hint_shards.is_empty()
2678        {
2679            return Err(FormatError::InvalidArchive(
2680                "IndexRoot file_count requires directory hints",
2681            ));
2682        }
2683
2684        let shards = self.load_all_index_shards()?;
2685        let mut file_count = 0u64;
2686        let mut frames = BTreeMap::<u64, FrameEntry>::new();
2687        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2688
2689        for shard in &shards {
2690            file_count = file_count
2691                .checked_add(shard.files.len() as u64)
2692                .ok_or(FormatError::InvalidArchive("file count overflow"))?;
2693            for frame in &shard.frames {
2694                if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
2695                    if existing != *frame {
2696                        return Err(FormatError::InvalidArchive(
2697                            "duplicate FrameEntry rows do not match",
2698                        ));
2699                    }
2700                }
2701            }
2702            for envelope in &shard.envelopes {
2703                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2704                {
2705                    if existing != *envelope {
2706                        return Err(FormatError::InvalidArchive(
2707                            "duplicate EnvelopeEntry rows do not match",
2708                        ));
2709                    }
2710                }
2711            }
2712        }
2713        validate_global_file_table_order(&shards)?;
2714
2715        if file_count != self.index_root.header.file_count {
2716            return Err(FormatError::InvalidArchive(
2717                "IndexRoot file_count does not match decoded shards",
2718            ));
2719        }
2720        verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
2721        verify_dense_keys(
2722            &envelopes,
2723            self.index_root.header.envelope_count,
2724            "EnvelopeEntry",
2725        )?;
2726        validate_envelope_frame_coverage(&frames, &envelopes)?;
2727        self.validate_encrypted_object_block_ranges(&envelopes)?;
2728
2729        let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
2730            sum.checked_add(envelope.data_block_count as u64)
2731                .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2732        })?;
2733        if payload_block_count != self.index_root.header.payload_block_count {
2734            return Err(FormatError::InvalidArchive(
2735                "IndexRoot payload_block_count does not match envelopes",
2736            ));
2737        }
2738
2739        Ok(PayloadIndexTables {
2740            shards,
2741            file_count,
2742            frames,
2743            envelopes,
2744        })
2745    }
2746
2747    fn scan_seekable_payload<O: TarStreamObserver>(
2748        &self,
2749        tables: &PayloadIndexTables,
2750        extraction_cap: u64,
2751        observer: O,
2752        hash_content: bool,
2753        parity_policy: ParityReadPolicy,
2754    ) -> Result<StreamedPayloadSummary, FormatError> {
2755        let mut tar = TarStreamSummaryValidator::with_observer(
2756            self.crypto_header.max_path_length,
2757            extraction_cap,
2758            usize::MAX,
2759            self.index_root.header.file_count,
2760            observer,
2761        );
2762        let mut content_hasher = hash_content.then(Sha256::new);
2763        let mut streamed_frames = Vec::with_capacity(tables.frames.len());
2764        let streamed_envelopes = tables
2765            .envelopes
2766            .values()
2767            .map(|envelope| StreamedEnvelopeSummary {
2768                envelope_index: envelope.envelope_index,
2769                first_block_index: envelope.first_block_index,
2770                data_block_count: envelope.data_block_count,
2771                parity_block_count: envelope.parity_block_count,
2772                encrypted_size: envelope.encrypted_size,
2773                plaintext_size: envelope.plaintext_size,
2774                first_frame_index: envelope.first_frame_index,
2775                frame_count: envelope.frame_count,
2776            })
2777            .collect::<Vec<_>>();
2778        let mut cached_envelope_index = None;
2779        let mut cached_envelope_plaintext = Vec::new();
2780        let mut decompressor = self.new_payload_decompressor()?;
2781
2782        for frame in tables.frames.values() {
2783            let envelope =
2784                tables
2785                    .envelopes
2786                    .get(&frame.envelope_index)
2787                    .ok_or(FormatError::InvalidArchive(
2788                        "FrameEntry references missing EnvelopeEntry",
2789                    ))?;
2790            if cached_envelope_index != Some(envelope.envelope_index) {
2791                cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
2792                cached_envelope_index = Some(envelope.envelope_index);
2793            }
2794            let compressed = slice(
2795                &cached_envelope_plaintext,
2796                frame.offset_in_envelope as usize,
2797                frame.compressed_size as usize,
2798                "FrameEntry",
2799            )?;
2800            let tar_stream_offset = tar.tar_total_size();
2801            let decoded = self.decompress_payload_frame_with(
2802                &mut decompressor,
2803                compressed,
2804                frame.decompressed_size,
2805            )?;
2806            if decoded.is_empty() {
2807                return Err(FormatError::InvalidArchive(
2808                    "zstd payload frame decompressed to zero bytes",
2809                ));
2810            }
2811            if let Some(hasher) = &mut content_hasher {
2812                hasher.update(&decoded);
2813            }
2814            tar.observe(&decoded)?;
2815            streamed_frames.push(StreamedFrameSummary {
2816                frame_index: frame.frame_index,
2817                envelope_index: frame.envelope_index,
2818                offset_in_envelope: frame.offset_in_envelope,
2819                compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2820                    FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2821                })?,
2822                decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2823                    FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2824                })?,
2825                tar_stream_offset,
2826            });
2827        }
2828
2829        let mut content_sha256 = [0u8; 32];
2830        if let Some(hasher) = content_hasher {
2831            let digest = hasher.finalize();
2832            content_sha256.copy_from_slice(&digest);
2833        }
2834        Ok(StreamedPayloadSummary {
2835            tar: tar.finish()?,
2836            content_sha256,
2837            envelopes: streamed_envelopes,
2838            frames: streamed_frames,
2839        })
2840    }
2841
2842    fn validate_streamed_payload_summary(
2843        &self,
2844        tables: &PayloadIndexTables,
2845        streamed: &StreamedPayloadSummary,
2846        enforce_total_extraction_cap: bool,
2847        enforce_content_sha256: bool,
2848    ) -> Result<(), FormatError> {
2849        if enforce_total_extraction_cap
2850            && streamed.tar.total_extraction_size
2851                > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2852        {
2853            return Err(FormatError::ReaderUnsupported(
2854                "total extraction size exceeds configured cap",
2855            ));
2856        }
2857
2858        let streamed_payload_block_count =
2859            streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2860                sum.checked_add(envelope.data_block_count as u64)
2861                    .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2862            })?;
2863        if streamed_payload_block_count != self.index_root.header.payload_block_count {
2864            return Err(FormatError::InvalidArchive(
2865                "streamed payload block count does not match IndexRoot",
2866            ));
2867        }
2868
2869        if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2870            return Err(FormatError::InvalidArchive(
2871                "IndexRoot tar_total_size does not match streamed tar stream",
2872            ));
2873        }
2874        if enforce_content_sha256
2875            && streamed.content_sha256 != self.index_root.header.content_sha256
2876        {
2877            return Err(FormatError::InvalidArchive(
2878                "IndexRoot content_sha256 does not match decoded tar stream",
2879            ));
2880        }
2881
2882        let streamed_envelopes = streamed.envelope_map()?;
2883        for envelope in tables.envelopes.values() {
2884            let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2885                FormatError::InvalidArchive(
2886                    "metadata references missing streamed payload envelope",
2887                ),
2888            )?;
2889            if actual.first_block_index != envelope.first_block_index
2890                || actual.data_block_count != envelope.data_block_count
2891                || actual.parity_block_count != envelope.parity_block_count
2892                || actual.encrypted_size != envelope.encrypted_size
2893                || actual.plaintext_size != envelope.plaintext_size
2894                || actual.first_frame_index != envelope.first_frame_index
2895                || actual.frame_count != envelope.frame_count
2896            {
2897                return Err(FormatError::InvalidArchive(
2898                    "EnvelopeEntry does not match streamed payload envelope",
2899                ));
2900            }
2901        }
2902
2903        let streamed_frames = streamed.frame_map()?;
2904        for frame in tables.frames.values() {
2905            let actual =
2906                streamed_frames
2907                    .get(&frame.frame_index)
2908                    .ok_or(FormatError::InvalidArchive(
2909                        "metadata references missing streamed payload frame",
2910                    ))?;
2911            if actual.envelope_index != frame.envelope_index
2912                || actual.offset_in_envelope != frame.offset_in_envelope
2913                || actual.compressed_size != frame.compressed_size
2914                || actual.decompressed_size != frame.decompressed_size
2915                || actual.tar_stream_offset != frame.tar_stream_offset
2916                || streamed.frame_flags(actual)? != frame.flags
2917            {
2918                return Err(FormatError::InvalidArchive(
2919                    "FrameEntry does not match streamed payload frame",
2920                ));
2921            }
2922        }
2923
2924        let streamed_members = streamed.member_start_map()?;
2925        if streamed.tar.members.len() as u64 != tables.file_count {
2926            return Err(FormatError::InvalidArchive(
2927                "streamed tar member count does not match decoded shards",
2928            ));
2929        }
2930        let mut file_extents = Vec::new();
2931        let mut directory_hint_map = DirectoryHintMap::new();
2932        for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2933            let shard_row_index = u32::try_from(shard_row_index)
2934                .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2935            for idx in 0..shard.files.len() {
2936                let file = &shard.files[idx];
2937                let start =
2938                    shard
2939                        .tar_member_group_start(idx)
2940                        .ok_or(FormatError::InvalidArchive(
2941                            "FileEntry tar member start is missing",
2942                        ))?;
2943                file_extents.push((start, file.tar_member_group_size));
2944                let path = shard
2945                    .file_path(idx)
2946                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2947                let member = streamed_members
2948                    .get(&start)
2949                    .ok_or(FormatError::InvalidArchive(
2950                        "FileEntry tar member start is missing from streamed tar",
2951                    ))?;
2952                if member.path != path {
2953                    return Err(FormatError::InvalidArchive(
2954                        "tar member path does not match FileEntry path",
2955                    ));
2956                }
2957                if member.logical_size != file.file_data_size {
2958                    return Err(FormatError::InvalidArchive(
2959                        "tar member size does not match FileEntry file_data_size",
2960                    ));
2961                }
2962                if member.file_entry_flags != file.flags {
2963                    return Err(FormatError::InvalidArchive(
2964                        "streamed tar member metadata flags do not match FileEntry flags",
2965                    ));
2966                }
2967                if member.group_size != file.tar_member_group_size {
2968                    return Err(FormatError::InvalidArchive(
2969                        "FileEntry does not match streamed tar member",
2970                    ));
2971                }
2972                add_expected_directory_hint_rows(
2973                    &mut directory_hint_map,
2974                    shard_row_index,
2975                    path,
2976                    member.kind,
2977                );
2978            }
2979        }
2980        validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2981        if !self.index_root.directory_hint_shards.is_empty() {
2982            let hint_tables = self.load_all_directory_hint_tables()?;
2983            validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2984        }
2985
2986        Ok(())
2987    }
2988
2989    pub(crate) fn from_streamed_parts(
2990        parts: StreamedArchiveOpenParts,
2991    ) -> Result<Self, FormatError> {
2992        let limits = metadata_limits(&parts.crypto_header);
2993        let index_root_plaintext = load_metadata_object_from_parts(
2994            &parts.blocks,
2995            ObjectLoadContext::index_root(
2996                &parts.volume_header,
2997                &parts.crypto_header,
2998                &parts.subkeys,
2999                ObjectExtent {
3000                    first_block_index: parts.manifest_footer.index_root_first_block,
3001                    data_block_count: parts.manifest_footer.index_root_data_block_count,
3002                    parity_block_count: parts.manifest_footer.index_root_parity_block_count,
3003                    encrypted_size: parts.manifest_footer.index_root_encrypted_size,
3004                },
3005            ),
3006            parts.manifest_footer.index_root_decompressed_size,
3007        )?;
3008        let index_root = IndexRoot::parse(
3009            &index_root_plaintext,
3010            parts.crypto_header.has_dictionary != 0,
3011            limits,
3012        )?;
3013        let payload_dictionary = load_archive_dictionary(
3014            &parts.blocks,
3015            &parts.subkeys,
3016            &parts.volume_header,
3017            &parts.crypto_header,
3018            &index_root,
3019        )?;
3020
3021        Ok(Self {
3022            options: parts.options,
3023            observed_archive_bytes: parts.observed_archive_bytes,
3024            observed_volume_count: 1,
3025            subkeys: parts.subkeys,
3026            blocks: parts.blocks,
3027            lazy_blocks: None,
3028            crypto_header_bytes: parts.crypto_header_bytes,
3029            volume_header: parts.volume_header,
3030            crypto_header: parts.crypto_header,
3031            manifest_footer: parts.manifest_footer,
3032            volume_trailer: Some(parts.volume_trailer),
3033            root_auth_footer: parts.root_auth_footer,
3034            index_root,
3035            payload_dictionary,
3036        })
3037    }
3038
3039    pub(crate) fn verify_streamed_payload_summary(
3040        &self,
3041        streamed: &StreamedPayloadSummary,
3042    ) -> Result<(), FormatError> {
3043        let tables = self.load_payload_index_tables()?;
3044        self.validate_streamed_payload_summary(&tables, streamed, true, true)
3045    }
3046
3047    pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
3048    where
3049        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3050    {
3051        let content_verification = self.verify_content()?;
3052        self.verify_root_auth_with_verified_content(&content_verification, verifier)
3053    }
3054
3055    pub fn verify_root_auth_with_verified_content<F>(
3056        &self,
3057        content_verification: &ArchiveContentVerification<'_>,
3058        mut verifier: F,
3059    ) -> Result<RootAuthVerification, FormatError>
3060    where
3061        F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3062    {
3063        if !std::ptr::eq(content_verification.archive, self) {
3064            return Err(FormatError::InvalidArchive(
3065                "content verification does not match archive",
3066            ));
3067        }
3068        if content_verification.mode != ContentVerificationMode::Full {
3069            return Err(FormatError::ReaderUnsupported(
3070                "RootAuth verification requires full archive content verification",
3071            ));
3072        }
3073        let footer = self
3074            .root_auth_footer
3075            .as_ref()
3076            .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
3077        let material = self.recompute_root_auth_material(footer)?;
3078        if material.critical_metadata_digest != footer.critical_metadata_digest
3079            || material.index_digest != footer.index_digest
3080            || material.fec_layout_digest != footer.fec_layout_digest
3081            || material.data_block_merkle_root != footer.data_block_merkle_root
3082            || material.signer_identity_digest != footer.signer_identity_digest
3083            || material.archive_root != footer.archive_root
3084            || material.total_data_block_count != footer.total_data_block_count
3085        {
3086            return Err(FormatError::InvalidArchive(
3087                "RootAuthFooter commitments do not match recomputed archive root",
3088            ));
3089        }
3090        if !verifier(footer, &material.archive_root)? {
3091            return Err(FormatError::InvalidArchive(
3092                "root-auth authenticator verification failed",
3093            ));
3094        }
3095        Ok(RootAuthVerification {
3096            format_version: footer.format_version,
3097            volume_format_rev: footer.volume_format_rev,
3098            archive_root: material.archive_root,
3099            authenticator_id: footer.authenticator_id,
3100            signer_identity_type: footer.signer_identity_type,
3101            signer_identity_bytes: footer.signer_identity_bytes.clone(),
3102            total_data_block_count: footer.total_data_block_count,
3103            diagnostics: self.root_auth_success_diagnostics(),
3104        })
3105    }
3106
3107    fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
3108        parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
3109            self.load_index_shard(entry)
3110        })
3111    }
3112
3113    fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
3114        let block_provider = self.block_provider();
3115        let plaintext = load_metadata_object_from_parts(
3116            &block_provider,
3117            ObjectLoadContext::index_shard(
3118                &self.volume_header,
3119                &self.crypto_header,
3120                &self.subkeys,
3121                entry,
3122            ),
3123            entry.decompressed_size,
3124        )?;
3125        IndexShard::parse(&plaintext, entry, self.metadata_limits())
3126    }
3127
3128    fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
3129        parallel_map_ref(
3130            &self.index_root.directory_hint_shards,
3131            self.options.jobs,
3132            |entry| self.load_directory_hint_table(entry),
3133        )
3134    }
3135
3136    fn load_directory_hint_table(
3137        &self,
3138        entry: &DirectoryHintShardEntry,
3139    ) -> Result<DirectoryHintTable, FormatError> {
3140        let block_provider = self.block_provider();
3141        let plaintext = load_metadata_object_from_parts(
3142            &block_provider,
3143            ObjectLoadContext::directory_hint(
3144                &self.volume_header,
3145                &self.crypto_header,
3146                &self.subkeys,
3147                entry,
3148            ),
3149            entry.decompressed_size,
3150        )?;
3151        DirectoryHintTable::parse(
3152            &plaintext,
3153            entry,
3154            self.index_root.header.shard_count,
3155            self.metadata_limits(),
3156        )
3157    }
3158
3159    fn load_payload_envelope(
3160        &self,
3161        envelope: &EnvelopeEntry,
3162        parity_policy: ParityReadPolicy,
3163    ) -> Result<Vec<u8>, FormatError> {
3164        let block_provider = self.block_provider();
3165        let plaintext = load_decrypted_object_from_parts_with_parity_policy(
3166            &block_provider,
3167            ObjectLoadContext::payload(
3168                &self.volume_header,
3169                &self.crypto_header,
3170                &self.subkeys,
3171                envelope,
3172            ),
3173            parity_policy,
3174        )?;
3175        if plaintext.len() != envelope.plaintext_size as usize {
3176            return Err(FormatError::InvalidArchive(
3177                "payload envelope plaintext_size mismatch",
3178            ));
3179        }
3180        Ok(plaintext)
3181    }
3182
3183    fn locate_index_file(
3184        &self,
3185        normalized: &[u8],
3186    ) -> Result<Option<LocatedIndexFile>, FormatError> {
3187        let candidate_indexes = self
3188            .index_root
3189            .candidate_shards_for_path(normalized, self.metadata_limits())?;
3190        let mut winner: Option<LocatedIndexFile> = None;
3191
3192        for row_index in candidate_indexes {
3193            let locating =
3194                self.index_root
3195                    .shards
3196                    .get(row_index)
3197                    .ok_or(FormatError::InvalidArchive(
3198                        "candidate shard row is out of bounds",
3199                    ))?;
3200            let shard = self.load_index_shard(locating)?;
3201            if let Some(file_index) = shard.lookup_file_index(normalized) {
3202                let start =
3203                    shard
3204                        .tar_member_group_start(file_index)
3205                        .ok_or(FormatError::InvalidArchive(
3206                            "FileEntry tar member start is missing",
3207                        ))?;
3208                if winner
3209                    .as_ref()
3210                    .map(|existing| start > existing.start)
3211                    .unwrap_or(true)
3212                {
3213                    winner = Some(LocatedIndexFile {
3214                        shard,
3215                        file_index,
3216                        start,
3217                    });
3218                }
3219            }
3220        }
3221
3222        Ok(winner)
3223    }
3224
3225    fn extract_loaded_member(
3226        &self,
3227        shard: &IndexShard,
3228        file_index: usize,
3229    ) -> Result<ExtractedArchiveMember, FormatError> {
3230        let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
3231        Ok(ExtractedArchiveMember {
3232            path: utf8_path(&member.path)?,
3233            kind: member.kind,
3234            data: member.data,
3235            link_target: member
3236                .link_target
3237                .map(|target| utf8_path(&target))
3238                .transpose()?,
3239            reparse_placeholder: member.reparse_placeholder,
3240            diagnostics: member.diagnostics,
3241        })
3242    }
3243
3244    fn extract_loaded_owned_tar_member(
3245        &self,
3246        shard: &IndexShard,
3247        file_index: usize,
3248    ) -> Result<OwnedTarMember, FormatError> {
3249        self.decode_loaded_owned_tar_member(shard, file_index, true)
3250    }
3251
3252    fn stream_loaded_file_to_writer<W: Write>(
3253        &self,
3254        shard: &IndexShard,
3255        file_index: usize,
3256        writer: &mut W,
3257    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3258        let file = shard
3259            .files
3260            .get(file_index)
3261            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3262        self.validate_total_extraction_size(file.file_data_size)?;
3263        let expected_path = shard
3264            .file_path(file_index)
3265            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3266        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3267        stream_regular_tar_member_group_to_writer(
3268            &mut reader,
3269            expected_path,
3270            file.file_data_size,
3271            file.flags,
3272            file.tar_member_group_size,
3273            self.crypto_header.max_path_length,
3274            writer,
3275        )
3276    }
3277
3278    fn stream_loaded_file_to_writer_with_progress<W: Write>(
3279        &self,
3280        shard: &IndexShard,
3281        file_index: usize,
3282        writer: &mut W,
3283        progress: &mut dyn ArchiveExtractProgressSink,
3284    ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3285        let file = shard
3286            .files
3287            .get(file_index)
3288            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3289        self.validate_total_extraction_size(file.file_data_size)?;
3290        let expected_path = shard
3291            .file_path(file_index)
3292            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3293        let archive_path = utf8_path(expected_path)?;
3294        let mut progress_writer =
3295            ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
3296        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3297        stream_regular_tar_member_group_to_writer(
3298            &mut reader,
3299            expected_path,
3300            file.file_data_size,
3301            file.flags,
3302            file.tar_member_group_size,
3303            self.crypto_header.max_path_length,
3304            &mut progress_writer,
3305        )
3306    }
3307
3308    fn stream_loaded_file_to_path(
3309        &self,
3310        shard: &IndexShard,
3311        file_index: usize,
3312        root: &std::path::Path,
3313        options: SafeExtractionOptions,
3314    ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3315        let file = shard
3316            .files
3317            .get(file_index)
3318            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3319        self.validate_total_extraction_size(file.file_data_size)?;
3320        let expected_path = shard
3321            .file_path(file_index)
3322            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3323        let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3324        restore_streaming_tar_member_group(
3325            root,
3326            StreamingMemberExpectation {
3327                path: expected_path,
3328                file_data_size: file.file_data_size,
3329                file_flags: file.flags,
3330                group_len: file.tar_member_group_size,
3331                max_path_length: self.crypto_header.max_path_length,
3332            },
3333            options,
3334            &mut reader,
3335        )
3336        .map_err(format_error_from_extract_error)
3337    }
3338
3339    fn extract_winning_index_entries_to(
3340        &self,
3341        shards: &[IndexShard],
3342        entries: Vec<(String, WinningIndexEntry)>,
3343        root: &std::path::Path,
3344        options: SafeExtractionOptions,
3345        jobs: usize,
3346    ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
3347        if entries.is_empty() {
3348            return Ok(Vec::new());
3349        }
3350        let metadata = parallel_map_ref(&entries, jobs, |(_, entry)| {
3351            let shard = shards
3352                .get(entry.shard_index)
3353                .ok_or(FormatError::InvalidArchive(
3354                    "winning FileEntry shard is out of bounds",
3355                ))?;
3356            self.decode_loaded_owned_tar_member(shard, entry.file_index, false)
3357        })?;
3358        validate_owned_restore_plan(&metadata.iter().collect::<Vec<_>>(), options)?;
3359        let mut planned = entries
3360            .into_iter()
3361            .zip(metadata)
3362            .map(|((path, entry), member)| (path, entry, member))
3363            .collect::<Vec<_>>();
3364        planned.sort_by(|left, right| {
3365            restore_phase(&left.2)
3366                .cmp(&restore_phase(&right.2))
3367                .then_with(|| left.2.path.cmp(&right.2.path))
3368        });
3369        planned
3370            .into_iter()
3371            .map(|(path, entry, _)| {
3372                let shard = &shards[entry.shard_index];
3373                let diagnostics =
3374                    self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
3375                Ok((path, diagnostics))
3376            })
3377            .collect()
3378    }
3379
3380    fn decode_loaded_owned_tar_member(
3381        &self,
3382        shard: &IndexShard,
3383        file_index: usize,
3384        enforce_extraction_cap: bool,
3385    ) -> Result<OwnedTarMember, FormatError> {
3386        let file = shard
3387            .files
3388            .get(file_index)
3389            .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3390        if enforce_extraction_cap {
3391            self.validate_total_extraction_size(file.file_data_size)?;
3392        }
3393        let expected_path = shard
3394            .file_path(file_index)
3395            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3396        let frames = frame_range_for_file(shard, file)?;
3397        let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
3398        let mut decoded = Vec::new();
3399
3400        for frame in frames {
3401            let envelope = envelope_by_index(shard, frame.envelope_index)?;
3402            if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
3403                entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
3404            }
3405            let envelope_plaintext = envelope_cache
3406                .get(&envelope.envelope_index)
3407                .expect("inserted above");
3408            let compressed = slice(
3409                envelope_plaintext,
3410                frame.offset_in_envelope as usize,
3411                frame.compressed_size as usize,
3412                "FrameEntry",
3413            )?;
3414            decoded.extend_from_slice(
3415                &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
3416            );
3417        }
3418
3419        let offset = file.offset_in_first_frame_plaintext as usize;
3420        let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
3421        let group = slice(&decoded, offset, group_len, "FileEntry")?;
3422        let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
3423        if member.path != expected_path {
3424            return Err(FormatError::InvalidArchive(
3425                "tar member path does not match FileEntry path",
3426            ));
3427        }
3428        if member.logical_size != file.file_data_size {
3429            return Err(FormatError::InvalidArchive(
3430                "tar member size does not match FileEntry file_data_size",
3431            ));
3432        }
3433        if member.v45_metadata.file_entry_flags != file.flags {
3434            return Err(FormatError::InvalidArchive(
3435                "FileEntry flags do not match decoded member-group metadata",
3436            ));
3437        }
3438        if enforce_extraction_cap {
3439            member.to_owned_member()
3440        } else {
3441            Ok(member.to_owned_metadata())
3442        }
3443    }
3444
3445    fn metadata_limits(&self) -> MetadataLimits {
3446        metadata_limits(&self.crypto_header)
3447    }
3448
3449    fn recompute_root_auth_material(
3450        &self,
3451        footer: &RootAuthFooterV1,
3452    ) -> Result<RootAuthMaterial, FormatError> {
3453        if footer.format_version != self.volume_header.format_version {
3454            return Err(FormatError::InvalidArchive(
3455                "RootAuthFooter format_version differs from authenticated VolumeHeader",
3456            ));
3457        }
3458        if footer.volume_format_rev != self.volume_header.volume_format_rev {
3459            return Err(FormatError::InvalidArchive(
3460                "RootAuthFooter volume_format_rev differs from authenticated VolumeHeader",
3461            ));
3462        }
3463        let format_version = self.volume_header.format_version;
3464        let volume_format_rev = self.volume_header.volume_format_rev;
3465        let footer_length = footer.footer_length()?;
3466        let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
3467            format_version,
3468            volume_format_rev,
3469            footer.authenticator_id,
3470            footer.signer_identity_type,
3471            &footer.signer_identity_bytes,
3472            u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3473                FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3474            })?,
3475            footer_length,
3476        )?;
3477        let signer_identity_digest =
3478            signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3479        let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
3480        let crypto_pre_hmac_len = self
3481            .crypto_header_bytes
3482            .len()
3483            .checked_sub(CRYPTO_HEADER_HMAC_LEN)
3484            .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
3485        let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
3486            archive_uuid: self.volume_header.archive_uuid,
3487            session_id: self.volume_header.session_id,
3488            format_version,
3489            volume_format_rev,
3490            stripe_width: self.crypto_header.stripe_width,
3491            total_volumes: self.manifest_footer.total_volumes,
3492            compression_algo: self.crypto_header.compression_algo,
3493            aead_algo: self.crypto_header.aead_algo,
3494            fec_algo: self.crypto_header.fec_algo,
3495            kdf_algo: self.crypto_header.kdf_algo,
3496            crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
3497            chunk_size: self.crypto_header.chunk_size,
3498            envelope_target_size: self.crypto_header.envelope_target_size,
3499            block_size: self.crypto_header.block_size,
3500            fec_data_shards: self.crypto_header.fec_data_shards,
3501            fec_parity_shards: self.crypto_header.fec_parity_shards,
3502            index_fec_data_shards: self.crypto_header.index_fec_data_shards,
3503            index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
3504            index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
3505            index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
3506            volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
3507            bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
3508            has_dictionary: self.crypto_header.has_dictionary,
3509            manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
3510            index_root_first_block: self.manifest_footer.index_root_first_block,
3511            index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
3512            index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
3513            index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
3514            index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
3515            root_auth_descriptor_digest,
3516        })?;
3517        let index_root_plaintext = self.index_root.to_bytes();
3518        let index_digest =
3519            index_digest_for_revision(format_version, volume_format_rev, &index_root_plaintext)?;
3520        let shards = self.load_all_index_shards()?;
3521        let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
3522        let fec_layout_digest =
3523            fec_layout_digest_for_revision(format_version, volume_format_rev, &fec_layout_rows)?;
3524        let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
3525        let total_data_block_count = u64::try_from(data_leaves.len())
3526            .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
3527        let data_block_merkle_root =
3528            data_block_merkle_root_for_revision(format_version, volume_format_rev, &data_leaves)?;
3529        let archive_root = archive_root_for_revision(ArchiveRootInputs {
3530            archive_uuid: self.volume_header.archive_uuid,
3531            session_id: self.volume_header.session_id,
3532            format_version,
3533            volume_format_rev,
3534            compression_algo: self.crypto_header.compression_algo,
3535            aead_algo: self.crypto_header.aead_algo,
3536            fec_algo: self.crypto_header.fec_algo,
3537            kdf_algo: self.crypto_header.kdf_algo,
3538            critical_metadata_digest,
3539            index_digest,
3540            fec_layout_digest,
3541            total_data_block_count,
3542            data_block_merkle_root,
3543            root_auth_descriptor_digest,
3544            signer_identity_digest,
3545        })?;
3546        Ok(RootAuthMaterial {
3547            critical_metadata_digest,
3548            index_digest,
3549            fec_layout_digest,
3550            data_block_merkle_root,
3551            signer_identity_digest,
3552            archive_root,
3553            total_data_block_count,
3554        })
3555    }
3556
3557    fn root_auth_fec_layout_rows(
3558        &self,
3559        shards: &[IndexShard],
3560    ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
3561        let mut rows = Vec::new();
3562        rows.push(FecLayoutObjectRow {
3563            object_class: 1,
3564            present: true,
3565            object_id: 0,
3566            first_block_index: self.manifest_footer.index_root_first_block,
3567            data_block_count: self.manifest_footer.index_root_data_block_count,
3568            parity_block_count: self.manifest_footer.index_root_parity_block_count,
3569            encrypted_size: self.manifest_footer.index_root_encrypted_size,
3570            plain_size: self.manifest_footer.index_root_decompressed_size,
3571        });
3572        if self.crypto_header.has_dictionary != 0 {
3573            rows.push(FecLayoutObjectRow {
3574                object_class: 2,
3575                present: true,
3576                object_id: 0,
3577                first_block_index: self.index_root.header.dictionary_first_block,
3578                data_block_count: self.index_root.header.dictionary_data_block_count,
3579                parity_block_count: self.index_root.header.dictionary_parity_block_count,
3580                encrypted_size: self.index_root.header.dictionary_encrypted_size,
3581                plain_size: self.index_root.header.dictionary_decompressed_size,
3582            });
3583        } else {
3584            rows.push(FecLayoutObjectRow {
3585                object_class: 2,
3586                present: false,
3587                object_id: 0,
3588                first_block_index: 0,
3589                data_block_count: 0,
3590                parity_block_count: 0,
3591                encrypted_size: 0,
3592                plain_size: 0,
3593            });
3594        }
3595        for entry in &self.index_root.shards {
3596            rows.push(FecLayoutObjectRow {
3597                object_class: 3,
3598                present: true,
3599                object_id: entry.shard_index,
3600                first_block_index: entry.first_block_index,
3601                data_block_count: entry.data_block_count,
3602                parity_block_count: entry.parity_block_count,
3603                encrypted_size: entry.encrypted_size,
3604                plain_size: entry.decompressed_size,
3605            });
3606        }
3607        let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
3608        for shard in shards {
3609            for envelope in &shard.envelopes {
3610                if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
3611                {
3612                    if existing != *envelope {
3613                        return Err(FormatError::InvalidArchive(
3614                            "duplicate EnvelopeEntry rows do not match",
3615                        ));
3616                    }
3617                }
3618            }
3619        }
3620        for envelope in envelopes.values() {
3621            rows.push(FecLayoutObjectRow {
3622                object_class: 4,
3623                present: true,
3624                object_id: envelope.envelope_index,
3625                first_block_index: envelope.first_block_index,
3626                data_block_count: envelope.data_block_count,
3627                parity_block_count: envelope.parity_block_count,
3628                encrypted_size: envelope.encrypted_size,
3629                plain_size: envelope.plaintext_size,
3630            });
3631        }
3632        for entry in &self.index_root.directory_hint_shards {
3633            rows.push(FecLayoutObjectRow {
3634                object_class: 5,
3635                present: true,
3636                object_id: entry.hint_shard_index,
3637                first_block_index: entry.first_block_index,
3638                data_block_count: entry.data_block_count,
3639                parity_block_count: entry.parity_block_count,
3640                encrypted_size: entry.encrypted_size,
3641                plain_size: entry.decompressed_size,
3642            });
3643        }
3644        Ok(rows)
3645    }
3646
3647    fn fec_object_class_shape(
3648        &self,
3649        object_class: u8,
3650    ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
3651        match object_class {
3652            1 => Ok((
3653                BlockKind::IndexRootData,
3654                BlockKind::IndexRootParity,
3655                self.crypto_header.index_root_fec_data_shards,
3656                self.crypto_header.index_root_fec_parity_shards,
3657            )),
3658            2 => Ok((
3659                BlockKind::DictionaryData,
3660                BlockKind::DictionaryParity,
3661                self.crypto_header.index_root_fec_data_shards,
3662                self.crypto_header.index_root_fec_parity_shards,
3663            )),
3664            3 => Ok((
3665                BlockKind::IndexShardData,
3666                BlockKind::IndexShardParity,
3667                self.crypto_header.index_fec_data_shards,
3668                self.crypto_header.index_fec_parity_shards,
3669            )),
3670            4 => Ok((
3671                BlockKind::PayloadData,
3672                BlockKind::PayloadParity,
3673                self.crypto_header.fec_data_shards,
3674                self.crypto_header.fec_parity_shards,
3675            )),
3676            5 => Ok((
3677                BlockKind::DirectoryHintData,
3678                BlockKind::DirectoryHintParity,
3679                self.crypto_header.index_fec_data_shards,
3680                self.crypto_header.index_fec_parity_shards,
3681            )),
3682            _ => Err(FormatError::InvalidArchive(
3683                "unknown root-auth FEC row class",
3684            )),
3685        }
3686    }
3687
3688    fn root_auth_data_block_leaves(
3689        &self,
3690        rows: &[FecLayoutObjectRow],
3691    ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
3692        let block_provider = self.block_provider();
3693        let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
3694        let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
3695            let row = **row;
3696            let (data_kind, parity_kind, data_max, parity_max) =
3697                self.fec_object_class_shape(row.object_class)?;
3698            let extent = ObjectExtent {
3699                first_block_index: row.first_block_index,
3700                data_block_count: row.data_block_count,
3701                parity_block_count: row.parity_block_count,
3702                encrypted_size: row.encrypted_size,
3703            };
3704            let repaired = load_repaired_object_data_shards_from_parts(
3705                &block_provider,
3706                &self.crypto_header,
3707                extent,
3708                data_kind,
3709                parity_kind,
3710                data_max,
3711                parity_max,
3712            )?;
3713            let mut leaves = Vec::new();
3714            for (offset, payload) in repaired.into_iter().enumerate() {
3715                leaves.push(DataBlockMerkleLeaf {
3716                    block_index: checked_u64_add(
3717                        row.first_block_index,
3718                        offset as u64,
3719                        "root-auth data block",
3720                    )?,
3721                    kind: data_kind,
3722                    flags: if offset + 1 == row.data_block_count as usize {
3723                        0x01
3724                    } else {
3725                        0
3726                    },
3727                    payload,
3728                });
3729            }
3730            Ok(leaves)
3731        })?;
3732        let mut leaves = Vec::new();
3733        for mut chunk in chunks {
3734            leaves.append(&mut chunk);
3735        }
3736        leaves.sort_by_key(|leaf| leaf.block_index);
3737        Ok(leaves)
3738    }
3739
3740    fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
3741        let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
3742        if logical_size > cap {
3743            return Err(FormatError::ReaderUnsupported(
3744                "total extraction size exceeds configured cap",
3745            ));
3746        }
3747        Ok(())
3748    }
3749
3750    fn decompress_payload_frame(
3751        &self,
3752        compressed: &[u8],
3753        decompressed_size: u32,
3754    ) -> Result<Vec<u8>, FormatError> {
3755        let mut decompressor = self.new_payload_decompressor()?;
3756        self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
3757    }
3758
3759    fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
3760        match &self.payload_dictionary {
3761            Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
3762            None => zstd::bulk::Decompressor::new(),
3763        }
3764        .map_err(|_| FormatError::ZstdDecompressionFailure)
3765    }
3766
3767    fn decompress_payload_frame_with(
3768        &self,
3769        decompressor: &mut zstd::bulk::Decompressor<'static>,
3770        compressed: &[u8],
3771        decompressed_size: u32,
3772    ) -> Result<Vec<u8>, FormatError> {
3773        validate_exact_zstd_frame(compressed)?;
3774        let expected = decompressed_size as usize;
3775        let decoded = decompressor
3776            .decompress(compressed, expected)
3777            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
3778        if decoded.len() != expected {
3779            return Err(FormatError::ZstdDecompressedSizeMismatch {
3780                expected,
3781                actual: decoded.len(),
3782            });
3783        }
3784        Ok(decoded)
3785    }
3786
3787    fn validate_encrypted_object_block_ranges(
3788        &self,
3789        envelopes: &BTreeMap<u64, EnvelopeEntry>,
3790    ) -> Result<(), FormatError> {
3791        let mut ranges = Vec::new();
3792        ranges.push(object_block_range(
3793            self.manifest_footer.index_root_first_block,
3794            self.manifest_footer.index_root_data_block_count,
3795            self.manifest_footer.index_root_parity_block_count,
3796            "IndexRoot",
3797        )?);
3798        for shard in &self.index_root.shards {
3799            ranges.push(object_block_range(
3800                shard.first_block_index,
3801                shard.data_block_count,
3802                shard.parity_block_count,
3803                "IndexShard",
3804            )?);
3805        }
3806        for hint in &self.index_root.directory_hint_shards {
3807            ranges.push(object_block_range(
3808                hint.first_block_index,
3809                hint.data_block_count,
3810                hint.parity_block_count,
3811                "DirectoryHintShardEntry",
3812            )?);
3813        }
3814        if self.crypto_header.has_dictionary != 0 {
3815            ranges.push(object_block_range(
3816                self.index_root.header.dictionary_first_block,
3817                self.index_root.header.dictionary_data_block_count,
3818                self.index_root.header.dictionary_parity_block_count,
3819                "dictionary",
3820            )?);
3821        }
3822        for envelope in envelopes.values() {
3823            ranges.push(object_block_range(
3824                envelope.first_block_index,
3825                envelope.data_block_count,
3826                envelope.parity_block_count,
3827                "EnvelopeEntry",
3828            )?);
3829        }
3830        validate_non_overlapping_object_ranges(&mut ranges)?;
3831        if let Some(source) = &self.lazy_blocks {
3832            if source.is_complete_volume_set() {
3833                validate_exact_coverage_ranges_u64(
3834                    &mut ranges,
3835                    source.total_block_count()?,
3836                    "encrypted object block ranges do not cover complete archive exactly",
3837                )?;
3838            }
3839        }
3840        Ok(())
3841    }
3842}
3843
3844impl<'a> DecodedTarMemberGroupReader<'a> {
3845    fn new(
3846        archive: &'a OpenedArchive,
3847        shard: &'a IndexShard,
3848        file: &'a FileEntry,
3849    ) -> Result<Self, FormatError> {
3850        Ok(Self {
3851            archive,
3852            shard,
3853            file,
3854            decompressor: archive.new_payload_decompressor()?,
3855            next_frame_offset: 0,
3856            cached_envelope_index: None,
3857            cached_envelope_plaintext: Vec::new(),
3858            current_frame: Vec::new(),
3859            current_frame_offset: 0,
3860            remaining_group_bytes: file.tar_member_group_size,
3861        })
3862    }
3863
3864    fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3865        while self.current_frame_offset >= self.current_frame.len() {
3866            if self.next_frame_offset >= self.file.frame_count as u64 {
3867                return Err(
3868                    FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3869                );
3870            }
3871            let frame_index = self
3872                .file
3873                .first_frame_index
3874                .checked_add(self.next_frame_offset)
3875                .ok_or(FormatError::InvalidArchive(
3876                    "FileEntry frame range overflow",
3877                ))?;
3878            let frame = frame_by_index(self.shard, frame_index)?;
3879            let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3880            if self.cached_envelope_index != Some(envelope.envelope_index) {
3881                self.cached_envelope_plaintext = self
3882                    .archive
3883                    .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3884                self.cached_envelope_index = Some(envelope.envelope_index);
3885            }
3886            let compressed = slice(
3887                &self.cached_envelope_plaintext,
3888                frame.offset_in_envelope as usize,
3889                frame.compressed_size as usize,
3890                "FrameEntry",
3891            )?;
3892            let decoded = self.archive.decompress_payload_frame_with(
3893                &mut self.decompressor,
3894                compressed,
3895                frame.decompressed_size,
3896            )?;
3897            let offset = if self.next_frame_offset == 0 {
3898                self.file.offset_in_first_frame_plaintext as usize
3899            } else {
3900                0
3901            };
3902            if offset > decoded.len() {
3903                return Err(FormatError::InvalidArchive(
3904                    "offset in first frame is outside the first referenced frame",
3905                )
3906                .into());
3907            }
3908            self.next_frame_offset += 1;
3909            self.current_frame = decoded;
3910            self.current_frame_offset = offset;
3911        }
3912        Ok(())
3913    }
3914}
3915
3916impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3917    fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3918        if buf.is_empty() {
3919            return Ok(0);
3920        }
3921        if self.remaining_group_bytes == 0 {
3922            return Ok(0);
3923        }
3924        self.ensure_frame_available()?;
3925        let available = self.current_frame.len() - self.current_frame_offset;
3926        let len = available
3927            .min(buf.len())
3928            .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3929        if len == 0 {
3930            return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3931        }
3932        buf[..len].copy_from_slice(
3933            &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3934        );
3935        self.current_frame_offset += len;
3936        self.remaining_group_bytes -= len as u64;
3937        Ok(len)
3938    }
3939}
3940
3941fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3942    shard
3943        .frames
3944        .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3945        .map(|idx| &shard.frames[idx])
3946        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3947}
3948
3949fn envelope_by_index(
3950    shard: &IndexShard,
3951    envelope_index: u64,
3952) -> Result<&EnvelopeEntry, FormatError> {
3953    shard
3954        .envelopes
3955        .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3956        .map(|idx| &shard.envelopes[idx])
3957        .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3958}
3959
3960fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3961    match err {
3962        ExtractError::Format(err) => err,
3963        ExtractError::Output(_) => {
3964            FormatError::FilesystemExtractionFailed("failed to write regular file")
3965        }
3966    }
3967}
3968
3969fn final_index_entry_winners(
3970    shards: &[IndexShard],
3971) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3972    let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3973    for (shard_index, shard) in shards.iter().enumerate() {
3974        for (idx, file) in shard.files.iter().enumerate() {
3975            let path = utf8_path(
3976                shard
3977                    .file_path(idx)
3978                    .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3979            )?;
3980            let start = shard
3981                .tar_member_group_start(idx)
3982                .ok_or(FormatError::InvalidArchive(
3983                    "FileEntry tar member start is missing",
3984                ))?;
3985            if let Some(winner) = final_entries.get_mut(&path) {
3986                if start >= winner.start {
3987                    winner.start = start;
3988                    winner.file_data_size = file.file_data_size;
3989                    winner.shard_index = shard_index;
3990                    winner.file_index = idx;
3991                }
3992            } else {
3993                final_entries.insert(
3994                    path,
3995                    WinningIndexEntry {
3996                        start,
3997                        file_data_size: file.file_data_size,
3998                        shard_index,
3999                        file_index: idx,
4000                    },
4001                );
4002            }
4003        }
4004    }
4005    Ok(final_entries)
4006}
4007
4008fn archive_index_entry_from_loaded_file(
4009    shard: &IndexShard,
4010    file_index: usize,
4011) -> Result<ArchiveIndexEntry, FormatError> {
4012    let path = utf8_path(
4013        shard
4014            .file_path(file_index)
4015            .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
4016    )?;
4017    archive_index_entry_from_loaded_file_with_path(path, shard, file_index)
4018}
4019
4020fn archive_index_entry_from_loaded_file_with_path(
4021    path: String,
4022    shard: &IndexShard,
4023    file_index: usize,
4024) -> Result<ArchiveIndexEntry, FormatError> {
4025    let file = shard
4026        .files
4027        .get(file_index)
4028        .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
4029    let layout = archive_index_entry_layout(shard, file)?;
4030    Ok(ArchiveIndexEntry {
4031        name: archive_entry_name(&path),
4032        path,
4033        file_data_size: file.file_data_size,
4034        flags: file.flags,
4035        path_hash: file.path_hash,
4036        tar_member_group_size: file.tar_member_group_size,
4037        first_frame_index: file.first_frame_index,
4038        frame_count: file.frame_count,
4039        offset_in_first_frame_plaintext: file.offset_in_first_frame_plaintext,
4040        layout,
4041    })
4042}
4043
4044fn archive_index_entry_layout(
4045    shard: &IndexShard,
4046    file: &FileEntry,
4047) -> Result<ArchiveIndexEntryLayout, FormatError> {
4048    let frames = frame_range_for_file(shard, file)?;
4049    if let [frame] = frames {
4050        let envelope = envelope_by_index(shard, frame.envelope_index)?;
4051        return Ok(ArchiveIndexEntryLayout {
4052            compressed_size: frame.compressed_size as u64,
4053            decompressed_frame_size: frame.decompressed_size as u64,
4054            envelope_count: 1,
4055            first_envelope_index: Some(envelope.envelope_index),
4056            last_envelope_index: Some(envelope.envelope_index),
4057            first_payload_block_index: Some(envelope.first_block_index),
4058            payload_data_block_count: envelope.data_block_count as u64,
4059            payload_parity_block_count: envelope.parity_block_count as u64,
4060            payload_encrypted_size: envelope.encrypted_size as u64,
4061        });
4062    }
4063
4064    let mut compressed_size = 0u64;
4065    let mut decompressed_frame_size = 0u64;
4066    let mut envelope_indexes = BTreeSet::new();
4067
4068    for frame in frames {
4069        compressed_size = checked_u64_add(
4070            compressed_size,
4071            frame.compressed_size as u64,
4072            "ArchiveIndexEntry.compressed_size",
4073        )?;
4074        decompressed_frame_size = checked_u64_add(
4075            decompressed_frame_size,
4076            frame.decompressed_size as u64,
4077            "ArchiveIndexEntry.decompressed_frame_size",
4078        )?;
4079        envelope_indexes.insert(frame.envelope_index);
4080    }
4081
4082    let mut first_payload_block_index = None::<u64>;
4083    let mut payload_data_block_count = 0u64;
4084    let mut payload_parity_block_count = 0u64;
4085    let mut payload_encrypted_size = 0u64;
4086
4087    for envelope_index in &envelope_indexes {
4088        let envelope = envelope_by_index(shard, *envelope_index)?;
4089        first_payload_block_index = Some(
4090            first_payload_block_index
4091                .map(|existing| existing.min(envelope.first_block_index))
4092                .unwrap_or(envelope.first_block_index),
4093        );
4094        payload_data_block_count = checked_u64_add(
4095            payload_data_block_count,
4096            envelope.data_block_count as u64,
4097            "ArchiveIndexEntry.payload_data_block_count",
4098        )?;
4099        payload_parity_block_count = checked_u64_add(
4100            payload_parity_block_count,
4101            envelope.parity_block_count as u64,
4102            "ArchiveIndexEntry.payload_parity_block_count",
4103        )?;
4104        payload_encrypted_size = checked_u64_add(
4105            payload_encrypted_size,
4106            envelope.encrypted_size as u64,
4107            "ArchiveIndexEntry.payload_encrypted_size",
4108        )?;
4109    }
4110
4111    Ok(ArchiveIndexEntryLayout {
4112        compressed_size,
4113        decompressed_frame_size,
4114        envelope_count: u32::try_from(envelope_indexes.len()).map_err(|_| {
4115            FormatError::InvalidArchive("ArchiveIndexEntry envelope count overflow")
4116        })?,
4117        first_envelope_index: envelope_indexes.iter().next().copied(),
4118        last_envelope_index: envelope_indexes.iter().next_back().copied(),
4119        first_payload_block_index,
4120        payload_data_block_count,
4121        payload_parity_block_count,
4122        payload_encrypted_size,
4123    })
4124}
4125
4126fn archive_entry_name(path: &str) -> String {
4127    path.rsplit('/').next().unwrap_or(path).to_owned()
4128}
4129
4130#[derive(Debug)]
4131struct ParsedSeekableVolume {
4132    volume_header: VolumeHeader,
4133    crypto_header: CryptoHeaderFixed,
4134    crypto_header_bytes: Vec<u8>,
4135    key_wrap_table_bytes: Option<Vec<u8>>,
4136    subkeys: Subkeys,
4137    manifest_footer: Option<ManifestFooter>,
4138    manifest_footer_error: Option<FormatError>,
4139    root_auth_footer: Option<RootAuthFooterV1>,
4140    root_auth_footer_bytes: Option<Vec<u8>>,
4141    volume_trailer: VolumeTrailer,
4142    blocks: BTreeMap<u64, BlockRecord>,
4143    erased_block_indices: BTreeSet<u64>,
4144}
4145
4146struct ParsedSeekableReadAtVolume {
4147    reader: Arc<dyn ArchiveReadAt>,
4148    volume_header: VolumeHeader,
4149    crypto_header: CryptoHeaderFixed,
4150    crypto_header_bytes: Vec<u8>,
4151    key_wrap_table_bytes: Option<Vec<u8>>,
4152    subkeys: Subkeys,
4153    manifest_footer: Option<ManifestFooter>,
4154    manifest_footer_error: Option<FormatError>,
4155    root_auth_footer: Option<RootAuthFooterV1>,
4156    root_auth_footer_bytes: Option<Vec<u8>>,
4157    volume_trailer: VolumeTrailer,
4158    block_records_start: u64,
4159}
4160
4161struct ParsedOpenPrefix {
4162    volume_header: VolumeHeader,
4163    crypto_header: CryptoHeaderFixed,
4164    crypto_header_bytes: Vec<u8>,
4165    key_wrap_table_bytes: Option<Vec<u8>>,
4166    block_records_start: u64,
4167    subkeys: Subkeys,
4168}
4169
4170struct ParsedReadAtOpenPrefix {
4171    volume_header: VolumeHeader,
4172    crypto_header: CryptoHeaderFixed,
4173    crypto_header_bytes: Vec<u8>,
4174    key_wrap_table_bytes: Option<Vec<u8>>,
4175    block_records_start: u64,
4176    subkeys: Subkeys,
4177}
4178
4179pub(crate) struct StartupKeyWrapTable {
4180    pub(crate) table: KeyWrapTableV1,
4181    pub(crate) bytes: Vec<u8>,
4182    pub(crate) block_records_start: u64,
4183}
4184
4185pub(crate) fn startup_block_records_start(
4186    volume_header: &VolumeHeader,
4187    kdf_params: &KdfParams,
4188    read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
4189) -> Result<u64, FormatError> {
4190    Ok(
4191        startup_key_wrap_table(volume_header, kdf_params, read_key_wrap_table)?
4192            .map(|startup| startup.block_records_start)
4193            .unwrap_or_else(|| {
4194                volume_header.crypto_header_offset as u64
4195                    + volume_header.crypto_header_length as u64
4196            }),
4197    )
4198}
4199
4200pub(crate) fn startup_key_wrap_table(
4201    volume_header: &VolumeHeader,
4202    kdf_params: &KdfParams,
4203    mut read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
4204) -> Result<Option<StartupKeyWrapTable>, FormatError> {
4205    let crypto_end = checked_u64_add(
4206        volume_header.crypto_header_offset as u64,
4207        volume_header.crypto_header_length as u64,
4208        "CryptoHeader",
4209    )?;
4210    let &KdfParams::RecipientWrap {
4211        key_wrap_table_length,
4212        ..
4213    } = kdf_params
4214    else {
4215        return Ok(None);
4216    };
4217    if volume_header.volume_format_rev != VOLUME_FORMAT_REV_45 {
4218        return Err(FormatError::InvalidArchive(
4219            "RecipientWrap KdfParams require volume_format_rev 45",
4220        ));
4221    }
4222    let key_wrap_table_length_usize =
4223        to_usize(u64::from(key_wrap_table_length), "KeyWrapTableV1 length")?;
4224    let key_wrap_table_bytes = read_key_wrap_table(crypto_end, key_wrap_table_length_usize)?;
4225    Ok(Some(parse_startup_key_wrap_table_bytes(
4226        volume_header,
4227        kdf_params,
4228        key_wrap_table_bytes,
4229    )?))
4230}
4231
4232pub(crate) fn parse_startup_key_wrap_table_bytes(
4233    volume_header: &VolumeHeader,
4234    kdf_params: &KdfParams,
4235    key_wrap_table_bytes: Vec<u8>,
4236) -> Result<StartupKeyWrapTable, FormatError> {
4237    let crypto_end = checked_u64_add(
4238        volume_header.crypto_header_offset as u64,
4239        volume_header.crypto_header_length as u64,
4240        "CryptoHeader",
4241    )?;
4242    let KdfParams::RecipientWrap {
4243        key_wrap_table_length,
4244        key_wrap_table_record_count,
4245        key_wrap_table_digest,
4246        ..
4247    } = kdf_params
4248    else {
4249        return Err(FormatError::KeyMaterialMismatch);
4250    };
4251    let key_wrap_table = KeyWrapTableV1::parse(
4252        &key_wrap_table_bytes,
4253        &volume_header.archive_uuid,
4254        &volume_header.session_id,
4255        *key_wrap_table_length,
4256        *key_wrap_table_record_count,
4257    )?;
4258    if compute_key_wrap_table_digest(*key_wrap_table_length, &key_wrap_table_bytes)
4259        != *key_wrap_table_digest
4260    {
4261        return Err(FormatError::IntegrityDigestMismatch {
4262            structure: "KeyWrapTableV1",
4263        });
4264    }
4265    let block_records_start = checked_u64_add(
4266        crypto_end,
4267        key_wrap_table.table_length as u64,
4268        "KeyWrapTableV1",
4269    )?;
4270    Ok(StartupKeyWrapTable {
4271        table: key_wrap_table,
4272        bytes: key_wrap_table_bytes,
4273        block_records_start,
4274    })
4275}
4276
4277fn parse_seekable_volume(
4278    bytes: &[u8],
4279    master_key: &MasterKey,
4280    options: ReaderOptions,
4281) -> Result<ParsedSeekableVolume, FormatError> {
4282    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
4283        return Err(FormatError::InvalidLength {
4284            structure: "archive",
4285            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4286            actual: bytes.len(),
4287        });
4288    }
4289
4290    let prefix = match parse_open_prefix(bytes, master_key) {
4291        Ok(prefix) => prefix,
4292        Err(prefix_err) => {
4293            if matches!(
4294                prefix_err,
4295                FormatError::UnsupportedVolumeFormatRevision { .. }
4296            ) {
4297                return Err(prefix_err);
4298            }
4299            if matches!(prefix_err, FormatError::KeyMaterialMismatch)
4300                && prefix_uses_recipient_wrap(bytes)
4301            {
4302                return Err(prefix_err);
4303            }
4304            return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
4305                .or(Err(prefix_err));
4306        }
4307    };
4308    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4309    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4310        Ok(parsed) => Ok(parsed),
4311        Err(prefix_err) => {
4312            match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
4313                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4314                    Ok(recovered)
4315                }
4316                Ok(_) | Err(_) => Err(prefix_err),
4317            }
4318        }
4319    }
4320}
4321
4322fn parse_seekable_volume_with_recipient_wrap_resolver<F>(
4323    bytes: &[u8],
4324    resolver: &mut F,
4325    options: ReaderOptions,
4326) -> Result<ParsedSeekableVolume, FormatError>
4327where
4328    F: FnMut(
4329        RecipientWrapRecordContext<'_>,
4330    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4331{
4332    let prefix = match parse_open_prefix_with_recipient_wrap_resolver(bytes, resolver) {
4333        Ok(prefix) => prefix,
4334        Err(prefix_err) => {
4335            if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4336                return Err(prefix_err);
4337            }
4338            return parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4339                bytes, resolver, options,
4340            )
4341            .or(Err(prefix_err));
4342        }
4343    };
4344    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4345    match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4346        Ok(parsed) => Ok(parsed),
4347        Err(prefix_err) => {
4348            match parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4349                bytes, resolver, options,
4350            ) {
4351                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4352                    Ok(recovered)
4353                }
4354                Ok(_) | Err(_) => Err(prefix_err),
4355            }
4356        }
4357    }
4358}
4359
4360fn parse_seekable_volume_with_prefix(
4361    bytes: &[u8],
4362    prefix: ParsedOpenPrefix,
4363    options: ReaderOptions,
4364) -> Result<ParsedSeekableVolume, FormatError> {
4365    let ParsedOpenPrefix {
4366        volume_header,
4367        crypto_header,
4368        crypto_header_bytes,
4369        key_wrap_table_bytes,
4370        block_records_start,
4371        subkeys,
4372    } = prefix;
4373    let crypto_bytes = crypto_header_bytes.as_slice();
4374
4375    let terminal = locate_v41_terminal(
4376        bytes,
4377        KeyHoldingTerminalContext {
4378            subkeys: &subkeys,
4379            volume_header: &volume_header,
4380            crypto_header: &crypto_header,
4381            crypto_header_bytes: crypto_bytes,
4382        },
4383        options,
4384    )?;
4385    finish_parse_seekable_volume(
4386        bytes,
4387        volume_header,
4388        crypto_header,
4389        crypto_header_bytes,
4390        key_wrap_table_bytes,
4391        block_records_start,
4392        subkeys,
4393        terminal,
4394    )
4395}
4396
4397fn parse_seekable_volume_from_recovered_terminal(
4398    bytes: &[u8],
4399    master_key: &MasterKey,
4400    options: ReaderOptions,
4401) -> Result<ParsedSeekableVolume, FormatError> {
4402    let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
4403    parse_volume_format_dispatch(&authority.volume_header)?;
4404    let startup_key_wrap_table = startup_key_wrap_table(
4405        &authority.volume_header,
4406        &authority.kdf_params,
4407        |start, length| {
4408            let start = to_usize(start, "KeyWrapTableV1")?;
4409            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4410        },
4411    )?;
4412    let crypto_end = checked_u64_add(
4413        authority.volume_header.crypto_header_offset as u64,
4414        authority.volume_header.crypto_header_length as u64,
4415        "CryptoHeader",
4416    )?;
4417    let (key_wrap_table_bytes, block_records_start) = startup_key_wrap_table
4418        .map(|startup| (Some(startup.bytes), startup.block_records_start))
4419        .unwrap_or((None, crypto_end));
4420    finish_parse_seekable_volume(
4421        bytes,
4422        authority.volume_header,
4423        authority.crypto_header,
4424        authority.crypto_header_bytes,
4425        key_wrap_table_bytes,
4426        block_records_start,
4427        authority.subkeys,
4428        authority.terminal,
4429    )
4430}
4431
4432fn parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4433    bytes: &[u8],
4434    resolver: &mut F,
4435    options: ReaderOptions,
4436) -> Result<ParsedSeekableVolume, FormatError>
4437where
4438    F: FnMut(
4439        RecipientWrapRecordContext<'_>,
4440    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4441{
4442    let authority = locate_v41_recipient_wrap_terminal_authority(bytes, resolver, options)?;
4443    finish_parse_seekable_volume(
4444        bytes,
4445        authority.volume_header,
4446        authority.crypto_header,
4447        authority.crypto_header_bytes,
4448        Some(authority.key_wrap_table_bytes),
4449        authority.block_records_start,
4450        authority.subkeys,
4451        authority.terminal,
4452    )
4453}
4454
4455fn recipient_wrap_prefix_error_precludes_recovery(error: &FormatError) -> bool {
4456    matches!(
4457        error,
4458        FormatError::UnsupportedVolumeFormatRevision { .. }
4459            | FormatError::ReaderUnsupported(_)
4460            | FormatError::InvalidArchive(
4461                "VolumeHeader and CryptoHeader stripe_width differ"
4462                    | "fec_parity_shards does not match v41 compute_parity"
4463                    | "index_fec_parity_shards does not match v41 compute_parity"
4464                    | "index_root_fec_parity_shards does not match v41 compute_parity"
4465            )
4466    )
4467}
4468
4469fn parse_open_prefix(
4470    bytes: &[u8],
4471    master_key: &MasterKey,
4472) -> Result<ParsedOpenPrefix, FormatError> {
4473    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4474    parse_volume_format_dispatch(&volume_header)?;
4475    let crypto_start = volume_header.crypto_header_offset as usize;
4476    let crypto_len = volume_header.crypto_header_length as usize;
4477    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4478    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4479    if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4480        return Err(FormatError::KeyMaterialMismatch);
4481    }
4482    let subkeys = subkeys_for_open(
4483        Some(master_key),
4484        parsed_crypto.fixed.aead_algo,
4485        &volume_header.archive_uuid,
4486        &volume_header.session_id,
4487    )?;
4488    verify_integrity_tag(
4489        HmacDomain::CryptoHeader,
4490        parsed_crypto.fixed.aead_algo,
4491        volume_header.volume_format_rev,
4492        Some(&subkeys.mac_key),
4493        &volume_header.archive_uuid,
4494        &volume_header.session_id,
4495        parsed_crypto.hmac_covered_bytes,
4496        &parsed_crypto.header_hmac,
4497    )?;
4498    parsed_crypto.validate_extension_semantics()?;
4499    validate_seekable_supported_volume(
4500        &volume_header,
4501        &parsed_crypto.fixed,
4502        &parsed_crypto.extensions,
4503    )?;
4504    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4505    let block_records_start = startup_block_records_start(
4506        &volume_header,
4507        &parsed_crypto.kdf_params,
4508        |start, length| {
4509            let start = to_usize(start, "KeyWrapTableV1")?;
4510            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4511        },
4512    )?;
4513    let crypto_header = parsed_crypto.fixed.clone();
4514    Ok(ParsedOpenPrefix {
4515        volume_header,
4516        crypto_header,
4517        crypto_header_bytes: crypto_bytes.to_vec(),
4518        key_wrap_table_bytes: None,
4519        block_records_start,
4520        subkeys,
4521    })
4522}
4523
4524fn prefix_uses_recipient_wrap(bytes: &[u8]) -> bool {
4525    let Ok(volume_header_bytes) = slice(bytes, 0, VOLUME_HEADER_LEN, "archive") else {
4526        return false;
4527    };
4528    let Ok(volume_header) = VolumeHeader::parse(volume_header_bytes) else {
4529        return false;
4530    };
4531    let crypto_start = volume_header.crypto_header_offset as usize;
4532    let crypto_len = volume_header.crypto_header_length as usize;
4533    let Ok(crypto_bytes) = slice(bytes, crypto_start, crypto_len, "CryptoHeader") else {
4534        return false;
4535    };
4536    let Ok(parsed_crypto) = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)
4537    else {
4538        return false;
4539    };
4540    matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4541}
4542
4543fn parse_open_prefix_with_recipient_wrap_resolver<F>(
4544    bytes: &[u8],
4545    resolver: &mut F,
4546) -> Result<ParsedOpenPrefix, FormatError>
4547where
4548    F: FnMut(
4549        RecipientWrapRecordContext<'_>,
4550    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4551{
4552    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4553    parse_volume_format_dispatch(&volume_header)?;
4554    let crypto_start = volume_header.crypto_header_offset as usize;
4555    let crypto_len = volume_header.crypto_header_length as usize;
4556    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4557    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4558    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4559        || !parsed_crypto.fixed.aead_algo.is_encrypted()
4560    {
4561        return Err(FormatError::KeyMaterialMismatch);
4562    }
4563
4564    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4565    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4566
4567    let startup_key_wrap_table = startup_key_wrap_table(
4568        &volume_header,
4569        &parsed_crypto.kdf_params,
4570        |start, length| {
4571            let start = to_usize(start, "KeyWrapTableV1")?;
4572            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4573        },
4574    )?
4575    .ok_or(FormatError::KeyMaterialMismatch)?;
4576    let key_wrap_table = startup_key_wrap_table.table;
4577    let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4578    let block_records_start = startup_key_wrap_table.block_records_start;
4579
4580    let subkeys = recipient_wrap_subkeys_from_table(
4581        &volume_header,
4582        &parsed_crypto,
4583        &key_wrap_table,
4584        resolver,
4585    )?;
4586    parsed_crypto.validate_extension_semantics()?;
4587    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4588
4589    let crypto_header = parsed_crypto.fixed.clone();
4590    Ok(ParsedOpenPrefix {
4591        volume_header,
4592        crypto_header,
4593        crypto_header_bytes: crypto_bytes.to_vec(),
4594        key_wrap_table_bytes,
4595        block_records_start,
4596        subkeys,
4597    })
4598}
4599
4600pub(crate) fn recipient_wrap_subkeys_from_table<F>(
4601    volume_header: &VolumeHeader,
4602    parsed_crypto: &CryptoHeader<'_>,
4603    key_wrap_table: &KeyWrapTableV1,
4604    resolver: &mut F,
4605) -> Result<Subkeys, FormatError>
4606where
4607    F: FnMut(
4608            RecipientWrapRecordContext<'_>,
4609        ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
4610        + ?Sized,
4611{
4612    let archive_identity = RecipientWrapArchiveIdentity {
4613        archive_uuid: volume_header.archive_uuid,
4614        session_id: volume_header.session_id,
4615        format_version: volume_header.format_version,
4616        volume_format_rev: volume_header.volume_format_rev,
4617    };
4618
4619    for record in &key_wrap_table.recipient_records {
4620        let candidates = resolver(RecipientWrapRecordContext {
4621            archive_identity,
4622            record,
4623        })?;
4624        for candidate in candidates {
4625            let master_key = MasterKey::from_raw_key(&candidate)?;
4626            let subkeys = subkeys_for_open(
4627                Some(&master_key),
4628                parsed_crypto.fixed.aead_algo,
4629                &volume_header.archive_uuid,
4630                &volume_header.session_id,
4631            )?;
4632            if verify_integrity_tag(
4633                HmacDomain::CryptoHeader,
4634                parsed_crypto.fixed.aead_algo,
4635                volume_header.volume_format_rev,
4636                Some(&subkeys.mac_key),
4637                &volume_header.archive_uuid,
4638                &volume_header.session_id,
4639                parsed_crypto.hmac_covered_bytes,
4640                &parsed_crypto.header_hmac,
4641            )
4642            .is_ok()
4643            {
4644                return Ok(subkeys);
4645            }
4646        }
4647    }
4648    Err(FormatError::KeyMaterialMismatch)
4649}
4650
4651#[allow(clippy::too_many_arguments)]
4652fn finish_parse_seekable_volume(
4653    bytes: &[u8],
4654    volume_header: VolumeHeader,
4655    crypto_header: CryptoHeaderFixed,
4656    crypto_header_bytes: Vec<u8>,
4657    key_wrap_table_bytes: Option<Vec<u8>>,
4658    block_records_start: u64,
4659    subkeys: Subkeys,
4660    terminal: V41Terminal,
4661) -> Result<ParsedSeekableVolume, FormatError> {
4662    let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
4663    let volume_trailer = terminal.volume_trailer.clone();
4664    validate_trailer_identity(&volume_header, &volume_trailer)?;
4665
4666    let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
4667    let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
4668    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4669        if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
4670            || volume_trailer
4671                .root_auth_footer_offset
4672                .checked_add(volume_trailer.root_auth_footer_length as u64)
4673                .ok_or(FormatError::InvalidArchive(
4674                    "RootAuthFooter terminal boundary overflow",
4675                ))?
4676                != trailer_offset as u64
4677        {
4678            return Err(FormatError::InvalidArchive(
4679                "RootAuthFooter does not sit before selected trailer",
4680            ));
4681        }
4682    } else if manifest_end != trailer_offset {
4683        return Err(FormatError::InvalidArchive(
4684            "ManifestFooter does not end at selected trailer",
4685        ));
4686    }
4687    let manifest_bytes = &terminal.manifest_footer_bytes;
4688    let (manifest_footer, manifest_footer_error) =
4689        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4690        {
4691            Ok(footer) => (Some(footer), None),
4692            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4693            Err(err) => return Err(err),
4694        };
4695
4696    let block_region = parse_block_region(
4697        bytes,
4698        to_usize(block_records_start, "BlockRecord")?,
4699        manifest_offset,
4700        crypto_header.block_size as usize,
4701        &volume_header,
4702        &volume_trailer,
4703    )?;
4704
4705    Ok(ParsedSeekableVolume {
4706        volume_header,
4707        crypto_header,
4708        crypto_header_bytes,
4709        key_wrap_table_bytes,
4710        subkeys,
4711        manifest_footer,
4712        manifest_footer_error,
4713        root_auth_footer: terminal.root_auth_footer,
4714        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4715        volume_trailer,
4716        blocks: block_region.blocks,
4717        erased_block_indices: block_region.erased_block_indices,
4718    })
4719}
4720
4721fn parse_seekable_read_at_volume(
4722    reader: Arc<dyn ArchiveReadAt>,
4723    master_key: &MasterKey,
4724    options: ReaderOptions,
4725) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4726    let observed_len = reader.len()?;
4727    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4728        return Err(FormatError::InvalidLength {
4729            structure: "archive",
4730            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4731            actual: to_usize(observed_len, "archive")?,
4732        });
4733    }
4734
4735    let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
4736        Ok(prefix) => prefix,
4737        Err(prefix_err) => {
4738            if matches!(
4739                prefix_err,
4740                FormatError::UnsupportedVolumeFormatRevision { .. }
4741            ) {
4742                return Err(prefix_err);
4743            }
4744            return parse_seekable_read_at_volume_from_recovered_terminal(
4745                reader,
4746                observed_len,
4747                master_key,
4748                options,
4749            )
4750            .or(Err(prefix_err));
4751        }
4752    };
4753    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4754    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4755        Ok(parsed) => Ok(parsed),
4756        Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
4757            reader,
4758            observed_len,
4759            master_key,
4760            options,
4761        ) {
4762            Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4763                Ok(recovered)
4764            }
4765            Ok(_) | Err(_) => Err(prefix_err),
4766        },
4767    }
4768}
4769
4770fn parse_seekable_read_at_volume_with_recipient_wrap_resolver<F>(
4771    reader: Arc<dyn ArchiveReadAt>,
4772    resolver: &mut F,
4773    options: ReaderOptions,
4774) -> Result<ParsedSeekableReadAtVolume, FormatError>
4775where
4776    F: FnMut(
4777        RecipientWrapRecordContext<'_>,
4778    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4779{
4780    let observed_len = reader.len()?;
4781    if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4782        return Err(FormatError::InvalidLength {
4783            structure: "archive",
4784            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4785            actual: to_usize(observed_len, "archive")?,
4786        });
4787    }
4788
4789    let prefix = match parse_read_at_open_prefix_with_recipient_wrap_resolver(
4790        reader.as_ref(),
4791        resolver,
4792    ) {
4793        Ok(prefix) => prefix,
4794        Err(prefix_err) => {
4795            if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4796                return Err(prefix_err);
4797            }
4798            return parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4799                reader,
4800                observed_len,
4801                resolver,
4802                options,
4803            )
4804            .or(Err(prefix_err));
4805        }
4806    };
4807    let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4808    match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4809        Ok(parsed) => Ok(parsed),
4810        Err(prefix_err) => {
4811            match parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4812                reader,
4813                observed_len,
4814                resolver,
4815                options,
4816            ) {
4817                Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4818                    Ok(recovered)
4819                }
4820                Ok(_) | Err(_) => Err(prefix_err),
4821            }
4822        }
4823    }
4824}
4825
4826fn parse_seekable_read_at_volume_with_prefix(
4827    reader: Arc<dyn ArchiveReadAt>,
4828    observed_len: u64,
4829    prefix: ParsedReadAtOpenPrefix,
4830    options: ReaderOptions,
4831) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4832    let ParsedReadAtOpenPrefix {
4833        volume_header,
4834        crypto_header,
4835        crypto_header_bytes,
4836        key_wrap_table_bytes,
4837        block_records_start,
4838        subkeys,
4839    } = prefix;
4840
4841    let terminal = locate_v41_terminal_read_at(
4842        reader.as_ref(),
4843        observed_len,
4844        KeyHoldingTerminalContext {
4845            subkeys: &subkeys,
4846            volume_header: &volume_header,
4847            crypto_header: &crypto_header,
4848            crypto_header_bytes: &crypto_header_bytes,
4849        },
4850        options,
4851    )?;
4852    finish_parse_seekable_read_at_volume(
4853        reader,
4854        volume_header,
4855        crypto_header,
4856        crypto_header_bytes,
4857        key_wrap_table_bytes,
4858        block_records_start,
4859        subkeys,
4860        terminal,
4861    )
4862}
4863
4864fn parse_seekable_read_at_volume_from_recovered_terminal(
4865    reader: Arc<dyn ArchiveReadAt>,
4866    observed_len: u64,
4867    master_key: &MasterKey,
4868    options: ReaderOptions,
4869) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4870    let authority =
4871        locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
4872    parse_volume_format_dispatch(&authority.volume_header)?;
4873    if matches!(authority.kdf_params, KdfParams::RecipientWrap { .. }) {
4874        return Err(FormatError::KeyMaterialMismatch);
4875    }
4876    let block_records_start = startup_block_records_start(
4877        &authority.volume_header,
4878        &authority.kdf_params,
4879        |start, length| read_at_vec(reader.as_ref(), start, length, "KeyWrapTableV1"),
4880    )?;
4881    finish_parse_seekable_read_at_volume(
4882        reader,
4883        authority.volume_header,
4884        authority.crypto_header,
4885        authority.crypto_header_bytes,
4886        None,
4887        block_records_start,
4888        authority.subkeys,
4889        authority.terminal,
4890    )
4891}
4892
4893fn parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4894    reader: Arc<dyn ArchiveReadAt>,
4895    observed_len: u64,
4896    resolver: &mut F,
4897    options: ReaderOptions,
4898) -> Result<ParsedSeekableReadAtVolume, FormatError>
4899where
4900    F: FnMut(
4901        RecipientWrapRecordContext<'_>,
4902    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4903{
4904    let authority = locate_v41_recipient_wrap_terminal_authority_read_at(
4905        reader.as_ref(),
4906        observed_len,
4907        resolver,
4908        options,
4909    )?;
4910    finish_parse_seekable_read_at_volume(
4911        reader,
4912        authority.volume_header,
4913        authority.crypto_header,
4914        authority.crypto_header_bytes,
4915        Some(authority.key_wrap_table_bytes),
4916        authority.block_records_start,
4917        authority.subkeys,
4918        authority.terminal,
4919    )
4920}
4921
4922fn parse_read_at_open_prefix(
4923    reader: &dyn ArchiveReadAt,
4924    master_key: &MasterKey,
4925) -> Result<ParsedReadAtOpenPrefix, FormatError> {
4926    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4927    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4928    parse_volume_format_dispatch(&volume_header)?;
4929    let crypto_start = volume_header.crypto_header_offset as u64;
4930    let crypto_len = volume_header.crypto_header_length as u64;
4931    let crypto_bytes = read_at_vec(
4932        reader,
4933        crypto_start,
4934        to_usize(crypto_len, "CryptoHeader")?,
4935        "CryptoHeader",
4936    )?;
4937    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4938    if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4939        return Err(FormatError::KeyMaterialMismatch);
4940    }
4941    let subkeys = subkeys_for_open(
4942        Some(master_key),
4943        parsed_crypto.fixed.aead_algo,
4944        &volume_header.archive_uuid,
4945        &volume_header.session_id,
4946    )?;
4947    verify_integrity_tag(
4948        HmacDomain::CryptoHeader,
4949        parsed_crypto.fixed.aead_algo,
4950        volume_header.volume_format_rev,
4951        Some(&subkeys.mac_key),
4952        &volume_header.archive_uuid,
4953        &volume_header.session_id,
4954        parsed_crypto.hmac_covered_bytes,
4955        &parsed_crypto.header_hmac,
4956    )?;
4957    parsed_crypto.validate_extension_semantics()?;
4958    validate_seekable_supported_volume(
4959        &volume_header,
4960        &parsed_crypto.fixed,
4961        &parsed_crypto.extensions,
4962    )?;
4963    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4964    let block_records_start = startup_block_records_start(
4965        &volume_header,
4966        &parsed_crypto.kdf_params,
4967        |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4968    )?;
4969    let crypto_header = parsed_crypto.fixed.clone();
4970    drop(parsed_crypto);
4971    Ok(ParsedReadAtOpenPrefix {
4972        volume_header,
4973        crypto_header,
4974        crypto_header_bytes: crypto_bytes,
4975        key_wrap_table_bytes: None,
4976        block_records_start,
4977        subkeys,
4978    })
4979}
4980
4981fn parse_read_at_open_prefix_with_recipient_wrap_resolver<F>(
4982    reader: &dyn ArchiveReadAt,
4983    resolver: &mut F,
4984) -> Result<ParsedReadAtOpenPrefix, FormatError>
4985where
4986    F: FnMut(
4987        RecipientWrapRecordContext<'_>,
4988    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4989{
4990    let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4991    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4992    parse_volume_format_dispatch(&volume_header)?;
4993    let crypto_start = volume_header.crypto_header_offset as u64;
4994    let crypto_len = volume_header.crypto_header_length as u64;
4995    let crypto_bytes = read_at_vec(
4996        reader,
4997        crypto_start,
4998        to_usize(crypto_len, "CryptoHeader")?,
4999        "CryptoHeader",
5000    )?;
5001    let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
5002    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
5003        || !parsed_crypto.fixed.aead_algo.is_encrypted()
5004    {
5005        return Err(FormatError::KeyMaterialMismatch);
5006    }
5007
5008    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
5009    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5010
5011    let startup_key_wrap_table = startup_key_wrap_table(
5012        &volume_header,
5013        &parsed_crypto.kdf_params,
5014        |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
5015    )?
5016    .ok_or(FormatError::KeyMaterialMismatch)?;
5017    let key_wrap_table = startup_key_wrap_table.table;
5018    let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
5019    let block_records_start = startup_key_wrap_table.block_records_start;
5020
5021    let subkeys = recipient_wrap_subkeys_from_table(
5022        &volume_header,
5023        &parsed_crypto,
5024        &key_wrap_table,
5025        resolver,
5026    )?;
5027    parsed_crypto.validate_extension_semantics()?;
5028    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
5029
5030    let crypto_header = parsed_crypto.fixed.clone();
5031    Ok(ParsedReadAtOpenPrefix {
5032        volume_header,
5033        crypto_header,
5034        crypto_header_bytes: crypto_bytes,
5035        key_wrap_table_bytes,
5036        block_records_start,
5037        subkeys,
5038    })
5039}
5040
5041#[allow(clippy::too_many_arguments)]
5042fn finish_parse_seekable_read_at_volume(
5043    reader: Arc<dyn ArchiveReadAt>,
5044    volume_header: VolumeHeader,
5045    crypto_header: CryptoHeaderFixed,
5046    crypto_header_bytes: Vec<u8>,
5047    key_wrap_table_bytes: Option<Vec<u8>>,
5048    block_records_start: u64,
5049    subkeys: Subkeys,
5050    terminal: V41Terminal,
5051) -> Result<ParsedSeekableReadAtVolume, FormatError> {
5052    let volume_trailer = terminal.volume_trailer.clone();
5053    validate_trailer_identity(&volume_header, &volume_trailer)?;
5054
5055    let manifest_offset = volume_trailer.manifest_footer_offset;
5056    let manifest_end = checked_u64_add(
5057        manifest_offset,
5058        MANIFEST_FOOTER_LEN as u64,
5059        "ManifestFooter",
5060    )?;
5061    if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
5062        if volume_trailer.root_auth_footer_offset != manifest_end
5063            || volume_trailer
5064                .root_auth_footer_offset
5065                .checked_add(volume_trailer.root_auth_footer_length as u64)
5066                .ok_or(FormatError::InvalidArchive(
5067                    "RootAuthFooter terminal boundary overflow",
5068                ))?
5069                != terminal.image.volume_trailer_offset
5070        {
5071            return Err(FormatError::InvalidArchive(
5072                "RootAuthFooter does not sit before selected trailer",
5073            ));
5074        }
5075    } else if manifest_end != terminal.image.volume_trailer_offset {
5076        return Err(FormatError::InvalidArchive(
5077            "ManifestFooter does not end at selected trailer",
5078        ));
5079    }
5080    validate_seekable_block_region_layout(
5081        block_records_start,
5082        manifest_offset,
5083        crypto_header.block_size as usize,
5084        &volume_trailer,
5085    )?;
5086
5087    let manifest_bytes = &terminal.manifest_footer_bytes;
5088    let (manifest_footer, manifest_footer_error) =
5089        match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
5090        {
5091            Ok(footer) => (Some(footer), None),
5092            Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
5093            Err(err) => return Err(err),
5094        };
5095
5096    Ok(ParsedSeekableReadAtVolume {
5097        reader,
5098        volume_header,
5099        crypto_header,
5100        crypto_header_bytes,
5101        key_wrap_table_bytes,
5102        subkeys,
5103        manifest_footer,
5104        manifest_footer_error,
5105        root_auth_footer: terminal.root_auth_footer,
5106        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
5107        volume_trailer,
5108        block_records_start,
5109    })
5110}
5111
5112#[derive(Debug)]
5113struct ParsedPublicNoKeyVolume {
5114    volume_header: VolumeHeader,
5115    crypto_header: CryptoHeaderFixed,
5116    kdf_params: KdfParams,
5117    root_auth_footer: RootAuthFooterV1,
5118    root_auth_footer_bytes: Vec<u8>,
5119    blocks: BTreeMap<u64, BlockRecord>,
5120}
5121
5122pub fn public_no_key_verify_volumes_with_options<F>(
5123    volumes: &[&[u8]],
5124    mut verifier: F,
5125    options: ReaderOptions,
5126) -> Result<PublicNoKeyVerification, FormatError>
5127where
5128    F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
5129{
5130    validate_reader_options(options)?;
5131    if volumes.is_empty() {
5132        return Err(FormatError::InvalidArchive("no volumes supplied"));
5133    }
5134    let mut parsed = Vec::with_capacity(volumes.len());
5135    for volume in volumes {
5136        parsed.push(parse_public_no_key_volume(volume, options)?);
5137    }
5138    let first = parsed
5139        .first()
5140        .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
5141    if parsed.len() != first.crypto_header.stripe_width as usize {
5142        return Err(FormatError::ReaderUnsupported(
5143            "public no-key verification requires a complete volume set",
5144        ));
5145    }
5146
5147    let mut seen_volume_indexes = BTreeSet::new();
5148    let mut blocks = BTreeMap::new();
5149    for volume in &parsed {
5150        if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
5151            || volume.volume_header.session_id != first.volume_header.session_id
5152            || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
5153            || !public_kdf_profiles_agree(&volume.kdf_params, &first.kdf_params)
5154        {
5155            return Err(FormatError::InvalidArchive(
5156                "public no-key volume global metadata differs",
5157            ));
5158        }
5159        if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
5160            return Err(FormatError::InvalidArchive(
5161                "public no-key RootAuthFooter copies differ",
5162            ));
5163        }
5164        if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
5165            return Err(FormatError::InvalidArchive(
5166                "duplicate public no-key volume index",
5167            ));
5168        }
5169        for (block_index, record) in &volume.blocks {
5170            if blocks.insert(*block_index, record.clone()).is_some() {
5171                return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
5172            }
5173        }
5174    }
5175    validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
5176
5177    let footer = &first.root_auth_footer;
5178    let mut data_leaves = blocks
5179        .values()
5180        .filter(|record| record.kind.is_data())
5181        .map(|record| DataBlockMerkleLeaf {
5182            block_index: record.block_index,
5183            kind: record.kind,
5184            flags: record.flags,
5185            payload: record.payload.clone(),
5186        })
5187        .collect::<Vec<_>>();
5188    data_leaves.sort_by_key(|leaf| leaf.block_index);
5189    let total_data_block_count = u64::try_from(data_leaves.len())
5190        .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
5191    let observed_data_root = data_block_merkle_root_for_revision(
5192        footer.format_version,
5193        footer.volume_format_rev,
5194        &data_leaves,
5195    )?;
5196    if total_data_block_count != footer.total_data_block_count
5197        || observed_data_root != footer.data_block_merkle_root
5198    {
5199        return Err(FormatError::InvalidArchive(
5200            "public no-key data-block commitment mismatch",
5201        ));
5202    }
5203    let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
5204    if archive_root != footer.archive_root {
5205        return Err(FormatError::InvalidArchive(
5206            "public no-key archive_root mismatch",
5207        ));
5208    }
5209    if !verifier(footer, &archive_root)? {
5210        return Err(FormatError::InvalidArchive(
5211            "public no-key authenticator verification failed",
5212        ));
5213    }
5214    Ok(PublicNoKeyVerification {
5215        format_version: footer.format_version,
5216        volume_format_rev: footer.volume_format_rev,
5217        archive_root,
5218        authenticator_id: footer.authenticator_id,
5219        signer_identity_type: footer.signer_identity_type,
5220        signer_identity_bytes: footer.signer_identity_bytes.clone(),
5221        total_data_block_count,
5222        diagnostics: vec![
5223            PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
5224            PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
5225            PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
5226        ],
5227    })
5228}
5229
5230fn parse_public_no_key_volume(
5231    bytes: &[u8],
5232    options: ReaderOptions,
5233) -> Result<ParsedPublicNoKeyVolume, FormatError> {
5234    if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
5235        return Err(FormatError::InvalidLength {
5236            structure: "archive",
5237            expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
5238            actual: bytes.len(),
5239        });
5240    }
5241    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
5242    parse_volume_format_dispatch(&volume_header)?;
5243    let crypto_start = volume_header.crypto_header_offset as usize;
5244    let crypto_len = volume_header.crypto_header_length as usize;
5245    let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
5246    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
5247    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
5248    parsed_crypto.validate_extension_semantics()?;
5249    validate_seekable_supported_volume(
5250        &volume_header,
5251        &parsed_crypto.fixed,
5252        &parsed_crypto.extensions,
5253    )?;
5254    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5255
5256    let terminal = locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto, options)?;
5257    let block_records_start = match &parsed_crypto.kdf_params {
5258        KdfParams::RecipientWrap {
5259            key_wrap_table_length,
5260            ..
5261        } => checked_add(
5262            crypto_end,
5263            *key_wrap_table_length as usize,
5264            "KeyWrapTableV1",
5265        )?,
5266        _ => crypto_end,
5267    };
5268    let block_region = parse_public_block_observation(
5269        bytes,
5270        block_records_start,
5271        &terminal.image,
5272        parsed_crypto.fixed.block_size as usize,
5273        &volume_header,
5274    )?;
5275    Ok(ParsedPublicNoKeyVolume {
5276        volume_header,
5277        crypto_header: parsed_crypto.fixed,
5278        kdf_params: parsed_crypto.kdf_params,
5279        root_auth_footer: terminal.root_auth_footer,
5280        root_auth_footer_bytes: terminal.root_auth_footer_bytes,
5281        blocks: block_region,
5282    })
5283}
5284
5285fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
5286    left.length == right.length
5287        && left.stripe_width == right.stripe_width
5288        && left.block_size == right.block_size
5289        && left.compression_algo == right.compression_algo
5290        && left.aead_algo == right.aead_algo
5291        && left.fec_algo == right.fec_algo
5292        && left.kdf_algo == right.kdf_algo
5293}
5294
5295fn public_kdf_profiles_agree(left: &KdfParams, right: &KdfParams) -> bool {
5296    match (left, right) {
5297        (
5298            KdfParams::RecipientWrap {
5299                key_wrap_table_length: left_length,
5300                key_wrap_table_record_count: left_count,
5301                key_wrap_table_version: left_version,
5302                key_wrap_table_digest: left_digest,
5303            },
5304            KdfParams::RecipientWrap {
5305                key_wrap_table_length: right_length,
5306                key_wrap_table_record_count: right_count,
5307                key_wrap_table_version: right_version,
5308                key_wrap_table_digest: right_digest,
5309            },
5310        ) => {
5311            left_length == right_length
5312                && left_count == right_count
5313                && left_version == right_version
5314                && left_digest == right_digest
5315        }
5316        (KdfParams::RecipientWrap { .. }, _) | (_, KdfParams::RecipientWrap { .. }) => false,
5317        _ => true,
5318    }
5319}
5320
5321fn recompute_public_archive_root(
5322    footer: &RootAuthFooterV1,
5323    crypto_header: &CryptoHeaderFixed,
5324) -> Result<[u8; 32], FormatError> {
5325    let descriptor_digest = root_auth_descriptor_digest_for_revision(
5326        footer.format_version,
5327        footer.volume_format_rev,
5328        footer.authenticator_id,
5329        footer.signer_identity_type,
5330        &footer.signer_identity_bytes,
5331        u32::try_from(footer.authenticator_value.len()).map_err(|_| {
5332            FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
5333        })?,
5334        footer.footer_length()?,
5335    )?;
5336    let signer_digest =
5337        signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
5338    if signer_digest != footer.signer_identity_digest {
5339        return Err(FormatError::InvalidArchive(
5340            "public no-key signer identity digest mismatch",
5341        ));
5342    }
5343    archive_root_for_revision(ArchiveRootInputs {
5344        archive_uuid: footer.archive_uuid,
5345        session_id: footer.session_id,
5346        format_version: footer.format_version,
5347        volume_format_rev: footer.volume_format_rev,
5348        compression_algo: crypto_header.compression_algo,
5349        aead_algo: crypto_header.aead_algo,
5350        fec_algo: crypto_header.fec_algo,
5351        kdf_algo: crypto_header.kdf_algo,
5352        critical_metadata_digest: footer.critical_metadata_digest,
5353        index_digest: footer.index_digest,
5354        fec_layout_digest: footer.fec_layout_digest,
5355        total_data_block_count: footer.total_data_block_count,
5356        data_block_merkle_root: footer.data_block_merkle_root,
5357        root_auth_descriptor_digest: descriptor_digest,
5358        signer_identity_digest: signer_digest,
5359    })
5360}
5361
5362fn parse_valid_manifest_footer(
5363    volume_header: &VolumeHeader,
5364    crypto_header: &CryptoHeaderFixed,
5365    subkeys: &Subkeys,
5366    manifest_bytes: &[u8],
5367) -> Result<ManifestFooter, FormatError> {
5368    let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5369    validate_manifest_footer(
5370        volume_header,
5371        crypto_header,
5372        &manifest_footer,
5373        subkeys,
5374        volume_header.volume_format_rev,
5375        manifest_bytes,
5376    )?;
5377    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5378    Ok(manifest_footer)
5379}
5380
5381fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
5382    matches!(
5383        error,
5384        FormatError::BadMagic {
5385            structure: "ManifestFooter",
5386        } | FormatError::NonZeroReserved {
5387            structure: "ManifestFooter",
5388        } | FormatError::InvalidAuthoritativeFlag(_)
5389            | FormatError::HmacMismatch {
5390                structure: "ManifestFooter",
5391            }
5392            | FormatError::IntegrityDigestMismatch {
5393                structure: "ManifestFooter",
5394            }
5395    )
5396}
5397
5398fn validate_seekable_supported_volume(
5399    volume_header: &VolumeHeader,
5400    crypto_header: &CryptoHeaderFixed,
5401    extensions: &[ExtensionTlv<'_>],
5402) -> Result<(), FormatError> {
5403    reject_unsupported_raw_stream_profile(extensions)?;
5404    if crypto_header.stripe_width != volume_header.stripe_width {
5405        return Err(FormatError::InvalidArchive(
5406            "VolumeHeader and CryptoHeader stripe_width differ",
5407        ));
5408    }
5409    Ok(())
5410}
5411
5412pub(crate) fn validate_crypto_class_parity_exactness(
5413    crypto_header: &CryptoHeaderFixed,
5414) -> Result<(), FormatError> {
5415    let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
5416    if crypto_header.fec_parity_shards as u32 != fec {
5417        return Err(FormatError::InvalidArchive(
5418            "fec_parity_shards does not match v41 compute_parity",
5419        ));
5420    }
5421    let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
5422    if crypto_header.index_fec_parity_shards as u32 != index {
5423        return Err(FormatError::InvalidArchive(
5424            "index_fec_parity_shards does not match v41 compute_parity",
5425        ));
5426    }
5427    let index_root = required_object_parity(
5428        crypto_header.index_root_fec_data_shards as u64,
5429        crypto_header,
5430    )?;
5431    if crypto_header.index_root_fec_parity_shards as u32 != index_root {
5432        return Err(FormatError::InvalidArchive(
5433            "index_root_fec_parity_shards does not match v41 compute_parity",
5434        ));
5435    }
5436    Ok(())
5437}
5438
5439fn validate_volume_set_member(
5440    first: &ParsedSeekableVolume,
5441    candidate: &ParsedSeekableVolume,
5442) -> Result<(), FormatError> {
5443    validate_volume_set_member_metadata(
5444        &first.volume_header,
5445        &first.crypto_header,
5446        &first.crypto_header_bytes,
5447        &candidate.volume_header,
5448        &candidate.crypto_header,
5449        &candidate.crypto_header_bytes,
5450    )?;
5451    validate_key_wrap_table_bytes_match(
5452        &first.key_wrap_table_bytes,
5453        &candidate.key_wrap_table_bytes,
5454    )
5455}
5456
5457fn validate_key_wrap_table_bytes_match(
5458    first_key_wrap_table_bytes: &Option<Vec<u8>>,
5459    candidate_key_wrap_table_bytes: &Option<Vec<u8>>,
5460) -> Result<(), FormatError> {
5461    if candidate_key_wrap_table_bytes != first_key_wrap_table_bytes {
5462        return Err(FormatError::InvalidArchive("KeyWrapTableV1 copies differ"));
5463    }
5464    Ok(())
5465}
5466
5467fn validate_volume_set_member_metadata(
5468    first_volume_header: &VolumeHeader,
5469    first_crypto_header: &CryptoHeaderFixed,
5470    first_crypto_header_bytes: &[u8],
5471    candidate_volume_header: &VolumeHeader,
5472    candidate_crypto_header: &CryptoHeaderFixed,
5473    candidate_crypto_header_bytes: &[u8],
5474) -> Result<(), FormatError> {
5475    if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
5476        || candidate_volume_header.session_id != first_volume_header.session_id
5477    {
5478        return Err(FormatError::InvalidArchive(
5479            "mixed archive or session IDs in volume set",
5480        ));
5481    }
5482    if candidate_crypto_header_bytes != first_crypto_header_bytes
5483        || candidate_crypto_header != first_crypto_header
5484    {
5485        return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
5486    }
5487    Ok(())
5488}
5489
5490pub(crate) fn manifest_bootstrap_fields_match(
5491    left: &ManifestFooter,
5492    right: &ManifestFooter,
5493) -> bool {
5494    left.archive_uuid == right.archive_uuid
5495        && left.session_id == right.session_id
5496        && left.is_authoritative == right.is_authoritative
5497        && left.total_volumes == right.total_volumes
5498        && left.index_root_first_block == right.index_root_first_block
5499        && left.index_root_data_block_count == right.index_root_data_block_count
5500        && left.index_root_parity_block_count == right.index_root_parity_block_count
5501        && left.index_root_encrypted_size == right.index_root_encrypted_size
5502        && left.index_root_decompressed_size == right.index_root_decompressed_size
5503}
5504
5505fn validate_complete_global_block_coverage(
5506    blocks: &BTreeMap<u64, BlockRecord>,
5507    erased_block_indices: &BTreeSet<u64>,
5508) -> Result<(), FormatError> {
5509    let mut expected = 0u64;
5510    let mut block_iter = blocks.keys().copied().peekable();
5511    let mut erasure_iter = erased_block_indices.iter().copied().peekable();
5512
5513    loop {
5514        let next_block = block_iter.peek().copied();
5515        let next_erasure = erasure_iter.peek().copied();
5516        let next = match (next_block, next_erasure) {
5517            (Some(block), Some(erasure)) if block == erasure => {
5518                return Err(FormatError::InvalidArchive(
5519                    "BlockRecord index is both present and erased",
5520                ));
5521            }
5522            (Some(block), Some(erasure)) => block.min(erasure),
5523            (Some(block), None) => block,
5524            (None, Some(erasure)) => erasure,
5525            (None, None) => return Ok(()),
5526        };
5527
5528        if next != expected {
5529            return Err(FormatError::InvalidArchive(
5530                "complete volume set has missing global blocks",
5531            ));
5532        }
5533        if next_block == Some(next) {
5534            block_iter.next();
5535        }
5536        if next_erasure == Some(next) {
5537            erasure_iter.next();
5538        }
5539        expected = expected
5540            .checked_add(1)
5541            .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
5542    }
5543}
5544
5545#[derive(Debug)]
5546struct V41Terminal {
5547    image: CriticalMetadataImage,
5548    manifest_footer_bytes: Vec<u8>,
5549    root_auth_footer_bytes: Option<Vec<u8>>,
5550    root_auth_footer: Option<RootAuthFooterV1>,
5551    volume_trailer: VolumeTrailer,
5552}
5553
5554pub(crate) struct SequentialTerminalMaterial {
5555    pub(crate) manifest_footer: ManifestFooter,
5556    pub(crate) volume_trailer: VolumeTrailer,
5557    pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
5558}
5559
5560#[derive(Debug)]
5561struct V41PublicTerminal {
5562    image: CriticalMetadataImage,
5563    root_auth_footer_bytes: Vec<u8>,
5564    root_auth_footer: RootAuthFooterV1,
5565}
5566
5567#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5568struct CmraDecoderTuple {
5569    shard_size: u32,
5570    data_shard_count: u16,
5571    parity_shard_count: u16,
5572    image_length: u32,
5573    image_sha256: [u8; 32],
5574}
5575
5576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5577struct CmraIdentityHints {
5578    archive_uuid: [u8; 16],
5579    session_id: [u8; 16],
5580    volume_index: u32,
5581}
5582
5583impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
5584    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5585        Self {
5586            shard_size: header.shard_size,
5587            data_shard_count: header.data_shard_count,
5588            parity_shard_count: header.parity_shard_count,
5589            image_length: header.image_length,
5590            image_sha256: header.image_sha256,
5591        }
5592    }
5593}
5594
5595impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
5596    fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5597        Self {
5598            archive_uuid: header.archive_uuid_hint,
5599            session_id: header.session_id_hint,
5600            volume_index: header.volume_index_hint,
5601        }
5602    }
5603}
5604
5605impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
5606    fn from(locator: CriticalRecoveryLocator) -> Self {
5607        Self {
5608            shard_size: locator.cmra_shard_size,
5609            data_shard_count: locator.cmra_data_shard_count,
5610            parity_shard_count: locator.cmra_parity_shard_count,
5611            image_length: locator.cmra_image_length,
5612            image_sha256: locator.cmra_image_sha256,
5613        }
5614    }
5615}
5616
5617impl From<CriticalRecoveryLocator> for CmraIdentityHints {
5618    fn from(locator: CriticalRecoveryLocator) -> Self {
5619        Self {
5620            archive_uuid: locator.archive_uuid_hint,
5621            session_id: locator.session_id_hint,
5622            volume_index: locator.volume_index_hint,
5623        }
5624    }
5625}
5626
5627#[derive(Debug)]
5628struct RecoveredCmra {
5629    image: CriticalMetadataImage,
5630    tuple: CmraDecoderTuple,
5631    header_hints: Option<CmraIdentityHints>,
5632    cmra_length: u64,
5633}
5634
5635#[derive(Debug)]
5636struct TerminalCandidate {
5637    terminal: V41Terminal,
5638    anchor: usize,
5639    locator_sequence: Option<u32>,
5640    cmra_offset: u64,
5641    cmra_length: u64,
5642}
5643
5644#[derive(Debug)]
5645struct PublicTerminalCandidate {
5646    terminal: V41PublicTerminal,
5647    anchor: usize,
5648    cmra_offset: u64,
5649    cmra_length: u64,
5650}
5651
5652#[derive(Debug)]
5653struct RecoveredTerminalAuthority {
5654    terminal: V41Terminal,
5655    volume_header: VolumeHeader,
5656    crypto_header: CryptoHeaderFixed,
5657    crypto_header_bytes: Vec<u8>,
5658    subkeys: Subkeys,
5659    kdf_params: KdfParams,
5660}
5661
5662#[derive(Debug)]
5663struct RecoveredRecipientWrapTerminalAuthority {
5664    terminal: V41Terminal,
5665    volume_header: VolumeHeader,
5666    crypto_header: CryptoHeaderFixed,
5667    crypto_header_bytes: Vec<u8>,
5668    key_wrap_table_bytes: Vec<u8>,
5669    block_records_start: u64,
5670    subkeys: Subkeys,
5671}
5672
5673#[derive(Debug)]
5674struct TerminalAuthorityCandidate {
5675    authority: RecoveredTerminalAuthority,
5676    anchor: usize,
5677    cmra_offset: u64,
5678    cmra_length: u64,
5679}
5680
5681#[derive(Debug)]
5682struct RecipientWrapTerminalAuthorityCandidate {
5683    authority: RecoveredRecipientWrapTerminalAuthority,
5684    anchor: usize,
5685    cmra_offset: u64,
5686    cmra_length: u64,
5687}
5688
5689#[derive(Debug, Clone, Copy)]
5690enum CmraRecoveryMode {
5691    KeyHolding,
5692    PublicNoKey,
5693}
5694
5695#[derive(Clone, Copy)]
5696pub(crate) struct KeyHoldingTerminalContext<'a> {
5697    pub(crate) subkeys: &'a Subkeys,
5698    pub(crate) volume_header: &'a VolumeHeader,
5699    pub(crate) crypto_header: &'a CryptoHeaderFixed,
5700    pub(crate) crypto_header_bytes: &'a [u8],
5701}
5702
5703fn locate_v41_terminal(
5704    bytes: &[u8],
5705    context: KeyHoldingTerminalContext<'_>,
5706    options: ReaderOptions,
5707) -> Result<V41Terminal, FormatError> {
5708    locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
5709}
5710
5711fn locate_v41_terminal_read_at(
5712    reader: &dyn ArchiveReadAt,
5713    len: u64,
5714    context: KeyHoldingTerminalContext<'_>,
5715    options: ReaderOptions,
5716) -> Result<V41Terminal, FormatError> {
5717    let mut candidates = Vec::new();
5718    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5719        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5720        collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
5721    }
5722    if len >= LOCATOR_PAIR_LEN as u64 {
5723        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5724        collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
5725    }
5726
5727    if candidates.is_empty() {
5728        let scan = max_critical_recovery_scan(options)? as u64;
5729        let scan_start = len.saturating_sub(scan);
5730        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5731        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5732        let mut offset = tail.len().saturating_sub(4);
5733        while offset < tail.len() {
5734            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5735            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5736                collect_v41_locator_candidate_read_at(
5737                    reader,
5738                    absolute_offset,
5739                    2,
5740                    context,
5741                    &mut candidates,
5742                );
5743            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5744                if let Ok(candidate) =
5745                    parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
5746                {
5747                    candidates.push(candidate);
5748                }
5749            }
5750            if offset == 0 {
5751                break;
5752            }
5753            offset -= 1;
5754        }
5755    }
5756
5757    choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5758}
5759
5760fn locate_v41_terminal_authority(
5761    bytes: &[u8],
5762    master_key: &MasterKey,
5763    options: ReaderOptions,
5764) -> Result<RecoveredTerminalAuthority, FormatError> {
5765    let mut candidates = Vec::new();
5766    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5767        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5768        collect_v41_locator_authority_candidate(
5769            bytes,
5770            final_offset,
5771            0,
5772            master_key,
5773            &mut candidates,
5774        );
5775    }
5776    if bytes.len() >= LOCATOR_PAIR_LEN {
5777        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5778        collect_v41_locator_authority_candidate(
5779            bytes,
5780            mirror_offset,
5781            1,
5782            master_key,
5783            &mut candidates,
5784        );
5785    }
5786
5787    if candidates.is_empty() {
5788        let scan = max_critical_recovery_scan(options)?;
5789        let scan_start = bytes.len().saturating_sub(scan);
5790        let mut offset = bytes.len().saturating_sub(4);
5791        while offset >= scan_start {
5792            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5793                collect_v41_locator_authority_candidate(
5794                    bytes,
5795                    offset,
5796                    2,
5797                    master_key,
5798                    &mut candidates,
5799                );
5800            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5801                if let Ok(candidate) =
5802                    parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
5803                {
5804                    candidates.push(candidate);
5805                }
5806            }
5807            if offset == 0 {
5808                break;
5809            }
5810            offset -= 1;
5811        }
5812    }
5813
5814    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5815}
5816
5817fn locate_v41_recipient_wrap_terminal_authority<F>(
5818    bytes: &[u8],
5819    resolver: &mut F,
5820    options: ReaderOptions,
5821) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5822where
5823    F: FnMut(
5824        RecipientWrapRecordContext<'_>,
5825    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5826{
5827    let mut candidates = Vec::new();
5828    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5829        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5830        collect_v41_recipient_wrap_locator_authority_candidate(
5831            bytes,
5832            final_offset,
5833            0,
5834            resolver,
5835            &mut candidates,
5836        );
5837    }
5838    if bytes.len() >= LOCATOR_PAIR_LEN {
5839        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5840        collect_v41_recipient_wrap_locator_authority_candidate(
5841            bytes,
5842            mirror_offset,
5843            1,
5844            resolver,
5845            &mut candidates,
5846        );
5847    }
5848
5849    if candidates.is_empty() {
5850        let scan = max_critical_recovery_scan(options)?;
5851        let scan_start = bytes.len().saturating_sub(scan);
5852        let mut offset = bytes.len().saturating_sub(4);
5853        while offset >= scan_start {
5854            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5855                collect_v41_recipient_wrap_locator_authority_candidate(
5856                    bytes,
5857                    offset,
5858                    2,
5859                    resolver,
5860                    &mut candidates,
5861                );
5862            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5863                if let Ok(candidate) = parse_locatorless_cmra_recipient_wrap_authority_candidate(
5864                    bytes, offset, resolver,
5865                ) {
5866                    candidates.push(candidate);
5867                }
5868            }
5869            if offset == 0 {
5870                break;
5871            }
5872            offset -= 1;
5873        }
5874    }
5875
5876    choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5877        .map(|candidate| candidate.authority)
5878}
5879
5880fn locate_v41_recipient_wrap_terminal_authority_read_at<F>(
5881    reader: &dyn ArchiveReadAt,
5882    len: u64,
5883    resolver: &mut F,
5884    options: ReaderOptions,
5885) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5886where
5887    F: FnMut(
5888        RecipientWrapRecordContext<'_>,
5889    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5890{
5891    let mut candidates = Vec::new();
5892    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5893        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5894        collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5895            reader,
5896            final_offset,
5897            0,
5898            resolver,
5899            &mut candidates,
5900        );
5901    }
5902    if len >= LOCATOR_PAIR_LEN as u64 {
5903        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5904        collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5905            reader,
5906            mirror_offset,
5907            1,
5908            resolver,
5909            &mut candidates,
5910        );
5911    }
5912
5913    if candidates.is_empty() {
5914        let scan = max_critical_recovery_scan(options)? as u64;
5915        let scan_start = len.saturating_sub(scan);
5916        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5917        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5918        let mut offset = tail.len().saturating_sub(4);
5919        while offset < tail.len() {
5920            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5921            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5922                collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5923                    reader,
5924                    absolute_offset,
5925                    2,
5926                    resolver,
5927                    &mut candidates,
5928                );
5929            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5930                if let Ok(candidate) =
5931                    parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at(
5932                        reader,
5933                        absolute_offset,
5934                        resolver,
5935                    )
5936                {
5937                    candidates.push(candidate);
5938                }
5939            }
5940            if offset == 0 {
5941                break;
5942            }
5943            offset -= 1;
5944        }
5945    }
5946
5947    choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5948        .map(|candidate| candidate.authority)
5949}
5950
5951fn locate_v41_terminal_authority_read_at(
5952    reader: &dyn ArchiveReadAt,
5953    len: u64,
5954    master_key: &MasterKey,
5955    options: ReaderOptions,
5956) -> Result<RecoveredTerminalAuthority, FormatError> {
5957    let mut candidates = Vec::new();
5958    if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5959        let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5960        collect_v41_locator_authority_candidate_read_at(
5961            reader,
5962            final_offset,
5963            0,
5964            master_key,
5965            &mut candidates,
5966        );
5967    }
5968    if len >= LOCATOR_PAIR_LEN as u64 {
5969        let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5970        collect_v41_locator_authority_candidate_read_at(
5971            reader,
5972            mirror_offset,
5973            1,
5974            master_key,
5975            &mut candidates,
5976        );
5977    }
5978
5979    if candidates.is_empty() {
5980        let scan = max_critical_recovery_scan(options)? as u64;
5981        let scan_start = len.saturating_sub(scan);
5982        let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5983        let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5984        let mut offset = tail.len().saturating_sub(4);
5985        while offset < tail.len() {
5986            let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5987            if tail.get(offset..offset + 4) == Some(b"TZCL") {
5988                collect_v41_locator_authority_candidate_read_at(
5989                    reader,
5990                    absolute_offset,
5991                    2,
5992                    master_key,
5993                    &mut candidates,
5994                );
5995            } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5996                if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
5997                    reader,
5998                    absolute_offset,
5999                    master_key,
6000                ) {
6001                    candidates.push(candidate);
6002                }
6003            }
6004            if offset == 0 {
6005                break;
6006            }
6007            offset -= 1;
6008        }
6009    }
6010
6011    choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
6012}
6013
6014fn locate_v41_terminal_candidate(
6015    bytes: &[u8],
6016    context: KeyHoldingTerminalContext<'_>,
6017    options: ReaderOptions,
6018) -> Result<TerminalCandidate, FormatError> {
6019    let mut candidates = Vec::new();
6020    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
6021        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
6022        collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
6023    }
6024    if bytes.len() >= LOCATOR_PAIR_LEN {
6025        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
6026        collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
6027    }
6028
6029    if candidates.is_empty() {
6030        let scan = max_critical_recovery_scan(options)?;
6031        let scan_start = bytes.len().saturating_sub(scan);
6032        let mut offset = bytes.len().saturating_sub(4);
6033        while offset >= scan_start {
6034            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
6035                collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
6036            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
6037                if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
6038                    candidates.push(candidate);
6039                }
6040            }
6041            if offset == 0 {
6042                break;
6043            }
6044            offset -= 1;
6045        }
6046    }
6047
6048    choose_v41_terminal_candidate(candidates)
6049}
6050
6051fn locate_v41_public_terminal(
6052    bytes: &[u8],
6053    volume_header: &VolumeHeader,
6054    crypto_header: &CryptoHeader<'_>,
6055    options: ReaderOptions,
6056) -> Result<V41PublicTerminal, FormatError> {
6057    let mut candidates = Vec::new();
6058    if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
6059        let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
6060        collect_v41_public_locator_candidate(
6061            bytes,
6062            final_offset,
6063            0,
6064            volume_header,
6065            crypto_header,
6066            &mut candidates,
6067        );
6068    }
6069    if bytes.len() >= LOCATOR_PAIR_LEN {
6070        let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
6071        collect_v41_public_locator_candidate(
6072            bytes,
6073            mirror_offset,
6074            1,
6075            volume_header,
6076            crypto_header,
6077            &mut candidates,
6078        );
6079    }
6080
6081    if candidates.is_empty() {
6082        let scan = max_critical_recovery_scan(options)?;
6083        let scan_start = bytes.len().saturating_sub(scan);
6084        let mut offset = bytes.len().saturating_sub(4);
6085        while offset >= scan_start {
6086            if bytes.get(offset..offset + 4) == Some(b"TZCL") {
6087                collect_v41_public_locator_candidate(
6088                    bytes,
6089                    offset,
6090                    2,
6091                    volume_header,
6092                    crypto_header,
6093                    &mut candidates,
6094                );
6095            } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
6096                if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
6097                    bytes,
6098                    offset,
6099                    volume_header,
6100                    crypto_header,
6101                ) {
6102                    candidates.push(candidate);
6103                }
6104            }
6105            if offset == 0 {
6106                break;
6107            }
6108            offset -= 1;
6109        }
6110    }
6111
6112    choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
6113}
6114
6115fn collect_v41_locator_candidate(
6116    bytes: &[u8],
6117    offset: usize,
6118    expected_sequence: u32,
6119    context: KeyHoldingTerminalContext<'_>,
6120    candidates: &mut Vec<TerminalCandidate>,
6121) {
6122    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6123        return;
6124    };
6125    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6126        return;
6127    };
6128    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6129        return;
6130    }
6131    if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
6132        candidates.push(candidate);
6133    }
6134}
6135
6136fn collect_v41_locator_candidate_read_at(
6137    reader: &dyn ArchiveReadAt,
6138    offset: u64,
6139    expected_sequence: u32,
6140    context: KeyHoldingTerminalContext<'_>,
6141    candidates: &mut Vec<TerminalCandidate>,
6142) {
6143    let Ok(raw) = read_at_vec(
6144        reader,
6145        offset,
6146        CRITICAL_RECOVERY_LOCATOR_LEN,
6147        "CriticalRecoveryLocator",
6148    ) else {
6149        return;
6150    };
6151    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6152        return;
6153    };
6154    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6155        return;
6156    }
6157    if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
6158        candidates.push(candidate);
6159    }
6160}
6161
6162fn collect_v41_locator_authority_candidate(
6163    bytes: &[u8],
6164    offset: usize,
6165    expected_sequence: u32,
6166    master_key: &MasterKey,
6167    candidates: &mut Vec<TerminalAuthorityCandidate>,
6168) {
6169    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6170        return;
6171    };
6172    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6173        return;
6174    };
6175    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6176        return;
6177    }
6178    if let Ok(candidate) =
6179        parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
6180    {
6181        candidates.push(candidate);
6182    }
6183}
6184
6185fn collect_v41_recipient_wrap_locator_authority_candidate<F>(
6186    bytes: &[u8],
6187    offset: usize,
6188    expected_sequence: u32,
6189    resolver: &mut F,
6190    candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
6191) where
6192    F: FnMut(
6193        RecipientWrapRecordContext<'_>,
6194    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6195{
6196    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6197        return;
6198    };
6199    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6200        return;
6201    };
6202    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6203        return;
6204    }
6205    if let Ok(candidate) =
6206        parse_locator_cmra_recipient_wrap_authority_candidate(bytes, offset, locator, resolver)
6207    {
6208        candidates.push(candidate);
6209    }
6210}
6211
6212fn collect_v41_recipient_wrap_locator_authority_candidate_read_at<F>(
6213    reader: &dyn ArchiveReadAt,
6214    offset: u64,
6215    expected_sequence: u32,
6216    resolver: &mut F,
6217    candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
6218) where
6219    F: FnMut(
6220        RecipientWrapRecordContext<'_>,
6221    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6222{
6223    let Ok(raw) = read_at_vec(
6224        reader,
6225        offset,
6226        CRITICAL_RECOVERY_LOCATOR_LEN,
6227        "CriticalRecoveryLocator",
6228    ) else {
6229        return;
6230    };
6231    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6232        return;
6233    };
6234    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6235        return;
6236    }
6237    if let Ok(candidate) = parse_locator_cmra_recipient_wrap_authority_candidate_read_at(
6238        reader, offset, locator, resolver,
6239    ) {
6240        candidates.push(candidate);
6241    }
6242}
6243
6244fn collect_v41_locator_authority_candidate_read_at(
6245    reader: &dyn ArchiveReadAt,
6246    offset: u64,
6247    expected_sequence: u32,
6248    master_key: &MasterKey,
6249    candidates: &mut Vec<TerminalAuthorityCandidate>,
6250) {
6251    let Ok(raw) = read_at_vec(
6252        reader,
6253        offset,
6254        CRITICAL_RECOVERY_LOCATOR_LEN,
6255        "CriticalRecoveryLocator",
6256    ) else {
6257        return;
6258    };
6259    let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6260        return;
6261    };
6262    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6263        return;
6264    }
6265    if let Ok(candidate) =
6266        parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
6267    {
6268        candidates.push(candidate);
6269    }
6270}
6271
6272fn collect_v41_public_locator_candidate(
6273    bytes: &[u8],
6274    offset: usize,
6275    expected_sequence: u32,
6276    volume_header: &VolumeHeader,
6277    crypto_header: &CryptoHeader<'_>,
6278    candidates: &mut Vec<PublicTerminalCandidate>,
6279) {
6280    let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6281        return;
6282    };
6283    let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6284        return;
6285    };
6286    if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6287        return;
6288    }
6289    if let Ok(candidate) =
6290        parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
6291    {
6292        candidates.push(candidate);
6293    }
6294}
6295
6296fn choose_v41_terminal_candidate(
6297    mut candidates: Vec<TerminalCandidate>,
6298) -> Result<TerminalCandidate, FormatError> {
6299    candidates.sort_by_key(|candidate| candidate.anchor);
6300    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6301        "no valid v41 CMRA candidate found",
6302    ))?;
6303    if let Some(previous) = candidates.last() {
6304        if previous.anchor == winner.anchor
6305            && (previous.cmra_offset != winner.cmra_offset
6306                || previous.cmra_length != winner.cmra_length)
6307        {
6308            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6309        }
6310    }
6311    Ok(winner)
6312}
6313
6314fn choose_v41_terminal_authority_candidate(
6315    mut candidates: Vec<TerminalAuthorityCandidate>,
6316) -> Result<TerminalAuthorityCandidate, FormatError> {
6317    candidates.sort_by_key(|candidate| candidate.anchor);
6318    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6319        "no valid v41 CMRA candidate found",
6320    ))?;
6321    if let Some(previous) = candidates.last() {
6322        if previous.anchor == winner.anchor
6323            && (previous.cmra_offset != winner.cmra_offset
6324                || previous.cmra_length != winner.cmra_length)
6325        {
6326            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6327        }
6328    }
6329    Ok(winner)
6330}
6331
6332fn choose_v41_recipient_wrap_terminal_authority_candidate(
6333    mut candidates: Vec<RecipientWrapTerminalAuthorityCandidate>,
6334) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError> {
6335    candidates.sort_by_key(|candidate| candidate.anchor);
6336    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6337        "no valid v41 CMRA candidate found",
6338    ))?;
6339    if let Some(previous) = candidates.last() {
6340        if previous.anchor == winner.anchor
6341            && (previous.cmra_offset != winner.cmra_offset
6342                || previous.cmra_length != winner.cmra_length)
6343        {
6344            return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6345        }
6346    }
6347    Ok(winner)
6348}
6349
6350fn choose_v41_public_terminal_candidate(
6351    mut candidates: Vec<PublicTerminalCandidate>,
6352) -> Result<PublicTerminalCandidate, FormatError> {
6353    candidates.sort_by_key(|candidate| candidate.anchor);
6354    let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6355        "no valid v41 public CMRA candidate found",
6356    ))?;
6357    if let Some(previous) = candidates.last() {
6358        if previous.anchor == winner.anchor
6359            && (previous.cmra_offset != winner.cmra_offset
6360                || previous.cmra_length != winner.cmra_length)
6361        {
6362            return Err(FormatError::InvalidArchive(
6363                "ambiguous v41 public CMRA candidates",
6364            ));
6365        }
6366    }
6367    Ok(winner)
6368}
6369
6370fn parse_locator_cmra_candidate(
6371    bytes: &[u8],
6372    locator_offset: usize,
6373    locator: CriticalRecoveryLocator,
6374    context: KeyHoldingTerminalContext<'_>,
6375) -> Result<TerminalCandidate, FormatError> {
6376    let tuple = CmraDecoderTuple::from(locator);
6377    validate_cmra_decoder_tuple(tuple)?;
6378    let expected_cmra_length = cmra_serialized_length(tuple)?;
6379    if locator.cmra_length as u64 != expected_cmra_length {
6380        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6381    }
6382    validate_locator_position(locator_offset, locator)?;
6383    let recovered = recover_cmra(
6384        bytes,
6385        locator.cmra_offset,
6386        Some(tuple),
6387        CmraRecoveryMode::KeyHolding,
6388    )?;
6389    if recovered.tuple != tuple {
6390        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6391    }
6392    if expected_cmra_length != recovered.cmra_length {
6393        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6394    }
6395    validate_locator_image_boundary(locator, &recovered.image)?;
6396    validate_cmra_identity_hints(
6397        recovered.header_hints,
6398        Some(CmraIdentityHints::from(locator)),
6399        &recovered.image,
6400    )?;
6401    let terminal =
6402        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
6403    Ok(TerminalCandidate {
6404        terminal,
6405        anchor: locator_offset
6406            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6407            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6408        locator_sequence: Some(locator.locator_sequence),
6409        cmra_offset: locator.cmra_offset,
6410        cmra_length: recovered.cmra_length,
6411    })
6412}
6413
6414fn parse_locator_cmra_candidate_read_at(
6415    reader: &dyn ArchiveReadAt,
6416    locator_offset: u64,
6417    locator: CriticalRecoveryLocator,
6418    context: KeyHoldingTerminalContext<'_>,
6419) -> Result<TerminalCandidate, FormatError> {
6420    let tuple = CmraDecoderTuple::from(locator);
6421    validate_cmra_decoder_tuple(tuple)?;
6422    let expected_cmra_length = cmra_serialized_length(tuple)?;
6423    if locator.cmra_length as u64 != expected_cmra_length {
6424        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6425    }
6426    validate_locator_position(
6427        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6428        locator,
6429    )?;
6430    let recovered = recover_cmra_read_at(
6431        reader,
6432        locator.cmra_offset,
6433        Some(tuple),
6434        CmraRecoveryMode::KeyHolding,
6435    )?;
6436    if recovered.tuple != tuple {
6437        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6438    }
6439    if expected_cmra_length != recovered.cmra_length {
6440        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6441    }
6442    validate_locator_image_boundary(locator, &recovered.image)?;
6443    validate_cmra_identity_hints(
6444        recovered.header_hints,
6445        Some(CmraIdentityHints::from(locator)),
6446        &recovered.image,
6447    )?;
6448    let terminal = validate_recovered_terminal_read_at(
6449        recovered.image,
6450        recovered.tuple,
6451        reader,
6452        context,
6453        false,
6454    )?;
6455    Ok(TerminalCandidate {
6456        terminal,
6457        anchor: to_usize(
6458            checked_u64_add(
6459                locator_offset,
6460                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6461                "locator anchor overflow",
6462            )?,
6463            "locator anchor overflow",
6464        )?,
6465        locator_sequence: Some(locator.locator_sequence),
6466        cmra_offset: locator.cmra_offset,
6467        cmra_length: recovered.cmra_length,
6468    })
6469}
6470
6471fn parse_locator_cmra_authority_candidate(
6472    bytes: &[u8],
6473    locator_offset: usize,
6474    locator: CriticalRecoveryLocator,
6475    master_key: &MasterKey,
6476) -> Result<TerminalAuthorityCandidate, FormatError> {
6477    let tuple = CmraDecoderTuple::from(locator);
6478    validate_cmra_decoder_tuple(tuple)?;
6479    let expected_cmra_length = cmra_serialized_length(tuple)?;
6480    if locator.cmra_length as u64 != expected_cmra_length {
6481        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6482    }
6483    validate_locator_position(locator_offset, locator)?;
6484    let recovered = recover_cmra(
6485        bytes,
6486        locator.cmra_offset,
6487        Some(tuple),
6488        CmraRecoveryMode::KeyHolding,
6489    )?;
6490    if recovered.tuple != tuple {
6491        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6492    }
6493    if expected_cmra_length != recovered.cmra_length {
6494        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6495    }
6496    validate_locator_image_boundary(locator, &recovered.image)?;
6497    validate_cmra_identity_hints(
6498        recovered.header_hints,
6499        Some(CmraIdentityHints::from(locator)),
6500        &recovered.image,
6501    )?;
6502    let cmra_length = recovered.cmra_length;
6503    let authority =
6504        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6505    Ok(TerminalAuthorityCandidate {
6506        authority,
6507        anchor: locator_offset
6508            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6509            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6510        cmra_offset: locator.cmra_offset,
6511        cmra_length,
6512    })
6513}
6514
6515fn parse_locator_cmra_recipient_wrap_authority_candidate<F>(
6516    bytes: &[u8],
6517    locator_offset: usize,
6518    locator: CriticalRecoveryLocator,
6519    resolver: &mut F,
6520) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6521where
6522    F: FnMut(
6523        RecipientWrapRecordContext<'_>,
6524    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6525{
6526    let tuple = CmraDecoderTuple::from(locator);
6527    validate_cmra_decoder_tuple(tuple)?;
6528    let expected_cmra_length = cmra_serialized_length(tuple)?;
6529    if locator.cmra_length as u64 != expected_cmra_length {
6530        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6531    }
6532    validate_locator_position(locator_offset, locator)?;
6533    let recovered = recover_cmra(
6534        bytes,
6535        locator.cmra_offset,
6536        Some(tuple),
6537        CmraRecoveryMode::KeyHolding,
6538    )?;
6539    if recovered.tuple != tuple {
6540        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6541    }
6542    if expected_cmra_length != recovered.cmra_length {
6543        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6544    }
6545    validate_locator_image_boundary(locator, &recovered.image)?;
6546    validate_cmra_identity_hints(
6547        recovered.header_hints,
6548        Some(CmraIdentityHints::from(locator)),
6549        &recovered.image,
6550    )?;
6551    let cmra_length = recovered.cmra_length;
6552    let authority = validate_recovered_recipient_wrap_terminal_authority(
6553        recovered.image,
6554        recovered.tuple,
6555        resolver,
6556        false,
6557    )?;
6558    Ok(RecipientWrapTerminalAuthorityCandidate {
6559        authority,
6560        anchor: locator_offset
6561            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6562            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6563        cmra_offset: locator.cmra_offset,
6564        cmra_length,
6565    })
6566}
6567
6568fn parse_locator_cmra_recipient_wrap_authority_candidate_read_at<F>(
6569    reader: &dyn ArchiveReadAt,
6570    locator_offset: u64,
6571    locator: CriticalRecoveryLocator,
6572    resolver: &mut F,
6573) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6574where
6575    F: FnMut(
6576        RecipientWrapRecordContext<'_>,
6577    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6578{
6579    let tuple = CmraDecoderTuple::from(locator);
6580    validate_cmra_decoder_tuple(tuple)?;
6581    let expected_cmra_length = cmra_serialized_length(tuple)?;
6582    if locator.cmra_length as u64 != expected_cmra_length {
6583        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6584    }
6585    validate_locator_position(
6586        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6587        locator,
6588    )?;
6589    let recovered = recover_cmra_read_at(
6590        reader,
6591        locator.cmra_offset,
6592        Some(tuple),
6593        CmraRecoveryMode::KeyHolding,
6594    )?;
6595    if recovered.tuple != tuple {
6596        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6597    }
6598    if expected_cmra_length != recovered.cmra_length {
6599        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6600    }
6601    validate_locator_image_boundary(locator, &recovered.image)?;
6602    validate_cmra_identity_hints(
6603        recovered.header_hints,
6604        Some(CmraIdentityHints::from(locator)),
6605        &recovered.image,
6606    )?;
6607    let cmra_length = recovered.cmra_length;
6608    let authority = validate_recovered_recipient_wrap_terminal_authority(
6609        recovered.image,
6610        recovered.tuple,
6611        resolver,
6612        false,
6613    )?;
6614    Ok(RecipientWrapTerminalAuthorityCandidate {
6615        authority,
6616        anchor: to_usize(
6617            checked_u64_add(
6618                locator_offset,
6619                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6620                "locator anchor overflow",
6621            )?,
6622            "locator anchor overflow",
6623        )?,
6624        cmra_offset: locator.cmra_offset,
6625        cmra_length,
6626    })
6627}
6628
6629fn parse_locator_cmra_authority_candidate_read_at(
6630    reader: &dyn ArchiveReadAt,
6631    locator_offset: u64,
6632    locator: CriticalRecoveryLocator,
6633    master_key: &MasterKey,
6634) -> Result<TerminalAuthorityCandidate, FormatError> {
6635    let tuple = CmraDecoderTuple::from(locator);
6636    validate_cmra_decoder_tuple(tuple)?;
6637    let expected_cmra_length = cmra_serialized_length(tuple)?;
6638    if locator.cmra_length as u64 != expected_cmra_length {
6639        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6640    }
6641    validate_locator_position(
6642        to_usize(locator_offset, "CriticalRecoveryLocator")?,
6643        locator,
6644    )?;
6645    let recovered = recover_cmra_read_at(
6646        reader,
6647        locator.cmra_offset,
6648        Some(tuple),
6649        CmraRecoveryMode::KeyHolding,
6650    )?;
6651    if recovered.tuple != tuple {
6652        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6653    }
6654    if expected_cmra_length != recovered.cmra_length {
6655        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6656    }
6657    validate_locator_image_boundary(locator, &recovered.image)?;
6658    validate_cmra_identity_hints(
6659        recovered.header_hints,
6660        Some(CmraIdentityHints::from(locator)),
6661        &recovered.image,
6662    )?;
6663    let cmra_length = recovered.cmra_length;
6664    let authority =
6665        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6666    Ok(TerminalAuthorityCandidate {
6667        authority,
6668        anchor: to_usize(
6669            checked_u64_add(
6670                locator_offset,
6671                CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6672                "locator anchor overflow",
6673            )?,
6674            "locator anchor overflow",
6675        )?,
6676        cmra_offset: locator.cmra_offset,
6677        cmra_length,
6678    })
6679}
6680
6681fn parse_public_locator_cmra_candidate(
6682    bytes: &[u8],
6683    locator_offset: usize,
6684    locator: CriticalRecoveryLocator,
6685    volume_header: &VolumeHeader,
6686    crypto_header: &CryptoHeader<'_>,
6687) -> Result<PublicTerminalCandidate, FormatError> {
6688    let tuple = CmraDecoderTuple::from(locator);
6689    validate_cmra_decoder_tuple(tuple)?;
6690    let expected_cmra_length = cmra_serialized_length(tuple)?;
6691    if locator.cmra_length as u64 != expected_cmra_length {
6692        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6693    }
6694    validate_locator_position(locator_offset, locator)?;
6695    let recovered = recover_cmra(
6696        bytes,
6697        locator.cmra_offset,
6698        Some(tuple),
6699        CmraRecoveryMode::PublicNoKey,
6700    )?;
6701    if recovered.tuple != tuple {
6702        return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6703    }
6704    if expected_cmra_length != recovered.cmra_length {
6705        return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6706    }
6707    validate_locator_image_boundary(locator, &recovered.image)?;
6708    validate_cmra_identity_hints(
6709        recovered.header_hints,
6710        Some(CmraIdentityHints::from(locator)),
6711        &recovered.image,
6712    )?;
6713    let terminal = validate_recovered_public_terminal(
6714        recovered.image,
6715        bytes,
6716        volume_header,
6717        crypto_header,
6718        false,
6719    )?;
6720    Ok(PublicTerminalCandidate {
6721        terminal,
6722        anchor: locator_offset
6723            .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6724            .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6725        cmra_offset: locator.cmra_offset,
6726        cmra_length: recovered.cmra_length,
6727    })
6728}
6729
6730fn parse_locatorless_cmra_candidate(
6731    bytes: &[u8],
6732    cmra_offset: usize,
6733    context: KeyHoldingTerminalContext<'_>,
6734) -> Result<TerminalCandidate, FormatError> {
6735    let recovered = recover_cmra(
6736        bytes,
6737        cmra_offset as u64,
6738        None,
6739        CmraRecoveryMode::KeyHolding,
6740    )?;
6741    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6742        return Err(FormatError::InvalidArchive(
6743            "locatorless CMRA boundary mismatch",
6744        ));
6745    }
6746    if recovered
6747        .image
6748        .volume_trailer_offset
6749        .checked_add(VOLUME_TRAILER_LEN as u64)
6750        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6751        != cmra_offset as u64
6752    {
6753        return Err(FormatError::InvalidArchive(
6754            "locatorless trailer boundary mismatch",
6755        ));
6756    }
6757    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6758    let terminal =
6759        validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
6760    Ok(TerminalCandidate {
6761        terminal,
6762        anchor: cmra_offset
6763            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6764            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6765        locator_sequence: None,
6766        cmra_offset: cmra_offset as u64,
6767        cmra_length: recovered.cmra_length,
6768    })
6769}
6770
6771fn parse_locatorless_cmra_candidate_read_at(
6772    reader: &dyn ArchiveReadAt,
6773    cmra_offset: u64,
6774    context: KeyHoldingTerminalContext<'_>,
6775) -> Result<TerminalCandidate, FormatError> {
6776    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6777    if recovered.image.body_bytes_before_cmra != cmra_offset {
6778        return Err(FormatError::InvalidArchive(
6779            "locatorless CMRA boundary mismatch",
6780        ));
6781    }
6782    if recovered
6783        .image
6784        .volume_trailer_offset
6785        .checked_add(VOLUME_TRAILER_LEN as u64)
6786        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6787        != cmra_offset
6788    {
6789        return Err(FormatError::InvalidArchive(
6790            "locatorless trailer boundary mismatch",
6791        ));
6792    }
6793    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6794    let terminal = validate_recovered_terminal_read_at(
6795        recovered.image,
6796        recovered.tuple,
6797        reader,
6798        context,
6799        true,
6800    )?;
6801    Ok(TerminalCandidate {
6802        terminal,
6803        anchor: to_usize(
6804            checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
6805            "CMRA anchor overflow",
6806        )?,
6807        locator_sequence: None,
6808        cmra_offset,
6809        cmra_length: recovered.cmra_length,
6810    })
6811}
6812
6813fn parse_locatorless_cmra_authority_candidate(
6814    bytes: &[u8],
6815    cmra_offset: usize,
6816    master_key: &MasterKey,
6817) -> Result<TerminalAuthorityCandidate, FormatError> {
6818    let recovered = recover_cmra(
6819        bytes,
6820        cmra_offset as u64,
6821        None,
6822        CmraRecoveryMode::KeyHolding,
6823    )?;
6824    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6825        return Err(FormatError::InvalidArchive(
6826            "locatorless CMRA boundary mismatch",
6827        ));
6828    }
6829    if recovered
6830        .image
6831        .volume_trailer_offset
6832        .checked_add(VOLUME_TRAILER_LEN as u64)
6833        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6834        != cmra_offset as u64
6835    {
6836        return Err(FormatError::InvalidArchive(
6837            "locatorless trailer boundary mismatch",
6838        ));
6839    }
6840    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6841    let cmra_length = recovered.cmra_length;
6842    let authority =
6843        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6844    Ok(TerminalAuthorityCandidate {
6845        authority,
6846        anchor: cmra_offset
6847            .checked_add(to_usize(cmra_length, "CMRA")?)
6848            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6849        cmra_offset: cmra_offset as u64,
6850        cmra_length,
6851    })
6852}
6853
6854fn parse_locatorless_cmra_recipient_wrap_authority_candidate<F>(
6855    bytes: &[u8],
6856    cmra_offset: usize,
6857    resolver: &mut F,
6858) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6859where
6860    F: FnMut(
6861        RecipientWrapRecordContext<'_>,
6862    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6863{
6864    let recovered = recover_cmra(
6865        bytes,
6866        cmra_offset as u64,
6867        None,
6868        CmraRecoveryMode::KeyHolding,
6869    )?;
6870    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6871        return Err(FormatError::InvalidArchive(
6872            "locatorless CMRA boundary mismatch",
6873        ));
6874    }
6875    if recovered
6876        .image
6877        .volume_trailer_offset
6878        .checked_add(VOLUME_TRAILER_LEN as u64)
6879        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6880        != cmra_offset as u64
6881    {
6882        return Err(FormatError::InvalidArchive(
6883            "locatorless trailer boundary mismatch",
6884        ));
6885    }
6886    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6887    let cmra_length = recovered.cmra_length;
6888    let authority = validate_recovered_recipient_wrap_terminal_authority(
6889        recovered.image,
6890        recovered.tuple,
6891        resolver,
6892        true,
6893    )?;
6894    Ok(RecipientWrapTerminalAuthorityCandidate {
6895        authority,
6896        anchor: cmra_offset
6897            .checked_add(to_usize(cmra_length, "CMRA")?)
6898            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6899        cmra_offset: cmra_offset as u64,
6900        cmra_length,
6901    })
6902}
6903
6904fn parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at<F>(
6905    reader: &dyn ArchiveReadAt,
6906    cmra_offset: u64,
6907    resolver: &mut F,
6908) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6909where
6910    F: FnMut(
6911        RecipientWrapRecordContext<'_>,
6912    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6913{
6914    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6915    if recovered.image.body_bytes_before_cmra != cmra_offset {
6916        return Err(FormatError::InvalidArchive(
6917            "locatorless CMRA boundary mismatch",
6918        ));
6919    }
6920    if recovered
6921        .image
6922        .volume_trailer_offset
6923        .checked_add(VOLUME_TRAILER_LEN as u64)
6924        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6925        != cmra_offset
6926    {
6927        return Err(FormatError::InvalidArchive(
6928            "locatorless trailer boundary mismatch",
6929        ));
6930    }
6931    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6932    let cmra_length = recovered.cmra_length;
6933    let authority = validate_recovered_recipient_wrap_terminal_authority(
6934        recovered.image,
6935        recovered.tuple,
6936        resolver,
6937        true,
6938    )?;
6939    Ok(RecipientWrapTerminalAuthorityCandidate {
6940        authority,
6941        anchor: to_usize(
6942            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6943            "CMRA anchor overflow",
6944        )?,
6945        cmra_offset,
6946        cmra_length,
6947    })
6948}
6949
6950fn parse_locatorless_cmra_authority_candidate_read_at(
6951    reader: &dyn ArchiveReadAt,
6952    cmra_offset: u64,
6953    master_key: &MasterKey,
6954) -> Result<TerminalAuthorityCandidate, FormatError> {
6955    let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6956    if recovered.image.body_bytes_before_cmra != cmra_offset {
6957        return Err(FormatError::InvalidArchive(
6958            "locatorless CMRA boundary mismatch",
6959        ));
6960    }
6961    if recovered
6962        .image
6963        .volume_trailer_offset
6964        .checked_add(VOLUME_TRAILER_LEN as u64)
6965        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6966        != cmra_offset
6967    {
6968        return Err(FormatError::InvalidArchive(
6969            "locatorless trailer boundary mismatch",
6970        ));
6971    }
6972    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6973    let cmra_length = recovered.cmra_length;
6974    let authority =
6975        validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6976    Ok(TerminalAuthorityCandidate {
6977        authority,
6978        anchor: to_usize(
6979            checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6980            "CMRA anchor overflow",
6981        )?,
6982        cmra_offset,
6983        cmra_length,
6984    })
6985}
6986
6987fn parse_public_locatorless_cmra_candidate(
6988    bytes: &[u8],
6989    cmra_offset: usize,
6990    volume_header: &VolumeHeader,
6991    crypto_header: &CryptoHeader<'_>,
6992) -> Result<PublicTerminalCandidate, FormatError> {
6993    let recovered = recover_cmra(
6994        bytes,
6995        cmra_offset as u64,
6996        None,
6997        CmraRecoveryMode::PublicNoKey,
6998    )?;
6999    if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
7000        return Err(FormatError::InvalidArchive(
7001            "locatorless CMRA boundary mismatch",
7002        ));
7003    }
7004    if recovered
7005        .image
7006        .volume_trailer_offset
7007        .checked_add(VOLUME_TRAILER_LEN as u64)
7008        .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
7009        != cmra_offset as u64
7010    {
7011        return Err(FormatError::InvalidArchive(
7012            "locatorless trailer boundary mismatch",
7013        ));
7014    }
7015    validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
7016    let terminal = validate_recovered_public_terminal(
7017        recovered.image,
7018        bytes,
7019        volume_header,
7020        crypto_header,
7021        true,
7022    )?;
7023    Ok(PublicTerminalCandidate {
7024        terminal,
7025        anchor: cmra_offset
7026            .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
7027            .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
7028        cmra_offset: cmra_offset as u64,
7029        cmra_length: recovered.cmra_length,
7030    })
7031}
7032
7033fn validate_locator_position(
7034    locator_offset: usize,
7035    locator: CriticalRecoveryLocator,
7036) -> Result<(), FormatError> {
7037    if locator.cmra_offset != locator.body_bytes_before_cmra {
7038        return Err(FormatError::InvalidArchive(
7039            "locator CMRA boundary mismatch",
7040        ));
7041    }
7042    if locator
7043        .volume_trailer_offset
7044        .checked_add(VOLUME_TRAILER_LEN as u64)
7045        .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
7046        != locator.cmra_offset
7047    {
7048        return Err(FormatError::InvalidArchive(
7049            "locator trailer boundary mismatch",
7050        ));
7051    }
7052    let expected_offset = match locator.locator_sequence {
7053        1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
7054        0 => locator
7055            .cmra_offset
7056            .checked_add(locator.cmra_length as u64)
7057            .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
7058        _ => None,
7059    }
7060    .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
7061    if expected_offset != locator_offset as u64 {
7062        return Err(FormatError::InvalidArchive(
7063            "locator position does not match sequence",
7064        ));
7065    }
7066    Ok(())
7067}
7068
7069fn validate_locator_image_boundary(
7070    locator: CriticalRecoveryLocator,
7071    image: &CriticalMetadataImage,
7072) -> Result<(), FormatError> {
7073    if locator.volume_format_rev != image.volume_format_rev
7074        || locator.volume_trailer_offset != image.volume_trailer_offset
7075        || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
7076        || image
7077            .volume_trailer_offset
7078            .checked_add(VOLUME_TRAILER_LEN as u64)
7079            .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7080            != locator.cmra_offset
7081    {
7082        return Err(FormatError::InvalidArchive(
7083            "locator and CMRA image boundaries differ",
7084        ));
7085    }
7086    Ok(())
7087}
7088
7089fn validate_cmra_identity_hints(
7090    header_hints: Option<CmraIdentityHints>,
7091    locator_hints: Option<CmraIdentityHints>,
7092    image: &CriticalMetadataImage,
7093) -> Result<(), FormatError> {
7094    if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
7095        if header != locator {
7096            return Err(FormatError::InvalidArchive(
7097                "CMRA header and locator identity hints differ",
7098            ));
7099        }
7100    }
7101    for hints in [header_hints, locator_hints].into_iter().flatten() {
7102        if hints.archive_uuid != image.archive_uuid
7103            || hints.session_id != image.session_id
7104            || hints.volume_index != image.volume_index
7105        {
7106            return Err(FormatError::InvalidArchive(
7107                "CMRA identity hints do not match recovered image",
7108            ));
7109        }
7110    }
7111    Ok(())
7112}
7113
7114fn recover_cmra(
7115    bytes: &[u8],
7116    cmra_offset: u64,
7117    locator_tuple: Option<CmraDecoderTuple>,
7118    mode: CmraRecoveryMode,
7119) -> Result<RecoveredCmra, FormatError> {
7120    let offset = to_usize(cmra_offset, "CMRA")?;
7121    let header_bytes = slice(
7122        bytes,
7123        offset,
7124        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
7125        "CriticalMetadataRecoveryHeader",
7126    )?;
7127    let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
7128    validate_cmra_decoder_tuple(tuple)?;
7129    let cmra_length = cmra_serialized_length(tuple)?;
7130    let cmra_len = to_usize(cmra_length, "CMRA")?;
7131    let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
7132    recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
7133}
7134
7135fn recover_cmra_read_at(
7136    reader: &dyn ArchiveReadAt,
7137    cmra_offset: u64,
7138    locator_tuple: Option<CmraDecoderTuple>,
7139    mode: CmraRecoveryMode,
7140) -> Result<RecoveredCmra, FormatError> {
7141    let header_bytes = read_at_vec(
7142        reader,
7143        cmra_offset,
7144        CRITICAL_METADATA_RECOVERY_HEADER_LEN,
7145        "CriticalMetadataRecoveryHeader",
7146    )?;
7147    let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
7148    validate_cmra_decoder_tuple(tuple)?;
7149    let cmra_length = cmra_serialized_length(tuple)?;
7150    let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
7151    recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
7152}
7153
7154fn recover_cmra_header_tuple(
7155    header_bytes: &[u8],
7156    locator_tuple: Option<CmraDecoderTuple>,
7157) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
7158    let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
7159    Ok(match (parsed_header, locator_tuple) {
7160        (Ok(header), Some(locator_tuple)) => {
7161            let header_tuple = CmraDecoderTuple::from(header);
7162            if header_tuple != locator_tuple {
7163                return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
7164            }
7165            (locator_tuple, Some(CmraIdentityHints::from(header)))
7166        }
7167        (Ok(header), None) => (
7168            CmraDecoderTuple::from(header),
7169            Some(CmraIdentityHints::from(header)),
7170        ),
7171        (Err(_), Some(tuple)) => (tuple, None),
7172        (Err(err), _) => return Err(err),
7173    })
7174}
7175
7176fn recover_cmra_from_bytes(
7177    cmra_bytes: &[u8],
7178    tuple: CmraDecoderTuple,
7179    header_hints: Option<CmraIdentityHints>,
7180    cmra_length: u64,
7181    mode: CmraRecoveryMode,
7182) -> Result<RecoveredCmra, FormatError> {
7183    let shard_size = tuple.shard_size as usize;
7184    let mut data_shards = vec![None; tuple.data_shard_count as usize];
7185    let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
7186    let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
7187    for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
7188        let raw = slice(
7189            cmra_bytes,
7190            cursor,
7191            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
7192            "CriticalMetadataRecoveryShard",
7193        )?;
7194        let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
7195        if let Some(shard) = shard {
7196            validate_cmra_shard(&shard, idx, tuple)?;
7197            if shard.shard_role == 0 {
7198                let data_slot = data_shards
7199                    .get_mut(idx)
7200                    .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
7201                *data_slot = Some(shard.payload);
7202            } else {
7203                let parity_idx = idx - tuple.data_shard_count as usize;
7204                let parity_slot =
7205                    parity_shards
7206                        .get_mut(parity_idx)
7207                        .ok_or(FormatError::InvalidArchive(
7208                            "CMRA parity shard out of range",
7209                        ))?;
7210                *parity_slot = Some(shard.payload);
7211            }
7212        }
7213        cursor = checked_add(
7214            cursor,
7215            CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
7216            "CriticalMetadataRecoveryShard",
7217        )?;
7218    }
7219    let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
7220    let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
7221    for shard in repaired {
7222        image_bytes.extend_from_slice(&shard);
7223    }
7224    image_bytes.truncate(tuple.image_length as usize);
7225    if sha256_bytes(&image_bytes) != tuple.image_sha256 {
7226        return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
7227    }
7228    let image = CriticalMetadataImage::parse(&image_bytes)?;
7229    validate_critical_metadata_image(&image, mode)?;
7230    Ok(RecoveredCmra {
7231        image,
7232        tuple,
7233        header_hints,
7234        cmra_length,
7235    })
7236}
7237
7238fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
7239    let shard_size = tuple.shard_size as u64;
7240    if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
7241        return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
7242    }
7243    let image_length = tuple.image_length as u64;
7244    let min = critical_image_min();
7245    let cap = critical_image_cap()?;
7246    if image_length < min || image_length > cap {
7247        return Err(FormatError::InvalidArchive(
7248            "CMRA image_length is outside bounds",
7249        ));
7250    }
7251    let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
7252    if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
7253        return Err(FormatError::InvalidArchive(
7254            "CMRA data_shard_count does not match image length",
7255        ));
7256    }
7257    let max_parity = 2u64.max(ceil_div_u64(
7258        checked_u64_mul(
7259            expected_data_shards,
7260            READER_MAX_CMRA_PARITY_PCT as u64,
7261            "CMRA parity overflow",
7262        )?,
7263        100,
7264    )?);
7265    if tuple.parity_shard_count as u64 > max_parity {
7266        return Err(FormatError::ReaderResourceLimitExceeded {
7267            field: "CMRA parity shard count",
7268            cap: max_parity,
7269            actual: tuple.parity_shard_count as u64,
7270        });
7271    }
7272    let total = expected_data_shards
7273        .checked_add(tuple.parity_shard_count as u64)
7274        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
7275    if total > 65_535 {
7276        return Err(FormatError::FecTooManyShards(total as usize));
7277    }
7278    Ok(())
7279}
7280
7281fn validate_cmra_writer_parity_lower_bound(
7282    tuple: CmraDecoderTuple,
7283    bit_rot_buffer_pct: u8,
7284) -> Result<(), FormatError> {
7285    let min_parity = 2u64.max(ceil_div_u64(
7286        checked_u64_mul(
7287            tuple.data_shard_count as u64,
7288            bit_rot_buffer_pct as u64,
7289            "CMRA parity lower-bound overflow",
7290        )?,
7291        100,
7292    )?);
7293    if (tuple.parity_shard_count as u64) < min_parity {
7294        return Err(FormatError::InvalidArchive(
7295            "CMRA parity shard count is below authenticated bit-rot lower bound",
7296        ));
7297    }
7298    Ok(())
7299}
7300
7301fn validate_cmra_shard(
7302    shard: &CriticalMetadataRecoveryShard,
7303    serialized_idx: usize,
7304    tuple: CmraDecoderTuple,
7305) -> Result<(), FormatError> {
7306    if shard.shard_index as usize != serialized_idx {
7307        return Err(FormatError::InvalidArchive(
7308            "CMRA shards are not in canonical order",
7309        ));
7310    }
7311    let data_count = tuple.data_shard_count as usize;
7312    let shard_size = tuple.shard_size as usize;
7313    if serialized_idx < data_count {
7314        if shard.shard_role != 0 {
7315            return Err(FormatError::InvalidArchive(
7316                "CMRA data shard has wrong role",
7317            ));
7318        }
7319        let expected_len = if serialized_idx + 1 == data_count {
7320            let used = tuple.image_length as usize - serialized_idx * shard_size;
7321            if used == 0 {
7322                shard_size
7323            } else {
7324                used
7325            }
7326        } else {
7327            shard_size
7328        };
7329        if shard.shard_payload_length as usize != expected_len {
7330            return Err(FormatError::InvalidArchive(
7331                "CMRA data shard payload length is non-canonical",
7332            ));
7333        }
7334        if serialized_idx + 1 == data_count
7335            && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
7336        {
7337            return Err(FormatError::InvalidArchive(
7338                "CMRA final data shard padding is non-zero",
7339            ));
7340        }
7341    } else {
7342        if shard.shard_role != 1 {
7343            return Err(FormatError::InvalidArchive(
7344                "CMRA parity shard has wrong role",
7345            ));
7346        }
7347        if shard.shard_payload_length as usize != shard_size {
7348            return Err(FormatError::InvalidArchive(
7349                "CMRA parity shard payload length is non-canonical",
7350            ));
7351        }
7352    }
7353    Ok(())
7354}
7355
7356fn validate_critical_metadata_image(
7357    image: &CriticalMetadataImage,
7358    mode: CmraRecoveryMode,
7359) -> Result<(), FormatError> {
7360    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7361    let key_wrap_layout_present = image.layout_flags & 0x0000_0002 != 0;
7362    let key_wrap_region = image.region(6);
7363    if key_wrap_layout_present != key_wrap_region.is_some() {
7364        return Err(FormatError::InvalidArchive(
7365            "CriticalMetadataImage key-wrap layout flag mismatch",
7366        ));
7367    }
7368    let key_wrap_present = key_wrap_layout_present;
7369    if image.volume_header_offset != 0
7370        || image.volume_header_length != VOLUME_HEADER_LEN as u32
7371        || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
7372        || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7373        || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
7374        || image.body_bytes_before_cmra
7375            != image
7376                .volume_trailer_offset
7377                .checked_add(VOLUME_TRAILER_LEN as u64)
7378                .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7379    {
7380        return Err(FormatError::InvalidArchive(
7381            "CriticalMetadataImage fixed layout is invalid",
7382        ));
7383    }
7384    if root_auth_present {
7385        if image.root_auth_footer_offset == 0
7386            || image.root_auth_footer_length == 0
7387            || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7388        {
7389            return Err(FormatError::InvalidArchive(
7390                "CriticalMetadataImage root-auth range is invalid",
7391            ));
7392        }
7393    } else if image.root_auth_footer_offset != 0
7394        || image.root_auth_footer_length != 0
7395        || image.root_auth_footer_sha256 != [0u8; 32]
7396    {
7397        return Err(FormatError::InvalidArchive(
7398            "CriticalMetadataImage root-auth fields must be zero when absent",
7399        ));
7400    }
7401    let block_record_len = image_block_record_len_from_region(image)?;
7402    let block_record_len_u64 = u64::try_from(block_record_len)
7403        .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
7404    match mode {
7405        CmraRecoveryMode::KeyHolding => {
7406            let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
7407                FormatError::InvalidArchive("BlockRecord region length overflow"),
7408            )?;
7409            if image.block_records_length != expected_len {
7410                return Err(FormatError::InvalidArchive(
7411                    "CriticalMetadataImage terminal equations are invalid",
7412                ));
7413            }
7414        }
7415        CmraRecoveryMode::PublicNoKey => {
7416            if image.block_records_length % block_record_len_u64 != 0 {
7417                return Err(FormatError::InvalidArchive(
7418                    "CriticalMetadataImage BlockRecord region is not aligned",
7419                ));
7420            }
7421        }
7422    }
7423    let crypto_header_end = image
7424        .crypto_header_offset
7425        .checked_add(image.crypto_header_length as u64)
7426        .ok_or(FormatError::InvalidArchive(
7427            "CryptoHeader boundary overflow",
7428        ))?;
7429    let expected_block_records_offset = if key_wrap_present {
7430        let key_wrap_region = key_wrap_region.ok_or(FormatError::InvalidArchive(
7431            "missing CriticalMetadataImage key-wrap region",
7432        ))?;
7433        if image.key_wrap_table_offset != crypto_header_end
7434            || image.key_wrap_table_length == 0
7435            || key_wrap_region.offset != image.key_wrap_table_offset
7436            || key_wrap_region.bytes.len() != image.key_wrap_table_length as usize
7437        {
7438            return Err(FormatError::InvalidArchive(
7439                "CriticalMetadataImage key-wrap region is malformed",
7440            ));
7441        }
7442        image
7443            .key_wrap_table_offset
7444            .checked_add(image.key_wrap_table_length as u64)
7445            .ok_or(FormatError::InvalidArchive(
7446                "KeyWrapTableV1 boundary overflow",
7447            ))?
7448    } else {
7449        if image.key_wrap_table_offset != 0
7450            || image.key_wrap_table_length != 0
7451            || image.key_wrap_table_sha256 != [0u8; 32]
7452        {
7453            return Err(FormatError::InvalidArchive(
7454                "CriticalMetadataImage key-wrap fields must be zero when absent",
7455            ));
7456        }
7457        crypto_header_end
7458    };
7459    if image.block_records_offset != expected_block_records_offset
7460        || image.manifest_footer_offset
7461            != image
7462                .block_records_offset
7463                .checked_add(image.block_records_length)
7464                .ok_or(FormatError::InvalidArchive(
7465                    "ManifestFooter boundary overflow",
7466                ))?
7467    {
7468        return Err(FormatError::InvalidArchive(
7469            "CriticalMetadataImage terminal equations are invalid",
7470        ));
7471    }
7472    let manifest_end = image
7473        .manifest_footer_offset
7474        .checked_add(MANIFEST_FOOTER_LEN as u64)
7475        .ok_or(FormatError::InvalidArchive(
7476            "RootAuthFooter boundary overflow",
7477        ))?;
7478    if root_auth_present {
7479        if image.root_auth_footer_offset != manifest_end
7480            || image
7481                .root_auth_footer_offset
7482                .checked_add(image.root_auth_footer_length as u64)
7483                .ok_or(FormatError::InvalidArchive(
7484                    "VolumeTrailer boundary overflow",
7485                ))?
7486                != image.volume_trailer_offset
7487        {
7488            return Err(FormatError::InvalidArchive(
7489                "CriticalMetadataImage root-auth terminal equations are invalid",
7490            ));
7491        }
7492    } else if image.volume_trailer_offset != manifest_end {
7493        return Err(FormatError::InvalidArchive(
7494            "CriticalMetadataImage unsigned terminal equations are invalid",
7495        ));
7496    }
7497    let expected_types: &[u16] = match (key_wrap_present, root_auth_present) {
7498        (false, false) => &[1, 2, 3, 5],
7499        (false, true) => &[1, 2, 3, 4, 5],
7500        (true, false) => &[1, 2, 6, 3, 5],
7501        (true, true) => &[1, 2, 6, 3, 4, 5],
7502    };
7503    if image.regions.len() != expected_types.len()
7504        || image
7505            .regions
7506            .iter()
7507            .map(|region| region.region_type)
7508            .ne(expected_types.iter().copied())
7509    {
7510        return Err(FormatError::InvalidArchive(
7511            "CriticalMetadataImage regions are not canonical",
7512        ));
7513    }
7514    validate_image_region(
7515        image,
7516        1,
7517        image.volume_header_offset,
7518        image.volume_header_length,
7519    )?;
7520    validate_image_region(
7521        image,
7522        2,
7523        image.crypto_header_offset,
7524        image.crypto_header_length,
7525    )?;
7526    validate_image_region(
7527        image,
7528        3,
7529        image.manifest_footer_offset,
7530        image.manifest_footer_length,
7531    )?;
7532    if key_wrap_present {
7533        validate_image_region(
7534            image,
7535            6,
7536            image.key_wrap_table_offset,
7537            image.key_wrap_table_length,
7538        )?;
7539    }
7540    if root_auth_present {
7541        validate_image_region(
7542            image,
7543            4,
7544            image.root_auth_footer_offset,
7545            image.root_auth_footer_length,
7546        )?;
7547    }
7548    validate_image_region(
7549        image,
7550        5,
7551        image.volume_trailer_offset,
7552        image.volume_trailer_length,
7553    )?;
7554    if sha256_region(image, 1)? != image.volume_header_sha256
7555        || sha256_region(image, 2)? != image.crypto_header_sha256
7556        || (key_wrap_present && sha256_region(image, 6)? != image.key_wrap_table_sha256)
7557        || (!key_wrap_present && image.key_wrap_table_sha256 != [0u8; 32])
7558        || sha256_region(image, 3)? != image.manifest_footer_sha256
7559        || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
7560        || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
7561        || sha256_region(image, 5)? != image.volume_trailer_sha256
7562    {
7563        return Err(FormatError::InvalidArchive(
7564            "CriticalMetadataImage region digest mismatch",
7565        ));
7566    }
7567    Ok(())
7568}
7569
7570fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
7571    let crypto_region = image
7572        .region(2)
7573        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7574    let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7575    crypto.fixed.validate_supported_profile()?;
7576    Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
7577}
7578
7579fn validate_image_region(
7580    image: &CriticalMetadataImage,
7581    region_type: u16,
7582    offset: u64,
7583    length: u32,
7584) -> Result<(), FormatError> {
7585    let region = image
7586        .region(region_type)
7587        .ok_or(FormatError::InvalidArchive(
7588            "missing CriticalMetadataImage region",
7589        ))?;
7590    if region.offset != offset || region.bytes.len() != length as usize {
7591        return Err(FormatError::InvalidArchive(
7592            "CriticalMetadataImage region range mismatch",
7593        ));
7594    }
7595    Ok(())
7596}
7597
7598fn validate_image_identity(
7599    image: &CriticalMetadataImage,
7600    volume_header: &VolumeHeader,
7601    crypto_header: &CryptoHeaderFixed,
7602) -> Result<(), FormatError> {
7603    if image.volume_format_rev != volume_header.volume_format_rev
7604        || image.archive_uuid != volume_header.archive_uuid
7605        || image.session_id != volume_header.session_id
7606        || image.volume_index != volume_header.volume_index
7607        || image.stripe_width != volume_header.stripe_width
7608        || image.stripe_width != crypto_header.stripe_width
7609    {
7610        return Err(FormatError::InvalidArchive(
7611            "CriticalMetadataImage identity does not match selected volume",
7612        ));
7613    }
7614    Ok(())
7615}
7616
7617fn validate_image_key_wrap_table(
7618    image: &CriticalMetadataImage,
7619    volume_header: &VolumeHeader,
7620    kdf_params: &KdfParams,
7621) -> Result<(), FormatError> {
7622    match kdf_params {
7623        KdfParams::RecipientWrap {
7624            key_wrap_table_length,
7625            key_wrap_table_record_count,
7626            key_wrap_table_digest,
7627            ..
7628        } => {
7629            if image.layout_flags & 0x0000_0002 == 0
7630                || image.key_wrap_table_length != *key_wrap_table_length
7631            {
7632                return Err(FormatError::InvalidArchive(
7633                    "CriticalMetadataImage key-wrap fields do not match KdfParams",
7634                ));
7635            }
7636            let region = image.region(6).ok_or(FormatError::InvalidArchive(
7637                "missing CriticalMetadataImage key-wrap region",
7638            ))?;
7639            if region.offset != image.key_wrap_table_offset
7640                || region.bytes.len() != *key_wrap_table_length as usize
7641            {
7642                return Err(FormatError::InvalidArchive(
7643                    "CriticalMetadataImage key-wrap region is malformed",
7644                ));
7645            }
7646            if compute_key_wrap_table_digest(*key_wrap_table_length, &region.bytes)
7647                != *key_wrap_table_digest
7648            {
7649                return Err(FormatError::IntegrityDigestMismatch {
7650                    structure: "KeyWrapTableV1",
7651                });
7652            }
7653            KeyWrapTableV1::parse(
7654                &region.bytes,
7655                &volume_header.archive_uuid,
7656                &volume_header.session_id,
7657                *key_wrap_table_length,
7658                *key_wrap_table_record_count,
7659            )?;
7660            Ok(())
7661        }
7662        _ => {
7663            if image.layout_flags & 0x0000_0002 != 0
7664                || image.region(6).is_some()
7665                || image.key_wrap_table_offset != 0
7666                || image.key_wrap_table_length != 0
7667                || image.key_wrap_table_sha256 != [0u8; 32]
7668            {
7669                return Err(FormatError::InvalidArchive(
7670                    "CriticalMetadataImage key-wrap fields must be zero when absent",
7671                ));
7672            }
7673            Ok(())
7674        }
7675    }
7676}
7677
7678fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
7679    Ok(sha256_bytes(
7680        &image
7681            .region(region_type)
7682            .ok_or(FormatError::InvalidArchive(
7683                "missing CriticalMetadataImage region",
7684            ))?
7685            .bytes,
7686    ))
7687}
7688
7689fn validate_recovered_terminal_authority(
7690    image: CriticalMetadataImage,
7691    tuple: CmraDecoderTuple,
7692    master_key: &MasterKey,
7693    require_cmra_boundary_magic: bool,
7694) -> Result<RecoveredTerminalAuthority, FormatError> {
7695    let volume_header_region = image
7696        .region(1)
7697        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7698    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7699    let crypto_region = image
7700        .region(2)
7701        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7702    let crypto_header_bytes = crypto_region.bytes.clone();
7703    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7704    let kdf_params = parsed_crypto.kdf_params.clone();
7705    let subkeys = subkeys_for_open(
7706        Some(master_key),
7707        parsed_crypto.fixed.aead_algo,
7708        &volume_header.archive_uuid,
7709        &volume_header.session_id,
7710    )?;
7711    verify_integrity_tag(
7712        HmacDomain::CryptoHeader,
7713        parsed_crypto.fixed.aead_algo,
7714        volume_header.volume_format_rev,
7715        Some(&subkeys.mac_key),
7716        &volume_header.archive_uuid,
7717        &volume_header.session_id,
7718        parsed_crypto.hmac_covered_bytes,
7719        &parsed_crypto.header_hmac,
7720    )?;
7721    parsed_crypto.validate_extension_semantics()?;
7722    validate_seekable_supported_volume(
7723        &volume_header,
7724        &parsed_crypto.fixed,
7725        &parsed_crypto.extensions,
7726    )?;
7727    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7728    let crypto_header = parsed_crypto.fixed.clone();
7729    if crypto_header.bit_rot_buffer_pct == 0 {
7730        return Err(FormatError::InvalidArchive(
7731            "CMRA startup recovery requires a nonzero bit-rot budget",
7732        ));
7733    }
7734    drop(parsed_crypto);
7735
7736    let terminal = validate_recovered_terminal_inner(
7737        image,
7738        tuple,
7739        require_cmra_boundary_magic,
7740        true,
7741        KeyHoldingTerminalContext {
7742            subkeys: &subkeys,
7743            volume_header: &volume_header,
7744            crypto_header: &crypto_header,
7745            crypto_header_bytes: &crypto_header_bytes,
7746        },
7747    )?;
7748    Ok(RecoveredTerminalAuthority {
7749        terminal,
7750        volume_header,
7751        crypto_header,
7752        crypto_header_bytes,
7753        subkeys,
7754        kdf_params,
7755    })
7756}
7757
7758fn validate_recovered_recipient_wrap_terminal_authority<F>(
7759    image: CriticalMetadataImage,
7760    tuple: CmraDecoderTuple,
7761    resolver: &mut F,
7762    require_cmra_boundary_magic: bool,
7763) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
7764where
7765    F: FnMut(
7766        RecipientWrapRecordContext<'_>,
7767    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
7768{
7769    let volume_header_region = image
7770        .region(1)
7771        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7772    let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7773    let crypto_region = image
7774        .region(2)
7775        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7776    let crypto_header_bytes = crypto_region.bytes.clone();
7777    let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7778    if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
7779        || !parsed_crypto.fixed.aead_algo.is_encrypted()
7780    {
7781        return Err(FormatError::KeyMaterialMismatch);
7782    }
7783    validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
7784    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7785    if parsed_crypto.fixed.bit_rot_buffer_pct == 0 {
7786        return Err(FormatError::InvalidArchive(
7787            "CMRA startup recovery requires a nonzero bit-rot budget",
7788        ));
7789    }
7790    validate_cmra_writer_parity_lower_bound(tuple, parsed_crypto.fixed.bit_rot_buffer_pct)?;
7791    validate_image_key_wrap_table(&image, &volume_header, &parsed_crypto.kdf_params)?;
7792    let key_wrap_region = image.region(6).ok_or(FormatError::InvalidArchive(
7793        "missing CriticalMetadataImage key-wrap region",
7794    ))?;
7795    let startup_key_wrap_table = parse_startup_key_wrap_table_bytes(
7796        &volume_header,
7797        &parsed_crypto.kdf_params,
7798        key_wrap_region.bytes.clone(),
7799    )?;
7800    let subkeys = recipient_wrap_subkeys_from_table(
7801        &volume_header,
7802        &parsed_crypto,
7803        &startup_key_wrap_table.table,
7804        resolver,
7805    )?;
7806    parsed_crypto.validate_extension_semantics()?;
7807    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
7808    let crypto_header = parsed_crypto.fixed.clone();
7809
7810    let terminal = validate_recovered_terminal_inner(
7811        image,
7812        tuple,
7813        require_cmra_boundary_magic,
7814        true,
7815        KeyHoldingTerminalContext {
7816            subkeys: &subkeys,
7817            volume_header: &volume_header,
7818            crypto_header: &crypto_header,
7819            crypto_header_bytes: &crypto_header_bytes,
7820        },
7821    )?;
7822    Ok(RecoveredRecipientWrapTerminalAuthority {
7823        terminal,
7824        volume_header,
7825        crypto_header,
7826        crypto_header_bytes,
7827        key_wrap_table_bytes: startup_key_wrap_table.bytes,
7828        block_records_start: startup_key_wrap_table.block_records_start,
7829        subkeys,
7830    })
7831}
7832
7833fn validate_recovered_terminal(
7834    image: CriticalMetadataImage,
7835    tuple: CmraDecoderTuple,
7836    bytes: &[u8],
7837    context: KeyHoldingTerminalContext<'_>,
7838    require_cmra_boundary_magic: bool,
7839) -> Result<V41Terminal, FormatError> {
7840    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7841    let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
7842    validate_recovered_terminal_inner(
7843        image,
7844        tuple,
7845        require_cmra_boundary_magic,
7846        cmra_boundary_magic_ok,
7847        context,
7848    )
7849}
7850
7851fn validate_recovered_terminal_read_at(
7852    image: CriticalMetadataImage,
7853    tuple: CmraDecoderTuple,
7854    reader: &dyn ArchiveReadAt,
7855    context: KeyHoldingTerminalContext<'_>,
7856    require_cmra_boundary_magic: bool,
7857) -> Result<V41Terminal, FormatError> {
7858    let mut magic = [0u8; 4];
7859    reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
7860    validate_recovered_terminal_inner(
7861        image,
7862        tuple,
7863        require_cmra_boundary_magic,
7864        magic == *b"TZCR",
7865        context,
7866    )
7867}
7868
7869fn validate_recovered_terminal_inner(
7870    image: CriticalMetadataImage,
7871    tuple: CmraDecoderTuple,
7872    require_cmra_boundary_magic: bool,
7873    cmra_boundary_magic_ok: bool,
7874    context: KeyHoldingTerminalContext<'_>,
7875) -> Result<V41Terminal, FormatError> {
7876    let subkeys = context.subkeys;
7877    let volume_header = context.volume_header;
7878    let crypto_header = context.crypto_header;
7879    let volume_header_region = image
7880        .region(1)
7881        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7882    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7883    if &recovered_volume_header != volume_header {
7884        return Err(FormatError::InvalidArchive(
7885            "CMRA VolumeHeader differs from parsed VolumeHeader",
7886        ));
7887    }
7888    validate_image_identity(&image, volume_header, crypto_header)?;
7889    let crypto_region = image
7890        .region(2)
7891        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7892    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7893    if recovered_crypto.fixed != *crypto_header {
7894        return Err(FormatError::InvalidArchive(
7895            "CMRA CryptoHeader differs from parsed CryptoHeader",
7896        ));
7897    }
7898    let recovered_pre_hmac_len = crypto_region
7899        .bytes
7900        .len()
7901        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7902        .ok_or(FormatError::InvalidArchive(
7903            "CMRA CryptoHeader is too short",
7904        ))?;
7905    let parsed_pre_hmac_len = context
7906        .crypto_header_bytes
7907        .len()
7908        .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7909        .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
7910    if recovered_pre_hmac_len != parsed_pre_hmac_len
7911        || crypto_region.bytes[..recovered_pre_hmac_len]
7912            != context.crypto_header_bytes[..parsed_pre_hmac_len]
7913    {
7914        return Err(FormatError::InvalidArchive(
7915            "CMRA CryptoHeader differs from parsed CryptoHeader",
7916        ));
7917    }
7918    verify_integrity_tag(
7919        HmacDomain::CryptoHeader,
7920        recovered_crypto.fixed.aead_algo,
7921        volume_header.volume_format_rev,
7922        Some(&subkeys.mac_key),
7923        &volume_header.archive_uuid,
7924        &volume_header.session_id,
7925        recovered_crypto.hmac_covered_bytes,
7926        &recovered_crypto.header_hmac,
7927    )?;
7928    validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
7929    recovered_crypto.validate_extension_semantics()?;
7930    validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7931
7932    let manifest_region = image
7933        .region(3)
7934        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7935    let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
7936    validate_manifest_footer(
7937        volume_header,
7938        crypto_header,
7939        &manifest_footer,
7940        subkeys,
7941        volume_header.volume_format_rev,
7942        &manifest_region.bytes,
7943    )?;
7944    manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
7945
7946    let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
7947        let root_auth_region = image
7948            .region(4)
7949            .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7950        let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7951        if footer.format_version != volume_header.format_version
7952            || footer.volume_format_rev != volume_header.volume_format_rev
7953        {
7954            return Err(FormatError::InvalidArchive(
7955                "RootAuthFooter format/revision does not match VolumeHeader",
7956            ));
7957        }
7958        if footer.archive_uuid != volume_header.archive_uuid
7959            || footer.session_id != volume_header.session_id
7960            || footer.footer_length()? != image.root_auth_footer_length
7961        {
7962            return Err(FormatError::InvalidArchive(
7963                "RootAuthFooter identity or length does not match terminal image",
7964            ));
7965        }
7966        Some(footer)
7967    } else {
7968        None
7969    };
7970
7971    let trailer_region = image
7972        .region(5)
7973        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7974    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7975    verify_integrity_tag(
7976        HmacDomain::VolumeTrailer,
7977        crypto_header.aead_algo,
7978        volume_header.volume_format_rev,
7979        Some(&subkeys.mac_key),
7980        &volume_header.archive_uuid,
7981        &volume_header.session_id,
7982        &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
7983        &trailer.trailer_hmac,
7984    )?;
7985    validate_trailer_identity(volume_header, &trailer)?;
7986    validate_v41_trailer_equations(&image, &trailer)?;
7987
7988    if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
7989        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7990    }
7991
7992    let manifest_footer_bytes = manifest_region.bytes.clone();
7993    let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
7994    Ok(V41Terminal {
7995        image,
7996        manifest_footer_bytes,
7997        root_auth_footer_bytes,
7998        root_auth_footer,
7999        volume_trailer: trailer,
8000    })
8001}
8002
8003fn validate_recovered_public_terminal(
8004    image: CriticalMetadataImage,
8005    bytes: &[u8],
8006    volume_header: &VolumeHeader,
8007    public_crypto_header: &CryptoHeader<'_>,
8008    require_cmra_boundary_magic: bool,
8009) -> Result<V41PublicTerminal, FormatError> {
8010    if image.layout_flags & 0x0000_0001 == 0 {
8011        return Err(FormatError::ReaderUnsupported(
8012            "public no-key verification requires root-auth",
8013        ));
8014    }
8015    let volume_header_region = image
8016        .region(1)
8017        .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
8018    let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
8019    if &recovered_volume_header != volume_header {
8020        return Err(FormatError::InvalidArchive(
8021            "CMRA VolumeHeader differs from parsed VolumeHeader",
8022        ));
8023    }
8024    validate_image_identity(&image, volume_header, &public_crypto_header.fixed)?;
8025    let crypto_region = image
8026        .region(2)
8027        .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
8028    let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
8029    if !public_crypto_headers_agree(&recovered_crypto.fixed, &public_crypto_header.fixed)
8030        || !public_kdf_profiles_agree(
8031            &recovered_crypto.kdf_params,
8032            &public_crypto_header.kdf_params,
8033        )
8034    {
8035        return Err(FormatError::InvalidArchive(
8036            "CMRA CryptoHeader differs from parsed CryptoHeader",
8037        ));
8038    }
8039    recovered_crypto.validate_extension_semantics()?;
8040    validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
8041
8042    image
8043        .region(3)
8044        .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
8045
8046    let root_auth_region = image
8047        .region(4)
8048        .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
8049    let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
8050    if root_auth_footer.format_version != volume_header.format_version
8051        || root_auth_footer.volume_format_rev != volume_header.volume_format_rev
8052    {
8053        return Err(FormatError::InvalidArchive(
8054            "public RootAuthFooter format/revision does not match VolumeHeader",
8055        ));
8056    }
8057    if root_auth_footer.archive_uuid != volume_header.archive_uuid
8058        || root_auth_footer.session_id != volume_header.session_id
8059        || root_auth_footer.footer_length()? != image.root_auth_footer_length
8060    {
8061        return Err(FormatError::InvalidArchive(
8062            "public RootAuthFooter identity or length does not match terminal image",
8063        ));
8064    }
8065
8066    let trailer_region = image
8067        .region(5)
8068        .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
8069    let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
8070    validate_trailer_identity(volume_header, &trailer)?;
8071    validate_v41_public_trailer_profile(&image, &trailer)?;
8072
8073    let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
8074    if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
8075        return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
8076    }
8077
8078    let root_auth_footer_bytes = root_auth_region.bytes.clone();
8079    Ok(V41PublicTerminal {
8080        image,
8081        root_auth_footer_bytes,
8082        root_auth_footer,
8083    })
8084}
8085
8086fn validate_v41_trailer_equations(
8087    image: &CriticalMetadataImage,
8088    trailer: &VolumeTrailer,
8089) -> Result<(), FormatError> {
8090    let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
8091    if trailer.bytes_written != image.volume_trailer_offset
8092        || trailer.manifest_footer_offset != image.manifest_footer_offset
8093        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
8094        || trailer.block_count != image.block_count
8095    {
8096        return Err(FormatError::InvalidArchive(
8097            "VolumeTrailer does not match v41 terminal layout",
8098        ));
8099    }
8100    if root_auth_present {
8101        if trailer.root_auth_flags != 0x0000_0001
8102            || trailer.root_auth_footer_offset != image.root_auth_footer_offset
8103            || trailer.root_auth_footer_length != image.root_auth_footer_length
8104            || image.root_auth_footer_offset
8105                != image
8106                    .manifest_footer_offset
8107                    .checked_add(MANIFEST_FOOTER_LEN as u64)
8108                    .ok_or(FormatError::InvalidArchive(
8109                        "RootAuthFooter trailer boundary overflow",
8110                    ))?
8111            || image
8112                .root_auth_footer_offset
8113                .checked_add(image.root_auth_footer_length as u64)
8114                .ok_or(FormatError::InvalidArchive(
8115                    "RootAuthFooter trailer boundary overflow",
8116                ))?
8117                != image.volume_trailer_offset
8118        {
8119            return Err(FormatError::InvalidArchive(
8120                "VolumeTrailer root-auth fields do not match v41 terminal layout",
8121            ));
8122        }
8123    } else if trailer.root_auth_footer_offset != 0
8124        || trailer.root_auth_footer_length != 0
8125        || trailer.root_auth_flags != 0
8126    {
8127        return Err(FormatError::InvalidArchive(
8128            "VolumeTrailer root-auth fields must be zero when absent",
8129        ));
8130    }
8131    Ok(())
8132}
8133
8134fn validate_v41_public_trailer_profile(
8135    image: &CriticalMetadataImage,
8136    trailer: &VolumeTrailer,
8137) -> Result<(), FormatError> {
8138    if trailer.bytes_written != image.volume_trailer_offset
8139        || trailer.manifest_footer_offset != image.manifest_footer_offset
8140        || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
8141    {
8142        return Err(FormatError::InvalidArchive(
8143            "VolumeTrailer does not match v41 public terminal layout",
8144        ));
8145    }
8146    if trailer.root_auth_flags != 0x0000_0001
8147        || trailer.root_auth_footer_offset == 0
8148        || trailer.root_auth_footer_length == 0
8149        || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
8150        || trailer.root_auth_footer_offset != image.root_auth_footer_offset
8151        || trailer.root_auth_footer_length != image.root_auth_footer_length
8152        || image.root_auth_footer_offset
8153            != image
8154                .manifest_footer_offset
8155                .checked_add(MANIFEST_FOOTER_LEN as u64)
8156                .ok_or(FormatError::InvalidArchive(
8157                    "RootAuthFooter trailer boundary overflow",
8158                ))?
8159        || image
8160            .root_auth_footer_offset
8161            .checked_add(image.root_auth_footer_length as u64)
8162            .ok_or(FormatError::InvalidArchive(
8163                "RootAuthFooter trailer boundary overflow",
8164            ))?
8165            != image.volume_trailer_offset
8166    {
8167        return Err(FormatError::InvalidArchive(
8168            "VolumeTrailer root-auth fields do not match v41 public terminal layout",
8169        ));
8170    }
8171    Ok(())
8172}
8173
8174fn critical_image_min() -> u64 {
8175    const MIN_CRYPTO_HEADER_LEN: u64 = 116;
8176    CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
8177        + 4 * SERIALIZED_REGION_HEADER_LEN as u64
8178        + VOLUME_HEADER_LEN as u64
8179        + MIN_CRYPTO_HEADER_LEN
8180        + MANIFEST_FOOTER_LEN as u64
8181        + VOLUME_TRAILER_LEN as u64
8182        + IMAGE_CRC_LEN as u64
8183}
8184
8185fn critical_image_cap() -> Result<u64, FormatError> {
8186    [
8187        CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
8188        6 * SERIALIZED_REGION_HEADER_LEN as u64,
8189        VOLUME_HEADER_LEN as u64,
8190        READER_MAX_CRYPTO_HEADER_LEN as u64,
8191        READER_MAX_KEY_WRAP_TABLE_LEN as u64,
8192        MANIFEST_FOOTER_LEN as u64,
8193        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
8194        VOLUME_TRAILER_LEN as u64,
8195        IMAGE_CRC_LEN as u64,
8196    ]
8197    .into_iter()
8198    .try_fold(0u64, |total, value| {
8199        total
8200            .checked_add(value)
8201            .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
8202    })
8203}
8204
8205fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
8206    let shard_total = (tuple.data_shard_count as u64)
8207        .checked_add(tuple.parity_shard_count as u64)
8208        .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
8209    let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
8210        .checked_add(tuple.shard_size as u64)
8211        .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
8212    checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
8213        .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
8214        .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
8215}
8216
8217fn cmra_worst_case_cap() -> Result<u64, FormatError> {
8218    let cap = critical_image_cap()?;
8219    let mut worst = 0u64;
8220    let mut shard_size = 512u64;
8221    while shard_size <= 4096 {
8222        let data = ceil_div_u64(cap, shard_size)?;
8223        let parity = 2u64.max(ceil_div_u64(
8224            checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
8225            100,
8226        )?);
8227        let tuple = CmraDecoderTuple {
8228            shard_size: shard_size as u32,
8229            data_shard_count: u16::try_from(data)
8230                .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
8231            parity_shard_count: u16::try_from(parity)
8232                .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
8233            image_length: u32::try_from(cap)
8234                .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
8235            image_sha256: [0u8; 32],
8236        };
8237        worst = worst.max(cmra_serialized_length(tuple)?);
8238        shard_size += 2;
8239    }
8240    Ok(worst)
8241}
8242
8243pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
8244    let total = [
8245        MANIFEST_FOOTER_LEN as u64,
8246        READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
8247        VOLUME_TRAILER_LEN as u64,
8248        cmra_worst_case_cap()?,
8249        LOCATOR_PAIR_LEN as u64,
8250    ]
8251    .into_iter()
8252    .try_fold(0u64, |sum, value| {
8253        sum.checked_add(value)
8254            .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
8255    })?;
8256    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
8257}
8258
8259fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
8260    let worst = cmra_worst_case_cap()?;
8261    let total = options
8262        .max_trailing_garbage_scan
8263        .try_into()
8264        .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8265        .and_then(|scan: u64| {
8266            scan.checked_add(worst)
8267                .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
8268                .ok_or(FormatError::InvalidArchive("scan cap overflow"))
8269        })?;
8270    usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8271}
8272
8273fn validate_bootstrap_single_volume_input(
8274    volume_header: &VolumeHeader,
8275    crypto_header: &CryptoHeaderFixed,
8276) -> Result<(), FormatError> {
8277    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
8278        return Err(FormatError::ReaderUnsupported(
8279            "bootstrap sidecar reader supports only single-volume archive input",
8280        ));
8281    }
8282    if crypto_header.stripe_width != volume_header.stripe_width {
8283        return Err(FormatError::InvalidArchive(
8284            "VolumeHeader and CryptoHeader stripe_width differ",
8285        ));
8286    }
8287    Ok(())
8288}
8289
8290#[derive(Debug)]
8291struct ParsedBootstrapSidecar {
8292    manifest_footer: Option<ManifestFooter>,
8293    index_root_records_section: Option<(u64, u64)>,
8294    dictionary_records_section: Option<(u64, u64)>,
8295}
8296
8297pub(crate) struct NonSeekableBootstrapMaterial {
8298    pub(crate) manifest_footer: ManifestFooter,
8299    pub(crate) payload_dictionary: Option<Vec<u8>>,
8300}
8301
8302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8303enum BootstrapSidecarUse {
8304    SeekableAssist,
8305    NonSeekableRandomAccess,
8306}
8307
8308impl ParsedBootstrapSidecar {
8309    fn require_sections_for(
8310        &self,
8311        sidecar_use: BootstrapSidecarUse,
8312        crypto_header: &CryptoHeaderFixed,
8313    ) -> Result<(), FormatError> {
8314        if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
8315            if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
8316                return Err(FormatError::ReaderUnsupported(
8317                    "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8318                ));
8319            }
8320            if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
8321                return Err(FormatError::ReaderUnsupported(
8322                    "dictionary bootstrap required",
8323                ));
8324            }
8325        }
8326        Ok(())
8327    }
8328}
8329
8330pub(crate) fn parse_non_seekable_bootstrap_material(
8331    bootstrap_sidecar: &[u8],
8332    volume_header: &VolumeHeader,
8333    crypto_header: &CryptoHeaderFixed,
8334    subkeys: &Subkeys,
8335) -> Result<NonSeekableBootstrapMaterial, FormatError> {
8336    validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
8337    let sidecar =
8338        parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
8339    sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
8340    let manifest_footer = sidecar
8341        .manifest_footer
8342        .clone()
8343        .ok_or(FormatError::ReaderUnsupported(
8344            "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8345        ))?;
8346
8347    let mut blocks = BTreeMap::new();
8348    let (offset, length) =
8349        sidecar
8350            .index_root_records_section
8351            .ok_or(FormatError::ReaderUnsupported(
8352                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8353            ))?;
8354    let index_root_records = parse_sidecar_block_records(
8355        bootstrap_sidecar,
8356        crypto_header.block_size as usize,
8357        SidecarBlockRecordsSection {
8358            offset,
8359            length,
8360            extent: index_root_extent_from_manifest(&manifest_footer),
8361            data_kind: BlockKind::IndexRootData,
8362            parity_kind: BlockKind::IndexRootParity,
8363            structure: "IndexRoot",
8364        },
8365    )?;
8366    insert_sidecar_records(&mut blocks, index_root_records)?;
8367
8368    let limits = metadata_limits(crypto_header);
8369    let index_root_plaintext = load_metadata_object_from_parts(
8370        &blocks,
8371        ObjectLoadContext::index_root(
8372            volume_header,
8373            crypto_header,
8374            subkeys,
8375            index_root_extent_from_manifest(&manifest_footer),
8376        ),
8377        manifest_footer.index_root_decompressed_size,
8378    )?;
8379    let index_root = IndexRoot::parse(
8380        &index_root_plaintext,
8381        crypto_header.has_dictionary != 0,
8382        limits,
8383    )?;
8384
8385    if crypto_header.has_dictionary != 0 {
8386        let (offset, length) =
8387            sidecar
8388                .dictionary_records_section
8389                .ok_or(FormatError::ReaderUnsupported(
8390                    "dictionary bootstrap required",
8391                ))?;
8392        let dictionary_records = parse_sidecar_block_records(
8393            bootstrap_sidecar,
8394            crypto_header.block_size as usize,
8395            SidecarBlockRecordsSection {
8396                offset,
8397                length,
8398                extent: dictionary_extent_from_index_root(&index_root)?,
8399                data_kind: BlockKind::DictionaryData,
8400                parity_kind: BlockKind::DictionaryParity,
8401                structure: "dictionary",
8402            },
8403        )?;
8404        insert_sidecar_records(&mut blocks, dictionary_records)?;
8405    }
8406    let payload_dictionary =
8407        load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
8408
8409    Ok(NonSeekableBootstrapMaterial {
8410        manifest_footer,
8411        payload_dictionary,
8412    })
8413}
8414
8415fn parse_bootstrap_sidecar(
8416    bytes: &[u8],
8417    volume_header: &VolumeHeader,
8418    crypto_header: &CryptoHeaderFixed,
8419    subkeys: &Subkeys,
8420) -> Result<ParsedBootstrapSidecar, FormatError> {
8421    let header_bytes = slice(
8422        bytes,
8423        0,
8424        BOOTSTRAP_SIDECAR_HEADER_LEN,
8425        "BootstrapSidecarHeader",
8426    )?;
8427    let header = BootstrapSidecarHeader::parse(header_bytes)?;
8428    if header.archive_uuid != volume_header.archive_uuid
8429        || header.session_id != volume_header.session_id
8430    {
8431        return Err(FormatError::InvalidArchive(
8432            "bootstrap sidecar identity does not match VolumeHeader",
8433        ));
8434    }
8435    verify_integrity_tag(
8436        HmacDomain::BootstrapSidecar,
8437        crypto_header.aead_algo,
8438        volume_header.volume_format_rev,
8439        Some(&subkeys.mac_key),
8440        &volume_header.archive_uuid,
8441        &volume_header.session_id,
8442        &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
8443        &header.sidecar_hmac,
8444    )?;
8445    header.validate_packed_layout(bytes.len() as u64)?;
8446    validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
8447
8448    if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
8449        return Err(FormatError::InvalidArchive(
8450            "bootstrap sidecar has dictionary records while has_dictionary is false",
8451        ));
8452    }
8453
8454    let manifest_footer = if header.has_manifest_footer() {
8455        let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
8456        let manifest_bytes = slice(
8457            bytes,
8458            manifest_offset,
8459            MANIFEST_FOOTER_LEN,
8460            "ManifestFooter",
8461        )?;
8462        let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
8463        validate_sidecar_manifest_footer(
8464            volume_header,
8465            crypto_header,
8466            &manifest_footer,
8467            subkeys,
8468            volume_header.volume_format_rev,
8469            manifest_bytes,
8470        )?;
8471        manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
8472        Some(manifest_footer)
8473    } else {
8474        None
8475    };
8476
8477    Ok(ParsedBootstrapSidecar {
8478        manifest_footer,
8479        index_root_records_section: header.has_index_root_records().then_some((
8480            header.index_root_records_offset,
8481            header.index_root_records_length,
8482        )),
8483        dictionary_records_section: header.has_dictionary_records().then_some((
8484            header.dictionary_records_offset,
8485            header.dictionary_records_length,
8486        )),
8487    })
8488}
8489
8490fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
8491    ObjectExtent {
8492        first_block_index: manifest_footer.index_root_first_block,
8493        data_block_count: manifest_footer.index_root_data_block_count,
8494        parity_block_count: manifest_footer.index_root_parity_block_count,
8495        encrypted_size: manifest_footer.index_root_encrypted_size,
8496    }
8497}
8498
8499fn insert_sidecar_records(
8500    blocks: &mut BTreeMap<u64, BlockRecord>,
8501    records: Vec<BlockRecord>,
8502) -> Result<(), FormatError> {
8503    for record in records {
8504        if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
8505            if existing != record {
8506                return Err(FormatError::InvalidArchive(
8507                    "bootstrap sidecar conflicts with volume BlockRecord",
8508                ));
8509            }
8510        }
8511    }
8512    Ok(())
8513}
8514
8515fn validate_sidecar_manifest_footer(
8516    volume_header: &VolumeHeader,
8517    crypto_header: &CryptoHeaderFixed,
8518    footer: &ManifestFooter,
8519    subkeys: &Subkeys,
8520    volume_format_rev: u16,
8521    raw: &[u8],
8522) -> Result<(), FormatError> {
8523    if footer.archive_uuid != volume_header.archive_uuid
8524        || footer.session_id != volume_header.session_id
8525    {
8526        return Err(FormatError::InvalidArchive(
8527            "sidecar ManifestFooter identity does not match VolumeHeader",
8528        ));
8529    }
8530    if footer.volume_index != 0 {
8531        return Err(FormatError::InvalidArchive(
8532            "sidecar ManifestFooter volume_index must be zero",
8533        ));
8534    }
8535    if footer.total_volumes != crypto_header.stripe_width {
8536        return Err(FormatError::InvalidArchive(
8537            "sidecar ManifestFooter total_volumes does not match stripe_width",
8538        ));
8539    }
8540    if footer.is_authoritative != 1 {
8541        return Err(FormatError::InvalidArchive(
8542            "sidecar ManifestFooter is not authoritative",
8543        ));
8544    }
8545    verify_integrity_tag(
8546        HmacDomain::ManifestFooter,
8547        crypto_header.aead_algo,
8548        volume_format_rev,
8549        Some(&subkeys.mac_key),
8550        &volume_header.archive_uuid,
8551        &volume_header.session_id,
8552        &raw[..MANIFEST_HMAC_COVERED_LEN],
8553        &footer.manifest_hmac,
8554    )
8555}
8556
8557fn validate_sidecar_size_cap(
8558    header: &BootstrapSidecarHeader,
8559    crypto_header: &CryptoHeaderFixed,
8560    file_size: u64,
8561) -> Result<(), FormatError> {
8562    let record_len = checked_u64_add(
8563        crypto_header.block_size as u64,
8564        BLOCK_RECORD_FRAMING_LEN as u64,
8565        "bootstrap sidecar cap overflow",
8566    )?;
8567    let max_index_records = crypto_header.index_root_fec_data_shards as u64
8568        + crypto_header.index_root_fec_parity_shards as u64;
8569    let max_record_section_bytes = checked_u64_mul(
8570        max_index_records,
8571        record_len,
8572        "bootstrap sidecar cap overflow",
8573    )?;
8574    if header.index_root_records_length % record_len != 0 {
8575        return Err(FormatError::InvalidArchive(
8576            "bootstrap sidecar IndexRoot records length is not aligned",
8577        ));
8578    }
8579    if header.index_root_records_length / record_len > max_index_records {
8580        return Err(FormatError::InvalidArchive(
8581            "bootstrap sidecar IndexRoot records exceed resource cap",
8582        ));
8583    }
8584    if header.dictionary_records_length % record_len != 0 {
8585        return Err(FormatError::InvalidArchive(
8586            "bootstrap sidecar dictionary records length is not aligned",
8587        ));
8588    }
8589    if header.dictionary_records_length / record_len > max_index_records {
8590        return Err(FormatError::InvalidArchive(
8591            "bootstrap sidecar dictionary records exceed resource cap",
8592        ));
8593    }
8594
8595    let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
8596    if header.has_manifest_footer() {
8597        cap = cap
8598            .checked_add(MANIFEST_FOOTER_LEN as u64)
8599            .ok_or(FormatError::InvalidArchive(
8600                "bootstrap sidecar cap overflow",
8601            ))?;
8602    }
8603    if header.has_index_root_records() {
8604        cap = checked_u64_add(
8605            cap,
8606            max_record_section_bytes,
8607            "bootstrap sidecar cap overflow",
8608        )?;
8609    }
8610    if header.has_dictionary_records() {
8611        cap = checked_u64_add(
8612            cap,
8613            max_record_section_bytes,
8614            "bootstrap sidecar cap overflow",
8615        )?;
8616    }
8617    if file_size > cap {
8618        return Err(FormatError::InvalidArchive(
8619            "bootstrap sidecar exceeds resource cap",
8620        ));
8621    }
8622    Ok(())
8623}
8624
8625#[derive(Debug, Clone, Copy)]
8626struct SidecarBlockRecordsSection {
8627    offset: u64,
8628    length: u64,
8629    extent: ObjectExtent,
8630    data_kind: BlockKind,
8631    parity_kind: BlockKind,
8632    structure: &'static str,
8633}
8634
8635fn parse_sidecar_block_records(
8636    sidecar_bytes: &[u8],
8637    block_size: usize,
8638    section: SidecarBlockRecordsSection,
8639) -> Result<Vec<BlockRecord>, FormatError> {
8640    let record_len = block_size
8641        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8642        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8643    if section.length % record_len as u64 != 0 {
8644        return Err(FormatError::InvalidArchive(
8645            "sidecar BlockRecord section is not aligned",
8646        ));
8647    }
8648    let expected_count =
8649        section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
8650    let actual_count = usize::try_from(section.length / record_len as u64)
8651        .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
8652    if actual_count != expected_count {
8653        return Err(FormatError::InvalidArchive(
8654            "sidecar BlockRecord section does not match declared extent",
8655        ));
8656    }
8657    let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
8658    let raw = slice(
8659        sidecar_bytes,
8660        start,
8661        to_usize(section.length, "BootstrapSidecarHeader")?,
8662        "BootstrapSidecarHeader",
8663    )?;
8664    let mut records = Vec::with_capacity(expected_count);
8665
8666    for idx in 0..expected_count {
8667        let record = BlockRecord::parse(
8668            slice(raw, idx * record_len, record_len, "BlockRecord")?,
8669            block_size,
8670        )?;
8671        let expected_block_index = checked_u64_add(
8672            section.extent.first_block_index,
8673            idx as u64,
8674            section.structure,
8675        )?;
8676        if record.block_index != expected_block_index {
8677            return Err(FormatError::InvalidArchive(
8678                "sidecar BlockRecord section has missing or duplicate blocks",
8679            ));
8680        }
8681        let expected_kind = if idx < section.extent.data_block_count as usize {
8682            section.data_kind
8683        } else {
8684            section.parity_kind
8685        };
8686        if record.kind != expected_kind {
8687            return Err(FormatError::InvalidArchive(
8688                "sidecar BlockRecord section has wrong kind",
8689            ));
8690        }
8691        let should_be_last = idx + 1 == section.extent.data_block_count as usize;
8692        if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
8693        {
8694            return Err(FormatError::InvalidArchive(
8695                "sidecar BlockRecord section has wrong last-data flag",
8696            ));
8697        }
8698        records.push(record);
8699    }
8700
8701    Ok(records)
8702}
8703
8704fn validate_trailer_identity(
8705    volume_header: &VolumeHeader,
8706    trailer: &VolumeTrailer,
8707) -> Result<(), FormatError> {
8708    if trailer.archive_uuid != volume_header.archive_uuid
8709        || trailer.session_id != volume_header.session_id
8710        || trailer.volume_index != volume_header.volume_index
8711    {
8712        return Err(FormatError::InvalidArchive(
8713            "VolumeTrailer identity does not match VolumeHeader",
8714        ));
8715    }
8716    Ok(())
8717}
8718
8719fn validate_manifest_footer(
8720    volume_header: &VolumeHeader,
8721    crypto_header: &CryptoHeaderFixed,
8722    footer: &ManifestFooter,
8723    subkeys: &Subkeys,
8724    volume_format_rev: u16,
8725    raw: &[u8],
8726) -> Result<(), FormatError> {
8727    if footer.archive_uuid != volume_header.archive_uuid
8728        || footer.session_id != volume_header.session_id
8729        || footer.volume_index != volume_header.volume_index
8730    {
8731        return Err(FormatError::InvalidArchive(
8732            "ManifestFooter identity does not match VolumeHeader",
8733        ));
8734    }
8735    if footer.total_volumes != volume_header.stripe_width {
8736        return Err(FormatError::InvalidArchive(
8737            "ManifestFooter total_volumes does not match stripe_width",
8738        ));
8739    }
8740    if footer.is_authoritative != 1 {
8741        return Err(FormatError::InvalidArchive(
8742            "ManifestFooter is not authoritative",
8743        ));
8744    }
8745    verify_integrity_tag(
8746        HmacDomain::ManifestFooter,
8747        crypto_header.aead_algo,
8748        volume_format_rev,
8749        Some(&subkeys.mac_key),
8750        &volume_header.archive_uuid,
8751        &volume_header.session_id,
8752        &raw[..MANIFEST_HMAC_COVERED_LEN],
8753        &footer.manifest_hmac,
8754    )
8755}
8756
8757#[derive(Debug)]
8758struct ParsedBlockRegion {
8759    blocks: BTreeMap<u64, BlockRecord>,
8760    erased_block_indices: BTreeSet<u64>,
8761}
8762
8763fn parse_block_region(
8764    bytes: &[u8],
8765    start: usize,
8766    end: usize,
8767    block_size: usize,
8768    volume_header: &VolumeHeader,
8769    trailer: &VolumeTrailer,
8770) -> Result<ParsedBlockRegion, FormatError> {
8771    if end < start {
8772        return Err(FormatError::InvalidArchive(
8773            "ManifestFooter starts before BlockRecord region",
8774        ));
8775    }
8776    let record_len = block_size
8777        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8778        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8779    let region_len = end - start;
8780    if region_len % record_len != 0 {
8781        return Err(FormatError::InvalidArchive(
8782            "BlockRecord region length is not aligned",
8783        ));
8784    }
8785    let observed_count = region_len / record_len;
8786    if observed_count as u64 != trailer.block_count {
8787        return Err(FormatError::InvalidArchive(
8788            "VolumeTrailer block_count does not match BlockRecord region",
8789        ));
8790    }
8791
8792    let mut blocks = BTreeMap::new();
8793    let mut erased_block_indices = BTreeSet::new();
8794    for idx in 0..observed_count {
8795        let offset = start + idx * record_len;
8796        let expected_block_index = checked_u64_add(
8797            volume_header.volume_index as u64,
8798            checked_u64_mul(
8799                idx as u64,
8800                volume_header.stripe_width as u64,
8801                "BlockRecord index overflow",
8802            )?,
8803            "BlockRecord index overflow",
8804        )?;
8805        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8806        match BlockRecord::parse(raw, block_size) {
8807            Ok(record) => {
8808                if record.block_index != expected_block_index {
8809                    return Err(FormatError::InvalidArchive(
8810                        "BlockRecord index does not match volume position",
8811                    ));
8812                }
8813                if blocks.insert(record.block_index, record).is_some() {
8814                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8815                }
8816            }
8817            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
8818                if !erased_block_indices.insert(expected_block_index) {
8819                    return Err(FormatError::InvalidArchive(
8820                        "duplicate erased BlockRecord index",
8821                    ));
8822                }
8823            }
8824            Err(err) => return Err(err),
8825        }
8826    }
8827
8828    Ok(ParsedBlockRegion {
8829        blocks,
8830        erased_block_indices,
8831    })
8832}
8833
8834fn validate_seekable_block_region_layout(
8835    start: u64,
8836    end: u64,
8837    block_size: usize,
8838    trailer: &VolumeTrailer,
8839) -> Result<(), FormatError> {
8840    if end < start {
8841        return Err(FormatError::InvalidArchive(
8842            "ManifestFooter starts before BlockRecord region",
8843        ));
8844    }
8845    let record_len = block_record_len(block_size)?;
8846    let region_len = end - start;
8847    if region_len % record_len != 0 {
8848        return Err(FormatError::InvalidArchive(
8849            "BlockRecord region length is not aligned",
8850        ));
8851    }
8852    let observed_count = region_len / record_len;
8853    if observed_count != trailer.block_count {
8854        return Err(FormatError::InvalidArchive(
8855            "VolumeTrailer block_count does not match BlockRecord region",
8856        ));
8857    }
8858    Ok(())
8859}
8860
8861fn parse_public_block_observation(
8862    bytes: &[u8],
8863    start: usize,
8864    image: &CriticalMetadataImage,
8865    block_size: usize,
8866    volume_header: &VolumeHeader,
8867) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
8868    let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
8869    if start != image_start {
8870        return Err(FormatError::InvalidArchive(
8871            "public BlockRecord observation start mismatch",
8872        ));
8873    }
8874    let scan_limit_u64 = image
8875        .block_records_offset
8876        .checked_add(image.block_records_length)
8877        .ok_or(FormatError::InvalidArchive(
8878            "public BlockRecord observation limit overflow",
8879        ))?;
8880    if scan_limit_u64 != image.manifest_footer_offset {
8881        return Err(FormatError::InvalidArchive(
8882            "public BlockRecord observation limit mismatch",
8883        ));
8884    }
8885    let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
8886    if scan_limit < start {
8887        return Err(FormatError::InvalidArchive(
8888            "public BlockRecord observation limit before start",
8889        ));
8890    }
8891    let record_len = block_size
8892        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8893        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8894    let region_len = scan_limit - start;
8895    if region_len % record_len != 0 {
8896        return Err(FormatError::InvalidArchive(
8897            "public BlockRecord observation window is not aligned",
8898        ));
8899    }
8900
8901    let mut blocks = BTreeMap::new();
8902    let mut offset = start;
8903    let mut observed_slot = 0u64;
8904    while offset < scan_limit {
8905        let magic_end = checked_add(offset, 4, "BlockRecord")?;
8906        if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
8907            break;
8908        }
8909        let record_end = checked_add(offset, record_len, "BlockRecord")?;
8910        if record_end > scan_limit {
8911            return Err(FormatError::InvalidArchive(
8912                "public BlockRecord observation slot is incomplete",
8913            ));
8914        }
8915        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8916        let record = BlockRecord::parse(raw, block_size)?;
8917        let expected_block_index = checked_u64_add(
8918            volume_header.volume_index as u64,
8919            checked_u64_mul(
8920                observed_slot,
8921                volume_header.stripe_width as u64,
8922                "BlockRecord index overflow",
8923            )?,
8924            "BlockRecord index overflow",
8925        )?;
8926        if record.block_index != expected_block_index {
8927            return Err(FormatError::InvalidArchive(
8928                "public BlockRecord index does not match volume position",
8929            ));
8930        }
8931        if blocks.insert(record.block_index, record).is_some() {
8932            return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8933        }
8934        offset = record_end;
8935        observed_slot = observed_slot
8936            .checked_add(1)
8937            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8938    }
8939
8940    let mut scan = if offset < scan_limit {
8941        checked_add(offset, record_len, "BlockRecord")?
8942    } else {
8943        scan_limit
8944    };
8945    while scan < scan_limit {
8946        let magic_end = checked_add(scan, 4, "BlockRecord")?;
8947        let record_end = checked_add(scan, record_len, "BlockRecord")?;
8948        if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
8949            let raw = slice(bytes, scan, record_len, "BlockRecord")?;
8950            if BlockRecord::parse(raw, block_size).is_ok() {
8951                return Err(FormatError::InvalidArchive(
8952                    "public observation has ambiguous extra BlockRecord",
8953                ));
8954            }
8955        }
8956        scan = record_end;
8957    }
8958
8959    Ok(blocks)
8960}
8961
8962pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
8963    matches!(
8964        error,
8965        FormatError::BadCrc {
8966            structure: "BlockRecord",
8967        } | FormatError::BadMagic {
8968            structure: "BlockRecord",
8969        } | FormatError::NonZeroReserved {
8970            structure: "BlockRecord",
8971        }
8972    )
8973}
8974
8975fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
8976    let len = block_size
8977        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8978        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8979    u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
8980}
8981
8982fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
8983    lhs.checked_mul(rhs)
8984        .ok_or(FormatError::InvalidArchive(reason))
8985}
8986
8987fn parse_stream_block_prefix(
8988    bytes: &[u8],
8989    start: usize,
8990    block_size: usize,
8991    volume_header: &VolumeHeader,
8992) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
8993    let record_len = block_size
8994        .checked_add(BLOCK_RECORD_FRAMING_LEN)
8995        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8996    let mut blocks = BTreeMap::new();
8997    let mut offset = start;
8998    let mut observed_block_count = 0u64;
8999
9000    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9001        let expected_block_index =
9002            expected_stream_block_index(volume_header, observed_block_count)?;
9003        let raw = slice(bytes, offset, record_len, "BlockRecord")?;
9004        match BlockRecord::parse(raw, block_size) {
9005            Ok(record) => {
9006                if record.block_index != expected_block_index {
9007                    return Err(FormatError::InvalidArchive(
9008                        "BlockRecord index does not match stream position",
9009                    ));
9010                }
9011                if blocks.insert(record.block_index, record).is_some() {
9012                    return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
9013                }
9014            }
9015            Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
9016            Err(err) => return Err(err),
9017        }
9018        offset = checked_add(offset, record_len, "BlockRecord")?;
9019        observed_block_count = observed_block_count
9020            .checked_add(1)
9021            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9022    }
9023
9024    Ok((blocks, offset, observed_block_count))
9025}
9026
9027pub(crate) fn expected_stream_block_index(
9028    volume_header: &VolumeHeader,
9029    observed_block_count: u64,
9030) -> Result<u64, FormatError> {
9031    checked_u64_add(
9032        volume_header.volume_index as u64,
9033        checked_u64_mul(
9034            observed_block_count,
9035            volume_header.stripe_width as u64,
9036            "BlockRecord index overflow",
9037        )?,
9038        "BlockRecord index overflow",
9039    )
9040}
9041
9042fn parse_sequential_block_or_erasure(
9043    bytes: &[u8],
9044    offset: usize,
9045    record_len: usize,
9046    block_size: usize,
9047    volume_header: &VolumeHeader,
9048    observed_block_count: u64,
9049) -> Result<Option<BlockRecord>, FormatError> {
9050    let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
9051    let raw = slice(bytes, offset, record_len, "BlockRecord")?;
9052    match BlockRecord::parse(raw, block_size) {
9053        Ok(record) => {
9054            if record.block_index != expected_block_index {
9055                return Err(FormatError::InvalidArchive(
9056                    "BlockRecord index does not match stream position",
9057                ));
9058            }
9059            Ok(Some(record))
9060        }
9061        Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
9062        Err(err) => Err(err),
9063    }
9064}
9065
9066fn parse_terminal_material(
9067    bytes: &[u8],
9068    manifest_offset: usize,
9069    observed_block_count: u64,
9070    context: KeyHoldingTerminalContext<'_>,
9071    options: ReaderOptions,
9072) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
9073    let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
9074    if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
9075        return Err(FormatError::InvalidArchive(
9076            "sequential terminal does not end at EOF",
9077        ));
9078    }
9079    let terminal = candidate.terminal;
9080    if terminal.image.manifest_footer_offset != manifest_offset as u64 {
9081        return Err(FormatError::InvalidArchive(
9082            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
9083        ));
9084    }
9085    if terminal.volume_trailer.block_count != observed_block_count {
9086        return Err(FormatError::InvalidArchive(
9087            "VolumeTrailer block_count does not match observed stream",
9088        ));
9089    }
9090    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
9091    Ok((
9092        manifest_footer,
9093        terminal.volume_trailer,
9094        terminal.root_auth_footer,
9095    ))
9096}
9097
9098pub(crate) fn parse_terminal_material_read_at(
9099    reader: &dyn ArchiveReadAt,
9100    input_len: u64,
9101    manifest_offset: u64,
9102    observed_block_count: u64,
9103    context: KeyHoldingTerminalContext<'_>,
9104) -> Result<SequentialTerminalMaterial, FormatError> {
9105    let mut candidates = Vec::new();
9106    if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
9107        collect_v41_locator_candidate_read_at(
9108            reader,
9109            input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
9110            0,
9111            context,
9112            &mut candidates,
9113        );
9114    }
9115    if input_len >= LOCATOR_PAIR_LEN as u64 {
9116        collect_v41_locator_candidate_read_at(
9117            reader,
9118            input_len - LOCATOR_PAIR_LEN as u64,
9119            1,
9120            context,
9121            &mut candidates,
9122        );
9123    }
9124
9125    let candidate = choose_v41_terminal_candidate(candidates)?;
9126    if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
9127        return Err(FormatError::InvalidArchive(
9128            "sequential terminal does not end at EOF",
9129        ));
9130    }
9131    let terminal = candidate.terminal;
9132    if terminal.image.manifest_footer_offset != manifest_offset {
9133        return Err(FormatError::InvalidArchive(
9134            "VolumeTrailer ManifestFooter offset does not match observed stream offset",
9135        ));
9136    }
9137    if terminal.volume_trailer.block_count != observed_block_count {
9138        return Err(FormatError::InvalidArchive(
9139            "VolumeTrailer block_count does not match observed stream",
9140        ));
9141    }
9142    let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
9143    Ok(SequentialTerminalMaterial {
9144        manifest_footer,
9145        volume_trailer: terminal.volume_trailer,
9146        root_auth_footer: terminal.root_auth_footer,
9147    })
9148}
9149
9150fn terminal_candidate_reaches_eof(
9151    candidate: &TerminalCandidate,
9152    input_len: usize,
9153) -> Result<bool, FormatError> {
9154    let expected_end =
9155        match candidate.locator_sequence {
9156            Some(0) => candidate.anchor,
9157            Some(1) => candidate
9158                .anchor
9159                .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
9160                .ok_or(FormatError::InvalidArchive(
9161                    "terminal EOF boundary overflow",
9162                ))?,
9163            None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
9164                FormatError::InvalidArchive("terminal EOF boundary overflow"),
9165            )?,
9166            Some(_) => {
9167                return Err(FormatError::InvalidArchive(
9168                    "invalid terminal locator sequence",
9169                ))
9170            }
9171        };
9172    Ok(expected_end == input_len)
9173}
9174
9175#[derive(Debug, Default)]
9176struct PendingSequentialEnvelope {
9177    data_shards: Vec<Option<Vec<u8>>>,
9178    parity_shards: Vec<Option<Vec<u8>>>,
9179    saw_last_data: bool,
9180    awaiting_tentative_parity: bool,
9181}
9182
9183impl PendingSequentialEnvelope {
9184    fn is_empty(&self) -> bool {
9185        self.data_shards.is_empty() && self.parity_shards.is_empty()
9186    }
9187}
9188
9189fn handle_sequential_payload_erasure(
9190    pending: &mut PendingSequentialEnvelope,
9191    crypto_header: &CryptoHeaderFixed,
9192    metadata_seen: bool,
9193) -> Result<(), FormatError> {
9194    if metadata_seen || pending.saw_last_data {
9195        return Err(FormatError::BadCrc {
9196            structure: "BlockRecord",
9197        });
9198    }
9199    if !sequential_payload_parity_is_guaranteed(crypto_header) {
9200        return Err(FormatError::BadCrc {
9201            structure: "BlockRecord",
9202        });
9203    }
9204    pending.data_shards.push(None);
9205    pending.awaiting_tentative_parity = true;
9206    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
9207        return Err(FormatError::InvalidArchive(
9208            "sequential payload envelope exceeds data-shard cap",
9209        ));
9210    }
9211    Ok(())
9212}
9213
9214fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
9215    crypto_header.fec_parity_shards > 0
9216        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
9217}
9218
9219fn sequential_extract_tar_stream_with_options(
9220    bytes: &[u8],
9221    master_key: &MasterKey,
9222    options: ReaderOptions,
9223) -> Result<Vec<u8>, FormatError> {
9224    validate_reader_options(options)?;
9225    if bytes.len() < VOLUME_HEADER_LEN {
9226        return Err(FormatError::InvalidLength {
9227            structure: "archive",
9228            expected: VOLUME_HEADER_LEN,
9229            actual: bytes.len(),
9230        });
9231    }
9232
9233    let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
9234    let crypto_start = volume_header.crypto_header_offset as usize;
9235    let crypto_len = volume_header.crypto_header_length as usize;
9236    let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
9237    let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
9238    let subkeys = subkeys_for_open(
9239        Some(master_key),
9240        parsed_crypto.fixed.aead_algo,
9241        &volume_header.archive_uuid,
9242        &volume_header.session_id,
9243    )?;
9244    verify_integrity_tag(
9245        HmacDomain::CryptoHeader,
9246        parsed_crypto.fixed.aead_algo,
9247        volume_header.volume_format_rev,
9248        Some(&subkeys.mac_key),
9249        &volume_header.archive_uuid,
9250        &volume_header.session_id,
9251        parsed_crypto.hmac_covered_bytes,
9252        &parsed_crypto.header_hmac,
9253    )?;
9254    parsed_crypto.validate_extension_semantics()?;
9255    validate_sequential_supported_volume(
9256        &volume_header,
9257        &parsed_crypto.fixed,
9258        &parsed_crypto.extensions,
9259    )?;
9260    validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
9261    let block_records_start = startup_block_records_start(
9262        &volume_header,
9263        &parsed_crypto.kdf_params,
9264        |start, length| {
9265            let start = to_usize(start, "KeyWrapTableV1")?;
9266            Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
9267        },
9268    )?;
9269
9270    let block_size = parsed_crypto.fixed.block_size as usize;
9271    let record_len = block_size
9272        .checked_add(BLOCK_RECORD_FRAMING_LEN)
9273        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
9274    let mut offset = to_usize(block_records_start, "BlockRecord")?;
9275    let mut observed_block_count = 0u64;
9276    let mut metadata_seen = false;
9277    let mut pending = PendingSequentialEnvelope::default();
9278    let mut next_envelope_index = 0u64;
9279    let mut tar_stream = Vec::new();
9280    let max_tar_stream_size = options.max_verify_tar_size;
9281    let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
9282    let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
9283    let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
9284        parsed_crypto.fixed.max_path_length,
9285        total_extraction_cap,
9286    );
9287
9288    while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9289        let record = parse_sequential_block_or_erasure(
9290            bytes,
9291            offset,
9292            record_len,
9293            block_size,
9294            &volume_header,
9295            observed_block_count,
9296        )?;
9297        observed_block_count = observed_block_count
9298            .checked_add(1)
9299            .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9300        let Some(record) = record else {
9301            handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
9302            offset = checked_add(offset, record_len, "BlockRecord")?;
9303            continue;
9304        };
9305
9306        match record.kind {
9307            BlockKind::PayloadData => {
9308                if metadata_seen {
9309                    return Err(FormatError::InvalidArchive(
9310                        "payload BlockRecord appears after metadata",
9311                    ));
9312                }
9313                if pending.awaiting_tentative_parity {
9314                    return Err(FormatError::InvalidArchive(
9315                        "sequential payload envelope boundary is ambiguous after CRC erasure",
9316                    ));
9317                }
9318                if pending.saw_last_data {
9319                    finalize_sequential_envelope(
9320                        &mut pending,
9321                        SequentialEnvelopeDecodeContext {
9322                            crypto_header: &parsed_crypto.fixed,
9323                            subkeys: &subkeys,
9324                            volume_header: &volume_header,
9325                            next_envelope_index: &mut next_envelope_index,
9326                            tar_stream: &mut tar_stream,
9327                            max_tar_stream_size,
9328                            tar_stream_total_validator: &mut tar_stream_total_validator,
9329                        },
9330                    )?;
9331                }
9332                let is_last_data = record.is_last_data();
9333                pending.data_shards.push(Some(record.payload));
9334                if is_last_data {
9335                    pending.saw_last_data = true;
9336                }
9337                if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
9338                    return Err(FormatError::InvalidArchive(
9339                        "sequential payload envelope exceeds data-shard cap",
9340                    ));
9341                }
9342            }
9343            BlockKind::PayloadParity => {
9344                if metadata_seen {
9345                    return Err(FormatError::InvalidArchive(
9346                        "payload parity BlockRecord appears after metadata",
9347                    ));
9348                }
9349                if pending.awaiting_tentative_parity {
9350                    pending.awaiting_tentative_parity = false;
9351                    pending.saw_last_data = true;
9352                } else if pending.data_shards.is_empty() || !pending.saw_last_data {
9353                    return Err(FormatError::InvalidArchive(
9354                        "payload parity appears before envelope data is complete",
9355                    ));
9356                }
9357                pending.parity_shards.push(Some(record.payload));
9358                if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
9359                    return Err(FormatError::InvalidArchive(
9360                        "sequential payload envelope exceeds parity-shard cap",
9361                    ));
9362                }
9363            }
9364            _ => {
9365                if !pending.is_empty() {
9366                    finalize_sequential_envelope(
9367                        &mut pending,
9368                        SequentialEnvelopeDecodeContext {
9369                            crypto_header: &parsed_crypto.fixed,
9370                            subkeys: &subkeys,
9371                            volume_header: &volume_header,
9372                            next_envelope_index: &mut next_envelope_index,
9373                            tar_stream: &mut tar_stream,
9374                            max_tar_stream_size,
9375                            tar_stream_total_validator: &mut tar_stream_total_validator,
9376                        },
9377                    )?;
9378                }
9379                metadata_seen = true;
9380            }
9381        }
9382
9383        offset = checked_add(offset, record_len, "BlockRecord")?;
9384    }
9385
9386    if !pending.is_empty() {
9387        finalize_sequential_envelope(
9388            &mut pending,
9389            SequentialEnvelopeDecodeContext {
9390                crypto_header: &parsed_crypto.fixed,
9391                subkeys: &subkeys,
9392                volume_header: &volume_header,
9393                next_envelope_index: &mut next_envelope_index,
9394                tar_stream: &mut tar_stream,
9395                max_tar_stream_size,
9396                tar_stream_total_validator: &mut tar_stream_total_validator,
9397            },
9398        )?;
9399    }
9400
9401    parse_terminal_material(
9402        bytes,
9403        offset,
9404        observed_block_count,
9405        KeyHoldingTerminalContext {
9406            subkeys: &subkeys,
9407            volume_header: &volume_header,
9408            crypto_header: &parsed_crypto.fixed,
9409            crypto_header_bytes: crypto_bytes,
9410        },
9411        options,
9412    )?;
9413    // This public helper is intentionally whole-buffer: decoded payload bytes
9414    // stay internal until terminal ManifestFooter and VolumeTrailer HMACs pass.
9415    validate_tar_stream_total_extraction_size(
9416        &tar_stream,
9417        parsed_crypto.fixed.max_path_length,
9418        total_extraction_cap,
9419    )?;
9420    Ok(tar_stream)
9421}
9422
9423fn validate_sequential_supported_volume(
9424    volume_header: &VolumeHeader,
9425    crypto_header: &CryptoHeaderFixed,
9426    extensions: &[ExtensionTlv<'_>],
9427) -> Result<(), FormatError> {
9428    reject_unsupported_raw_stream_profile(extensions)?;
9429    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
9430        return Err(FormatError::ReaderUnsupported(
9431            "sequential reader supports only single-volume archive input",
9432        ));
9433    }
9434    if crypto_header.stripe_width != volume_header.stripe_width {
9435        return Err(FormatError::InvalidArchive(
9436            "VolumeHeader and CryptoHeader stripe_width differ",
9437        ));
9438    }
9439    if crypto_header.has_dictionary != 0 {
9440        return Err(FormatError::ReaderUnsupported(
9441            "dictionary bootstrap required for non-seekable sequential extraction",
9442        ));
9443    }
9444    Ok(())
9445}
9446
9447struct SequentialEnvelopeDecodeContext<'a> {
9448    crypto_header: &'a CryptoHeaderFixed,
9449    subkeys: &'a Subkeys,
9450    volume_header: &'a VolumeHeader,
9451    next_envelope_index: &'a mut u64,
9452    tar_stream: &'a mut Vec<u8>,
9453    max_tar_stream_size: usize,
9454    tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
9455}
9456
9457fn finalize_sequential_envelope(
9458    pending: &mut PendingSequentialEnvelope,
9459    context: SequentialEnvelopeDecodeContext<'_>,
9460) -> Result<(), FormatError> {
9461    if !pending.saw_last_data {
9462        return Err(FormatError::InvalidArchive(
9463            "sequential payload envelope is missing last-data flag",
9464        ));
9465    }
9466    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
9467        return Err(FormatError::InvalidArchive(
9468            "sequential payload envelope exceeds data-shard cap",
9469        ));
9470    }
9471    if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
9472        return Err(FormatError::InvalidArchive(
9473            "sequential payload envelope exceeds parity-shard cap",
9474        ));
9475    }
9476    let required_parity =
9477        required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
9478    if pending.parity_shards.len() < required_parity as usize {
9479        return Err(FormatError::InvalidArchive(
9480            "sequential payload envelope has insufficient parity for recovery settings",
9481        ));
9482    }
9483
9484    let repaired = repair_data_gf16(
9485        &pending.data_shards,
9486        &pending.parity_shards,
9487        context.crypto_header.block_size as usize,
9488    )?;
9489    let mut encrypted =
9490        Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
9491    for shard in repaired {
9492        encrypted.extend_from_slice(&shard);
9493    }
9494    let plaintext = decrypt_padded_aead_object(
9495        AeadObjectContext {
9496            algo: context.crypto_header.aead_algo,
9497            key: &context.subkeys.enc_key,
9498            nonce_seed: &context.subkeys.nonce_seed,
9499            domain: b"envelope",
9500            archive_uuid: &context.volume_header.archive_uuid,
9501            session_id: &context.volume_header.session_id,
9502            counter: *context.next_envelope_index,
9503        },
9504        &encrypted,
9505    )?;
9506    decode_concatenated_zstd_frames_with_cap(
9507        &plaintext,
9508        None,
9509        context.tar_stream,
9510        context.max_tar_stream_size,
9511        Some(context.tar_stream_total_validator),
9512    )?;
9513    *context.next_envelope_index = (*context.next_envelope_index)
9514        .checked_add(1)
9515        .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
9516    *pending = PendingSequentialEnvelope::default();
9517    Ok(())
9518}
9519
9520fn decode_concatenated_zstd_frames_with_cap(
9521    plaintext: &[u8],
9522    dictionary: Option<&[u8]>,
9523    output: &mut Vec<u8>,
9524    max_output_len: usize,
9525    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9526) -> Result<(), FormatError> {
9527    let mut cursor = 0usize;
9528    while cursor < plaintext.len() {
9529        let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
9530            .map_err(|_| FormatError::InvalidZstdFrame)?;
9531        if frame_len == 0 {
9532            return Err(FormatError::InvalidZstdFrame);
9533        }
9534        let end = checked_add(cursor, frame_len, "zstd frame")?;
9535        validate_exact_zstd_frame(&plaintext[cursor..end])?;
9536        if let Some(dictionary) = dictionary {
9537            let mut decoder =
9538                zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
9539                    .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9540            read_zstd_frame_to_capped_output(
9541                &mut decoder,
9542                output,
9543                max_output_len,
9544                tar_stream_total_validator.as_deref_mut(),
9545            )?;
9546        } else {
9547            let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
9548                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9549            read_zstd_frame_to_capped_output(
9550                &mut decoder,
9551                output,
9552                max_output_len,
9553                tar_stream_total_validator.as_deref_mut(),
9554            )?;
9555        }
9556        cursor = end;
9557    }
9558    Ok(())
9559}
9560
9561fn read_zstd_frame_to_capped_output<R: Read>(
9562    decoder: &mut R,
9563    output: &mut Vec<u8>,
9564    max_output_len: usize,
9565    mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9566) -> Result<(), FormatError> {
9567    let mut buf = [0u8; 64 * 1024];
9568    loop {
9569        let read = decoder
9570            .read(&mut buf)
9571            .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9572        if read == 0 {
9573            return Ok(());
9574        }
9575        let next_len = output
9576            .len()
9577            .checked_add(read)
9578            .ok_or(FormatError::ReaderUnsupported(
9579                "sequential tar stream exceeds configured verification cap",
9580            ))?;
9581        if next_len > max_output_len {
9582            return Err(FormatError::ReaderUnsupported(
9583                "sequential tar stream exceeds configured verification cap",
9584            ));
9585        }
9586        output.extend_from_slice(&buf[..read]);
9587        if let Some(validator) = tar_stream_total_validator.as_mut() {
9588            validator.observe(output)?;
9589        }
9590    }
9591}
9592
9593fn load_archive_dictionary(
9594    blocks: &impl BlockProvider,
9595    subkeys: &Subkeys,
9596    volume_header: &VolumeHeader,
9597    crypto_header: &CryptoHeaderFixed,
9598    index_root: &IndexRoot,
9599) -> Result<Option<Vec<u8>>, FormatError> {
9600    if crypto_header.has_dictionary == 0 {
9601        return Ok(None);
9602    }
9603    let plaintext = load_metadata_object_from_parts(
9604        blocks,
9605        ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
9606        index_root.header.dictionary_decompressed_size,
9607    )?;
9608    Ok(Some(plaintext))
9609}
9610
9611#[derive(Clone, Copy)]
9612struct ObjectLoadContext<'a> {
9613    volume_header: &'a VolumeHeader,
9614    crypto_header: &'a CryptoHeaderFixed,
9615    extent: ObjectExtent,
9616    data_kind: BlockKind,
9617    parity_kind: BlockKind,
9618    key: &'a [u8; 32],
9619    nonce_seed: &'a [u8; 32],
9620    domain: &'a [u8],
9621    counter: u64,
9622    class_data_shard_max: u16,
9623    class_parity_shard_max: u16,
9624}
9625
9626impl<'a> ObjectLoadContext<'a> {
9627    fn index_root(
9628        volume_header: &'a VolumeHeader,
9629        crypto_header: &'a CryptoHeaderFixed,
9630        subkeys: &'a Subkeys,
9631        extent: ObjectExtent,
9632    ) -> Self {
9633        Self {
9634            volume_header,
9635            crypto_header,
9636            extent,
9637            data_kind: BlockKind::IndexRootData,
9638            parity_kind: BlockKind::IndexRootParity,
9639            key: &subkeys.index_root_key,
9640            nonce_seed: &subkeys.index_nonce_seed,
9641            domain: b"idxroot",
9642            counter: 0,
9643            class_data_shard_max: crypto_header.index_root_fec_data_shards,
9644            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9645        }
9646    }
9647
9648    fn index_shard(
9649        volume_header: &'a VolumeHeader,
9650        crypto_header: &'a CryptoHeaderFixed,
9651        subkeys: &'a Subkeys,
9652        entry: &ShardEntry,
9653    ) -> Self {
9654        Self {
9655            volume_header,
9656            crypto_header,
9657            extent: ObjectExtent {
9658                first_block_index: entry.first_block_index,
9659                data_block_count: entry.data_block_count,
9660                parity_block_count: entry.parity_block_count,
9661                encrypted_size: entry.encrypted_size,
9662            },
9663            data_kind: BlockKind::IndexShardData,
9664            parity_kind: BlockKind::IndexShardParity,
9665            key: &subkeys.index_shard_key,
9666            nonce_seed: &subkeys.index_nonce_seed,
9667            domain: b"idxshard",
9668            counter: entry.shard_index,
9669            class_data_shard_max: crypto_header.index_fec_data_shards,
9670            class_parity_shard_max: crypto_header.index_fec_parity_shards,
9671        }
9672    }
9673
9674    fn directory_hint(
9675        volume_header: &'a VolumeHeader,
9676        crypto_header: &'a CryptoHeaderFixed,
9677        subkeys: &'a Subkeys,
9678        entry: &DirectoryHintShardEntry,
9679    ) -> Self {
9680        Self {
9681            volume_header,
9682            crypto_header,
9683            extent: ObjectExtent {
9684                first_block_index: entry.first_block_index,
9685                data_block_count: entry.data_block_count,
9686                parity_block_count: entry.parity_block_count,
9687                encrypted_size: entry.encrypted_size,
9688            },
9689            data_kind: BlockKind::DirectoryHintData,
9690            parity_kind: BlockKind::DirectoryHintParity,
9691            key: &subkeys.dir_hint_key,
9692            nonce_seed: &subkeys.index_nonce_seed,
9693            domain: b"dirhint",
9694            counter: entry.hint_shard_index,
9695            class_data_shard_max: crypto_header.index_fec_data_shards,
9696            class_parity_shard_max: crypto_header.index_fec_parity_shards,
9697        }
9698    }
9699
9700    fn dictionary(
9701        volume_header: &'a VolumeHeader,
9702        crypto_header: &'a CryptoHeaderFixed,
9703        subkeys: &'a Subkeys,
9704        index_root: &IndexRoot,
9705    ) -> Result<Self, FormatError> {
9706        Ok(Self {
9707            volume_header,
9708            crypto_header,
9709            extent: dictionary_extent_from_index_root(index_root)?,
9710            data_kind: BlockKind::DictionaryData,
9711            parity_kind: BlockKind::DictionaryParity,
9712            key: &subkeys.dictionary_key,
9713            nonce_seed: &subkeys.index_nonce_seed,
9714            domain: b"dict",
9715            counter: 0,
9716            class_data_shard_max: crypto_header.index_root_fec_data_shards,
9717            class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9718        })
9719    }
9720
9721    fn payload(
9722        volume_header: &'a VolumeHeader,
9723        crypto_header: &'a CryptoHeaderFixed,
9724        subkeys: &'a Subkeys,
9725        envelope: &EnvelopeEntry,
9726    ) -> Self {
9727        Self {
9728            volume_header,
9729            crypto_header,
9730            extent: ObjectExtent {
9731                first_block_index: envelope.first_block_index,
9732                data_block_count: envelope.data_block_count,
9733                parity_block_count: envelope.parity_block_count,
9734                encrypted_size: envelope.encrypted_size,
9735            },
9736            data_kind: BlockKind::PayloadData,
9737            parity_kind: BlockKind::PayloadParity,
9738            key: &subkeys.enc_key,
9739            nonce_seed: &subkeys.nonce_seed,
9740            domain: b"envelope",
9741            counter: envelope.envelope_index,
9742            class_data_shard_max: crypto_header.fec_data_shards,
9743            class_parity_shard_max: crypto_header.fec_parity_shards,
9744        }
9745    }
9746}
9747
9748fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
9749    if index_root.header.dictionary_data_block_count == 0
9750        || index_root.header.dictionary_encrypted_size == 0
9751        || index_root.header.dictionary_decompressed_size == 0
9752    {
9753        return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
9754    }
9755    Ok(ObjectExtent {
9756        first_block_index: index_root.header.dictionary_first_block,
9757        data_block_count: index_root.header.dictionary_data_block_count,
9758        parity_block_count: index_root.header.dictionary_parity_block_count,
9759        encrypted_size: index_root.header.dictionary_encrypted_size,
9760    })
9761}
9762
9763fn load_metadata_object_from_parts(
9764    blocks: &impl BlockProvider,
9765    context: ObjectLoadContext<'_>,
9766    decompressed_size: u32,
9767) -> Result<Vec<u8>, FormatError> {
9768    let compressed = load_decrypted_object_from_parts(blocks, context)?;
9769    decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
9770}
9771
9772fn load_decrypted_object_from_parts(
9773    blocks: &impl BlockProvider,
9774    context: ObjectLoadContext<'_>,
9775) -> Result<Vec<u8>, FormatError> {
9776    load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
9777}
9778
9779fn load_decrypted_object_from_parts_with_parity_policy(
9780    blocks: &impl BlockProvider,
9781    context: ObjectLoadContext<'_>,
9782    parity_policy: ParityReadPolicy,
9783) -> Result<Vec<u8>, FormatError> {
9784    let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
9785        blocks,
9786        context.crypto_header,
9787        context.extent,
9788        context.data_kind,
9789        context.parity_kind,
9790        context.class_data_shard_max,
9791        context.class_parity_shard_max,
9792        parity_policy,
9793    )?;
9794    let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
9795    for shard in repaired {
9796        encrypted.extend_from_slice(&shard);
9797    }
9798    if encrypted.len() != context.extent.encrypted_size as usize {
9799        return Err(FormatError::InvalidArchive(
9800            "object encrypted size does not match repaired shards",
9801        ));
9802    }
9803
9804    decrypt_padded_aead_object(
9805        AeadObjectContext {
9806            algo: context.crypto_header.aead_algo,
9807            key: context.key,
9808            nonce_seed: context.nonce_seed,
9809            domain: context.domain,
9810            archive_uuid: &context.volume_header.archive_uuid,
9811            session_id: &context.volume_header.session_id,
9812            counter: context.counter,
9813        },
9814        &encrypted,
9815    )
9816}
9817
9818fn load_repaired_object_data_shards_from_parts(
9819    blocks: &impl BlockProvider,
9820    crypto_header: &CryptoHeaderFixed,
9821    extent: ObjectExtent,
9822    data_kind: BlockKind,
9823    parity_kind: BlockKind,
9824    class_data_shard_max: u16,
9825    class_parity_shard_max: u16,
9826) -> Result<Vec<Vec<u8>>, FormatError> {
9827    load_repaired_object_data_shards_from_parts_with_parity_policy(
9828        blocks,
9829        crypto_header,
9830        extent,
9831        data_kind,
9832        parity_kind,
9833        class_data_shard_max,
9834        class_parity_shard_max,
9835        ParityReadPolicy::Always,
9836    )
9837}
9838
9839#[allow(clippy::too_many_arguments)]
9840fn load_repaired_object_data_shards_from_parts_with_parity_policy(
9841    blocks: &impl BlockProvider,
9842    crypto_header: &CryptoHeaderFixed,
9843    extent: ObjectExtent,
9844    data_kind: BlockKind,
9845    parity_kind: BlockKind,
9846    class_data_shard_max: u16,
9847    class_parity_shard_max: u16,
9848    parity_policy: ParityReadPolicy,
9849) -> Result<Vec<Vec<u8>>, FormatError> {
9850    validate_object_extent(
9851        extent,
9852        crypto_header,
9853        class_data_shard_max,
9854        class_parity_shard_max,
9855    )?;
9856    let block_size = crypto_header.block_size as usize;
9857    let data_count = extent.data_block_count as usize;
9858    let parity_count = extent.parity_block_count as usize;
9859    let mut data_shards = Vec::with_capacity(data_count);
9860    let mut parity_shards = Vec::with_capacity(parity_count);
9861
9862    for offset in 0..data_count {
9863        let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
9864        if let Some(record) = blocks.block(block_index)? {
9865            if record.kind != data_kind {
9866                return Err(FormatError::InvalidArchive(
9867                    "object data block has unexpected kind",
9868                ));
9869            }
9870            let should_be_last = offset + 1 == data_count;
9871            if record.is_last_data() != should_be_last {
9872                return Err(FormatError::InvalidArchive(
9873                    "object last-data flag is not on the final data block",
9874                ));
9875            }
9876            data_shards.push(Some(record.payload.clone()));
9877        } else {
9878            data_shards.push(None);
9879        }
9880    }
9881
9882    if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
9883        return repair_data_gf16(&data_shards, &[], block_size);
9884    }
9885
9886    for offset in 0..parity_count {
9887        let block_index = checked_u64_add(
9888            extent.first_block_index,
9889            data_count as u64 + offset as u64,
9890            "object",
9891        )?;
9892        if let Some(record) = blocks.block(block_index)? {
9893            if record.kind != parity_kind {
9894                return Err(FormatError::InvalidArchive(
9895                    "object parity block has unexpected kind",
9896                ));
9897            }
9898            if record.is_last_data() {
9899                return Err(FormatError::InvalidArchive(
9900                    "object parity block has last-data flag",
9901                ));
9902            }
9903            parity_shards.push(Some(record.payload.clone()));
9904        } else {
9905            parity_shards.push(None);
9906        }
9907    }
9908
9909    repair_data_gf16(&data_shards, &parity_shards, block_size)
9910}
9911
9912fn validate_object_extent(
9913    extent: ObjectExtent,
9914    crypto_header: &CryptoHeaderFixed,
9915    class_data_shard_max: u16,
9916    class_parity_shard_max: u16,
9917) -> Result<(), FormatError> {
9918    if extent.data_block_count == 0 || extent.encrypted_size == 0 {
9919        return Err(FormatError::InvalidArchive(
9920            "encrypted object has zero data blocks or size",
9921        ));
9922    }
9923    if extent.data_block_count > class_data_shard_max as u32 {
9924        return Err(FormatError::InvalidArchive(
9925            "encrypted object exceeds its class data-shard maximum",
9926        ));
9927    }
9928    if extent.parity_block_count > class_parity_shard_max as u32 {
9929        return Err(FormatError::InvalidArchive(
9930            "encrypted object exceeds its class parity-shard maximum",
9931        ));
9932    }
9933    let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
9934    if extent.parity_block_count != required_parity {
9935        return Err(FormatError::InvalidArchive(
9936            "encrypted object parity does not match v41 compute_parity",
9937        ));
9938    }
9939    let total = checked_u64_add(
9940        extent.data_block_count as u64,
9941        extent.parity_block_count as u64,
9942        "encrypted object shard count overflow",
9943    )?;
9944    if total > 65_535 {
9945        return Err(FormatError::FecTooManyShards(total as usize));
9946    }
9947    let expected = checked_u64_mul(
9948        extent.data_block_count as u64,
9949        crypto_header.block_size as u64,
9950        "encrypted object size overflow",
9951    )?;
9952    if expected != extent.encrypted_size as u64 {
9953        return Err(FormatError::InvalidArchive(
9954            "encrypted object size is not data_block_count * block_size",
9955        ));
9956    }
9957    if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
9958        return Err(FormatError::InvalidArchive(
9959            "encrypted object is too small for AEAD tag",
9960        ));
9961    }
9962    Ok(())
9963}
9964
9965pub(crate) fn required_object_parity(
9966    data_block_count: u64,
9967    crypto_header: &CryptoHeaderFixed,
9968) -> Result<u32, FormatError> {
9969    let min_parity =
9970        if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
9971            1
9972        } else {
9973            0
9974        };
9975    let mut parity = 0u64;
9976    for _ in 0..100 {
9977        let total = data_block_count
9978            .checked_add(parity)
9979            .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
9980        let by_volume = checked_u64_mul(
9981            crypto_header.volume_loss_tolerance as u64,
9982            ceil_div_u64(total, crypto_header.stripe_width as u64)?,
9983            "volume-loss parity overflow",
9984        )?;
9985        let by_bitrot = ceil_div_u64(
9986            checked_u64_mul(
9987                total,
9988                crypto_header.bit_rot_buffer_pct as u64,
9989                "bit-rot parity overflow",
9990            )?,
9991            100,
9992        )?;
9993        let next = by_volume
9994            .checked_add(by_bitrot)
9995            .ok_or(FormatError::InvalidArchive("parity overflow"))?
9996            .max(min_parity);
9997        if next == parity {
9998            return u32::try_from(next)
9999                .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
10000        }
10001        parity = next;
10002    }
10003    Err(FormatError::InvalidArchive(
10004        "parity calculation did not converge",
10005    ))
10006}
10007
10008fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
10009    if denominator == 0 {
10010        return Err(FormatError::InvalidArchive("division by zero"));
10011    }
10012    numerator
10013        .checked_add(denominator - 1)
10014        .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
10015        .map(|value| value / denominator)
10016}
10017
10018fn frame_range_for_file<'b>(
10019    shard: &'b IndexShard,
10020    file: &FileEntry,
10021) -> Result<&'b [FrameEntry], FormatError> {
10022    let start = shard
10023        .frames
10024        .binary_search_by_key(&file.first_frame_index, |entry| entry.frame_index)
10025        .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))?;
10026    let count = usize::try_from(file.frame_count)
10027        .map_err(|_| FormatError::InvalidArchive("FileEntry frame count overflow"))?;
10028    let end = start.checked_add(count).ok_or(FormatError::InvalidArchive(
10029        "FileEntry frame range overflow",
10030    ))?;
10031    let frames = shard
10032        .frames
10033        .get(start..end)
10034        .ok_or(FormatError::InvalidArchive(
10035            "FileEntry references missing FrameEntry",
10036        ))?;
10037    for (offset, frame) in frames.iter().enumerate() {
10038        let expected = file.first_frame_index.checked_add(offset as u64).ok_or(
10039            FormatError::InvalidArchive("FileEntry frame range overflow"),
10040        )?;
10041        if frame.frame_index != expected {
10042            return Err(FormatError::InvalidArchive(
10043                "FileEntry references missing FrameEntry",
10044            ));
10045        }
10046    }
10047    Ok(frames)
10048}
10049
10050fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
10051    MetadataLimits {
10052        block_size: crypto_header.block_size,
10053        max_path_length: crypto_header.max_path_length,
10054        max_payload_data_shards: crypto_header.fec_data_shards,
10055        max_payload_parity_shards: crypto_header.fec_parity_shards,
10056        max_index_data_shards: crypto_header.index_fec_data_shards,
10057        max_index_parity_shards: crypto_header.index_fec_parity_shards,
10058        max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
10059        max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
10060        ..MetadataLimits::default()
10061    }
10062}
10063
10064fn verify_dense_keys<T>(
10065    entries: &BTreeMap<u64, T>,
10066    expected_count: u64,
10067    structure: &'static str,
10068) -> Result<(), FormatError> {
10069    if entries.len() as u64 != expected_count {
10070        return Err(FormatError::InvalidArchive(
10071            "decoded table count does not match IndexRoot",
10072        ));
10073    }
10074    for expected in 0..expected_count {
10075        if !entries.contains_key(&expected) {
10076            return Err(FormatError::InvalidMetadata {
10077                structure,
10078                reason: "global index coverage has a gap",
10079            });
10080        }
10081    }
10082    Ok(())
10083}
10084
10085fn validate_envelope_frame_coverage(
10086    frames: &BTreeMap<u64, FrameEntry>,
10087    envelopes: &BTreeMap<u64, EnvelopeEntry>,
10088) -> Result<(), FormatError> {
10089    let mut accounted_frames = BTreeSet::new();
10090    for envelope in envelopes.values() {
10091        let first = envelope.first_frame_index;
10092        let end =
10093            first
10094                .checked_add(envelope.frame_count as u64)
10095                .ok_or(FormatError::InvalidArchive(
10096                    "EnvelopeEntry frame range overflow",
10097                ))?;
10098        let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
10099        for frame_index in first..end {
10100            let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
10101                "EnvelopeEntry references missing FrameEntry",
10102            ))?;
10103            if frame.envelope_index != envelope.envelope_index {
10104                return Err(FormatError::InvalidArchive(
10105                    "FrameEntry envelope_index does not match containing EnvelopeEntry",
10106                ));
10107            }
10108            if !accounted_frames.insert(frame_index) {
10109                return Err(FormatError::InvalidArchive(
10110                    "FrameEntry is covered by multiple EnvelopeEntries",
10111                ));
10112            }
10113            let start = frame.offset_in_envelope as usize;
10114            let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
10115            if end > envelope.plaintext_size as usize {
10116                return Err(FormatError::InvalidArchive(
10117                    "FrameEntry exceeds EnvelopeEntry plaintext_size",
10118                ));
10119            }
10120            ranges.push((start, end));
10121        }
10122        validate_exact_coverage_ranges(
10123            &mut ranges,
10124            envelope.plaintext_size as usize,
10125            "EnvelopeEntry frame coverage has a gap or overlap",
10126        )?;
10127    }
10128
10129    for frame_index in frames.keys() {
10130        if !accounted_frames.contains(frame_index) {
10131            return Err(FormatError::InvalidArchive(
10132                "FrameEntry is not covered by any EnvelopeEntry",
10133            ));
10134        }
10135    }
10136    Ok(())
10137}
10138
10139fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
10140    let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
10141    for shard in shards {
10142        for (idx, file) in shard.files.iter().enumerate() {
10143            let path = shard
10144                .file_path(idx)
10145                .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
10146                .to_vec();
10147            let start = shard
10148                .tar_member_group_start(idx)
10149                .ok_or(FormatError::InvalidArchive(
10150                    "FileEntry tar member start is missing",
10151                ))?;
10152            let key = (file.path_hash, path, start);
10153            validate_global_file_table_key_step(previous.as_ref(), &key)?;
10154            previous = Some(key);
10155        }
10156    }
10157    Ok(())
10158}
10159
10160fn validate_global_file_table_key_step(
10161    previous: Option<&([u8; 8], Vec<u8>, u64)>,
10162    current: &([u8; 8], Vec<u8>, u64),
10163) -> Result<(), FormatError> {
10164    if let Some(previous) = previous {
10165        if previous >= current {
10166            return Err(FormatError::InvalidArchive(
10167                "global FileEntry rows are not sorted and unique",
10168            ));
10169        }
10170    }
10171    Ok(())
10172}
10173
10174fn validate_file_extent_coverage_ranges(
10175    extents: &[(u64, u64)],
10176    tar_len: u64,
10177) -> Result<(), FormatError> {
10178    let mut ranges = Vec::with_capacity(extents.len());
10179    for (start, len) in extents {
10180        let end = checked_u64_add(*start, *len, "FileEntry")?;
10181        if end > tar_len {
10182            return Err(FormatError::InvalidArchive(
10183                "FileEntry extent exceeds IndexRoot tar_total_size",
10184            ));
10185        }
10186        ranges.push((*start, end));
10187    }
10188    validate_exact_coverage_ranges_u64(
10189        &mut ranges,
10190        tar_len,
10191        "FileEntry extents do not cover tar stream exactly",
10192    )
10193}
10194
10195fn add_expected_directory_hint_rows(
10196    map: &mut DirectoryHintMap,
10197    shard_row_index: u32,
10198    path: &[u8],
10199    kind: TarEntryKind,
10200) {
10201    map.entry(Vec::new()).or_default().insert(shard_row_index);
10202    for (idx, byte) in path.iter().enumerate() {
10203        if *byte == b'/' {
10204            map.entry(path[..idx].to_vec())
10205                .or_default()
10206                .insert(shard_row_index);
10207        }
10208    }
10209    if kind == TarEntryKind::Directory {
10210        map.entry(path.to_vec())
10211            .or_default()
10212            .insert(shard_row_index);
10213    }
10214}
10215
10216fn validate_directory_hint_tables_against_expected(
10217    tables: &[DirectoryHintTable],
10218    expected: &DirectoryHintMap,
10219) -> Result<(), FormatError> {
10220    let mut actual = Vec::new();
10221    let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
10222
10223    for table in tables {
10224        for entry_index in 0..table.entries.len() {
10225            let path = table
10226                .entry_path(entry_index)
10227                .ok_or(FormatError::InvalidArchive(
10228                    "DirectoryHintEntry path is missing",
10229                ))?;
10230            let key = (hash_prefix(path), path.to_vec());
10231            if let Some(previous) = &previous_key {
10232                if previous >= &key {
10233                    return Err(FormatError::InvalidArchive(
10234                        "DirectoryHintEntry rows are not globally sorted",
10235                    ));
10236                }
10237            }
10238            previous_key = Some(key);
10239
10240            let rows =
10241                table
10242                    .shard_rows_for_entry(entry_index)
10243                    .ok_or(FormatError::InvalidArchive(
10244                        "DirectoryHintEntry shard rows are missing",
10245                    ))?;
10246            actual.push((path.to_vec(), rows.to_vec()));
10247        }
10248    }
10249
10250    if actual != sorted_directory_hint_rows(expected) {
10251        return Err(FormatError::InvalidArchive(
10252            "directory hint map does not match decoded files",
10253        ));
10254    }
10255    Ok(())
10256}
10257
10258fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
10259    let mut rows = map
10260        .iter()
10261        .map(|(path, shard_rows)| {
10262            (
10263                path.clone(),
10264                shard_rows.iter().copied().collect::<Vec<u32>>(),
10265            )
10266        })
10267        .collect::<Vec<_>>();
10268    rows.sort_by(|(left_path, _), (right_path, _)| {
10269        hash_prefix(left_path)
10270            .cmp(&hash_prefix(right_path))
10271            .then_with(|| left_path.cmp(right_path))
10272    });
10273    rows
10274}
10275
10276fn validate_exact_coverage_ranges(
10277    ranges: &mut [(usize, usize)],
10278    expected_end: usize,
10279    reason: &'static str,
10280) -> Result<(), FormatError> {
10281    ranges.sort_unstable();
10282    let mut cursor = 0usize;
10283    for (start, end) in ranges.iter().copied() {
10284        if start != cursor || end < start {
10285            return Err(FormatError::InvalidArchive(reason));
10286        }
10287        cursor = end;
10288    }
10289    if cursor != expected_end {
10290        return Err(FormatError::InvalidArchive(reason));
10291    }
10292    Ok(())
10293}
10294
10295fn validate_exact_coverage_ranges_u64(
10296    ranges: &mut [(u64, u64)],
10297    expected_end: u64,
10298    reason: &'static str,
10299) -> Result<(), FormatError> {
10300    ranges.sort_unstable();
10301    let mut cursor = 0u64;
10302    for (start, end) in ranges.iter().copied() {
10303        if start != cursor || end < start {
10304            return Err(FormatError::InvalidArchive(reason));
10305        }
10306        cursor = end;
10307    }
10308    if cursor != expected_end {
10309        return Err(FormatError::InvalidArchive(reason));
10310    }
10311    Ok(())
10312}
10313
10314fn object_block_range(
10315    first_block_index: u64,
10316    data_block_count: u32,
10317    parity_block_count: u32,
10318    structure: &'static str,
10319) -> Result<(u64, u64), FormatError> {
10320    let total = data_block_count as u64 + parity_block_count as u64;
10321    if total == 0 {
10322        return Err(FormatError::InvalidArchive(structure));
10323    }
10324    let end = checked_u64_add(first_block_index, total, structure)?;
10325    Ok((first_block_index, end))
10326}
10327
10328fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
10329    ranges.sort_unstable();
10330    for pair in ranges.windows(2) {
10331        if pair[0].1 > pair[1].0 {
10332            return Err(FormatError::InvalidArchive(
10333                "encrypted object block ranges overlap",
10334            ));
10335        }
10336    }
10337    Ok(())
10338}
10339
10340pub(crate) fn observed_archive_size(
10341    sizes: impl IntoIterator<Item = u64>,
10342) -> Result<u64, FormatError> {
10343    sizes.into_iter().try_fold(0u64, |sum, size| {
10344        sum.checked_add(size).ok_or(FormatError::InvalidArchive(
10345            "observed archive size overflow",
10346        ))
10347    })
10348}
10349
10350pub(crate) fn total_extraction_size_cap(
10351    options: ReaderOptions,
10352    observed_archive_bytes: u64,
10353) -> u64 {
10354    options
10355        .max_total_extraction_size
10356        .min(observed_archive_bytes.saturating_mul(10))
10357}
10358
10359fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
10360    std::str::from_utf8(bytes)
10361        .map(|path| path.to_owned())
10362        .map_err(|_| FormatError::UnsafeArchivePath)
10363}
10364
10365fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
10366    let mut bytes = [0u8; 104];
10367    bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
10368    bytes[36..40].fill(0);
10369    bytes
10370}
10371
10372fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
10373    let digest = Sha256::digest(bytes);
10374    let mut out = [0u8; 32];
10375    out.copy_from_slice(&digest);
10376    out
10377}
10378
10379fn slice<'b>(
10380    bytes: &'b [u8],
10381    offset: usize,
10382    len: usize,
10383    structure: &'static str,
10384) -> Result<&'b [u8], FormatError> {
10385    let end = checked_add(offset, len, structure)?;
10386    bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10387        structure,
10388        expected: end,
10389        actual: bytes.len(),
10390    })
10391}
10392
10393fn read_at_vec(
10394    reader: &dyn ArchiveReadAt,
10395    offset: u64,
10396    len: usize,
10397    structure: &'static str,
10398) -> Result<Vec<u8>, FormatError> {
10399    let expected_end = offset
10400        .checked_add(len as u64)
10401        .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
10402    let observed_len = reader.len()?;
10403    if expected_end > observed_len {
10404        return Err(FormatError::InvalidLength {
10405            structure,
10406            expected: to_usize(expected_end, structure)?,
10407            actual: to_usize(observed_len, structure)?,
10408        });
10409    }
10410    let mut out = vec![0u8; len];
10411    reader.read_exact_at(offset, &mut out)?;
10412    Ok(out)
10413}
10414
10415fn read_at_vec_unchecked(
10416    reader: &dyn ArchiveReadAt,
10417    offset: u64,
10418    len: usize,
10419) -> Result<Vec<u8>, FormatError> {
10420    let mut out = vec![0u8; len];
10421    reader.read_exact_at(offset, &mut out)?;
10422    Ok(out)
10423}
10424
10425fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
10426where
10427    T: Sync,
10428    U: Send,
10429    F: Fn(&T) -> Result<U, FormatError> + Sync,
10430{
10431    if jobs <= 1 || items.len() <= 1 {
10432        return items.iter().map(f).collect();
10433    }
10434    let worker_count = jobs.min(items.len());
10435    let chunk_size = items.len().div_ceil(worker_count);
10436    let mut out = Vec::with_capacity(items.len());
10437    thread::scope(|scope| {
10438        let handles = items
10439            .chunks(chunk_size)
10440            .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
10441            .collect::<Vec<_>>();
10442        for handle in handles {
10443            let mut chunk = handle
10444                .join()
10445                .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
10446            out.append(&mut chunk);
10447        }
10448        Ok(out)
10449    })
10450}
10451
10452fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
10453    lhs.checked_add(rhs)
10454        .ok_or(FormatError::InvalidArchive(structure))
10455}
10456
10457fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
10458    lhs.checked_add(rhs)
10459        .ok_or(FormatError::InvalidArchive(structure))
10460}
10461
10462fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
10463    usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
10464}
10465
10466#[cfg(test)]
10467mod tests {
10468    use std::fs;
10469
10470    use super::*;
10471    use crate::compression::compress_zstd_frame;
10472    use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
10473    use crate::entry_metadata::RestorePolicy;
10474    use crate::fec::encode_parity_gf16;
10475    use crate::format::{
10476        AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
10477        CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION, READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10478        VOLUME_FORMAT_REV, VOLUME_FORMAT_REV_45,
10479    };
10480    use crate::metadata::{
10481        DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
10482        ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
10483    };
10484    use crate::non_seekable_reader::{
10485        extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
10486        verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
10487        verify_non_seekable_stream_with_recipient_wrap_resolver_options, NonSeekableReaderOptions,
10488        SequentialRootAuthStatus,
10489    };
10490    use crate::raw_stream_profile::{
10491        serialize_raw_stream_content_model_extension, RAW_STREAM_UNSUPPORTED_MESSAGE,
10492    };
10493    use crate::wire::RecipientRecordV1;
10494    use crate::writer::NativeFileMetadata;
10495    use crate::writer::{
10496        write_archive, write_archive_unencrypted, write_archive_with_dictionary,
10497        write_archive_with_kdf, write_archive_with_recipient_wrap_records,
10498        write_archive_with_root_auth, write_archive_with_root_auth_and_recipient_wrap_records,
10499        PortableFileMetadata, PortableModeOrigin, PortablePosixOwner, RegularFile,
10500        RootAuthSigningRequest, RootAuthWriterConfig, WriterOptions,
10501    };
10502    #[cfg(target_os = "linux")]
10503    use crate::writer::{NativeAuxiliaryMetadata, NativeAuxiliaryNameEncoding};
10504
10505    fn master_key() -> MasterKey {
10506        MasterKey::from_raw_key(&[0x42; 32]).unwrap()
10507    }
10508
10509    fn recipient_wrap_test_record() -> RecipientRecordV1 {
10510        RecipientRecordV1 {
10511            record_length: 0,
10512            profile_id: 1,
10513            recipient_identity_type: 2,
10514            flags: 0,
10515            recipient_identity_length: 0,
10516            profile_payload_length: 0,
10517            recipient_identity_digest: [0u8; 32],
10518            recipient_identity_bytes: b"recipient-a".to_vec(),
10519            profile_payload_bytes: b"profile-payload".to_vec(),
10520        }
10521    }
10522
10523    fn recipient_wrap_layout(volume: &[u8]) -> (usize, usize, usize, usize, u32) {
10524        let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10525        let crypto_start = header.crypto_header_offset as usize;
10526        let crypto_len = header.crypto_header_length as usize;
10527        let crypto_end = crypto_start + crypto_len;
10528        let crypto = CryptoHeader::parse(
10529            &volume[crypto_start..crypto_end],
10530            header.crypto_header_length,
10531        )
10532        .unwrap();
10533        let KdfParams::RecipientWrap {
10534            key_wrap_table_length,
10535            ..
10536        } = crypto.kdf_params
10537        else {
10538            panic!("expected RecipientWrap KdfParams");
10539        };
10540        (
10541            crypto_start,
10542            crypto_len,
10543            crypto_end,
10544            key_wrap_table_length as usize,
10545            key_wrap_table_length,
10546        )
10547    }
10548
10549    fn rewrite_recipient_wrap_kdf_digest(crypto_bytes: &mut [u8], digest: [u8; 32]) {
10550        let digest_start = CRYPTO_HEADER_FIXED_LEN + 14;
10551        crypto_bytes[digest_start..digest_start + 32].copy_from_slice(&digest);
10552    }
10553
10554    fn mutate_top_level_recipient_wrap_public_profile(volume: &mut [u8]) {
10555        let (crypto_start, crypto_len, table_start, table_len, table_len_u32) =
10556            recipient_wrap_layout(volume);
10557        let table_end = table_start + table_len;
10558        volume[table_end - 1] ^= 0x5a;
10559        let digest = compute_key_wrap_table_digest(table_len_u32, &volume[table_start..table_end]);
10560        rewrite_recipient_wrap_kdf_digest(
10561            &mut volume[crypto_start..crypto_start + crypto_len],
10562            digest,
10563        );
10564    }
10565
10566    fn mutate_cmra_recipient_wrap_public_profile(volume: &mut [u8]) {
10567        rewrite_public_cmra_image(volume, |image| {
10568            let table_region = image
10569                .regions
10570                .iter_mut()
10571                .find(|region| region.region_type == 6)
10572                .unwrap();
10573            *table_region.bytes.last_mut().unwrap() ^= 0x5a;
10574            let digest =
10575                compute_key_wrap_table_digest(table_region.bytes.len() as u32, &table_region.bytes);
10576            let crypto_region = image
10577                .regions
10578                .iter_mut()
10579                .find(|region| region.region_type == 2)
10580                .unwrap();
10581            rewrite_recipient_wrap_kdf_digest(&mut crypto_region.bytes, digest);
10582        });
10583    }
10584
10585    fn add_raw_stream_profile_to_physical_crypto_header(volume: &mut Vec<u8>) {
10586        let mut header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10587        let crypto_start = header.crypto_header_offset as usize;
10588        let crypto_len = header.crypto_header_length as usize;
10589        let hmac_start = crypto_start + crypto_len - CRYPTO_HEADER_HMAC_LEN;
10590        let terminator_start = hmac_start - CRYPTO_EXTENSION_HEADER_LEN;
10591        let extension = serialize_raw_stream_content_model_extension();
10592        let new_crypto_len = header.crypto_header_length + extension.len() as u32;
10593
10594        volume.splice(terminator_start..terminator_start, extension);
10595        header.crypto_header_length = new_crypto_len;
10596        volume[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10597        volume[crypto_start + 4..crypto_start + 8].copy_from_slice(&new_crypto_len.to_le_bytes());
10598    }
10599
10600    fn recompute_physical_crypto_header_hmac(volume: &mut [u8], master_key: &MasterKey) {
10601        let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10602        let crypto_start = header.crypto_header_offset as usize;
10603        let crypto_end = crypto_start + header.crypto_header_length as usize;
10604        let hmac_start = crypto_end - CRYPTO_HEADER_HMAC_LEN;
10605        let subkeys =
10606            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
10607        let hmac = compute_hmac(
10608            HmacDomain::CryptoHeader,
10609            &subkeys.mac_key,
10610            &header.archive_uuid,
10611            &header.session_id,
10612            &volume[crypto_start..hmac_start],
10613        );
10614        volume[hmac_start..crypto_end].copy_from_slice(&hmac);
10615    }
10616
10617    #[test]
10618    fn reader_defaults_use_available_parallelism_jobs() {
10619        let options = ReaderOptions::default();
10620
10621        assert_eq!(options.jobs, default_jobs());
10622        assert!(options.jobs >= 1);
10623    }
10624
10625    #[test]
10626    fn reader_options_reject_zero_jobs() {
10627        let err = OpenedArchive::open_with_options(
10628            &[],
10629            &master_key(),
10630            ReaderOptions {
10631                jobs: 0,
10632                ..ReaderOptions::default()
10633            },
10634        )
10635        .unwrap_err();
10636
10637        assert_eq!(
10638            err,
10639            FormatError::ReaderUnsupported("jobs must be at least 1")
10640        );
10641    }
10642
10643    const TEST_ROOT_AUTH_ID: u16 = 0xe001;
10644    const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
10645
10646    fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
10647        RootAuthWriterConfig {
10648            authenticator_id: TEST_ROOT_AUTH_ID,
10649            signer_identity_type: 0,
10650            signer_identity: &[],
10651            authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
10652        }
10653    }
10654
10655    fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
10656        request.archive_root.to_vec()
10657    }
10658
10659    fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
10660        footer.authenticator_id == TEST_ROOT_AUTH_ID
10661            && footer.signer_identity_type == 0
10662            && footer.signer_identity_bytes.is_empty()
10663            && footer.authenticator_value.as_slice() == archive_root
10664    }
10665
10666    fn dictionary() -> &'static [u8] {
10667        b"dir/dict.txt common words common words common words dictionary payload"
10668    }
10669
10670    #[derive(Clone)]
10671    struct CountingReadAt {
10672        bytes: std::sync::Arc<Vec<u8>>,
10673        reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
10674        denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
10675    }
10676
10677    impl CountingReadAt {
10678        fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
10679            Self {
10680                bytes: std::sync::Arc::new(bytes),
10681                reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
10682                denied_ranges: std::sync::Arc::new(denied_ranges),
10683            }
10684        }
10685
10686        fn reads(&self) -> Vec<(u64, u64)> {
10687            self.reads.lock().unwrap().clone()
10688        }
10689    }
10690
10691    impl ArchiveReadAt for CountingReadAt {
10692        fn len(&self) -> Result<u64, FormatError> {
10693            u64::try_from(self.bytes.as_ref().len())
10694                .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
10695        }
10696
10697        fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
10698            let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
10699            self.reads.lock().unwrap().push((offset, end));
10700            if self
10701                .denied_ranges
10702                .iter()
10703                .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
10704            {
10705                return Err(FormatError::InvalidArchive("denied test read"));
10706            }
10707            let start = to_usize(offset, "archive")?;
10708            let end_usize = checked_add(start, buf.len(), "archive")?;
10709            let source = self
10710                .bytes
10711                .get(start..end_usize)
10712                .ok_or(FormatError::InvalidLength {
10713                    structure: "archive",
10714                    expected: end_usize,
10715                    actual: self.bytes.as_ref().len(),
10716                })?;
10717            buf.copy_from_slice(source);
10718            Ok(())
10719        }
10720    }
10721
10722    fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
10723        left_start < right_end && right_start < left_end
10724    }
10725
10726    fn single_stream_options() -> WriterOptions {
10727        WriterOptions {
10728            stripe_width: 1,
10729            volume_loss_tolerance: 0,
10730            ..WriterOptions::default()
10731        }
10732    }
10733
10734    struct ChunkedReader {
10735        bytes: Vec<u8>,
10736        cursor: usize,
10737        max_chunk: usize,
10738    }
10739
10740    impl ChunkedReader {
10741        fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
10742            Self {
10743                bytes,
10744                cursor: 0,
10745                max_chunk,
10746            }
10747        }
10748    }
10749
10750    impl std::io::Read for ChunkedReader {
10751        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
10752            if self.cursor >= self.bytes.len() {
10753                return Ok(0);
10754            }
10755            let available = self.bytes.len() - self.cursor;
10756            let len = available.min(buf.len()).min(self.max_chunk);
10757            buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
10758            self.cursor += len;
10759            Ok(len)
10760        }
10761    }
10762
10763    #[test]
10764    fn global_file_table_key_step_rejects_distinct_path_regression() {
10765        let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
10766        let current = ([1u8; 8], b"a.txt".to_vec(), 0);
10767
10768        assert_eq!(
10769            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
10770            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10771        );
10772    }
10773
10774    #[test]
10775    fn global_file_table_key_step_rejects_duplicate_full_key() {
10776        let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
10777        let current = ([1u8; 8], b"a.txt".to_vec(), 7);
10778
10779        assert_eq!(
10780            validate_global_file_table_key_step(Some(&previous), &current).unwrap_err(),
10781            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10782        );
10783    }
10784
10785    fn small_block_recovery_options() -> WriterOptions {
10786        WriterOptions {
10787            block_size: 4096,
10788            chunk_size: 32 * 1024,
10789            envelope_target_size: 32 * 1024,
10790            stripe_width: 1,
10791            volume_loss_tolerance: 0,
10792            bit_rot_buffer_pct: 1,
10793            fec_data_shards: 16,
10794            fec_parity_shards: 1,
10795            index_fec_data_shards: 4,
10796            index_fec_parity_shards: 1,
10797            index_root_fec_data_shards: 16,
10798            index_root_fec_parity_shards: 1,
10799            ..WriterOptions::default()
10800        }
10801    }
10802
10803    fn parity_rich_recovery_options() -> WriterOptions {
10804        WriterOptions {
10805            block_size: 4096,
10806            chunk_size: 32 * 1024,
10807            envelope_target_size: 32 * 1024,
10808            stripe_width: 1,
10809            volume_loss_tolerance: 0,
10810            bit_rot_buffer_pct: 40,
10811            fec_data_shards: 16,
10812            fec_parity_shards: 16,
10813            index_fec_data_shards: 4,
10814            index_fec_parity_shards: 4,
10815            index_root_fec_data_shards: 16,
10816            index_root_fec_parity_shards: 16,
10817            ..WriterOptions::default()
10818        }
10819    }
10820
10821    fn pseudo_random_bytes(len: usize) -> Vec<u8> {
10822        let mut state = 0x1234_5678u32;
10823        (0..len)
10824            .map(|_| {
10825                state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10826                (state >> 24) as u8
10827            })
10828            .collect()
10829    }
10830
10831    #[test]
10832    fn opens_lists_verifies_and_extracts_one_file_archive() {
10833        let archive = write_archive(
10834            &[RegularFile::new("dir/hello.txt", b"hello m7")],
10835            &master_key(),
10836            single_stream_options(),
10837        )
10838        .unwrap();
10839        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10840
10841        assert_eq!(
10842            opened.list_files().unwrap(),
10843            vec![ArchiveEntry {
10844                path: "dir/hello.txt".to_string(),
10845                file_data_size: 8,
10846                kind: TarEntryKind::Regular,
10847                mode: 0o644,
10848                mtime: ArchiveTimestamp::UNIX_EPOCH,
10849                diagnostics: Vec::new(),
10850            }]
10851        );
10852        opened.verify().unwrap();
10853        assert_eq!(
10854            opened.extract_file("dir/hello.txt").unwrap(),
10855            Some(b"hello m7".to_vec())
10856        );
10857        assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
10858    }
10859
10860    #[test]
10861    fn root_auth_archive_round_trips_and_verifies_with_callback() {
10862        let archive = write_archive_with_root_auth(
10863            &[RegularFile::new("signed.txt", b"root-auth payload")],
10864            &master_key(),
10865            single_stream_options(),
10866            RootAuthWriterConfig {
10867                authenticator_id: 0x7777,
10868                signer_identity_type: 1,
10869                signer_identity: b"test signer",
10870                authenticator_value_length: 32,
10871            },
10872            |request| Ok(request.archive_root.to_vec()),
10873        )
10874        .unwrap();
10875
10876        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10877        opened.verify().unwrap();
10878        let verified = opened
10879            .verify_root_auth_with(|footer, archive_root| {
10880                Ok(footer.authenticator_value == archive_root.as_slice())
10881            })
10882            .unwrap();
10883
10884        assert_eq!(verified.authenticator_id, 0x7777);
10885        assert_eq!(verified.signer_identity_type, 1);
10886        assert_eq!(verified.signer_identity_bytes, b"test signer");
10887        assert_eq!(
10888            verified.archive_root,
10889            opened.root_auth_footer.as_ref().unwrap().archive_root
10890        );
10891        assert_eq!(
10892            verified.diagnostics,
10893            vec![
10894                RootAuthDiagnostic::RootAuthContentVerified,
10895                RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
10896                RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
10897                RootAuthDiagnostic::RecoveryMarginUnchecked,
10898            ]
10899        );
10900    }
10901
10902    #[test]
10903    fn root_auth_rejects_fast_content_verification_token() {
10904        let archive = write_archive_with_root_auth(
10905            &[RegularFile::new("signed.txt", b"root-auth payload")],
10906            &master_key(),
10907            single_stream_options(),
10908            RootAuthWriterConfig {
10909                authenticator_id: 0x7777,
10910                signer_identity_type: 1,
10911                signer_identity: b"test signer",
10912                authenticator_value_length: 32,
10913            },
10914            |request| Ok(request.archive_root.to_vec()),
10915        )
10916        .unwrap();
10917
10918        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10919        let content_verification = opened.verify_content_fast().unwrap();
10920        assert_eq!(
10921            opened
10922                .verify_root_auth_with_verified_content(&content_verification, |_, _| Ok(true))
10923                .unwrap_err(),
10924            FormatError::ReaderUnsupported(
10925                "RootAuth verification requires full archive content verification"
10926            )
10927        );
10928    }
10929
10930    #[test]
10931    fn root_auth_verification_requires_authenticator_success() {
10932        let archive = write_archive_with_root_auth(
10933            &[RegularFile::new("signed.txt", b"root-auth payload")],
10934            &master_key(),
10935            single_stream_options(),
10936            RootAuthWriterConfig {
10937                authenticator_id: 9,
10938                signer_identity_type: 1,
10939                signer_identity: b"test signer",
10940                authenticator_value_length: 32,
10941            },
10942            |request| Ok(request.archive_root.to_vec()),
10943        )
10944        .unwrap();
10945        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10946
10947        assert_eq!(
10948            opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
10949            FormatError::InvalidArchive("root-auth authenticator verification failed")
10950        );
10951    }
10952
10953    #[test]
10954    fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
10955        let archive = write_archive_with_root_auth(
10956            &[RegularFile::new("public.txt", b"public commitment")],
10957            &master_key(),
10958            single_stream_options(),
10959            RootAuthWriterConfig {
10960                authenticator_id: 0x2222,
10961                signer_identity_type: 1,
10962                signer_identity: b"public verifier",
10963                authenticator_value_length: 32,
10964            },
10965            |request| Ok(request.archive_root.to_vec()),
10966        )
10967        .unwrap();
10968
10969        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
10970            Ok(footer.authenticator_value == archive_root.as_slice())
10971        })
10972        .unwrap();
10973
10974        assert_eq!(verified.authenticator_id, 0x2222);
10975        assert_eq!(verified.signer_identity_bytes, b"public verifier");
10976        assert!(verified.total_data_block_count > 0);
10977    }
10978
10979    #[test]
10980    fn public_no_key_verifier_not_invoked_for_future_revision() {
10981        let archive = write_archive_with_root_auth(
10982            &[RegularFile::new("public.txt", b"public callback")],
10983            &master_key(),
10984            single_stream_options(),
10985            RootAuthWriterConfig {
10986                authenticator_id: 0x2222,
10987                signer_identity_type: 1,
10988                signer_identity: b"public verifier",
10989                authenticator_value_length: 32,
10990            },
10991            |request| Ok(request.archive_root.to_vec()),
10992        )
10993        .unwrap();
10994        let mut bytes = archive.bytes;
10995        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10996        header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
10997        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10998
10999        let mut called = false;
11000        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11001            called = true;
11002            Ok(true)
11003        })
11004        .unwrap_err();
11005
11006        assert!(!called);
11007        assert_eq!(
11008            err,
11009            FormatError::UnsupportedVolumeFormatRevision {
11010                format_version: FORMAT_VERSION,
11011                volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
11012                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11013            }
11014        );
11015    }
11016
11017    #[test]
11018    fn public_no_key_rejects_public_header_revision_mismatch() {
11019        let archive = write_archive_with_root_auth(
11020            &[RegularFile::new("public.txt", b"public v45 only")],
11021            &master_key(),
11022            single_stream_options(),
11023            RootAuthWriterConfig {
11024                authenticator_id: 0x2222,
11025                signer_identity_type: 1,
11026                signer_identity: b"public verifier",
11027                authenticator_value_length: 32,
11028            },
11029            |request| Ok(request.archive_root.to_vec()),
11030        )
11031        .unwrap();
11032        let mut bytes = archive.bytes;
11033        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
11034        header.volume_format_rev = 43;
11035        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11036
11037        let mut called = false;
11038        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11039            called = true;
11040            Ok(true)
11041        })
11042        .unwrap_err();
11043
11044        assert!(!called);
11045        assert_eq!(
11046            err,
11047            FormatError::UnsupportedVolumeFormatRevision {
11048                format_version: FORMAT_VERSION,
11049                volume_format_rev: 43,
11050                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11051            }
11052        );
11053    }
11054
11055    #[test]
11056    fn public_no_key_rejects_recovered_footer_revision_mismatch() {
11057        let archive = write_archive_with_root_auth(
11058            &[RegularFile::new("public.txt", b"public footer mismatch")],
11059            &master_key(),
11060            single_stream_options(),
11061            RootAuthWriterConfig {
11062                authenticator_id: 0x2222,
11063                signer_identity_type: 1,
11064                signer_identity: b"public verifier",
11065                authenticator_value_length: 32,
11066            },
11067            |request| Ok(request.archive_root.to_vec()),
11068        )
11069        .unwrap();
11070        let mut bytes = archive.bytes;
11071        rewrite_public_cmra_image(&mut bytes, |image| {
11072            let root_auth_region = image
11073                .regions
11074                .iter_mut()
11075                .find(|region| region.region_type == 4)
11076                .unwrap();
11077            rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
11078        });
11079
11080        let mut called = false;
11081        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11082            called = true;
11083            Ok(true)
11084        })
11085        .unwrap_err();
11086
11087        assert!(!called);
11088        assert_eq!(
11089            err,
11090            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11091        );
11092    }
11093
11094    #[test]
11095    fn public_no_key_rejects_recovered_image_with_unknown_layout_flags() {
11096        let archive = write_archive_with_root_auth(
11097            &[RegularFile::new("public.txt", b"public image mismatch")],
11098            &master_key(),
11099            single_stream_options(),
11100            RootAuthWriterConfig {
11101                authenticator_id: 0x2222,
11102                signer_identity_type: 1,
11103                signer_identity: b"public verifier",
11104                authenticator_value_length: 32,
11105            },
11106            |request| Ok(request.archive_root.to_vec()),
11107        )
11108        .unwrap();
11109        let bytes = rewrite_cmra_image_variable_len(
11110            &archive.bytes,
11111            CmraRecoveryMode::PublicNoKey,
11112            |image| {
11113                image.layout_flags |= 0x8000_0000;
11114            },
11115        );
11116
11117        let mut called = false;
11118        let err = public_no_key_verify_archive_with(&bytes, |_, _| {
11119            called = true;
11120            Ok(true)
11121        })
11122        .unwrap_err();
11123
11124        assert!(!called);
11125        assert_eq!(
11126            err,
11127            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11128        );
11129    }
11130
11131    #[test]
11132    fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
11133        let archive = write_archive_with_root_auth(
11134            &[RegularFile::new(
11135                "public-fields.txt",
11136                b"public source authority",
11137            )],
11138            &master_key(),
11139            single_stream_options(),
11140            RootAuthWriterConfig {
11141                authenticator_id: 0x2222,
11142                signer_identity_type: 1,
11143                signer_identity: b"public verifier",
11144                authenticator_value_length: 32,
11145            },
11146            |request| Ok(request.archive_root.to_vec()),
11147        )
11148        .unwrap();
11149        let mut bytes = archive.bytes.clone();
11150
11151        rewrite_public_cmra_image(&mut bytes, |image| {
11152            let manifest_region = image
11153                .regions
11154                .iter_mut()
11155                .find(|region| region.region_type == 3)
11156                .unwrap();
11157            manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
11158
11159            let trailer_region = image
11160                .regions
11161                .iter_mut()
11162                .find(|region| region.region_type == 5)
11163                .unwrap();
11164            let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
11165            trailer.block_count += 7;
11166            trailer_region.bytes = trailer.to_bytes().to_vec();
11167        });
11168
11169        public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
11170            Ok(footer.authenticator_value == archive_root.as_slice())
11171        })
11172        .unwrap();
11173    }
11174
11175    #[test]
11176    fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
11177        let archive = write_archive_with_root_auth(
11178            &[RegularFile::new(
11179                "public-crypto.txt",
11180                b"cross-volume public profile",
11181            )],
11182            &master_key(),
11183            WriterOptions {
11184                stripe_width: 2,
11185                volume_loss_tolerance: 0,
11186                ..WriterOptions::default()
11187            },
11188            RootAuthWriterConfig {
11189                authenticator_id: 0x3333,
11190                signer_identity_type: 1,
11191                signer_identity: b"public verifier",
11192                authenticator_value_length: 32,
11193            },
11194            |request| Ok(request.archive_root.to_vec()),
11195        )
11196        .unwrap();
11197        let mut volumes = archive.volumes.clone();
11198        let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
11199        let crypto_offset = volume_header.crypto_header_offset as usize;
11200        let expected_volume_size = 123_456_789u64;
11201        volumes[1][crypto_offset + 52..crypto_offset + 60]
11202            .copy_from_slice(&expected_volume_size.to_le_bytes());
11203        rewrite_public_cmra_image(&mut volumes[1], |image| {
11204            let crypto_region = image
11205                .regions
11206                .iter_mut()
11207                .find(|region| region.region_type == 2)
11208                .unwrap();
11209            crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
11210        });
11211
11212        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11213        public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
11214            Ok(footer.authenticator_value == archive_root.as_slice())
11215        })
11216        .unwrap();
11217    }
11218
11219    #[test]
11220    fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
11221        let archive = write_archive_with_root_auth(
11222            &[RegularFile::new("cmra-header.txt", b"header fallback")],
11223            &master_key(),
11224            single_stream_options(),
11225            RootAuthWriterConfig {
11226                authenticator_id: 0x4444,
11227                signer_identity_type: 1,
11228                signer_identity: b"public verifier",
11229                authenticator_value_length: 32,
11230            },
11231            |request| Ok(request.archive_root.to_vec()),
11232        )
11233        .unwrap();
11234        let final_locator = final_recovery_locator(&archive.bytes);
11235
11236        let mut bad_crc = archive.bytes.clone();
11237        let crc_offset =
11238            final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
11239        bad_crc[crc_offset] ^= 0x55;
11240        public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
11241            Ok(footer.authenticator_value == archive_root.as_slice())
11242        })
11243        .unwrap();
11244
11245        let mut bad_magic = archive.bytes.clone();
11246        bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
11247        public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
11248            Ok(footer.authenticator_value == archive_root.as_slice())
11249        })
11250        .unwrap();
11251
11252        let mut bad_hint = archive.bytes.clone();
11253        bad_hint[crc_offset] ^= 0xAA;
11254        for offset in [
11255            bad_hint.len() - LOCATOR_PAIR_LEN,
11256            bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
11257        ] {
11258            let mut locator = CriticalRecoveryLocator::parse(
11259                &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11260            )
11261            .unwrap();
11262            locator.volume_index_hint += 1;
11263            bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
11264                .copy_from_slice(&locator.to_bytes());
11265        }
11266        assert_eq!(
11267            public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
11268            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11269        );
11270    }
11271
11272    #[test]
11273    fn recovers_physical_volume_header_magic_from_cmra_authority() {
11274        let payload = b"front header authority".to_vec();
11275        let archive = write_archive(
11276            &[RegularFile::new("volume-header.txt", &payload)],
11277            &master_key(),
11278            small_block_recovery_options(),
11279        )
11280        .unwrap();
11281
11282        let mut corrupted = archive.bytes;
11283        corrupted[0] ^= 0x55;
11284
11285        let opened = open_archive(&corrupted, &master_key()).unwrap();
11286        assert_eq!(
11287            opened.extract_file("volume-header.txt").unwrap(),
11288            Some(payload)
11289        );
11290        opened.verify().unwrap();
11291    }
11292
11293    #[test]
11294    fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11295        let payload = b"crc-valid wrong volume index".to_vec();
11296        let mut options = small_block_recovery_options();
11297        options.stripe_width = 2;
11298        options.volume_loss_tolerance = 0;
11299        let archive = write_archive(
11300            &[RegularFile::new("volume-index.txt", &payload)],
11301            &master_key(),
11302            options,
11303        )
11304        .unwrap();
11305
11306        let mut corrupted = archive.volumes[0].clone();
11307        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11308        assert_eq!(header.volume_index, 0);
11309        header.volume_index = 1;
11310        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11311        assert_eq!(
11312            VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
11313                .unwrap()
11314                .volume_index,
11315            1
11316        );
11317
11318        let opened = open_archive_volumes(
11319            &[corrupted.as_slice(), archive.volumes[1].as_slice()],
11320            &master_key(),
11321        )
11322        .unwrap();
11323        assert_eq!(opened.volume_header.volume_index, 0);
11324        assert_eq!(
11325            opened.extract_file("volume-index.txt").unwrap(),
11326            Some(payload)
11327        );
11328        opened.verify().unwrap();
11329    }
11330
11331    #[test]
11332    fn recovers_physical_crypto_header_magic_from_cmra_authority() {
11333        let payload = b"crypto header authority".to_vec();
11334        let archive = write_archive(
11335            &[RegularFile::new("crypto-header.txt", &payload)],
11336            &master_key(),
11337            small_block_recovery_options(),
11338        )
11339        .unwrap();
11340        let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
11341            .unwrap()
11342            .crypto_header_offset;
11343
11344        let mut corrupted = archive.bytes;
11345        corrupted[crypto_offset as usize] ^= 0x55;
11346
11347        let opened = open_archive(&corrupted, &master_key()).unwrap();
11348        assert_eq!(
11349            opened.extract_file("crypto-header.txt").unwrap(),
11350            Some(payload)
11351        );
11352        opened.verify().unwrap();
11353    }
11354
11355    #[test]
11356    fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
11357        let payload = b"read-at header authority".to_vec();
11358        let archive = write_archive(
11359            &[RegularFile::new("read-at-header.txt", &payload)],
11360            &master_key(),
11361            small_block_recovery_options(),
11362        )
11363        .unwrap();
11364
11365        let mut corrupted = archive.bytes;
11366        corrupted[0] ^= 0x55;
11367
11368        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
11369        assert_eq!(
11370            opened.extract_file("read-at-header.txt").unwrap(),
11371            Some(payload)
11372        );
11373        opened.verify().unwrap();
11374    }
11375
11376    #[test]
11377    fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11378        let payload = b"read-at crc-valid wrong volume index".to_vec();
11379        let mut options = small_block_recovery_options();
11380        options.stripe_width = 2;
11381        options.volume_loss_tolerance = 0;
11382        let archive = write_archive(
11383            &[RegularFile::new("read-at-volume-index.txt", &payload)],
11384            &master_key(),
11385            options,
11386        )
11387        .unwrap();
11388
11389        let mut corrupted = archive.volumes[0].clone();
11390        let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11391        assert_eq!(header.volume_index, 0);
11392        header.volume_index = 1;
11393        corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11394
11395        let opened = open_seekable_archive_volumes(
11396            vec![corrupted, archive.volumes[1].clone()],
11397            &master_key(),
11398        )
11399        .unwrap();
11400        assert_eq!(opened.volume_header.volume_index, 0);
11401        assert_eq!(
11402            opened.extract_file("read-at-volume-index.txt").unwrap(),
11403            Some(payload)
11404        );
11405        opened.verify().unwrap();
11406    }
11407
11408    #[test]
11409    fn recovers_cmra_header_magic_from_locator_tuple() {
11410        let payload = b"cmra header authority".to_vec();
11411        let archive = write_archive(
11412            &[RegularFile::new("cmra-header-magic.txt", &payload)],
11413            &master_key(),
11414            small_block_recovery_options(),
11415        )
11416        .unwrap();
11417        let locator = final_recovery_locator(&archive.bytes);
11418
11419        let mut corrupted = archive.bytes;
11420        corrupted[locator.cmra_offset as usize] ^= 0x55;
11421
11422        let opened = open_archive(&corrupted, &master_key()).unwrap();
11423        assert_eq!(
11424            opened.extract_file("cmra-header-magic.txt").unwrap(),
11425            Some(payload)
11426        );
11427        opened.verify().unwrap();
11428    }
11429
11430    #[test]
11431    fn recovers_cmra_shard_magic_as_erasure() {
11432        let payload = b"cmra shard authority".to_vec();
11433        let archive = write_archive(
11434            &[RegularFile::new("cmra-shard-magic.txt", &payload)],
11435            &master_key(),
11436            small_block_recovery_options(),
11437        )
11438        .unwrap();
11439        let locator = final_recovery_locator(&archive.bytes);
11440        let first_shard_offset =
11441            locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11442
11443        let mut corrupted = archive.bytes;
11444        corrupted[first_shard_offset] ^= 0x55;
11445
11446        let opened = open_archive(&corrupted, &master_key()).unwrap();
11447        assert_eq!(
11448            opened.extract_file("cmra-shard-magic.txt").unwrap(),
11449            Some(payload)
11450        );
11451        opened.verify().unwrap();
11452    }
11453
11454    #[test]
11455    fn key_holding_rejects_recovered_image_with_unknown_layout_flags() {
11456        let archive = write_archive(
11457            &[RegularFile::new("cmra-image-revision.txt", b"payload")],
11458            &master_key(),
11459            small_block_recovery_options(),
11460        )
11461        .unwrap();
11462        let mut mutated = rewrite_cmra_image_variable_len(
11463            &archive.bytes,
11464            CmraRecoveryMode::KeyHolding,
11465            |image| {
11466                image.layout_flags |= 0x8000_0000;
11467            },
11468        );
11469        mutated[0] ^= 0x55;
11470
11471        assert!(open_archive(&mutated, &master_key()).is_err());
11472    }
11473
11474    #[test]
11475    fn key_holding_rejects_recovered_footer_revision_mismatch() {
11476        let archive = write_archive_with_root_auth(
11477            &[RegularFile::new("cmra-footer-revision.txt", b"payload")],
11478            &master_key(),
11479            single_stream_options(),
11480            test_root_auth_config(),
11481            |request| Ok(test_root_auth_value(request)),
11482        )
11483        .unwrap();
11484        let mut mutated = archive.bytes;
11485        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11486            let root_auth_region = image
11487                .regions
11488                .iter_mut()
11489                .find(|region| region.region_type == 4)
11490                .unwrap();
11491            rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
11492        });
11493        mutated[0] ^= 0x55;
11494
11495        assert!(open_archive(&mutated, &master_key()).is_err());
11496    }
11497
11498    #[test]
11499    fn key_holding_rejects_locator_image_revision_mismatch() {
11500        let archive = write_archive(
11501            &[RegularFile::new("cmra-locator-revision.txt", b"payload")],
11502            &master_key(),
11503            small_block_recovery_options(),
11504        )
11505        .unwrap();
11506        let mut mutated = archive.bytes;
11507        let locator = final_recovery_locator(&mutated);
11508        let mirror_offset = mutated.len() - LOCATOR_PAIR_LEN;
11509        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11510        for offset in [mirror_offset, final_offset] {
11511            rewrite_recovery_locator(&mut mutated, offset, |locator| {
11512                locator.volume_format_rev = 43;
11513            });
11514        }
11515        mutated[0] ^= 0x55;
11516        mutated[locator.cmra_offset as usize] ^= 0x55;
11517
11518        assert!(open_archive(&mutated, &master_key()).is_err());
11519    }
11520
11521    #[test]
11522    fn image_identity_allows_matching_current_revision() {
11523        let archive = write_archive(
11524            &[RegularFile::new("matching-v45.txt", b"payload")],
11525            &master_key(),
11526            single_stream_options(),
11527        )
11528        .unwrap();
11529        let locator = final_recovery_locator(&archive.bytes);
11530        let recovered = recover_cmra(
11531            &archive.bytes,
11532            locator.cmra_offset,
11533            Some(CmraDecoderTuple::from(locator)),
11534            CmraRecoveryMode::KeyHolding,
11535        )
11536        .unwrap();
11537        let image = recovered.image;
11538        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11539        let crypto_start = header.crypto_header_offset as usize;
11540        let crypto_end = crypto_start + header.crypto_header_length as usize;
11541        let crypto = CryptoHeader::parse(
11542            &archive.bytes[crypto_start..crypto_end],
11543            header.crypto_header_length,
11544        )
11545        .unwrap();
11546
11547        validate_image_identity(&image, &header, &crypto.fixed).unwrap();
11548    }
11549
11550    #[test]
11551    fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
11552        let archive = write_archive(
11553            &[RegularFile::new(
11554                "cmra-floor.txt",
11555                b"authenticated CMRA floor",
11556            )],
11557            &master_key(),
11558            single_stream_options(),
11559        )
11560        .unwrap();
11561        let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
11562        let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11563        let locator = CriticalRecoveryLocator::parse(
11564            &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11565        )
11566        .unwrap();
11567        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11568        let crypto_start = volume_header.crypto_header_offset as usize;
11569        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11570        let crypto_header = CryptoHeader::parse(
11571            &malformed[crypto_start..crypto_end],
11572            volume_header.crypto_header_length,
11573        )
11574        .unwrap();
11575        let subkeys = Subkeys::derive(
11576            &master_key(),
11577            &volume_header.archive_uuid,
11578            &volume_header.session_id,
11579        )
11580        .unwrap();
11581
11582        assert_eq!(
11583            parse_locator_cmra_candidate(
11584                &malformed,
11585                final_offset,
11586                locator,
11587                KeyHoldingTerminalContext {
11588                    subkeys: &subkeys,
11589                    volume_header: &volume_header,
11590                    crypto_header: &crypto_header.fixed,
11591                    crypto_header_bytes: &malformed[crypto_start..crypto_end],
11592                },
11593            )
11594            .unwrap_err(),
11595            FormatError::InvalidArchive(
11596                "CMRA parity shard count is below authenticated bit-rot lower bound"
11597            )
11598        );
11599        assert!(open_archive(&malformed, &master_key()).is_err());
11600    }
11601
11602    #[test]
11603    fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
11604        let archive = write_archive(
11605            &[RegularFile::new(
11606                "locator-order.txt",
11607                b"locator tuple first",
11608            )],
11609            &master_key(),
11610            single_stream_options(),
11611        )
11612        .unwrap();
11613        let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11614        let mut locator = final_recovery_locator(&archive.bytes);
11615        locator.cmra_shard_size = 513;
11616        locator.body_bytes_before_cmra = locator.cmra_offset + 1;
11617        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11618        let crypto_start = volume_header.crypto_header_offset as usize;
11619        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11620        let crypto_header = CryptoHeader::parse(
11621            &archive.bytes[crypto_start..crypto_end],
11622            volume_header.crypto_header_length,
11623        )
11624        .unwrap();
11625        let subkeys = Subkeys::derive(
11626            &master_key(),
11627            &volume_header.archive_uuid,
11628            &volume_header.session_id,
11629        )
11630        .unwrap();
11631
11632        assert_eq!(
11633            parse_locator_cmra_candidate(
11634                &archive.bytes,
11635                final_offset,
11636                locator,
11637                KeyHoldingTerminalContext {
11638                    subkeys: &subkeys,
11639                    volume_header: &volume_header,
11640                    crypto_header: &crypto_header.fixed,
11641                    crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
11642                },
11643            )
11644            .unwrap_err(),
11645            FormatError::InvalidArchive("CMRA shard_size is invalid")
11646        );
11647    }
11648
11649    #[test]
11650    fn sequential_extract_rejects_bytes_after_terminal_locator() {
11651        let archive = write_archive(
11652            &[RegularFile::new("seq.txt", b"sequential EOF")],
11653            &master_key(),
11654            single_stream_options(),
11655        )
11656        .unwrap();
11657        let mut appended = archive.bytes.clone();
11658        appended.extend_from_slice(&[0xAA; 32]);
11659
11660        assert_eq!(
11661            sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
11662            FormatError::InvalidArchive("sequential terminal does not end at EOF")
11663        );
11664    }
11665
11666    #[test]
11667    fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
11668        let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
11669        let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
11670
11671        assert_eq!(
11672            validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
11673            FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
11674        );
11675    }
11676
11677    #[test]
11678    fn root_auth_verifies_key_holding_and_public_no_key_modes() {
11679        let archive = write_archive_with_root_auth(
11680            &[RegularFile::new("signed.txt", b"ed25519 payload")],
11681            &master_key(),
11682            single_stream_options(),
11683            test_root_auth_config(),
11684            |request| Ok(test_root_auth_value(request)),
11685        )
11686        .unwrap();
11687
11688        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11689        let root_auth = opened
11690            .verify_root_auth_with(|footer, archive_root| {
11691                Ok(test_root_auth_verifies(footer, archive_root))
11692            })
11693            .unwrap();
11694        assert_eq!(
11695            root_auth.archive_root,
11696            opened.root_auth_footer.as_ref().unwrap().archive_root
11697        );
11698
11699        let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
11700            Ok(test_root_auth_verifies(footer, archive_root))
11701        })
11702        .unwrap();
11703        assert_eq!(public.archive_root, root_auth.archive_root);
11704        assert_eq!(
11705            public.diagnostics,
11706            vec![
11707                PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
11708                PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
11709                PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
11710            ]
11711        );
11712    }
11713
11714    #[test]
11715    fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
11716        let options = WriterOptions {
11717            block_size: 4096,
11718            chunk_size: 16 * 1024,
11719            envelope_target_size: 16 * 1024,
11720            stripe_width: 2,
11721            volume_loss_tolerance: 1,
11722            bit_rot_buffer_pct: 0,
11723            fec_data_shards: 16,
11724            fec_parity_shards: 1,
11725            index_fec_data_shards: 4,
11726            index_fec_parity_shards: 1,
11727            index_root_fec_data_shards: 16,
11728            index_root_fec_parity_shards: 1,
11729            ..WriterOptions::default()
11730        };
11731        let archive = write_archive_with_root_auth(
11732            &[RegularFile::new("missing-volume.txt", b"recover me")],
11733            &master_key(),
11734            options,
11735            test_root_auth_config(),
11736            |request| Ok(test_root_auth_value(request)),
11737        )
11738        .unwrap();
11739
11740        let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
11741        let root_auth = opened
11742            .verify_root_auth_with(|footer, archive_root| {
11743                Ok(test_root_auth_verifies(footer, archive_root))
11744            })
11745            .unwrap();
11746        assert!(root_auth
11747            .diagnostics
11748            .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
11749    }
11750
11751    #[test]
11752    fn public_no_key_rejects_unsigned_archives() {
11753        let archive = write_archive(
11754            &[RegularFile::new("plain.txt", b"unsigned")],
11755            &master_key(),
11756            single_stream_options(),
11757        )
11758        .unwrap();
11759
11760        assert_eq!(
11761            public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
11762            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11763        );
11764    }
11765
11766    #[test]
11767    fn unsigned_archive_reports_root_auth_absent() {
11768        let archive = write_archive(
11769            &[RegularFile::new("plain.txt", b"unsigned")],
11770            &master_key(),
11771            single_stream_options(),
11772        )
11773        .unwrap();
11774        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11775
11776        assert_eq!(
11777            opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
11778            FormatError::ReaderUnsupported("root-auth footer is absent")
11779        );
11780    }
11781
11782    #[test]
11783    fn safe_extract_writes_regular_file_under_root() {
11784        let archive = write_archive(
11785            &[RegularFile::new("dir/hello.txt", b"safe m8")],
11786            &master_key(),
11787            single_stream_options(),
11788        )
11789        .unwrap();
11790        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11791        let tmp = tempfile::tempdir().unwrap();
11792
11793        opened
11794            .extract_file_to(
11795                "dir/hello.txt",
11796                tmp.path(),
11797                SafeExtractionOptions::default(),
11798            )
11799            .unwrap()
11800            .unwrap();
11801
11802        assert_eq!(
11803            std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
11804            b"safe m8"
11805        );
11806    }
11807
11808    fn decoded_primary_metadata_storage(
11809        opened: &OpenedArchive,
11810        path: &str,
11811    ) -> (crate::entry_metadata::PaxRecords, [u8; 512]) {
11812        let located = opened.locate_index_file(path.as_bytes()).unwrap().unwrap();
11813        let file = &located.shard.files[located.file_index];
11814        let mut reader = DecodedTarMemberGroupReader::new(opened, &located.shard, file).unwrap();
11815        let mut group = vec![0u8; usize::try_from(file.tar_member_group_size).unwrap()];
11816        let mut offset = 0usize;
11817        while offset < group.len() {
11818            let read = reader.read_some_member_bytes(&mut group[offset..]).unwrap();
11819            assert_ne!(read, 0, "decoded member group ended early");
11820            offset += read;
11821        }
11822
11823        assert_eq!(group[156], b'x');
11824        let pax_size = usize::try_from(parse_test_tar_octal(&group[124..136])).unwrap();
11825        let pax_end = 512 + pax_size;
11826        let records = crate::entry_metadata::parse_canonical_pax(&group[512..pax_end]).unwrap();
11827        let primary_offset = pax_end + padding_to_512(pax_size);
11828        let primary = group[primary_offset..primary_offset + 512]
11829            .try_into()
11830            .unwrap();
11831        (records, primary)
11832    }
11833
11834    fn parse_test_tar_octal(field: &[u8]) -> u64 {
11835        let field = nul_trimmed_test_field(field);
11836        let text = std::str::from_utf8(field).unwrap().trim();
11837        u64::from_str_radix(text, 8).unwrap()
11838    }
11839
11840    fn nul_trimmed_test_field(field: &[u8]) -> &[u8] {
11841        let end = field
11842            .iter()
11843            .position(|byte| *byte == 0)
11844            .unwrap_or(field.len());
11845        &field[..end]
11846    }
11847
11848    #[test]
11849    fn compressed_archive_round_trips_header_and_pax_portable_metadata_stores() {
11850        let files = [
11851            RegularFile {
11852                mode: 0o640,
11853                mtime: ArchiveTimestamp::from_seconds(1_700_000_000),
11854                portable_metadata: PortableFileMetadata {
11855                    source_os: "other-unix".into(),
11856                    source_filesystem: "ext4".into(),
11857                    mode_origin: PortableModeOrigin::Native,
11858                    posix_owner: Some(PortablePosixOwner {
11859                        uid: 42,
11860                        gid: 84,
11861                        uname: Some("alice".into()),
11862                        gname: Some("staff".into()),
11863                    }),
11864                    attributes: Some(1),
11865                    native: Default::default(),
11866                },
11867                ..RegularFile::new("header-fields.txt", b"header metadata")
11868            },
11869            RegularFile {
11870                mode: 0o604,
11871                mtime: ArchiveTimestamp::new(-1, 500_000_000),
11872                portable_metadata: PortableFileMetadata {
11873                    source_os: "other-unix".into(),
11874                    source_filesystem: "zfs".into(),
11875                    mode_origin: PortableModeOrigin::Native,
11876                    posix_owner: Some(PortablePosixOwner {
11877                        uid: 9_000_000,
11878                        gid: 8_000_000,
11879                        uname: Some("u".repeat(40)),
11880                        gname: Some("g".repeat(40)),
11881                    }),
11882                    attributes: Some(2),
11883                    native: Default::default(),
11884                },
11885                ..RegularFile::new("pax-overrides.txt", b"PAX metadata")
11886            },
11887        ];
11888        let archive = write_archive(&files, &master_key(), single_stream_options()).unwrap();
11889        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11890
11891        assert_eq!(
11892            opened.crypto_header.compression_algo,
11893            CompressionAlgo::ZstdFramed
11894        );
11895
11896        let (header_records, header_primary) =
11897            decoded_primary_metadata_storage(&opened, "header-fields.txt");
11898        for key in ["uid", "gid", "uname", "gname", "mtime"] {
11899            assert!(!header_records.contains_key(key), "{key} should use ustar");
11900        }
11901        assert_eq!(parse_test_tar_octal(&header_primary[108..116]), 42);
11902        assert_eq!(parse_test_tar_octal(&header_primary[116..124]), 84);
11903        assert_eq!(nul_trimmed_test_field(&header_primary[265..297]), b"alice");
11904        assert_eq!(nul_trimmed_test_field(&header_primary[297..329]), b"staff");
11905        assert_eq!(
11906            parse_test_tar_octal(&header_primary[136..148]),
11907            1_700_000_000
11908        );
11909
11910        let (pax_records, pax_primary) =
11911            decoded_primary_metadata_storage(&opened, "pax-overrides.txt");
11912        let long_uname = "u".repeat(40);
11913        let long_gname = "g".repeat(40);
11914        for (key, expected) in [
11915            ("uid", b"9000000".as_slice()),
11916            ("gid", b"8000000".as_slice()),
11917            ("uname", long_uname.as_bytes()),
11918            ("gname", long_gname.as_bytes()),
11919            ("mtime", b"-1.5".as_slice()),
11920        ] {
11921            assert_eq!(pax_records.get(key).map(Vec::as_slice), Some(expected));
11922        }
11923        assert_eq!(parse_test_tar_octal(&pax_primary[108..116]), 0);
11924        assert_eq!(parse_test_tar_octal(&pax_primary[116..124]), 0);
11925        assert!(nul_trimmed_test_field(&pax_primary[265..297]).is_empty());
11926        assert!(nul_trimmed_test_field(&pax_primary[297..329]).is_empty());
11927        assert_eq!(parse_test_tar_octal(&pax_primary[136..148]), 0);
11928
11929        for expected in &files {
11930            let index = opened.lookup_index_entry(expected.path).unwrap().unwrap();
11931            assert!(index.layout.compressed_size > 0);
11932            assert_eq!(
11933                index.flags,
11934                crate::entry_metadata::EXTENDED_METADATA_V1
11935                    | crate::entry_metadata::REQUIRES_SYSTEM_RESTORE
11936            );
11937
11938            let located = opened
11939                .locate_index_file(expected.path.as_bytes())
11940                .unwrap()
11941                .unwrap();
11942            let member = opened
11943                .decode_loaded_owned_tar_member(&located.shard, located.file_index, false)
11944                .unwrap();
11945            let metadata = member.v45_metadata.unwrap();
11946            let owner = expected.portable_metadata.posix_owner.as_ref().unwrap();
11947
11948            assert_eq!(member.mode, expected.mode);
11949            assert_eq!(member.mtime, expected.mtime);
11950            assert_eq!(
11951                metadata.file_entry_flags,
11952                crate::entry_metadata::EXTENDED_METADATA_V1
11953                    | crate::entry_metadata::REQUIRES_SYSTEM_RESTORE
11954            );
11955            assert_eq!(
11956                metadata.declaration.source_os,
11957                expected.portable_metadata.source_os
11958            );
11959            assert_eq!(
11960                metadata.declaration.source_filesystem,
11961                expected.portable_metadata.source_filesystem
11962            );
11963            assert!(metadata.declaration.mode_origin_native);
11964            assert_eq!(metadata.portable_mirror.mode, expected.mode);
11965            assert_eq!(
11966                metadata.portable_mirror.mtime,
11967                (expected.mtime.seconds, expected.mtime.nanoseconds)
11968            );
11969            assert_eq!(
11970                metadata.portable_mirror.attributes,
11971                expected.portable_metadata.attributes
11972            );
11973            assert_eq!(metadata.portable_mirror.uid, Some(owner.uid));
11974            assert_eq!(metadata.portable_mirror.gid, Some(owner.gid));
11975            assert_eq!(
11976                metadata.portable_mirror.uname.as_deref(),
11977                owner.uname.as_deref().map(str::as_bytes)
11978            );
11979            assert_eq!(
11980                metadata.portable_mirror.gname.as_deref(),
11981                owner.gname.as_deref().map(str::as_bytes)
11982            );
11983        }
11984    }
11985
11986    #[test]
11987    fn compressed_archive_extraction_applies_portable_mode_and_pax_mtime() {
11988        let archived_mtime = ArchiveTimestamp::new(946_684_800, 123_456_700);
11989        let archive = write_archive(
11990            &[RegularFile {
11991                mode: 0o604,
11992                mtime: archived_mtime,
11993                ..RegularFile::new("dated.txt", b"dated")
11994            }],
11995            &master_key(),
11996            single_stream_options(),
11997        )
11998        .unwrap();
11999        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12000        assert_eq!(
12001            opened
12002                .lookup_index_entry("dated.txt")
12003                .unwrap()
12004                .unwrap()
12005                .flags,
12006            crate::entry_metadata::EXTENDED_METADATA_V1
12007        );
12008        let content_root = tempfile::tempdir().unwrap();
12009
12010        let content_diagnostics = opened
12011            .extract_file_to(
12012                "dated.txt",
12013                content_root.path(),
12014                SafeExtractionOptions {
12015                    restore_policy: crate::entry_metadata::RestorePolicy::Content,
12016                    ..SafeExtractionOptions::default()
12017                },
12018            )
12019            .unwrap()
12020            .unwrap();
12021        for metadata_class in ["mode", "mtime"] {
12022            assert!(content_diagnostics.iter().any(|diagnostic| {
12023                diagnostic.metadata_class == metadata_class
12024                    && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12025            }));
12026        }
12027        let content_mtime = fs::metadata(content_root.path().join("dated.txt"))
12028            .unwrap()
12029            .modified()
12030            .unwrap();
12031        assert_ne!(
12032            content_mtime,
12033            std::time::UNIX_EPOCH
12034                + std::time::Duration::new(
12035                    archived_mtime.seconds as u64,
12036                    archived_mtime.nanoseconds,
12037                )
12038        );
12039
12040        let portable_root = tempfile::tempdir().unwrap();
12041        opened
12042            .extract_file_to(
12043                "dated.txt",
12044                portable_root.path(),
12045                SafeExtractionOptions::default(),
12046            )
12047            .unwrap()
12048            .unwrap();
12049        let portable_mtime = fs::metadata(portable_root.path().join("dated.txt"))
12050            .unwrap()
12051            .modified()
12052            .unwrap();
12053        assert_eq!(
12054            portable_mtime,
12055            std::time::UNIX_EPOCH
12056                + std::time::Duration::new(
12057                    archived_mtime.seconds as u64,
12058                    archived_mtime.nanoseconds,
12059                )
12060        );
12061        #[cfg(unix)]
12062        {
12063            use std::os::unix::fs::PermissionsExt;
12064
12065            assert_eq!(
12066                fs::metadata(portable_root.path().join("dated.txt"))
12067                    .unwrap()
12068                    .permissions()
12069                    .mode()
12070                    & 0o7777,
12071                0o604
12072            );
12073        }
12074    }
12075
12076    #[test]
12077    fn same_os_restore_rejects_required_native_profile_from_another_os() {
12078        let (source_os, required_profiles) = if cfg!(target_os = "macos") {
12079            (
12080                "linux",
12081                vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12082            )
12083        } else {
12084            (
12085                "macos",
12086                vec!["posix-backup-v1".into(), "macos-backup-v1".into()],
12087            )
12088        };
12089        let archive = write_archive(
12090            &[RegularFile {
12091                portable_metadata: PortableFileMetadata {
12092                    source_os: source_os.into(),
12093                    native: NativeFileMetadata {
12094                        required_profiles,
12095                        ..NativeFileMetadata::default()
12096                    },
12097                    ..PortableFileMetadata::default()
12098                },
12099                ..RegularFile::new("foreign-native.txt", b"payload")
12100            }],
12101            &master_key(),
12102            single_stream_options(),
12103        )
12104        .unwrap();
12105        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12106
12107        assert_eq!(
12108            opened
12109                .plan_metadata_restore(SafeExtractionOptions {
12110                    restore_policy: RestorePolicy::SameOs,
12111                    ..SafeExtractionOptions::default()
12112                })
12113                .unwrap_err(),
12114            FormatError::ReaderUnsupported(
12115                "requested native metadata is not supported by this conformance class"
12116            )
12117        );
12118    }
12119
12120    #[cfg(unix)]
12121    #[test]
12122    fn compressed_archive_extraction_restores_setid_bits_only_for_authorized_system_policy() {
12123        use std::os::unix::fs::{MetadataExt, PermissionsExt};
12124
12125        // SAFETY: these process identity getters have no preconditions.
12126        let uid = unsafe { libc::geteuid() } as u64;
12127        let gid = unsafe { libc::getegid() } as u64;
12128
12129        let archive = write_archive(
12130            &[RegularFile {
12131                mode: 0o7751,
12132                mtime: ArchiveTimestamp::from_seconds(1_700_000_000),
12133                portable_metadata: PortableFileMetadata {
12134                    source_os: "other-unix".into(),
12135                    source_filesystem: "unknown".into(),
12136                    mode_origin: PortableModeOrigin::Native,
12137                    posix_owner: Some(PortablePosixOwner {
12138                        uid,
12139                        gid,
12140                        uname: None,
12141                        gname: None,
12142                    }),
12143                    attributes: None,
12144                    native: Default::default(),
12145                },
12146                ..RegularFile::new("privileged.sh", b"#!/bin/sh\n")
12147            }],
12148            &master_key(),
12149            single_stream_options(),
12150        )
12151        .unwrap();
12152        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12153        assert_eq!(
12154            opened
12155                .lookup_index_entry("privileged.sh")
12156                .unwrap()
12157                .unwrap()
12158                .flags,
12159            crate::entry_metadata::EXTENDED_METADATA_V1
12160                | crate::entry_metadata::REQUIRES_SYSTEM_RESTORE
12161        );
12162
12163        let portable_root = tempfile::tempdir().unwrap();
12164        let portable_diagnostics = opened
12165            .extract_file_to(
12166                "privileged.sh",
12167                portable_root.path(),
12168                SafeExtractionOptions::default(),
12169            )
12170            .unwrap()
12171            .unwrap();
12172        assert!(portable_diagnostics.iter().any(|diagnostic| {
12173            diagnostic.metadata_class == "setid-mode"
12174                && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12175        }));
12176        assert!(portable_diagnostics.iter().any(|diagnostic| {
12177            diagnostic.metadata_class == "numeric-ownership"
12178                && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12179        }));
12180        assert_eq!(
12181            fs::metadata(portable_root.path().join("privileged.sh"))
12182                .unwrap()
12183                .permissions()
12184                .mode()
12185                & 0o7777,
12186            0o1751
12187        );
12188
12189        let same_os_root = tempfile::tempdir().unwrap();
12190        opened
12191            .extract_file_to(
12192                "privileged.sh",
12193                same_os_root.path(),
12194                SafeExtractionOptions {
12195                    restore_policy: crate::entry_metadata::RestorePolicy::SameOs,
12196                    ..SafeExtractionOptions::default()
12197                },
12198            )
12199            .unwrap()
12200            .unwrap();
12201        assert_eq!(
12202            fs::metadata(same_os_root.path().join("privileged.sh"))
12203                .unwrap()
12204                .permissions()
12205                .mode()
12206                & 0o7777,
12207            0o1751
12208        );
12209
12210        let unauthorized_root = tempfile::tempdir().unwrap();
12211        assert_eq!(
12212            opened
12213                .extract_file_to(
12214                    "privileged.sh",
12215                    unauthorized_root.path(),
12216                    SafeExtractionOptions {
12217                        restore_policy: crate::entry_metadata::RestorePolicy::System,
12218                        ..SafeExtractionOptions::default()
12219                    },
12220                )
12221                .unwrap_err(),
12222            FormatError::ReaderUnsupported(
12223                "system restore policy requires explicit caller authorization"
12224            )
12225        );
12226        assert!(!unauthorized_root.path().join("privileged.sh").exists());
12227
12228        let system_root = tempfile::tempdir().unwrap();
12229        opened
12230            .extract_file_to(
12231                "privileged.sh",
12232                system_root.path(),
12233                SafeExtractionOptions {
12234                    restore_policy: crate::entry_metadata::RestorePolicy::System,
12235                    system_authorized: true,
12236                    ..SafeExtractionOptions::default()
12237                },
12238            )
12239            .unwrap()
12240            .unwrap();
12241        assert_eq!(
12242            fs::metadata(system_root.path().join("privileged.sh"))
12243                .unwrap()
12244                .permissions()
12245                .mode()
12246                & 0o7777,
12247            0o7751
12248        );
12249        let restored = fs::metadata(system_root.path().join("privileged.sh")).unwrap();
12250        assert_eq!(restored.uid() as u64, uid);
12251        assert_eq!(restored.gid() as u64, gid);
12252    }
12253
12254    #[cfg(unix)]
12255    #[test]
12256    fn compressed_archive_extraction_restores_native_xattrs_only_under_native_policy() {
12257        let mut native = NativeFileMetadata {
12258            required_profiles: vec!["posix-backup-v1".into()],
12259            ..NativeFileMetadata::default()
12260        };
12261        native.primary_pax_records.insert(
12262            "LIBARCHIVE.xattr.user.tzap-test".into(),
12263            crate::entry_metadata::canonical_base64_encode(b"native value"),
12264        );
12265        let archive = write_archive(
12266            &[RegularFile {
12267                portable_metadata: PortableFileMetadata {
12268                    source_os: source_os_for_test().into(),
12269                    native,
12270                    ..PortableFileMetadata::default()
12271                },
12272                ..RegularFile::new("xattr.txt", b"payload")
12273            }],
12274            &master_key(),
12275            single_stream_options(),
12276        )
12277        .unwrap();
12278        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12279
12280        let portable_root = tempfile::tempdir().unwrap();
12281        opened
12282            .extract_file_to(
12283                "xattr.txt",
12284                portable_root.path(),
12285                SafeExtractionOptions {
12286                    restore_policy: RestorePolicy::Portable,
12287                    ..SafeExtractionOptions::default()
12288                },
12289            )
12290            .unwrap();
12291        assert_eq!(
12292            xattr::get(portable_root.path().join("xattr.txt"), "user.tzap-test").unwrap(),
12293            None
12294        );
12295
12296        let native_root = tempfile::tempdir().unwrap();
12297        opened
12298            .extract_file_to(
12299                "xattr.txt",
12300                native_root.path(),
12301                SafeExtractionOptions {
12302                    restore_policy: RestorePolicy::SameOs,
12303                    ..SafeExtractionOptions::default()
12304                },
12305            )
12306            .unwrap();
12307        assert_eq!(
12308            xattr::get(native_root.path().join("xattr.txt"), "user.tzap-test").unwrap(),
12309            Some(b"native value".to_vec())
12310        );
12311    }
12312
12313    #[cfg(target_os = "linux")]
12314    #[test]
12315    fn compressed_archive_extraction_restores_auxiliary_linux_xattr() {
12316        let mut record = NativeAuxiliaryMetadata::new(
12317            "generic.xattr",
12318            "posix-backup-v1",
12319            crate::entry_metadata::RestoreClass::SameOs,
12320            b"auxiliary xattr value".to_vec(),
12321        );
12322        record.name_encoding = NativeAuxiliaryNameEncoding::Bytes;
12323        record.name = b"user.tzap-aux".to_vec();
12324        let native = NativeFileMetadata {
12325            required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12326            auxiliary_records: vec![record],
12327            ..NativeFileMetadata::default()
12328        };
12329        let archive = write_archive(
12330            &[RegularFile {
12331                portable_metadata: PortableFileMetadata {
12332                    source_os: "linux".into(),
12333                    native,
12334                    ..PortableFileMetadata::default()
12335                },
12336                ..RegularFile::new("aux-xattr.txt", b"payload")
12337            }],
12338            &master_key(),
12339            single_stream_options(),
12340        )
12341        .unwrap();
12342        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12343        let root = tempfile::tempdir().unwrap();
12344        opened
12345            .extract_file_to(
12346                "aux-xattr.txt",
12347                root.path(),
12348                SafeExtractionOptions {
12349                    restore_policy: RestorePolicy::SameOs,
12350                    ..SafeExtractionOptions::default()
12351                },
12352            )
12353            .unwrap();
12354        assert_eq!(
12355            xattr::get(root.path().join("aux-xattr.txt"), "user.tzap-aux").unwrap(),
12356            Some(b"auxiliary xattr value".to_vec())
12357        );
12358    }
12359
12360    #[cfg(target_os = "linux")]
12361    #[test]
12362    fn same_os_restore_reports_system_xattr_as_policy_skipped() {
12363        let mut native = NativeFileMetadata {
12364            required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12365            ..NativeFileMetadata::default()
12366        };
12367        native.primary_pax_records.insert(
12368            "LIBARCHIVE.xattr.security.selinux".into(),
12369            crate::entry_metadata::canonical_base64_encode(b"label"),
12370        );
12371        let archive = write_archive(
12372            &[RegularFile {
12373                portable_metadata: PortableFileMetadata {
12374                    source_os: "linux".into(),
12375                    native,
12376                    ..PortableFileMetadata::default()
12377                },
12378                ..RegularFile::new("label.txt", b"payload")
12379            }],
12380            &master_key(),
12381            single_stream_options(),
12382        )
12383        .unwrap();
12384        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12385        let root = tempfile::tempdir().unwrap();
12386        let diagnostics = opened
12387            .extract_file_to(
12388                "label.txt",
12389                root.path(),
12390                SafeExtractionOptions {
12391                    restore_policy: RestorePolicy::SameOs,
12392                    ..SafeExtractionOptions::default()
12393                },
12394            )
12395            .unwrap()
12396            .unwrap();
12397        assert!(diagnostics.iter().any(|diagnostic| {
12398            diagnostic.metadata_class == "system-extended-attribute"
12399                && diagnostic.status == crate::tar_model::MetadataDiagnosticStatus::Skipped
12400        }));
12401        assert_eq!(
12402            xattr::get(root.path().join("label.txt"), "security.selinux").unwrap(),
12403            None
12404        );
12405    }
12406
12407    #[cfg(target_os = "linux")]
12408    fn source_os_for_test() -> &'static str {
12409        "linux"
12410    }
12411
12412    #[cfg(target_os = "macos")]
12413    fn source_os_for_test() -> &'static str {
12414        "macos"
12415    }
12416
12417    #[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))]
12418    fn source_os_for_test() -> &'static str {
12419        "other-unix"
12420    }
12421
12422    #[cfg(target_os = "linux")]
12423    #[test]
12424    fn compressed_archive_extraction_restores_linux_inode_flags() {
12425        use std::os::fd::AsRawFd;
12426
12427        let root = tempfile::tempdir().unwrap();
12428        let probe_path = root.path().join("flags-probe.txt");
12429        fs::write(&probe_path, b"probe").unwrap();
12430        let probe = fs::File::open(&probe_path).unwrap();
12431        let mut intrinsic_flags: libc::c_long = 0;
12432        assert_eq!(
12433            unsafe {
12434                libc::ioctl(
12435                    probe.as_raw_fd(),
12436                    libc::FS_IOC_GETFLAGS,
12437                    &mut intrinsic_flags,
12438                )
12439            },
12440            0
12441        );
12442        drop(probe);
12443        fs::remove_file(probe_path).unwrap();
12444        let expected_flags =
12445            intrinsic_flags as u64 | u64::from(linux_raw_sys::general::FS_NODUMP_FL);
12446        let mut native = NativeFileMetadata {
12447            required_profiles: vec!["posix-backup-v1".into(), "linux-backup-v1".into()],
12448            ..NativeFileMetadata::default()
12449        };
12450        native.primary_pax_records.insert(
12451            "TZAP.linux.fsflags".into(),
12452            format!("{expected_flags:016x}").into_bytes(),
12453        );
12454        let archive = write_archive(
12455            &[RegularFile {
12456                portable_metadata: PortableFileMetadata {
12457                    source_os: "linux".into(),
12458                    native,
12459                    ..PortableFileMetadata::default()
12460                },
12461                ..RegularFile::new("flags.txt", b"payload")
12462            }],
12463            &master_key(),
12464            single_stream_options(),
12465        )
12466        .unwrap();
12467        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12468        opened
12469            .extract_file_to(
12470                "flags.txt",
12471                root.path(),
12472                SafeExtractionOptions {
12473                    restore_policy: RestorePolicy::SameOs,
12474                    ..SafeExtractionOptions::default()
12475                },
12476            )
12477            .unwrap();
12478        let file = fs::File::open(root.path().join("flags.txt")).unwrap();
12479        let mut flags: libc::c_long = 0;
12480        // SAFETY: GETFLAGS writes one c_long to a valid pointer.
12481        assert_eq!(
12482            unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut flags,) },
12483            0
12484        );
12485        assert_eq!(flags as u64, expected_flags);
12486    }
12487
12488    #[cfg(target_os = "linux")]
12489    #[test]
12490    fn compressed_archive_extraction_restores_canonical_posix_acl() {
12491        let acl = b"user::rw-,group::r--,other::---,user:123:r--,mask::r--";
12492        let mut native = NativeFileMetadata {
12493            required_profiles: vec!["posix-backup-v1".into()],
12494            ..NativeFileMetadata::default()
12495        };
12496        native
12497            .primary_pax_records
12498            .insert("SCHILY.acl.access".into(), acl.to_vec());
12499        native
12500            .primary_pax_records
12501            .insert("TZAP.acl.projection".into(), b"exact".to_vec());
12502        native.primary_pax_records.insert(
12503            "TZAP.acl.syntax".into(),
12504            b"schily-posix1e-extra-id-v1".to_vec(),
12505        );
12506        let archive = write_archive(
12507            &[RegularFile {
12508                mode: 0o640,
12509                portable_metadata: PortableFileMetadata {
12510                    source_os: "linux".into(),
12511                    native,
12512                    ..PortableFileMetadata::default()
12513                },
12514                ..RegularFile::new("acl.txt", b"payload")
12515            }],
12516            &master_key(),
12517            single_stream_options(),
12518        )
12519        .unwrap();
12520        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12521        let root = tempfile::tempdir().unwrap();
12522        opened
12523            .extract_file_to(
12524                "acl.txt",
12525                root.path(),
12526                SafeExtractionOptions {
12527                    restore_policy: RestorePolicy::SameOs,
12528                    ..SafeExtractionOptions::default()
12529                },
12530            )
12531            .unwrap();
12532        let binary = xattr::get(root.path().join("acl.txt"), "system.posix_acl_access")
12533            .unwrap()
12534            .unwrap();
12535        assert_eq!(
12536            crate::entry_metadata::linux_posix_acl_xattr_to_schily(&binary).unwrap(),
12537            acl
12538        );
12539    }
12540
12541    #[cfg(windows)]
12542    #[test]
12543    #[allow(clippy::permissions_set_readonly_false)]
12544    fn compressed_archive_extraction_restores_portable_readonly_attribute() {
12545        let archive = write_archive(
12546            &[RegularFile {
12547                portable_metadata: PortableFileMetadata {
12548                    source_os: "windows".into(),
12549                    source_filesystem: "ntfs".into(),
12550                    mode_origin: PortableModeOrigin::Projected,
12551                    posix_owner: None,
12552                    attributes: Some(1),
12553                    native: Default::default(),
12554                },
12555                ..RegularFile::new("readonly.txt", b"readonly")
12556            }],
12557            &master_key(),
12558            single_stream_options(),
12559        )
12560        .unwrap();
12561        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12562        let root = tempfile::tempdir().unwrap();
12563
12564        opened
12565            .extract_file_to(
12566                "readonly.txt",
12567                root.path(),
12568                SafeExtractionOptions::default(),
12569            )
12570            .unwrap()
12571            .unwrap();
12572        let path = root.path().join("readonly.txt");
12573        assert!(fs::metadata(&path).unwrap().permissions().readonly());
12574
12575        // Let TempDir clean up the restored file on Windows.
12576        let mut permissions = fs::metadata(&path).unwrap().permissions();
12577        permissions.set_readonly(false);
12578        fs::set_permissions(path, permissions).unwrap();
12579    }
12580
12581    #[test]
12582    fn seekable_extract_all_to_streams_unique_archive() {
12583        let archive = write_archive(
12584            &[
12585                RegularFile::new("alpha.txt", b"alpha"),
12586                RegularFile::new("dir/beta.txt", b"beta"),
12587            ],
12588            &master_key(),
12589            single_stream_options(),
12590        )
12591        .unwrap();
12592        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12593        let tmp = tempfile::tempdir().unwrap();
12594
12595        let diagnostics = opened
12596            .extract_all_to(tmp.path(), SafeExtractionOptions::default())
12597            .unwrap();
12598
12599        assert_eq!(diagnostics.len(), 2);
12600        assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
12601        assert_eq!(
12602            fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
12603            b"beta"
12604        );
12605    }
12606
12607    #[test]
12608    fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
12609        let archive = write_archive(
12610            &[
12611                RegularFile::new("same.txt", b"old"),
12612                RegularFile::new("same.txt", b"new"),
12613            ],
12614            &master_key(),
12615            single_stream_options(),
12616        )
12617        .unwrap();
12618        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12619        let tmp = tempfile::tempdir().unwrap();
12620
12621        assert_eq!(
12622            opened
12623                .extract_all_to(tmp.path(), SafeExtractionOptions::default())
12624                .unwrap_err(),
12625            FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
12626        );
12627    }
12628
12629    #[test]
12630    fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
12631        let archive = write_archive(
12632            &[
12633                RegularFile::new("same.txt", b"old"),
12634                RegularFile::new("same.txt", b"new"),
12635            ],
12636            &master_key(),
12637            single_stream_options(),
12638        )
12639        .unwrap();
12640        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12641        let tmp = tempfile::tempdir().unwrap();
12642
12643        let diagnostics = opened
12644            .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
12645            .unwrap();
12646
12647        assert_eq!(diagnostics.len(), 1);
12648        assert_eq!(diagnostics[0].0, "same.txt");
12649        assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
12650    }
12651
12652    #[test]
12653    fn selected_restore_preflights_every_path_before_writing() {
12654        let archive = write_archive(
12655            &[RegularFile::new("alpha.txt", b"alpha")],
12656            &master_key(),
12657            single_stream_options(),
12658        )
12659        .unwrap();
12660        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12661        let tmp = tempfile::tempdir().unwrap();
12662
12663        let error = opened
12664            .extract_selected_files_to(
12665                &["alpha.txt".into(), "missing.txt".into()],
12666                tmp.path(),
12667                SafeExtractionOptions::default(),
12668                2,
12669            )
12670            .unwrap_err();
12671
12672        assert_eq!(
12673            error,
12674            FormatError::ReaderUnsupported("selected archive path is absent from the final index")
12675        );
12676        assert!(!tmp.path().join("alpha.txt").exists());
12677    }
12678
12679    #[test]
12680    fn safe_extract_rejects_overwriting_existing_file_by_default() {
12681        let archive = write_archive(
12682            &[RegularFile::new("hello.txt", b"new")],
12683            &master_key(),
12684            single_stream_options(),
12685        )
12686        .unwrap();
12687        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12688        let tmp = tempfile::tempdir().unwrap();
12689        std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
12690
12691        assert_eq!(
12692            opened
12693                .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
12694                .unwrap_err(),
12695            FormatError::UnsafeOverwrite
12696        );
12697        assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
12698    }
12699
12700    #[test]
12701    fn opens_and_verifies_empty_archive() {
12702        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12703        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12704
12705        assert!(opened.list_files().unwrap().is_empty());
12706        opened.verify().unwrap();
12707    }
12708
12709    #[test]
12710    fn default_reader_options_allow_v36_trailing_garbage_scan() {
12711        let archive = write_archive(
12712            &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
12713            &master_key(),
12714            single_stream_options(),
12715        )
12716        .unwrap();
12717        let mut with_trailing_garbage = archive.bytes.clone();
12718        with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
12719
12720        let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
12721        assert_eq!(
12722            opened.extract_file("garbage-tolerant.txt").unwrap(),
12723            Some(b"still intact".to_vec())
12724        );
12725    }
12726
12727    #[test]
12728    fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
12729        assert_eq!(
12730            open_archive(
12731                &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
12732                &master_key()
12733            )
12734            .unwrap_err(),
12735            FormatError::InvalidLength {
12736                structure: "archive",
12737                expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
12738                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
12739            }
12740        );
12741
12742        let mut header = test_volume_header();
12743        header.crypto_header_length = 512;
12744        let mut unavailable_crypto = header.to_bytes().to_vec();
12745        unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
12746
12747        assert_eq!(
12748            open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
12749            FormatError::InvalidLength {
12750                structure: "CryptoHeader",
12751                expected: VOLUME_HEADER_LEN + 512,
12752                actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
12753            }
12754        );
12755    }
12756
12757    #[test]
12758    fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
12759        let archive = write_archive(
12760            &[RegularFile::new("offset.txt", b"offset")],
12761            &master_key(),
12762            small_block_recovery_options(),
12763        )
12764        .unwrap();
12765        let mut mutated = archive.bytes;
12766        let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
12767        header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
12768        mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12769
12770        let opened = open_archive(&mutated, &master_key()).unwrap();
12771        assert_eq!(
12772            opened.volume_header.crypto_header_offset,
12773            VOLUME_HEADER_LEN as u32
12774        );
12775        assert_eq!(
12776            opened.extract_file("offset.txt").unwrap(),
12777            Some(b"offset".to_vec())
12778        );
12779        opened.verify().unwrap();
12780    }
12781
12782    #[test]
12783    fn rejects_wrong_key_before_metadata_release() {
12784        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12785        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
12786
12787        assert_eq!(
12788            open_archive(&archive.bytes, &wrong).unwrap_err(),
12789            FormatError::HmacMismatch {
12790                structure: "CryptoHeader"
12791            }
12792        );
12793    }
12794
12795    #[test]
12796    fn ordinary_encrypted_writers_emit_v45_archives() {
12797        let raw_key_archive = write_archive(
12798            &[RegularFile::new("raw.txt", b"raw key payload")],
12799            &master_key(),
12800            single_stream_options(),
12801        )
12802        .unwrap();
12803        let raw_header = VolumeHeader::parse(&raw_key_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12804        assert_eq!(raw_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12805        let raw_opened = open_archive(&raw_key_archive.bytes, &master_key()).unwrap();
12806        assert_eq!(
12807            raw_opened.volume_header.volume_format_rev,
12808            VOLUME_FORMAT_REV_45
12809        );
12810        assert_eq!(
12811            raw_opened.extract_file("raw.txt").unwrap(),
12812            Some(b"raw key payload".to_vec())
12813        );
12814
12815        let passphrase_kdf = KdfParams::Argon2id {
12816            t_cost: 1,
12817            m_cost_kib: 8,
12818            parallelism: 1,
12819            salt: b"0123456789abcdef".to_vec(),
12820        };
12821        let passphrase_archive = write_archive_with_kdf(
12822            &[RegularFile::new("pass.txt", b"passphrase payload")],
12823            &master_key(),
12824            single_stream_options(),
12825            &passphrase_kdf,
12826        )
12827        .unwrap();
12828        let passphrase_header =
12829            VolumeHeader::parse(&passphrase_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12830        assert_eq!(passphrase_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12831        let passphrase_opened = open_archive(&passphrase_archive.bytes, &master_key()).unwrap();
12832        assert_eq!(
12833            passphrase_opened.volume_header.volume_format_rev,
12834            VOLUME_FORMAT_REV_45
12835        );
12836        assert_eq!(
12837            passphrase_opened.extract_file("pass.txt").unwrap(),
12838            Some(b"passphrase payload".to_vec())
12839        );
12840    }
12841
12842    #[test]
12843    fn rejects_future_volume_format_revision_before_key_mismatch() {
12844        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12845        let mut bytes = archive.bytes;
12846        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12847        header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
12848        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12849        let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
12850
12851        assert_eq!(
12852            open_archive(&bytes, &wrong).unwrap_err(),
12853            FormatError::UnsupportedVolumeFormatRevision {
12854                format_version: FORMAT_VERSION,
12855                volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
12856                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12857            }
12858        );
12859    }
12860
12861    #[test]
12862    fn open_archive_unencrypted_accepts_v45_profile() {
12863        let archive = write_archive_unencrypted(
12864            &[RegularFile::new("payload.txt", b"smoke-v45-unencrypted")],
12865            WriterOptions {
12866                aead_algo: AeadAlgo::None,
12867                ..single_stream_options()
12868            },
12869        )
12870        .unwrap();
12871
12872        let opened = open_archive_unencrypted(&archive.bytes).unwrap();
12873        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12874
12875        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_45);
12876        assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12877        assert_eq!(
12878            opened.extract_file("payload.txt").unwrap(),
12879            Some(b"smoke-v45-unencrypted".to_vec())
12880        );
12881        opened.verify().unwrap();
12882    }
12883
12884    #[test]
12885    fn full_verification_reports_v45_metadata_capabilities_separately() {
12886        let archive = write_archive(
12887            &[RegularFile::new("report.txt", b"report")],
12888            &master_key(),
12889            single_stream_options(),
12890        )
12891        .unwrap();
12892        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12893        let verification = opened.verify_content().unwrap();
12894        let report = verification.metadata_report().unwrap();
12895
12896        assert!(report.all_capture_complete);
12897        assert!(report.full_fidelity_possible);
12898        assert_eq!(report.profiles_present, ["portable-v1"]);
12899        assert!(report.auxiliary_kinds_present.is_empty());
12900        assert_eq!(report.entries.len(), 1);
12901        assert_eq!(report.entries[0].path, b"report.txt");
12902        assert!(report.entries[0]
12903            .policy_capabilities
12904            .iter()
12905            .all(|capability| capability.policy_complete));
12906        assert!(report.entries[0].diagnostics.is_empty());
12907    }
12908
12909    #[test]
12910    fn root_auth_unencrypted_v45_round_trips_with_recomputed_archive_root() {
12911        let archive = write_archive_with_root_auth(
12912            &[RegularFile::new(
12913                "signed-v45.txt",
12914                b"root-auth v45 plaintext",
12915            )],
12916            &master_key(),
12917            WriterOptions {
12918                aead_algo: AeadAlgo::None,
12919                ..single_stream_options()
12920            },
12921            test_root_auth_config(),
12922            |request| Ok(test_root_auth_value(request)),
12923        )
12924        .unwrap();
12925        let opened = open_archive_unencrypted(&archive.bytes).unwrap();
12926        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12927
12928        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_45);
12929        assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_45);
12930        assert_eq!(
12931            opened.extract_file("signed-v45.txt").unwrap(),
12932            Some(b"root-auth v45 plaintext".to_vec())
12933        );
12934
12935        let verified = opened
12936            .verify_root_auth_with(|footer, archive_root| {
12937                Ok(test_root_auth_verifies(footer, archive_root))
12938            })
12939            .unwrap();
12940
12941        assert_eq!(verified.format_version, FORMAT_VERSION);
12942        assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_45);
12943        assert_eq!(
12944            verified.archive_root,
12945            opened.root_auth_footer.as_ref().unwrap().archive_root
12946        );
12947    }
12948
12949    #[test]
12950    fn recipientwrap_open_accepts_candidate_after_header_hmac() {
12951        let master = master_key();
12952        let archive = write_archive_with_recipient_wrap_records(
12953            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12954            &master,
12955            single_stream_options(),
12956            vec![recipient_wrap_test_record()],
12957        )
12958        .unwrap();
12959
12960        let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
12961            assert_eq!(
12962                context.archive_identity.volume_format_rev,
12963                VOLUME_FORMAT_REV_45
12964            );
12965            assert_eq!(context.record.profile_id, 1);
12966            Ok(vec![master.0])
12967        })
12968        .unwrap();
12969
12970        assert_eq!(
12971            opened.extract_file("wrapped.txt").unwrap(),
12972            Some(b"recipient payload".to_vec())
12973        );
12974        opened.verify().unwrap();
12975    }
12976
12977    #[test]
12978    fn recipientwrap_seekable_open_uses_lazy_block_source() {
12979        let master = master_key();
12980        let archive = write_archive_with_recipient_wrap_records(
12981            &[RegularFile::new("wrapped.txt", b"recipient payload")],
12982            &master,
12983            single_stream_options(),
12984            vec![recipient_wrap_test_record()],
12985        )
12986        .unwrap();
12987
12988        let opened = open_seekable_archive_with_recipient_wrap_resolver_options(
12989            CountingReadAt::new(archive.bytes, vec![]),
12990            |context| {
12991                assert_eq!(
12992                    context.archive_identity.volume_format_rev,
12993                    VOLUME_FORMAT_REV_45
12994                );
12995                assert_eq!(context.record.profile_id, 1);
12996                Ok(vec![master.0])
12997            },
12998            ReaderOptions::default(),
12999        )
13000        .unwrap();
13001
13002        assert!(opened.blocks.is_empty());
13003        assert!(opened.lazy_blocks.is_some());
13004        assert_eq!(
13005            opened.extract_file("wrapped.txt").unwrap(),
13006            Some(b"recipient payload".to_vec())
13007        );
13008        opened.verify().unwrap();
13009    }
13010
13011    #[test]
13012    fn recipientwrap_seekable_volume_set_opens_with_resolver() {
13013        let master = master_key();
13014        let archive = write_archive_with_recipient_wrap_records(
13015            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13016            &master,
13017            WriterOptions {
13018                stripe_width: 2,
13019                volume_loss_tolerance: 0,
13020                ..WriterOptions::default()
13021            },
13022            vec![recipient_wrap_test_record()],
13023        )
13024        .unwrap();
13025        assert_eq!(archive.volumes.len(), 2);
13026
13027        let opened = open_seekable_archive_volumes_with_recipient_wrap_resolver_options(
13028            archive.volumes,
13029            |context| {
13030                assert_eq!(
13031                    context.archive_identity.volume_format_rev,
13032                    VOLUME_FORMAT_REV_45
13033                );
13034                assert_eq!(context.record.profile_id, 1);
13035                Ok(vec![master.0])
13036            },
13037            ReaderOptions::default(),
13038        )
13039        .unwrap();
13040
13041        assert_eq!(opened.observed_volume_count, 2);
13042        assert!(opened.blocks.is_empty());
13043        assert!(opened.lazy_blocks.is_some());
13044        assert_eq!(
13045            opened.extract_file("wrapped.txt").unwrap(),
13046            Some(b"recipient payload".to_vec())
13047        );
13048        opened.verify().unwrap();
13049    }
13050
13051    #[test]
13052    fn recipientwrap_open_tries_subsequent_records_after_failed_candidate() {
13053        let master = master_key();
13054        let mut first_record = recipient_wrap_test_record();
13055        first_record.recipient_identity_bytes = b"first-candidate".to_vec();
13056        let mut second_record = recipient_wrap_test_record();
13057        second_record.recipient_identity_bytes = b"second-candidate".to_vec();
13058
13059        let archive = write_archive_with_recipient_wrap_records(
13060            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13061            &master,
13062            single_stream_options(),
13063            vec![first_record, second_record],
13064        )
13065        .unwrap();
13066
13067        let mut attempts = Vec::new();
13068        let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
13069            attempts.push(context.record.recipient_identity_bytes.clone());
13070            if context.record.recipient_identity_bytes.as_slice() == b"second-candidate" {
13071                Ok(vec![master.0])
13072            } else {
13073                Ok(vec![[0x99u8; 32]])
13074            }
13075        })
13076        .unwrap();
13077
13078        assert_eq!(
13079            attempts,
13080            vec![b"first-candidate".to_vec(), b"second-candidate".to_vec(),]
13081        );
13082        assert_eq!(
13083            opened.extract_file("wrapped.txt").unwrap(),
13084            Some(b"recipient payload".to_vec())
13085        );
13086        opened.verify().unwrap();
13087    }
13088
13089    #[test]
13090    fn recipientwrap_startup_rejects_malformed_record_length() {
13091        let master = master_key();
13092        let options = WriterOptions {
13093            bit_rot_buffer_pct: 0,
13094            ..single_stream_options()
13095        };
13096        let archive = write_archive_with_recipient_wrap_records(
13097            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13098            &master,
13099            options,
13100            vec![recipient_wrap_test_record()],
13101        )
13102        .unwrap();
13103        let mut bytes = archive.bytes;
13104        let (_, _, table_start, _table_len, _) = recipient_wrap_layout(&bytes);
13105        bytes[table_start + 96..table_start + 100].copy_from_slice(&1u32.to_le_bytes());
13106
13107        assert_eq!(
13108            open_archive_with_recipient_wrap_resolver(&bytes, |_| { Ok(vec![master.0]) })
13109                .unwrap_err(),
13110            FormatError::InvalidArchive("RecipientRecordV1 record_length is too small")
13111        );
13112    }
13113
13114    #[test]
13115    fn recipientwrap_future_revision_rejects_before_resolver_callback() {
13116        let archive = write_archive_with_recipient_wrap_records(
13117            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13118            &master_key(),
13119            single_stream_options(),
13120            vec![recipient_wrap_test_record()],
13121        )
13122        .unwrap();
13123        let mut bytes = archive.bytes;
13124        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13125        header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
13126        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13127
13128        let mut called = false;
13129        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
13130            called = true;
13131            Ok(vec![master_key().0])
13132        })
13133        .unwrap_err();
13134
13135        assert!(!called);
13136        assert_eq!(
13137            err,
13138            FormatError::UnsupportedVolumeFormatRevision {
13139                format_version: FORMAT_VERSION,
13140                volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
13141                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
13142            }
13143        );
13144    }
13145
13146    #[test]
13147    fn recipientwrap_stripe_width_mismatch_rejects_before_resolver_callback() {
13148        let archive = write_archive_with_recipient_wrap_records(
13149            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13150            &master_key(),
13151            single_stream_options(),
13152            vec![recipient_wrap_test_record()],
13153        )
13154        .unwrap();
13155        let mut bytes = archive.bytes;
13156        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13157        header.stripe_width += 1;
13158        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13159
13160        let mut called = false;
13161        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
13162            called = true;
13163            Ok(vec![master_key().0])
13164        })
13165        .unwrap_err();
13166
13167        assert!(!called);
13168        assert_eq!(
13169            err,
13170            FormatError::InvalidArchive("VolumeHeader and CryptoHeader stripe_width differ")
13171        );
13172    }
13173
13174    #[test]
13175    fn recipientwrap_defers_raw_stream_profile_rejection_until_after_resolver_callback() {
13176        let master = master_key();
13177        let archive = write_archive_with_recipient_wrap_records(
13178            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13179            &master,
13180            single_stream_options(),
13181            vec![recipient_wrap_test_record()],
13182        )
13183        .unwrap();
13184        let mut bytes = archive.bytes;
13185        add_raw_stream_profile_to_physical_crypto_header(&mut bytes);
13186        recompute_physical_crypto_header_hmac(&mut bytes, &master);
13187
13188        let mut called = false;
13189        let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
13190            called = true;
13191            Ok(vec![master.0])
13192        })
13193        .unwrap_err();
13194
13195        assert!(called);
13196        assert_eq!(
13197            err,
13198            FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE)
13199        );
13200    }
13201
13202    #[test]
13203    fn recipientwrap_recovers_physical_key_wrap_table_from_cmra_authority() {
13204        let master = master_key();
13205        let archive = write_archive_with_recipient_wrap_records(
13206            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13207            &master,
13208            single_stream_options(),
13209            vec![recipient_wrap_test_record()],
13210        )
13211        .unwrap();
13212        let mut bytes = archive.bytes;
13213        let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13214        let crypto_start = header.crypto_header_offset as usize;
13215        let crypto_end = crypto_start + header.crypto_header_length as usize;
13216        let crypto_header = CryptoHeader::parse(
13217            &bytes[crypto_start..crypto_end],
13218            header.crypto_header_length,
13219        )
13220        .unwrap();
13221        let KdfParams::RecipientWrap {
13222            key_wrap_table_length,
13223            ..
13224        } = crypto_header.kdf_params
13225        else {
13226            panic!("expected RecipientWrap KdfParams");
13227        };
13228        let table_start = crypto_end;
13229        let table_end = table_start + key_wrap_table_length as usize;
13230        bytes[table_end - 1] ^= 0x01;
13231
13232        let mut called = false;
13233        let opened = open_archive_with_recipient_wrap_resolver(&bytes, |context| {
13234            called = true;
13235            assert_eq!(
13236                context.archive_identity.volume_format_rev,
13237                VOLUME_FORMAT_REV_45
13238            );
13239            assert_eq!(context.record.profile_id, 1);
13240            Ok(vec![master.0])
13241        })
13242        .unwrap();
13243
13244        assert!(called);
13245        assert_eq!(
13246            opened.extract_file("wrapped.txt").unwrap(),
13247            Some(b"recipient payload".to_vec())
13248        );
13249        opened.verify().unwrap();
13250    }
13251
13252    #[test]
13253    fn recipientwrap_open_rejects_wrong_candidate_header_hmac() {
13254        let archive = write_archive_with_recipient_wrap_records(
13255            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13256            &master_key(),
13257            single_stream_options(),
13258            vec![recipient_wrap_test_record()],
13259        )
13260        .unwrap();
13261        let wrong_candidate = [0x99u8; 32];
13262
13263        assert_eq!(
13264            open_archive_with_recipient_wrap_resolver(&archive.bytes, |_| {
13265                Ok(vec![wrong_candidate])
13266            })
13267            .unwrap_err(),
13268            FormatError::KeyMaterialMismatch
13269        );
13270    }
13271
13272    #[test]
13273    fn recipientwrap_open_recovers_tampered_physical_crypto_header_hmac_from_cmra() {
13274        let master = master_key();
13275        let archive = write_archive_with_recipient_wrap_records(
13276            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13277            &master,
13278            single_stream_options(),
13279            vec![recipient_wrap_test_record()],
13280        )
13281        .unwrap();
13282        let mut bytes = archive.bytes.clone();
13283        let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13284        let hmac_end = header.crypto_header_offset as usize + header.crypto_header_length as usize;
13285        bytes[hmac_end - 1] ^= 0x55;
13286
13287        let opened =
13288            open_archive_with_recipient_wrap_resolver(&bytes, |_| Ok(vec![master.0])).unwrap();
13289
13290        assert_eq!(
13291            opened.extract_file("wrapped.txt").unwrap(),
13292            Some(b"recipient payload".to_vec())
13293        );
13294        assert_eq!(
13295            opened.crypto_header_bytes,
13296            archive.bytes[header.crypto_header_offset as usize..hmac_end]
13297        );
13298        opened.verify().unwrap();
13299    }
13300
13301    #[test]
13302    fn recipientwrap_archive_does_not_fall_back_to_raw_master_key_open() {
13303        let master = master_key();
13304        let archive = write_archive_with_recipient_wrap_records(
13305            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13306            &master,
13307            single_stream_options(),
13308            vec![recipient_wrap_test_record()],
13309        )
13310        .unwrap();
13311
13312        assert_eq!(
13313            open_archive(&archive.bytes, &master).unwrap_err(),
13314            FormatError::KeyMaterialMismatch
13315        );
13316    }
13317
13318    #[test]
13319    fn recipientwrap_seekable_archive_does_not_fall_back_to_raw_master_key_open() {
13320        let master = master_key();
13321        let archive = write_archive_with_recipient_wrap_records(
13322            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13323            &master,
13324            single_stream_options(),
13325            vec![recipient_wrap_test_record()],
13326        )
13327        .unwrap();
13328
13329        assert_eq!(
13330            open_seekable_archive(archive.bytes, &master).unwrap_err(),
13331            FormatError::KeyMaterialMismatch
13332        );
13333    }
13334
13335    #[test]
13336    fn public_no_key_verifies_signed_recipientwrap_block_commitment() {
13337        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
13338            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13339            &master_key(),
13340            single_stream_options(),
13341            vec![recipient_wrap_test_record()],
13342            test_root_auth_config(),
13343            |request| Ok(test_root_auth_value(request)),
13344        )
13345        .unwrap();
13346
13347        let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
13348            Ok(test_root_auth_verifies(footer, archive_root))
13349        })
13350        .unwrap();
13351
13352        assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_45);
13353        assert_eq!(verified.total_data_block_count, 3);
13354    }
13355
13356    #[test]
13357    fn public_no_key_rejects_recipientwrap_startup_and_cmra_kdf_mismatch() {
13358        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
13359            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13360            &master_key(),
13361            single_stream_options(),
13362            vec![recipient_wrap_test_record()],
13363            test_root_auth_config(),
13364            |request| Ok(test_root_auth_value(request)),
13365        )
13366        .unwrap();
13367        let mut bytes = archive.bytes;
13368        mutate_top_level_recipient_wrap_public_profile(&mut bytes);
13369
13370        let err = public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
13371            Ok(test_root_auth_verifies(footer, archive_root))
13372        })
13373        .unwrap_err();
13374
13375        assert_eq!(
13376            err,
13377            FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
13378        );
13379    }
13380
13381    #[test]
13382    fn public_no_key_rejects_recipientwrap_kdf_profile_mismatch_across_volumes() {
13383        let archive = write_archive_with_root_auth_and_recipient_wrap_records(
13384            &[RegularFile::new("wrapped.txt", b"recipient payload")],
13385            &master_key(),
13386            WriterOptions {
13387                stripe_width: 2,
13388                volume_loss_tolerance: 0,
13389                ..WriterOptions::default()
13390            },
13391            vec![recipient_wrap_test_record()],
13392            test_root_auth_config(),
13393            |request| Ok(test_root_auth_value(request)),
13394        )
13395        .unwrap();
13396        let mut volumes = archive.volumes;
13397        mutate_top_level_recipient_wrap_public_profile(&mut volumes[1]);
13398        mutate_cmra_recipient_wrap_public_profile(&mut volumes[1]);
13399
13400        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
13401        let err = public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
13402            Ok(test_root_auth_verifies(footer, archive_root))
13403        })
13404        .unwrap_err();
13405
13406        assert_eq!(
13407            err,
13408            FormatError::InvalidArchive("public no-key volume global metadata differs")
13409        );
13410    }
13411
13412    #[test]
13413    fn write_archive_defaults_to_current_revision() {
13414        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13415        let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
13416
13417        assert_eq!(header.format_version, FORMAT_VERSION);
13418        assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV);
13419    }
13420
13421    #[test]
13422    fn non_seekable_stream_rejects_future_volume_format_revision() {
13423        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13424        let mut bytes = archive.bytes;
13425        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13426        header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
13427        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13428
13429        assert_eq!(
13430            verify_non_seekable_stream(std::io::Cursor::new(bytes), &master_key()).unwrap_err(),
13431            FormatError::UnsupportedVolumeFormatRevision {
13432                format_version: FORMAT_VERSION,
13433                volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
13434                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
13435            }
13436        );
13437    }
13438
13439    #[test]
13440    fn open_seekable_archive_rejects_future_volume_format_revision() {
13441        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13442        let mut bytes = archive.bytes;
13443        let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
13444        header.volume_format_rev = VOLUME_FORMAT_REV_45 + 1;
13445        bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
13446
13447        assert_eq!(
13448            open_seekable_archive(CountingReadAt::new(bytes, vec![]), &master_key()).unwrap_err(),
13449            FormatError::UnsupportedVolumeFormatRevision {
13450                format_version: FORMAT_VERSION,
13451                volume_format_rev: VOLUME_FORMAT_REV_45 + 1,
13452                reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
13453            }
13454        );
13455    }
13456
13457    #[test]
13458    fn rejects_payload_tamper_even_with_recomputed_block_crc() {
13459        let mut archive = write_archive(
13460            &[RegularFile::new("file.txt", b"authenticated")],
13461            &master_key(),
13462            single_stream_options(),
13463        )
13464        .unwrap()
13465        .bytes;
13466        let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
13467        let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
13468        let crypto = CryptoHeader::parse(
13469            &archive[VOLUME_HEADER_LEN..crypto_end],
13470            volume.crypto_header_length,
13471        )
13472        .unwrap();
13473        let block_size = crypto.fixed.block_size as usize;
13474        archive[crypto_end + 16] ^= 1;
13475        let crc_offset = crypto_end + 16 + block_size;
13476        let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
13477        archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
13478
13479        let opened = open_archive(&archive, &master_key()).unwrap();
13480        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
13481    }
13482
13483    #[test]
13484    fn list_and_extract_use_final_view_for_duplicate_paths() {
13485        let archive = write_archive(
13486            &[
13487                RegularFile {
13488                    mtime: crate::ArchiveTimestamp::from_seconds(1_700_000_000),
13489                    ..RegularFile::new("same.txt", b"old")
13490                },
13491                RegularFile {
13492                    mtime: crate::ArchiveTimestamp::from_seconds(1_700_000_100),
13493                    ..RegularFile::new("same.txt", b"newer")
13494                },
13495            ],
13496            &master_key(),
13497            single_stream_options(),
13498        )
13499        .unwrap();
13500        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13501
13502        let listed = opened.list_index_entries().unwrap();
13503        assert_eq!(listed.len(), 1);
13504        assert_eq!(listed[0].path, "same.txt");
13505        assert_eq!(listed[0].name, "same.txt");
13506        assert_eq!(listed[0].file_data_size, 5);
13507        assert_eq!(listed[0].flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13508        assert_eq!(listed[0].frame_count, 1);
13509        assert!(listed[0].layout.compressed_size > 0);
13510        let looked_up = opened.lookup_index_entry("same.txt").unwrap().unwrap();
13511        assert_eq!(looked_up.path, "same.txt");
13512        assert_eq!(looked_up.file_data_size, 5);
13513        assert_eq!(looked_up.flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13514        assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
13515        assert_eq!(
13516            opened.list_files().unwrap(),
13517            vec![ArchiveEntry {
13518                path: "same.txt".to_string(),
13519                file_data_size: 5,
13520                kind: TarEntryKind::Regular,
13521                mode: 0o644,
13522                mtime: ArchiveTimestamp::from_seconds(1_700_000_100),
13523                diagnostics: Vec::new(),
13524            }]
13525        );
13526        assert_eq!(
13527            opened.extract_file("same.txt").unwrap(),
13528            Some(b"newer".to_vec())
13529        );
13530        opened.verify().unwrap();
13531    }
13532
13533    #[test]
13534    fn index_entries_do_not_decrypt_payload_envelopes() {
13535        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
13536        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
13537
13538        let listed = opened.list_index_entries().unwrap();
13539        assert_eq!(listed.len(), 2);
13540        assert_eq!(listed[0].path, "broken.txt");
13541        assert_eq!(listed[0].file_data_size, b"broken payload\n".len() as u64);
13542        assert_eq!(listed[0].flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13543        assert_eq!(listed[1].path, "healthy.txt");
13544        assert_eq!(listed[1].file_data_size, b"healthy payload\n".len() as u64);
13545        assert_eq!(listed[1].flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13546        let looked_up = opened.lookup_index_entry("broken.txt").unwrap().unwrap();
13547        assert_eq!(looked_up.path, "broken.txt");
13548        assert_eq!(looked_up.file_data_size, b"broken payload\n".len() as u64);
13549        assert_eq!(looked_up.flags, crate::entry_metadata::EXTENDED_METADATA_V1);
13550        assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
13551    }
13552
13553    #[test]
13554    fn index_entry_layout_stats_match_frame_and_envelope_tables() {
13555        let payload = pseudo_random_bytes(12 * 1024);
13556        let archive = write_archive(
13557            &[RegularFile::new("chunked.bin", &payload)],
13558            &master_key(),
13559            WriterOptions {
13560                block_size: 4096,
13561                chunk_size: 1024,
13562                envelope_target_size: 2048,
13563                stripe_width: 1,
13564                volume_loss_tolerance: 0,
13565                bit_rot_buffer_pct: 0,
13566                fec_data_shards: 16,
13567                fec_parity_shards: 0,
13568                ..WriterOptions::default()
13569            },
13570        )
13571        .unwrap();
13572        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13573        let entry = opened.list_index_entries().unwrap().remove(0);
13574        let located = opened
13575            .locate_index_file(b"chunked.bin")
13576            .unwrap()
13577            .expect("indexed file exists");
13578        let file = &located.shard.files[located.file_index];
13579        let frames = frame_range_for_file(&located.shard, file).unwrap();
13580        assert!(frames.len() > 1);
13581
13582        let expected_compressed_size = frames
13583            .iter()
13584            .map(|frame| frame.compressed_size as u64)
13585            .sum::<u64>();
13586        let expected_decompressed_frame_size = frames
13587            .iter()
13588            .map(|frame| frame.decompressed_size as u64)
13589            .sum::<u64>();
13590        let envelope_indexes = frames
13591            .iter()
13592            .map(|frame| frame.envelope_index)
13593            .collect::<BTreeSet<_>>();
13594        assert!(envelope_indexes.len() > 1);
13595
13596        let envelopes = envelope_indexes
13597            .iter()
13598            .map(|index| envelope_by_index(&located.shard, *index).unwrap())
13599            .collect::<Vec<_>>();
13600        let expected_first_payload_block_index = envelopes
13601            .iter()
13602            .map(|envelope| envelope.first_block_index)
13603            .min();
13604        let expected_payload_data_block_count = envelopes
13605            .iter()
13606            .map(|envelope| envelope.data_block_count as u64)
13607            .sum::<u64>();
13608        let expected_payload_parity_block_count = envelopes
13609            .iter()
13610            .map(|envelope| envelope.parity_block_count as u64)
13611            .sum::<u64>();
13612        let expected_payload_encrypted_size = envelopes
13613            .iter()
13614            .map(|envelope| envelope.encrypted_size as u64)
13615            .sum::<u64>();
13616
13617        assert_eq!(entry.path, "chunked.bin");
13618        assert_eq!(entry.name, "chunked.bin");
13619        assert_eq!(entry.file_data_size, payload.len() as u64);
13620        assert_eq!(entry.flags, file.flags);
13621        assert_eq!(entry.tar_member_group_size, file.tar_member_group_size);
13622        assert_eq!(entry.first_frame_index, file.first_frame_index);
13623        assert_eq!(entry.frame_count, file.frame_count);
13624        assert_eq!(
13625            entry.offset_in_first_frame_plaintext,
13626            file.offset_in_first_frame_plaintext
13627        );
13628        assert_eq!(entry.layout.compressed_size, expected_compressed_size);
13629        assert_eq!(
13630            entry.layout.decompressed_frame_size,
13631            expected_decompressed_frame_size
13632        );
13633        assert_eq!(entry.layout.envelope_count as usize, envelope_indexes.len());
13634        assert_eq!(
13635            entry.layout.first_envelope_index,
13636            envelope_indexes.iter().next().copied()
13637        );
13638        assert_eq!(
13639            entry.layout.last_envelope_index,
13640            envelope_indexes.iter().next_back().copied()
13641        );
13642        assert_eq!(
13643            entry.layout.first_payload_block_index,
13644            expected_first_payload_block_index
13645        );
13646        assert_eq!(
13647            entry.layout.payload_data_block_count,
13648            expected_payload_data_block_count
13649        );
13650        assert_eq!(
13651            entry.layout.payload_parity_block_count,
13652            expected_payload_parity_block_count
13653        );
13654        assert_eq!(
13655            entry.layout.payload_encrypted_size,
13656            expected_payload_encrypted_size
13657        );
13658    }
13659
13660    #[test]
13661    fn extract_file_does_not_decrypt_unselected_payload_envelope() {
13662        // This fixture corrupts only the unselected envelope, proving selected
13663        // extraction does not decrypt unrelated payload envelopes.
13664        let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
13665        corrupt_payload_record(&mut opened.blocks, broken_payload_block);
13666
13667        assert_eq!(
13668            opened.extract_file("healthy.txt").unwrap(),
13669            Some(b"healthy payload\n".to_vec())
13670        );
13671        assert_eq!(
13672            opened.extract_file("broken.txt").unwrap_err(),
13673            FormatError::AeadFailure
13674        );
13675        assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
13676    }
13677
13678    #[test]
13679    fn seekable_extract_does_not_read_unselected_payload_envelope() {
13680        let healthy = pseudo_random_bytes(64 * 1024);
13681        let broken = pseudo_random_bytes(64 * 1024);
13682        let options = WriterOptions {
13683            block_size: 4096,
13684            chunk_size: 4096,
13685            envelope_target_size: 8192,
13686            stripe_width: 1,
13687            volume_loss_tolerance: 0,
13688            bit_rot_buffer_pct: 0,
13689            fec_data_shards: 4,
13690            fec_parity_shards: 0,
13691            index_fec_data_shards: 4,
13692            index_fec_parity_shards: 0,
13693            index_root_fec_data_shards: 4,
13694            index_root_fec_parity_shards: 0,
13695            ..WriterOptions::default()
13696        };
13697        let archive = write_archive(
13698            &[
13699                RegularFile::new("healthy.bin", &healthy),
13700                RegularFile::new("broken.bin", &broken),
13701            ],
13702            &master_key(),
13703            options,
13704        )
13705        .unwrap();
13706        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
13707        let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
13708        let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
13709        let denied_block_indices = broken_envelopes
13710            .iter()
13711            .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
13712            .flat_map(|envelope| {
13713                let block_count =
13714                    envelope.data_block_count as u64 + envelope.parity_block_count as u64;
13715                envelope.first_block_index..envelope.first_block_index + block_count
13716            })
13717            .collect::<BTreeSet<_>>();
13718        assert!(
13719            !denied_block_indices.is_empty(),
13720            "fixture must place broken.bin in at least one unshared envelope"
13721        );
13722        let denied_ranges = block_record_slots(&archive.bytes)
13723            .into_iter()
13724            .filter_map(|(offset, len, record)| {
13725                denied_block_indices
13726                    .contains(&record.block_index)
13727                    .then_some((offset as u64, (offset + len) as u64))
13728            })
13729            .collect::<Vec<_>>();
13730        assert!(!denied_ranges.is_empty());
13731
13732        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
13733        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
13734
13735        assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
13736        for (read_start, read_end) in reader.reads() {
13737            assert!(
13738                denied_ranges
13739                    .iter()
13740                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
13741                "targeted extract read an unrelated payload BlockRecord range"
13742            );
13743        }
13744        assert_eq!(
13745            opened.extract_file("broken.bin").unwrap_err(),
13746            FormatError::InvalidArchive("denied test read")
13747        );
13748    }
13749
13750    #[test]
13751    fn extract_file_to_writer_streams_before_reading_later_envelopes() {
13752        struct FailOnFirstWrite;
13753
13754        impl std::io::Write for FailOnFirstWrite {
13755            fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
13756                Err(std::io::Error::other("sink stopped"))
13757            }
13758
13759            fn flush(&mut self) -> std::io::Result<()> {
13760                Ok(())
13761            }
13762        }
13763
13764        let payload = pseudo_random_bytes(128 * 1024);
13765        let options = WriterOptions {
13766            block_size: 4096,
13767            chunk_size: 4096,
13768            envelope_target_size: 8192,
13769            stripe_width: 1,
13770            volume_loss_tolerance: 0,
13771            bit_rot_buffer_pct: 0,
13772            fec_data_shards: 4,
13773            fec_parity_shards: 0,
13774            index_fec_data_shards: 4,
13775            index_fec_parity_shards: 0,
13776            index_root_fec_data_shards: 4,
13777            index_root_fec_parity_shards: 0,
13778            ..WriterOptions::default()
13779        };
13780        let archive = write_archive(
13781            &[RegularFile::new("large.bin", &payload)],
13782            &master_key(),
13783            options,
13784        )
13785        .unwrap();
13786        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
13787        let envelopes = envelope_entries_for_path(&eager, "large.bin");
13788        let first_envelope = envelopes
13789            .first()
13790            .expect("large fixture should have at least one envelope")
13791            .envelope_index;
13792        let later_envelope_blocks = envelopes
13793            .iter()
13794            .filter(|entry| entry.envelope_index != first_envelope)
13795            .flat_map(|entry| {
13796                let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
13797                entry.first_block_index..entry.first_block_index + block_count
13798            })
13799            .collect::<BTreeSet<_>>();
13800        assert!(
13801            !later_envelope_blocks.is_empty(),
13802            "fixture must span more than one payload envelope"
13803        );
13804        let denied_ranges = block_record_slots(&archive.bytes)
13805            .into_iter()
13806            .filter_map(|(offset, len, record)| {
13807                later_envelope_blocks
13808                    .contains(&record.block_index)
13809                    .then_some((offset as u64, (offset + len) as u64))
13810            })
13811            .collect::<Vec<_>>();
13812        assert!(!denied_ranges.is_empty());
13813
13814        let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
13815        let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
13816        let mut writer = FailOnFirstWrite;
13817
13818        let err = opened
13819            .extract_file_to_writer("large.bin", &mut writer)
13820            .unwrap_err();
13821        assert_eq!(err.to_string(), "extraction output write failed");
13822        for (read_start, read_end) in reader.reads() {
13823            assert!(
13824                denied_ranges
13825                    .iter()
13826                    .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
13827                "streaming writer read a later payload envelope before surfacing writer failure"
13828            );
13829        }
13830    }
13831
13832    #[test]
13833    fn extract_file_to_writer_writes_bounded_chunks() {
13834        struct ChunkRecorder {
13835            total: usize,
13836            max_write: usize,
13837            writes: usize,
13838        }
13839
13840        impl std::io::Write for ChunkRecorder {
13841            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
13842                self.total += buf.len();
13843                self.max_write = self.max_write.max(buf.len());
13844                self.writes += 1;
13845                Ok(buf.len())
13846            }
13847
13848            fn flush(&mut self) -> std::io::Result<()> {
13849                Ok(())
13850            }
13851        }
13852
13853        let payload = pseudo_random_bytes(128 * 1024);
13854        let options = WriterOptions {
13855            block_size: 4096,
13856            chunk_size: 4096,
13857            envelope_target_size: 8192,
13858            stripe_width: 1,
13859            volume_loss_tolerance: 0,
13860            bit_rot_buffer_pct: 0,
13861            fec_data_shards: 4,
13862            fec_parity_shards: 0,
13863            index_fec_data_shards: 4,
13864            index_fec_parity_shards: 0,
13865            index_root_fec_data_shards: 4,
13866            index_root_fec_parity_shards: 0,
13867            ..WriterOptions::default()
13868        };
13869        let archive = write_archive(
13870            &[RegularFile::new("large.bin", &payload)],
13871            &master_key(),
13872            options,
13873        )
13874        .unwrap();
13875        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13876        let mut writer = ChunkRecorder {
13877            total: 0,
13878            max_write: 0,
13879            writes: 0,
13880        };
13881
13882        opened
13883            .extract_file_to_writer("large.bin", &mut writer)
13884            .unwrap()
13885            .unwrap();
13886
13887        assert_eq!(writer.total, payload.len());
13888        assert!(writer.writes > 1);
13889        assert!(
13890            writer.max_write <= options.chunk_size as usize,
13891            "writer saw a {} byte chunk, larger than the {} byte frame target",
13892            writer.max_write,
13893            options.chunk_size
13894        );
13895    }
13896
13897    #[test]
13898    fn extract_file_to_writer_with_progress_reports_payload_bytes() {
13899        struct ChunkRecorder {
13900            total: usize,
13901        }
13902
13903        impl std::io::Write for ChunkRecorder {
13904            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
13905                self.total += buf.len();
13906                Ok(buf.len())
13907            }
13908
13909            fn flush(&mut self) -> std::io::Result<()> {
13910                Ok(())
13911            }
13912        }
13913
13914        let payload = pseudo_random_bytes(128 * 1024);
13915        let options = WriterOptions {
13916            block_size: 4096,
13917            chunk_size: 4096,
13918            envelope_target_size: 8192,
13919            stripe_width: 1,
13920            volume_loss_tolerance: 0,
13921            bit_rot_buffer_pct: 0,
13922            fec_data_shards: 4,
13923            fec_parity_shards: 0,
13924            index_fec_data_shards: 4,
13925            index_fec_parity_shards: 0,
13926            index_root_fec_data_shards: 4,
13927            index_root_fec_parity_shards: 0,
13928            ..WriterOptions::default()
13929        };
13930        let archive = write_archive(
13931            &[RegularFile::new("large.bin", &payload)],
13932            &master_key(),
13933            options,
13934        )
13935        .unwrap();
13936        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13937        let mut writer = ChunkRecorder { total: 0 };
13938        let mut progress_events = Vec::new();
13939        let mut progress = |archive_path: &str, bytes: u64| {
13940            progress_events.push((archive_path.to_owned(), bytes));
13941        };
13942
13943        opened
13944            .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
13945            .unwrap()
13946            .unwrap();
13947
13948        let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
13949        assert_eq!(writer.total, payload.len());
13950        assert_eq!(reported_bytes, payload.len() as u64);
13951        assert!(progress_events.len() > 1);
13952        assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
13953    }
13954
13955    #[test]
13956    fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
13957        let payload = pseudo_random_bytes(128 * 1024);
13958        let options = WriterOptions {
13959            block_size: 4096,
13960            chunk_size: 4096,
13961            envelope_target_size: 8192,
13962            stripe_width: 1,
13963            volume_loss_tolerance: 0,
13964            bit_rot_buffer_pct: 0,
13965            fec_data_shards: 4,
13966            fec_parity_shards: 0,
13967            index_fec_data_shards: 4,
13968            index_fec_parity_shards: 0,
13969            index_root_fec_data_shards: 4,
13970            index_root_fec_parity_shards: 0,
13971            ..WriterOptions::default()
13972        };
13973        let archive = write_archive(
13974            &[RegularFile::new("large.bin", &payload)],
13975            &master_key(),
13976            options,
13977        )
13978        .unwrap();
13979        let eager = open_archive(&archive.bytes, &master_key()).unwrap();
13980        let envelopes = envelope_entries_for_path(&eager, "large.bin");
13981        let last_envelope = envelopes
13982            .last()
13983            .expect("large fixture should have at least one envelope");
13984        assert_ne!(
13985            envelopes.first().unwrap().envelope_index,
13986            last_envelope.envelope_index,
13987            "fixture must span more than one payload envelope"
13988        );
13989        let corrupt_slot = block_record_slots(&archive.bytes)
13990            .into_iter()
13991            .enumerate()
13992            .find_map(|(slot, (_, _, record))| {
13993                (record.block_index == last_envelope.first_block_index).then_some(slot)
13994            })
13995            .unwrap();
13996        let mut corrupted = archive.bytes;
13997        corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
13998        let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
13999        let tmp = tempfile::tempdir().unwrap();
14000
14001        assert!(matches!(
14002            opened
14003                .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
14004                .unwrap_err(),
14005            FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
14006        ));
14007        assert!(!tmp.path().join("large.bin").exists());
14008        assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
14009    }
14010
14011    #[test]
14012    fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
14013        let archive = write_archive(
14014            &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
14015            &master_key(),
14016            single_stream_options(),
14017        )
14018        .unwrap();
14019        let opened = open_archive_with_bootstrap_sidecar(
14020            &archive.bytes,
14021            &archive.bootstrap_sidecar,
14022            &master_key(),
14023        )
14024        .unwrap();
14025
14026        assert_eq!(
14027            opened.list_files().unwrap(),
14028            vec![ArchiveEntry {
14029                path: "dir/sidecar.txt".to_string(),
14030                file_data_size: 13,
14031                kind: TarEntryKind::Regular,
14032                mode: 0o644,
14033                mtime: ArchiveTimestamp::UNIX_EPOCH,
14034                diagnostics: Vec::new(),
14035            }]
14036        );
14037        assert_eq!(
14038            opened.extract_file("dir/sidecar.txt").unwrap(),
14039            Some(b"hello sidecar".to_vec())
14040        );
14041        opened.verify().unwrap();
14042    }
14043
14044    #[test]
14045    fn fast_verify_plaintext_zero_recovery_defers_payload_semantics() {
14046        let options = WriterOptions {
14047            aead_algo: AeadAlgo::None,
14048            volume_loss_tolerance: 0,
14049            bit_rot_buffer_pct: 0,
14050            ..single_stream_options()
14051        };
14052        let archive = write_archive_unencrypted(
14053            &[RegularFile::new(
14054                "payload.txt",
14055                b"payload bytes large enough to produce a zstd frame",
14056            )],
14057            options,
14058        )
14059        .unwrap();
14060        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14061        let tables = opened.load_payload_index_tables().unwrap();
14062        let first_envelope = tables.envelopes.values().next().unwrap();
14063        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14064        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
14065        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14066        let payload_offset = crypto_end + first_envelope.first_block_index as usize * record_len;
14067
14068        let mut tampered = archive.bytes.clone();
14069        tampered[payload_offset + 16] ^= 0x01;
14070        let crc_offset = payload_offset + 16 + opened.crypto_header.block_size as usize;
14071        let crc = crc32c::crc32c(&tampered[payload_offset..crc_offset]);
14072        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
14073
14074        let tampered_opened = open_archive(&tampered, &master_key()).unwrap();
14075        assert!(tampered_opened.fast_verify_defers_payload_semantics());
14076        tampered_opened.verify_content_fast().unwrap();
14077        assert!(tampered_opened.verify_content().is_err());
14078    }
14079
14080    #[test]
14081    fn fast_verify_root_auth_archive_requires_full_root_auth_scan() {
14082        let archive = write_archive_with_root_auth(
14083            &[RegularFile::new("signed.txt", b"root-auth payload")],
14084            &master_key(),
14085            single_stream_options(),
14086            RootAuthWriterConfig {
14087                authenticator_id: 0x7777,
14088                signer_identity_type: 1,
14089                signer_identity: b"test signer",
14090                authenticator_value_length: 32,
14091            },
14092            |request| Ok(request.archive_root.to_vec()),
14093        )
14094        .unwrap();
14095
14096        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14097        assert!(!opened.fast_verify_defers_payload_semantics());
14098        assert!(matches!(
14099            opened.verify_content_fast().unwrap().mode,
14100            ContentVerificationMode::Fast
14101        ));
14102    }
14103
14104    #[test]
14105    fn fast_verify_dictionary_archive_does_not_defer_payload_semantics() {
14106        let archive = write_archive_with_dictionary(
14107            &[RegularFile::new("dict.txt", b"dictionary payload")],
14108            &master_key(),
14109            single_stream_options(),
14110            dictionary(),
14111        )
14112        .unwrap();
14113
14114        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14115        assert!(!opened.fast_verify_defers_payload_semantics());
14116    }
14117
14118    #[test]
14119    fn fast_verify_encrypted_archive_does_not_defer_payload_semantics() {
14120        let options = WriterOptions {
14121            aead_algo: AeadAlgo::AesGcmSiv256,
14122            volume_loss_tolerance: 0,
14123            bit_rot_buffer_pct: 0,
14124            ..single_stream_options()
14125        };
14126        let archive = write_archive(
14127            &[RegularFile::new("payload.txt", b"encrypted payload")],
14128            &master_key(),
14129            options,
14130        )
14131        .unwrap();
14132
14133        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14134        assert!(!opened.fast_verify_defers_payload_semantics());
14135    }
14136
14137    #[test]
14138    fn fast_verify_repair_archive_does_not_defer_payload_semantics() {
14139        let options = WriterOptions {
14140            fec_parity_shards: 2,
14141            volume_loss_tolerance: 0,
14142            bit_rot_buffer_pct: 0,
14143            ..single_stream_options()
14144        };
14145        let archive = write_archive(
14146            &[RegularFile::new("payload.txt", b"payload for repair")],
14147            &master_key(),
14148            options,
14149        )
14150        .unwrap();
14151
14152        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14153        assert!(!opened.fast_verify_defers_payload_semantics());
14154    }
14155
14156    #[test]
14157    fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
14158        let archive = write_archive_with_dictionary(
14159            &[RegularFile::new(
14160                "dir/dict.txt",
14161                b"common words common words dictionary payload",
14162            )],
14163            &master_key(),
14164            single_stream_options(),
14165            dictionary(),
14166        )
14167        .unwrap();
14168        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14169
14170        assert_eq!(opened.crypto_header.has_dictionary, 1);
14171        assert!(opened.index_root.header.dictionary_data_block_count > 0);
14172        assert_eq!(
14173            opened.list_files().unwrap(),
14174            vec![ArchiveEntry {
14175                path: "dir/dict.txt".to_string(),
14176                file_data_size: 44,
14177                kind: TarEntryKind::Regular,
14178                mode: 0o644,
14179                mtime: ArchiveTimestamp::UNIX_EPOCH,
14180                diagnostics: Vec::new(),
14181            }]
14182        );
14183        assert_eq!(
14184            opened.extract_file("dir/dict.txt").unwrap(),
14185            Some(b"common words common words dictionary payload".to_vec())
14186        );
14187        opened.verify().unwrap();
14188    }
14189
14190    #[test]
14191    fn dictionary_object_tamper_fails_before_payload_decompression() {
14192        let archive = write_archive_with_dictionary(
14193            &[RegularFile::new(
14194                "dir/dict.txt",
14195                b"common words common words dictionary payload",
14196            )],
14197            &master_key(),
14198            single_stream_options(),
14199            dictionary(),
14200        )
14201        .unwrap();
14202        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14203        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14204        let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
14205        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14206        let dictionary_offset =
14207            crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
14208
14209        let mut tampered = archive.bytes.clone();
14210        tampered[dictionary_offset + 16] ^= 0x01;
14211        let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
14212        let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
14213        tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
14214
14215        assert_eq!(
14216            open_archive(&tampered, &master_key()).unwrap_err(),
14217            FormatError::AeadFailure
14218        );
14219    }
14220
14221    #[test]
14222    fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
14223        let archive = write_archive_with_dictionary(
14224            &[RegularFile::new(
14225                "dict-sidecar.txt",
14226                b"common words common words sidecar payload",
14227            )],
14228            &master_key(),
14229            single_stream_options(),
14230            dictionary(),
14231        )
14232        .unwrap();
14233        let opened = open_non_seekable_archive(
14234            &archive.bytes,
14235            &master_key(),
14236            Some(&archive.bootstrap_sidecar),
14237        )
14238        .unwrap();
14239
14240        assert_eq!(
14241            opened.extract_file("dict-sidecar.txt").unwrap(),
14242            Some(b"common words common words sidecar payload".to_vec())
14243        );
14244        opened.verify().unwrap();
14245    }
14246
14247    #[test]
14248    fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
14249        let archive = write_archive(
14250            &[RegularFile::new(
14251                "sidecar-terminal.txt",
14252                b"sidecar authority",
14253            )],
14254            &master_key(),
14255            single_stream_options(),
14256        )
14257        .unwrap();
14258        let mut corrupted = archive.bytes.clone();
14259        corrupt_v41_terminal_recovery(&mut corrupted);
14260        assert!(open_archive(&corrupted, &master_key()).is_err());
14261
14262        let opened =
14263            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
14264                .unwrap();
14265
14266        assert!(opened.volume_trailer.is_none());
14267        assert_eq!(
14268            opened.extract_file("sidecar-terminal.txt").unwrap(),
14269            Some(b"sidecar authority".to_vec())
14270        );
14271        opened.verify().unwrap();
14272    }
14273
14274    #[test]
14275    fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
14276        let archive = write_archive_with_dictionary(
14277            &[RegularFile::new(
14278                "dict-no-terminal.txt",
14279                b"common words common words without terminal",
14280            )],
14281            &master_key(),
14282            single_stream_options(),
14283            dictionary(),
14284        )
14285        .unwrap();
14286        let terminal_offset = terminal_material_offset(&archive.bytes);
14287        let truncated = archive.bytes[..terminal_offset].to_vec();
14288        assert!(open_archive(&truncated, &master_key()).is_err());
14289
14290        let opened =
14291            open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
14292                .unwrap();
14293
14294        assert!(opened.volume_trailer.is_none());
14295        assert_eq!(
14296            opened.extract_file("dict-no-terminal.txt").unwrap(),
14297            Some(b"common words common words without terminal".to_vec())
14298        );
14299        opened.verify().unwrap();
14300    }
14301
14302    #[test]
14303    fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
14304        let archive = write_archive(
14305            &[RegularFile::new(
14306                "sidecar-erasure.txt",
14307                b"repair through sidecar",
14308            )],
14309            &master_key(),
14310            single_stream_options(),
14311        )
14312        .unwrap();
14313        let mut corrupted = archive.bytes.clone();
14314        corrupt_first_block_record_payload(&mut corrupted);
14315
14316        let opened = open_archive_with_bootstrap_sidecar(
14317            &corrupted,
14318            &archive.bootstrap_sidecar,
14319            &master_key(),
14320        )
14321        .unwrap();
14322        assert_eq!(
14323            opened.extract_file("sidecar-erasure.txt").unwrap(),
14324            Some(b"repair through sidecar".to_vec())
14325        );
14326    }
14327
14328    #[test]
14329    fn extraction_rejects_logical_payload_above_total_size_cap() {
14330        let archive = write_archive(
14331            &[RegularFile::new("cap.txt", b"payload")],
14332            &master_key(),
14333            single_stream_options(),
14334        )
14335        .unwrap();
14336        let options = ReaderOptions {
14337            max_total_extraction_size: 3,
14338            ..ReaderOptions::default()
14339        };
14340        let opened =
14341            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
14342
14343        assert_eq!(
14344            opened.extract_file("cap.txt").unwrap_err(),
14345            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14346        );
14347    }
14348
14349    #[test]
14350    fn verify_does_not_apply_extraction_payload_cap() {
14351        let archive = write_archive(
14352            &[RegularFile::new("verify-cap.txt", b"payload")],
14353            &master_key(),
14354            single_stream_options(),
14355        )
14356        .unwrap();
14357        let options = ReaderOptions {
14358            max_total_extraction_size: 3,
14359            ..ReaderOptions::default()
14360        };
14361        let opened =
14362            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
14363
14364        opened.verify().unwrap();
14365        assert_eq!(
14366            opened.extract_file("verify-cap.txt").unwrap_err(),
14367            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14368        );
14369    }
14370
14371    #[test]
14372    fn verify_streams_past_legacy_in_memory_tar_cap() {
14373        let data = vec![0x5a; 4096];
14374        let archive = write_archive(
14375            &[RegularFile::new("verify-large.txt", &data)],
14376            &master_key(),
14377            single_stream_options(),
14378        )
14379        .unwrap();
14380        let options = ReaderOptions {
14381            max_verify_tar_size: 1,
14382            ..ReaderOptions::default()
14383        };
14384        let opened =
14385            OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
14386
14387        opened.verify().unwrap();
14388    }
14389
14390    #[test]
14391    fn dictionary_sidecar_requires_dictionary_record_section() {
14392        let archive = write_archive_with_dictionary(
14393            &[RegularFile::new("dict-missing.txt", b"common words")],
14394            &master_key(),
14395            single_stream_options(),
14396            dictionary(),
14397        )
14398        .unwrap();
14399        let header = BootstrapSidecarHeader::parse(
14400            &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
14401        )
14402        .unwrap();
14403        let mut missing_dictionary =
14404            archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
14405        rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
14406            header.flags &= !0x04;
14407            header.dictionary_records_offset = 0;
14408            header.dictionary_records_length = 0;
14409        });
14410
14411        assert_eq!(
14412            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
14413                .unwrap_err(),
14414            FormatError::ReaderUnsupported("dictionary bootstrap required")
14415        );
14416    }
14417
14418    #[test]
14419    fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
14420        let archive = write_archive_with_dictionary(
14421            &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
14422            &master_key(),
14423            single_stream_options(),
14424            dictionary(),
14425        )
14426        .unwrap();
14427
14428        let mut wrong_kind = archive.bootstrap_sidecar.clone();
14429        mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
14430            record.kind = BlockKind::IndexRootData;
14431        });
14432        assert_eq!(
14433            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
14434                .unwrap_err(),
14435            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
14436        );
14437
14438        let mut wrong_last = archive.bootstrap_sidecar.clone();
14439        mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
14440            record.flags = 0;
14441        });
14442        assert_eq!(
14443            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
14444                .unwrap_err(),
14445            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
14446        );
14447    }
14448
14449    #[test]
14450    fn non_seekable_random_access_requires_sidecar() {
14451        let archive = write_archive(
14452            &[RegularFile::new("file.txt", b"payload")],
14453            &master_key(),
14454            single_stream_options(),
14455        )
14456        .unwrap();
14457
14458        assert_eq!(
14459            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
14460            FormatError::ReaderUnsupported(
14461                "non-seekable random access requires a bootstrap sidecar"
14462            )
14463        );
14464        assert!(open_non_seekable_archive(
14465            &archive.bytes,
14466            &master_key(),
14467            Some(&archive.bootstrap_sidecar)
14468        )
14469        .is_ok());
14470    }
14471
14472    #[test]
14473    fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
14474        let archive = write_archive(
14475            &[RegularFile::new("sparse.txt", b"sparse sidecar")],
14476            &master_key(),
14477            single_stream_options(),
14478        )
14479        .unwrap();
14480        let index_root_only = sparse_bootstrap_sidecar(
14481            &archive.bootstrap_sidecar,
14482            &master_key(),
14483            false,
14484            true,
14485            false,
14486        );
14487
14488        assert_eq!(
14489            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
14490                .unwrap_err(),
14491            FormatError::ReaderUnsupported(
14492                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
14493            )
14494        );
14495    }
14496
14497    #[test]
14498    fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
14499        let archive = write_archive(
14500            &[RegularFile::new("sparse-index.txt", b"recover index root")],
14501            &master_key(),
14502            single_stream_options(),
14503        )
14504        .unwrap();
14505        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14506        let mut corrupted = archive.bytes.clone();
14507        corrupt_object_extent_records(
14508            &mut corrupted,
14509            index_root_extent_from_manifest(&opened.manifest_footer),
14510        );
14511        assert!(open_archive(&corrupted, &master_key()).is_err());
14512
14513        let index_root_only = sparse_bootstrap_sidecar(
14514            &archive.bootstrap_sidecar,
14515            &master_key(),
14516            false,
14517            true,
14518            false,
14519        );
14520        let recovered =
14521            open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
14522                .unwrap();
14523
14524        assert_eq!(
14525            recovered.extract_file("sparse-index.txt").unwrap(),
14526            Some(b"recover index root".to_vec())
14527        );
14528        recovered.verify().unwrap();
14529    }
14530
14531    #[test]
14532    fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
14533        let archive = write_archive_with_dictionary(
14534            &[RegularFile::new(
14535                "sparse-dict.txt",
14536                b"common words common words sparse dictionary",
14537            )],
14538            &master_key(),
14539            single_stream_options(),
14540            dictionary(),
14541        )
14542        .unwrap();
14543        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14544        let mut corrupted = archive.bytes.clone();
14545        corrupt_object_extent_records(
14546            &mut corrupted,
14547            dictionary_extent_from_index_root(&opened.index_root).unwrap(),
14548        );
14549        assert!(open_archive(&corrupted, &master_key()).is_err());
14550
14551        let dictionary_only = sparse_bootstrap_sidecar(
14552            &archive.bootstrap_sidecar,
14553            &master_key(),
14554            false,
14555            false,
14556            true,
14557        );
14558        assert_eq!(
14559            open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
14560                .unwrap_err(),
14561            FormatError::ReaderUnsupported(
14562                "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
14563            )
14564        );
14565
14566        let recovered =
14567            open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
14568                .unwrap();
14569        assert_eq!(
14570            recovered.extract_file("sparse-dict.txt").unwrap(),
14571            Some(b"common words common words sparse dictionary".to_vec())
14572        );
14573        recovered.verify().unwrap();
14574    }
14575
14576    #[test]
14577    fn sequential_extracts_dictionary_free_tar_stream() {
14578        let archive = write_archive(
14579            &[RegularFile::new("seq.txt", b"streaming")],
14580            &master_key(),
14581            single_stream_options(),
14582        )
14583        .unwrap();
14584
14585        let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
14586        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
14587        assert_eq!(member.path, b"seq.txt");
14588        assert_eq!(member.data, b"streaming");
14589    }
14590
14591    #[test]
14592    fn sequential_rejects_logical_payload_above_total_size_cap() {
14593        let archive = write_archive(
14594            &[RegularFile::new("seq-cap.txt", b"payload")],
14595            &master_key(),
14596            single_stream_options(),
14597        )
14598        .unwrap();
14599        let options = ReaderOptions {
14600            max_total_extraction_size: 3,
14601            ..ReaderOptions::default()
14602        };
14603
14604        assert_eq!(
14605            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
14606                .unwrap_err(),
14607            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14608        );
14609    }
14610
14611    #[test]
14612    fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
14613        let archive = write_archive(
14614            &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
14615            &master_key(),
14616            single_stream_options(),
14617        )
14618        .unwrap();
14619        let options = ReaderOptions {
14620            max_verify_tar_size: 512,
14621            ..ReaderOptions::default()
14622        };
14623
14624        assert_eq!(
14625            sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
14626                .unwrap_err(),
14627            FormatError::ReaderUnsupported(
14628                "sequential tar stream exceeds configured verification cap"
14629            )
14630        );
14631    }
14632
14633    #[test]
14634    fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
14635        let archive = write_archive(
14636            &[RegularFile::new("seq-erasure.txt", b"stream repair")],
14637            &master_key(),
14638            single_stream_options(),
14639        )
14640        .unwrap();
14641        let mut corrupted = archive.bytes;
14642        corrupt_first_block_record_payload(&mut corrupted);
14643
14644        let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
14645        let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
14646        assert_eq!(member.path, b"seq-erasure.txt");
14647        assert_eq!(member.data, b"stream repair");
14648    }
14649
14650    #[test]
14651    fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
14652        let archive = write_archive(
14653            &[RegularFile::new("seq-no-parity.txt", b"no repair")],
14654            &master_key(),
14655            WriterOptions {
14656                bit_rot_buffer_pct: 0,
14657                fec_parity_shards: 0,
14658                index_fec_parity_shards: 0,
14659                index_root_fec_parity_shards: 0,
14660                ..single_stream_options()
14661            },
14662        )
14663        .unwrap();
14664        let mut corrupted = archive.bytes;
14665        corrupt_first_block_record_payload(&mut corrupted);
14666
14667        assert_eq!(
14668            sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
14669            FormatError::BadCrc {
14670                structure: "BlockRecord"
14671            }
14672        );
14673    }
14674
14675    #[test]
14676    fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
14677        let archive = write_archive(
14678            &[RegularFile::new(
14679                "seq.txt",
14680                b"payload must not be returned after terminal auth failure",
14681            )],
14682            &master_key(),
14683            single_stream_options(),
14684        )
14685        .unwrap();
14686        let mut corrupted = archive.bytes;
14687        corrupt_v41_terminal_recovery(&mut corrupted);
14688
14689        match sequential_extract_tar_stream(&corrupted, &master_key()) {
14690            Ok(bytes) => panic!(
14691                "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
14692                bytes.len()
14693            ),
14694            Err(err) => assert_eq!(
14695                err,
14696                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
14697            ),
14698        }
14699    }
14700
14701    #[test]
14702    fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
14703        let archive = write_archive_with_dictionary(
14704            &[RegularFile::new(
14705                "seq-dict.txt",
14706                b"common words common words dictionary payload",
14707            )],
14708            &master_key(),
14709            single_stream_options(),
14710            b"common words dictionary",
14711        )
14712        .unwrap();
14713
14714        match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
14715            Ok(bytes) => panic!(
14716                "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
14717                bytes.len()
14718            ),
14719            Err(err) => assert_eq!(
14720                err,
14721                FormatError::ReaderUnsupported(
14722                    "dictionary bootstrap required for non-seekable sequential extraction"
14723                )
14724            ),
14725        }
14726    }
14727
14728    #[test]
14729    fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
14730        let archive = write_archive_with_dictionary(
14731            &[RegularFile::new(
14732                "seq-dict-open.txt",
14733                b"common words common words bootstrap required",
14734            )],
14735            &master_key(),
14736            single_stream_options(),
14737            b"common words bootstrap",
14738        )
14739        .unwrap();
14740
14741        assert_eq!(
14742            open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
14743            FormatError::ReaderUnsupported(
14744                "non-seekable random access requires a bootstrap sidecar"
14745            )
14746        );
14747    }
14748
14749    #[test]
14750    fn sequential_zstd_stream_rejects_skippable_frame_segments() {
14751        let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
14752        let mut output = Vec::new();
14753
14754        assert_eq!(
14755            decode_concatenated_zstd_frames_with_cap(
14756                &skippable,
14757                None,
14758                &mut output,
14759                usize::MAX,
14760                None,
14761            )
14762            .unwrap_err(),
14763            FormatError::NotStandardZstdFrame
14764        );
14765        assert!(output.is_empty());
14766    }
14767
14768    #[test]
14769    fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
14770        let archive = write_archive(
14771            &[RegularFile::new("live.txt", b"stream verify")],
14772            &master_key(),
14773            single_stream_options(),
14774        )
14775        .unwrap();
14776
14777        let report =
14778            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
14779
14780        assert_eq!(report.file_count, 1);
14781        assert_eq!(report.total_volumes, 1);
14782        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
14783        assert!(report.payload_block_count > 0);
14784    }
14785
14786    #[test]
14787    fn live_non_seekable_verify_stream_accepts_recipientwrap_archive() {
14788        let master = master_key();
14789        let archive = write_archive_with_recipient_wrap_records(
14790            &[RegularFile::new(
14791                "wrapped-live.txt",
14792                b"stream recipient verify",
14793            )],
14794            &master,
14795            single_stream_options(),
14796            vec![recipient_wrap_test_record()],
14797        )
14798        .unwrap();
14799
14800        let mut called = false;
14801        let report = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
14802            std::io::Cursor::new(archive.bytes),
14803            |context| {
14804                called = true;
14805                assert_eq!(
14806                    context.archive_identity.volume_format_rev,
14807                    VOLUME_FORMAT_REV_45
14808                );
14809                assert_eq!(context.record.profile_id, 1);
14810                Ok(vec![master.0])
14811            },
14812            NonSeekableReaderOptions::default(),
14813        )
14814        .unwrap();
14815
14816        assert!(called);
14817        assert_eq!(report.file_count, 1);
14818        assert_eq!(report.total_volumes, 1);
14819        assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
14820    }
14821
14822    #[test]
14823    fn live_non_seekable_recipientwrap_resolver_rejects_unencrypted_archive() {
14824        let archive = write_archive_unencrypted(
14825            &[RegularFile::new("plain-live.txt", b"plaintext payload")],
14826            single_stream_options(),
14827        )
14828        .unwrap();
14829
14830        let mut called = false;
14831        let err = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
14832            std::io::Cursor::new(archive.bytes),
14833            |_| {
14834                called = true;
14835                Ok(vec![master_key().0])
14836            },
14837            NonSeekableReaderOptions::default(),
14838        )
14839        .unwrap_err();
14840
14841        assert!(!called);
14842        assert_eq!(err, FormatError::KeyMaterialMismatch);
14843    }
14844
14845    #[test]
14846    fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
14847        let archive = write_archive(
14848            &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
14849            &master_key(),
14850            single_stream_options(),
14851        )
14852        .unwrap();
14853
14854        let report =
14855            verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
14856                .unwrap();
14857
14858        assert_eq!(report.file_count, 1);
14859        assert_eq!(report.tar_total_size % 512, 0);
14860    }
14861
14862    #[test]
14863    fn live_non_seekable_verify_stream_accepts_empty_archive() {
14864        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14865
14866        let report =
14867            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
14868
14869        assert_eq!(report.file_count, 0);
14870        assert_eq!(report.payload_block_count, 0);
14871        assert_eq!(report.tar_total_size, 0);
14872    }
14873
14874    #[test]
14875    fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
14876        let archive = write_archive_with_dictionary(
14877            &[RegularFile::new(
14878                "live-dict.txt",
14879                b"common words common words dictionary payload",
14880            )],
14881            &master_key(),
14882            single_stream_options(),
14883            b"common words dictionary",
14884        )
14885        .unwrap();
14886
14887        assert_eq!(
14888            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
14889                .unwrap_err(),
14890            FormatError::ReaderUnsupported(
14891                "dictionary bootstrap required for non-seekable sequential verification"
14892            )
14893        );
14894    }
14895
14896    #[test]
14897    fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
14898        let archive = write_archive_with_dictionary(
14899            &[RegularFile::new(
14900                "live-dict-sidecar.txt",
14901                b"common words common words dictionary payload",
14902            )],
14903            &master_key(),
14904            single_stream_options(),
14905            b"common words dictionary",
14906        )
14907        .unwrap();
14908
14909        let report = verify_non_seekable_stream_with_bootstrap_sidecar(
14910            std::io::Cursor::new(archive.bytes),
14911            &archive.bootstrap_sidecar,
14912            &master_key(),
14913            NonSeekableReaderOptions::default(),
14914        )
14915        .unwrap();
14916
14917        assert_eq!(report.file_count, 1);
14918        assert_eq!(report.total_volumes, 1);
14919    }
14920
14921    #[test]
14922    fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
14923        let archive = write_archive(
14924            &[RegularFile::new("tail-cap.txt", b"payload")],
14925            &master_key(),
14926            single_stream_options(),
14927        )
14928        .unwrap();
14929        let options = NonSeekableReaderOptions {
14930            max_terminal_tail_size: 8,
14931            ..NonSeekableReaderOptions::default()
14932        };
14933
14934        assert_eq!(
14935            verify_non_seekable_stream_with_options(
14936                std::io::Cursor::new(archive.bytes),
14937                &master_key(),
14938                options
14939            )
14940            .unwrap_err(),
14941            FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
14942        );
14943    }
14944
14945    #[test]
14946    fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
14947        let archive = write_archive(
14948            &[RegularFile::new("metadata-cap.txt", b"payload")],
14949            &master_key(),
14950            single_stream_options(),
14951        )
14952        .unwrap();
14953        let options = NonSeekableReaderOptions {
14954            max_retained_metadata_bytes: 1,
14955            ..NonSeekableReaderOptions::default()
14956        };
14957
14958        assert_eq!(
14959            verify_non_seekable_stream_with_options(
14960                std::io::Cursor::new(archive.bytes),
14961                &master_key(),
14962                options
14963            )
14964            .unwrap_err(),
14965            FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
14966        );
14967    }
14968
14969    #[test]
14970    fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
14971        let archive = write_archive(
14972            &[RegularFile::new("metadata-erasure.txt", b"payload")],
14973            &master_key(),
14974            single_stream_options(),
14975        )
14976        .unwrap();
14977        let mut corrupted = archive.bytes;
14978        let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
14979        corrupt_block_record_payload_at_slot(&mut corrupted, slot);
14980
14981        let report =
14982            verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
14983
14984        assert_eq!(report.file_count, 1);
14985    }
14986
14987    #[test]
14988    fn live_non_seekable_verify_rejects_member_count_above_cap() {
14989        let archive = write_archive(
14990            &[RegularFile::new("member-cap.txt", b"payload")],
14991            &master_key(),
14992            single_stream_options(),
14993        )
14994        .unwrap();
14995        let options = NonSeekableReaderOptions {
14996            max_streamed_member_count: 0,
14997            ..NonSeekableReaderOptions::default()
14998        };
14999
15000        assert_eq!(
15001            verify_non_seekable_stream_with_options(
15002                std::io::Cursor::new(archive.bytes),
15003                &master_key(),
15004                options
15005            )
15006            .unwrap_err(),
15007            FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
15008        );
15009    }
15010
15011    #[test]
15012    fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
15013        let archive = write_archive(
15014            &[RegularFile::new("live-total-cap.txt", b"payload")],
15015            &master_key(),
15016            single_stream_options(),
15017        )
15018        .unwrap();
15019        let mut options = NonSeekableReaderOptions::default();
15020        options.reader.max_total_extraction_size = 3;
15021
15022        assert_eq!(
15023            verify_non_seekable_stream_with_options(
15024                std::io::Cursor::new(archive.bytes),
15025                &master_key(),
15026                options
15027            )
15028            .unwrap_err(),
15029            FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
15030        );
15031    }
15032
15033    #[test]
15034    fn live_non_seekable_verify_reports_root_auth_wire_only() {
15035        let archive = write_archive_with_root_auth(
15036            &[RegularFile::new("signed-live.txt", b"root-auth stream")],
15037            &master_key(),
15038            single_stream_options(),
15039            test_root_auth_config(),
15040            |request| Ok(test_root_auth_value(request)),
15041        )
15042        .unwrap();
15043
15044        let report =
15045            verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
15046
15047        assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
15048    }
15049
15050    #[test]
15051    fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
15052        let archive = write_archive(
15053            &[
15054                RegularFile::new("alpha.txt", b"alpha"),
15055                RegularFile::new("nested/beta.txt", b"beta"),
15056            ],
15057            &master_key(),
15058            single_stream_options(),
15059        )
15060        .unwrap();
15061        let tmp = tempfile::tempdir().unwrap();
15062        let out = tmp.path().join("out");
15063
15064        let report = extract_non_seekable_stream_to_dir(
15065            std::io::Cursor::new(archive.bytes),
15066            &master_key(),
15067            &out,
15068            NonSeekableReaderOptions::default(),
15069            SafeExtractionOptions::default(),
15070        )
15071        .unwrap();
15072
15073        assert_eq!(report.verification.file_count, 2);
15074        assert_eq!(report.extracted_member_count, 2);
15075        assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
15076        assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
15077    }
15078
15079    #[test]
15080    fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
15081        let archive = write_archive(
15082            &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
15083            &master_key(),
15084            single_stream_options(),
15085        )
15086        .unwrap();
15087        let tmp = tempfile::tempdir().unwrap();
15088        let out = tmp.path().join("out");
15089
15090        extract_non_seekable_stream_to_dir(
15091            ChunkedReader::new(archive.bytes, 1),
15092            &master_key(),
15093            &out,
15094            NonSeekableReaderOptions::default(),
15095            SafeExtractionOptions::default(),
15096        )
15097        .unwrap();
15098
15099        assert_eq!(
15100            fs::read(out.join("tiny-extract.txt")).unwrap(),
15101            b"chunked extraction"
15102        );
15103    }
15104
15105    #[test]
15106    fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
15107        let archive = write_archive(
15108            &[RegularFile::new("late-fail.txt", b"must remain staged")],
15109            &master_key(),
15110            single_stream_options(),
15111        )
15112        .unwrap();
15113        let mut corrupted = archive.bytes;
15114        corrupt_v41_terminal_recovery(&mut corrupted);
15115        let tmp = tempfile::tempdir().unwrap();
15116        let out = tmp.path().join("out");
15117
15118        match extract_non_seekable_stream_to_dir(
15119            std::io::Cursor::new(corrupted),
15120            &master_key(),
15121            &out,
15122            NonSeekableReaderOptions::default(),
15123            SafeExtractionOptions::default(),
15124        )
15125        .unwrap_err()
15126        {
15127            ExtractError::Format(err) => assert_eq!(
15128                err,
15129                FormatError::InvalidArchive("no valid v41 CMRA candidate found")
15130            ),
15131            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
15132        }
15133        assert!(!out.exists());
15134    }
15135
15136    #[test]
15137    fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
15138        let archive = write_archive(
15139            &[RegularFile::new("same.txt", b"new")],
15140            &master_key(),
15141            single_stream_options(),
15142        )
15143        .unwrap();
15144        let tmp = tempfile::tempdir().unwrap();
15145        let out = tmp.path().join("out");
15146        fs::create_dir(&out).unwrap();
15147        fs::write(out.join("same.txt"), b"old").unwrap();
15148
15149        match extract_non_seekable_stream_to_dir(
15150            std::io::Cursor::new(archive.bytes.clone()),
15151            &master_key(),
15152            &out,
15153            NonSeekableReaderOptions::default(),
15154            SafeExtractionOptions::default(),
15155        )
15156        .unwrap_err()
15157        {
15158            ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
15159            ExtractError::Output(err) => panic!("unexpected output error: {err}"),
15160        }
15161        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
15162
15163        extract_non_seekable_stream_to_dir(
15164            std::io::Cursor::new(archive.bytes),
15165            &master_key(),
15166            &out,
15167            NonSeekableReaderOptions::default(),
15168            SafeExtractionOptions {
15169                overwrite_existing: true,
15170                ..SafeExtractionOptions::default()
15171            },
15172        )
15173        .unwrap();
15174        assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
15175    }
15176
15177    #[test]
15178    fn live_non_seekable_list_stream_matches_seekable_final_view() {
15179        let archive = write_archive(
15180            &[
15181                RegularFile::new("a.txt", b"a"),
15182                RegularFile::new("b.txt", b"bb"),
15183            ],
15184            &master_key(),
15185            single_stream_options(),
15186        )
15187        .unwrap();
15188        let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
15189        let expected = seekable.list_files().unwrap();
15190
15191        let report = list_non_seekable_stream(
15192            std::io::Cursor::new(archive.bytes),
15193            &master_key(),
15194            NonSeekableReaderOptions::default(),
15195        )
15196        .unwrap();
15197
15198        assert_eq!(report.verification.file_count, 2);
15199        assert_eq!(report.entries, expected);
15200    }
15201
15202    #[test]
15203    fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
15204        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15205        let mut bad_flags = archive.bootstrap_sidecar.clone();
15206        rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
15207            header.flags |= 0x08;
15208        });
15209        assert_eq!(
15210            open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
15211                .unwrap_err(),
15212            FormatError::UnknownBootstrapSidecarFlags(0x0b)
15213        );
15214
15215        let mut trailing = archive.bootstrap_sidecar.clone();
15216        trailing.push(0);
15217        assert_eq!(
15218            open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
15219                .unwrap_err(),
15220            FormatError::NonCanonicalBootstrapSidecarLayout
15221        );
15222    }
15223
15224    #[test]
15225    fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
15226        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15227        let mut wrong_volume = archive.bootstrap_sidecar.clone();
15228        mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
15229            footer.volume_index = 1;
15230        });
15231        assert_eq!(
15232            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
15233                .unwrap_err(),
15234            FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
15235        );
15236
15237        let mut non_authoritative = archive.bootstrap_sidecar.clone();
15238        mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
15239            footer.is_authoritative = 0;
15240        });
15241        assert_eq!(
15242            open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
15243                .unwrap_err(),
15244            FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
15245        );
15246    }
15247
15248    #[test]
15249    fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
15250        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15251        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
15252        let crypto_start = volume_header.crypto_header_offset as usize;
15253        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
15254        let crypto_header = CryptoHeader::parse(
15255            &archive.bytes[crypto_start..crypto_end],
15256            volume_header.crypto_header_length,
15257        )
15258        .unwrap();
15259        let subkeys = Subkeys::derive(
15260            &master_key(),
15261            &volume_header.archive_uuid,
15262            &volume_header.session_id,
15263        )
15264        .unwrap();
15265        let mut opened_header = volume_header;
15266        opened_header.volume_index = 1;
15267
15268        let parsed = parse_bootstrap_sidecar(
15269            &archive.bootstrap_sidecar,
15270            &opened_header,
15271            &crypto_header.fixed,
15272            &subkeys,
15273        )
15274        .unwrap();
15275
15276        assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
15277    }
15278
15279    #[test]
15280    fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
15281        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15282        let mut conflicting = archive.bootstrap_sidecar.clone();
15283        mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
15284            footer.index_root_first_block += 1;
15285        });
15286
15287        assert_eq!(
15288            open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
15289                .unwrap_err(),
15290            FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
15291        );
15292    }
15293
15294    #[test]
15295    fn sidecar_size_cap_counts_only_present_sparse_sections() {
15296        let mut crypto_header = test_crypto_header();
15297        crypto_header.has_dictionary = 1;
15298        crypto_header.index_root_fec_data_shards = 1;
15299        crypto_header.index_root_fec_parity_shards = 0;
15300        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
15301        let header = BootstrapSidecarHeader {
15302            archive_uuid: [0x31; 16],
15303            session_id: [0x42; 16],
15304            flags: 0x04,
15305            manifest_footer_offset: 0,
15306            manifest_footer_length: 0,
15307            index_root_records_offset: 0,
15308            index_root_records_length: 0,
15309            dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
15310            dictionary_records_length: record_len,
15311            sidecar_hmac: [0u8; 32],
15312            header_crc32c: 0,
15313        };
15314
15315        validate_sidecar_size_cap(
15316            &header,
15317            &crypto_header,
15318            BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
15319        )
15320        .unwrap();
15321        assert_eq!(
15322            validate_sidecar_size_cap(
15323                &header,
15324                &crypto_header,
15325                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
15326            )
15327            .unwrap_err(),
15328            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
15329        );
15330    }
15331
15332    #[test]
15333    fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
15334        let mut crypto_header = test_crypto_header();
15335        crypto_header.index_root_fec_data_shards = 1;
15336        crypto_header.index_root_fec_parity_shards = 0;
15337        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
15338        let header = BootstrapSidecarHeader {
15339            archive_uuid: [0x31; 16],
15340            session_id: [0x42; 16],
15341            flags: 0x02,
15342            manifest_footer_offset: 0,
15343            manifest_footer_length: 0,
15344            index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
15345            index_root_records_length: record_len * 2,
15346            dictionary_records_offset: 0,
15347            dictionary_records_length: 0,
15348            sidecar_hmac: [0u8; 32],
15349            header_crc32c: 0,
15350        };
15351
15352        assert_eq!(
15353            validate_sidecar_size_cap(
15354                &header,
15355                &crypto_header,
15356                BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
15357            )
15358            .unwrap_err(),
15359            FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
15360        );
15361    }
15362
15363    #[test]
15364    fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
15365        let mut crypto_header = test_crypto_header();
15366        crypto_header.block_size = u32::MAX;
15367        crypto_header.index_root_fec_data_shards = u16::MAX;
15368        crypto_header.index_root_fec_parity_shards = u16::MAX;
15369        let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
15370        let max_records = crypto_header.index_root_fec_data_shards as u64
15371            + crypto_header.index_root_fec_parity_shards as u64;
15372        let max_section_len = max_records * record_len;
15373        let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
15374            + MANIFEST_FOOTER_LEN as u64
15375            + max_section_len
15376            + max_section_len;
15377        let header = BootstrapSidecarHeader {
15378            archive_uuid: [0x31; 16],
15379            session_id: [0x42; 16],
15380            flags: 0x01 | 0x02 | 0x04,
15381            manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
15382            manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
15383            index_root_records_offset: 0,
15384            index_root_records_length: max_section_len,
15385            dictionary_records_offset: 0,
15386            dictionary_records_length: max_section_len,
15387            sidecar_hmac: [0u8; 32],
15388            header_crc32c: 0,
15389        };
15390
15391        validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
15392        assert_eq!(
15393            validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
15394            FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
15395        );
15396    }
15397
15398    #[test]
15399    fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
15400        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15401        let mut with_dictionary = archive.bootstrap_sidecar.clone();
15402        let header =
15403            BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
15404                .unwrap();
15405        let record_len = sidecar_record_len(&with_dictionary);
15406        let first_record = header.index_root_records_offset as usize;
15407        let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
15408        let dictionary_offset = with_dictionary.len() as u64;
15409        with_dictionary.extend_from_slice(&copied_record);
15410        rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
15411            header.flags |= 0x04;
15412            header.dictionary_records_offset = dictionary_offset;
15413            header.dictionary_records_length = record_len as u64;
15414        });
15415
15416        assert_eq!(
15417            open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
15418                .unwrap_err(),
15419            FormatError::InvalidArchive(
15420                "bootstrap sidecar has dictionary records while has_dictionary is false"
15421            )
15422        );
15423    }
15424
15425    #[test]
15426    fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
15427        let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
15428        let mut missing = archive.bootstrap_sidecar.clone();
15429        let record_len = sidecar_record_len(&missing);
15430        let new_len = missing.len() - record_len;
15431        missing.truncate(new_len);
15432        rewrite_sidecar_header(&mut missing, &master_key(), |header| {
15433            header.index_root_records_length -= record_len as u64;
15434        });
15435        assert_eq!(
15436            open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
15437                .unwrap_err(),
15438            FormatError::InvalidArchive(
15439                "sidecar BlockRecord section does not match declared extent"
15440            )
15441        );
15442
15443        let mut duplicate = archive.bootstrap_sidecar.clone();
15444        mutate_sidecar_index_record(&mut duplicate, 1, |record| {
15445            record.block_index -= 1;
15446        });
15447        assert_eq!(
15448            open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
15449                .unwrap_err(),
15450            FormatError::InvalidArchive(
15451                "sidecar BlockRecord section has missing or duplicate blocks"
15452            )
15453        );
15454
15455        let mut misordered = archive.bootstrap_sidecar.clone();
15456        swap_sidecar_index_records(&mut misordered, 0, 1);
15457        assert_eq!(
15458            open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
15459                .unwrap_err(),
15460            FormatError::InvalidArchive(
15461                "sidecar BlockRecord section has missing or duplicate blocks"
15462            )
15463        );
15464
15465        let mut wrong_kind = archive.bootstrap_sidecar.clone();
15466        mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
15467            record.kind = BlockKind::PayloadData;
15468        });
15469        assert_eq!(
15470            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
15471                .unwrap_err(),
15472            FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
15473        );
15474
15475        let mut wrong_last = archive.bootstrap_sidecar.clone();
15476        mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
15477            record.flags = 0;
15478        });
15479        assert_eq!(
15480            open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
15481                .unwrap_err(),
15482            FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
15483        );
15484    }
15485
15486    #[test]
15487    fn verify_helper_rejects_envelope_frame_coverage_gap() {
15488        let frames = BTreeMap::from([(
15489            0,
15490            FrameEntry {
15491                frame_index: 0,
15492                envelope_index: 0,
15493                offset_in_envelope: 0,
15494                compressed_size: 10,
15495                decompressed_size: 512,
15496                flags: 0,
15497                tar_stream_offset: 0,
15498            },
15499        )]);
15500        let envelopes = BTreeMap::from([(
15501            0,
15502            EnvelopeEntry {
15503                envelope_index: 0,
15504                first_block_index: 0,
15505                data_block_count: 1,
15506                parity_block_count: 1,
15507                encrypted_size: 4096,
15508                plaintext_size: 11,
15509                first_frame_index: 0,
15510                frame_count: 1,
15511            },
15512        )]);
15513
15514        assert_eq!(
15515            validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
15516            FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
15517        );
15518    }
15519
15520    #[test]
15521    fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
15522        assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
15523        assert_eq!(
15524            validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
15525            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
15526        );
15527        assert_eq!(
15528            validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
15529            FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
15530        );
15531    }
15532
15533    #[test]
15534    fn verify_rejects_authenticated_content_hash_mismatch() {
15535        let options = WriterOptions {
15536            index_root_fec_parity_shards: 0,
15537            ..single_stream_options()
15538        };
15539        let archive = write_archive(
15540            &[RegularFile::new("content-hash.txt", b"hash covered")],
15541            &master_key(),
15542            options,
15543        )
15544        .unwrap();
15545        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
15546
15547        let mut root = opened.index_root.clone();
15548        root.header.content_sha256 = [0xa5; 32];
15549        let root_plaintext = root.to_bytes();
15550        IndexRoot::parse(
15551            &root_plaintext,
15552            false,
15553            metadata_limits(&opened.crypto_header),
15554        )
15555        .unwrap();
15556        assert_eq!(
15557            root_plaintext.len() as u32,
15558            opened.manifest_footer.index_root_decompressed_size
15559        );
15560
15561        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
15562        let mut next_block_index = opened.manifest_footer.index_root_first_block;
15563        let replacement = encrypt_test_object(
15564            &compressed_root,
15565            &opened.subkeys.index_root_key,
15566            &opened.subkeys.index_nonce_seed,
15567            b"idxroot",
15568            0,
15569            BlockKind::IndexRootData,
15570            &mut next_block_index,
15571            &opened.crypto_header,
15572            &opened.volume_header,
15573        );
15574        assert_eq!(
15575            replacement.extent.data_block_count,
15576            opened.manifest_footer.index_root_data_block_count
15577        );
15578        assert_eq!(
15579            replacement.extent.encrypted_size,
15580            opened.manifest_footer.index_root_encrypted_size
15581        );
15582
15583        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
15584        let crypto_end = volume_header.crypto_header_offset as usize
15585            + volume_header.crypto_header_length as usize;
15586        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
15587        let mut malformed = archive.bytes.clone();
15588        for record in replacement.records {
15589            let offset = crypto_end + record.block_index as usize * record_len;
15590            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
15591        }
15592
15593        let reopened = open_archive(&malformed, &master_key()).unwrap();
15594        assert_eq!(
15595            reopened.verify().unwrap_err(),
15596            FormatError::InvalidArchive(
15597                "IndexRoot content_sha256 does not match decoded tar stream"
15598            )
15599        );
15600    }
15601
15602    #[test]
15603    fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
15604        let (mut path_mismatch, _) = multi_envelope_reader_fixture();
15605        rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
15606            path[0] = b'x';
15607        });
15608        assert_eq!(
15609            path_mismatch.verify().unwrap_err(),
15610            FormatError::InvalidArchive("tar member path does not match FileEntry path")
15611        );
15612
15613        let (mut size_mismatch, _) = multi_envelope_reader_fixture();
15614        rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
15615            file.file_data_size += 1;
15616        });
15617        assert_eq!(
15618            size_mismatch.verify().unwrap_err(),
15619            FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
15620        );
15621    }
15622
15623    #[test]
15624    fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
15625        let (mut opened, _) = multi_envelope_reader_fixture();
15626        let locating = opened.index_root.shards[0].clone();
15627        let mut duplicate = opened.load_index_shard(&locating).unwrap();
15628        duplicate.header.shard_index = 1;
15629        duplicate.frames[0].flags ^= 0x0000_0001;
15630        let duplicate_plaintext = duplicate.to_bytes();
15631        let mut next_block_index = opened
15632            .blocks
15633            .keys()
15634            .last()
15635            .copied()
15636            .map(|index| index + 1)
15637            .unwrap_or(0);
15638        let duplicate_object = encrypt_test_object(
15639            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
15640            &opened.subkeys.index_shard_key,
15641            &opened.subkeys.index_nonce_seed,
15642            b"idxshard",
15643            1,
15644            BlockKind::IndexShardData,
15645            &mut next_block_index,
15646            &opened.crypto_header,
15647            &opened.volume_header,
15648        );
15649        insert_records(&mut opened.blocks, &duplicate_object.records);
15650        opened.index_root.shards.push(ShardEntry {
15651            shard_index: 1,
15652            first_block_index: duplicate_object.extent.first_block_index,
15653            data_block_count: duplicate_object.extent.data_block_count,
15654            parity_block_count: 0,
15655            encrypted_size: duplicate_object.extent.encrypted_size,
15656            decompressed_size: duplicate_plaintext.len() as u32,
15657            file_count: locating.file_count,
15658            first_path_hash: locating.first_path_hash,
15659            last_path_hash: locating.last_path_hash,
15660        });
15661        opened.index_root.header.file_count += locating.file_count as u64;
15662
15663        assert_eq!(
15664            opened.verify().unwrap_err(),
15665            FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
15666        );
15667    }
15668
15669    #[test]
15670    fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
15671        let (mut opened, _) = multi_envelope_reader_fixture();
15672        let locating = opened.index_root.shards[0].clone();
15673        let mut duplicate = opened.load_index_shard(&locating).unwrap();
15674        duplicate.header.shard_index = 1;
15675        duplicate.envelopes[0].first_block_index += 1;
15676        let duplicate_plaintext = duplicate.to_bytes();
15677        let mut next_block_index = opened
15678            .blocks
15679            .keys()
15680            .last()
15681            .copied()
15682            .map(|index| index + 1)
15683            .unwrap_or(0);
15684        let duplicate_object = encrypt_test_object(
15685            &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
15686            &opened.subkeys.index_shard_key,
15687            &opened.subkeys.index_nonce_seed,
15688            b"idxshard",
15689            1,
15690            BlockKind::IndexShardData,
15691            &mut next_block_index,
15692            &opened.crypto_header,
15693            &opened.volume_header,
15694        );
15695        insert_records(&mut opened.blocks, &duplicate_object.records);
15696        opened.index_root.shards.push(ShardEntry {
15697            shard_index: 1,
15698            first_block_index: duplicate_object.extent.first_block_index,
15699            data_block_count: duplicate_object.extent.data_block_count,
15700            parity_block_count: 0,
15701            encrypted_size: duplicate_object.extent.encrypted_size,
15702            decompressed_size: duplicate_plaintext.len() as u32,
15703            file_count: locating.file_count,
15704            first_path_hash: locating.first_path_hash,
15705            last_path_hash: locating.last_path_hash,
15706        });
15707        opened.index_root.header.file_count += locating.file_count as u64;
15708
15709        assert_eq!(
15710            opened.verify().unwrap_err(),
15711            FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
15712        );
15713    }
15714
15715    #[test]
15716    fn verify_rejects_non_contiguous_global_envelope_indexes() {
15717        let (mut opened, _) = multi_envelope_reader_fixture();
15718        replace_first_index_shard(&mut opened, |shard| {
15719            let frame = shard
15720                .frames
15721                .iter_mut()
15722                .find(|entry| entry.frame_index == 1)
15723                .unwrap();
15724            frame.envelope_index = 2;
15725
15726            let envelope = shard
15727                .envelopes
15728                .iter_mut()
15729                .find(|entry| entry.envelope_index == 1)
15730                .unwrap();
15731            envelope.envelope_index = 2;
15732        });
15733
15734        assert_eq!(
15735            opened.verify().unwrap_err(),
15736            FormatError::InvalidMetadata {
15737                structure: "EnvelopeEntry",
15738                reason: "global index coverage has a gap",
15739            }
15740        );
15741    }
15742
15743    #[test]
15744    fn verify_rejects_payload_object_extent_overlap() {
15745        let (mut opened, _) = multi_envelope_reader_fixture();
15746        replace_first_index_shard(&mut opened, |shard| {
15747            let first_block_index = shard.envelopes[0].first_block_index;
15748            shard.envelopes[1].first_block_index = first_block_index;
15749        });
15750
15751        assert_eq!(
15752            opened.verify().unwrap_err(),
15753            FormatError::InvalidArchive("encrypted object block ranges overlap")
15754        );
15755    }
15756
15757    #[test]
15758    fn verify_accepts_cross_shard_shared_envelope_frame_union() {
15759        let volume_header = test_volume_header();
15760        let crypto_header = test_crypto_header();
15761        let subkeys = Subkeys::derive(
15762            &master_key(),
15763            &volume_header.archive_uuid,
15764            &volume_header.session_id,
15765        )
15766        .unwrap();
15767        let mut next_block_index = 0u64;
15768        let mut blocks = BTreeMap::new();
15769
15770        let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
15771        let beta = test_member(b"beta.txt", b"beta cross shard\n");
15772        let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
15773        let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
15774        let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
15775        let envelope_plaintext =
15776            [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
15777        let payload = encrypt_test_object(
15778            &envelope_plaintext,
15779            &subkeys.enc_key,
15780            &subkeys.nonce_seed,
15781            b"envelope",
15782            0,
15783            BlockKind::PayloadData,
15784            &mut next_block_index,
15785            &crypto_header,
15786            &volume_header,
15787        );
15788        insert_records(&mut blocks, &payload.records);
15789
15790        let envelope = EnvelopeEntry {
15791            envelope_index: 0,
15792            first_block_index: payload.extent.first_block_index,
15793            data_block_count: payload.extent.data_block_count,
15794            parity_block_count: 0,
15795            encrypted_size: payload.extent.encrypted_size,
15796            plaintext_size: envelope_plaintext.len() as u32,
15797            first_frame_index: 0,
15798            frame_count: 2,
15799        };
15800        let frame0 = FrameEntry {
15801            frame_index: 0,
15802            envelope_index: 0,
15803            offset_in_envelope: 0,
15804            compressed_size: frame0_plaintext.len() as u32,
15805            decompressed_size: alpha.len() as u32,
15806            flags: 0x0000_0003,
15807            tar_stream_offset: 0,
15808        };
15809        let frame1 = FrameEntry {
15810            frame_index: 1,
15811            envelope_index: 0,
15812            offset_in_envelope: frame0_plaintext.len() as u32,
15813            compressed_size: frame1_plaintext.len() as u32,
15814            decompressed_size: beta.len() as u32,
15815            flags: 0x0000_0003,
15816            tar_stream_offset: alpha.len() as u64,
15817        };
15818
15819        let (shard0_plaintext, first0, last0) = build_test_index_shard(
15820            &[TestFileMeta {
15821                path: b"alpha.txt".to_vec(),
15822                frame_index: 0,
15823                tar_stream_offset: 0,
15824                member_group_size: alpha.len() as u64,
15825                file_data_size: b"alpha cross shard\n".len() as u64,
15826            }],
15827            &[frame0],
15828            std::slice::from_ref(&envelope),
15829        );
15830        let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
15831            &[TestFileMeta {
15832                path: b"beta.txt".to_vec(),
15833                frame_index: 1,
15834                tar_stream_offset: alpha.len() as u64,
15835                member_group_size: beta.len() as u64,
15836                file_data_size: b"beta cross shard\n".len() as u64,
15837            }],
15838            &[frame1],
15839            std::slice::from_ref(&envelope),
15840        );
15841        shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
15842
15843        let shard0 = encrypt_test_object(
15844            &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
15845            &subkeys.index_shard_key,
15846            &subkeys.index_nonce_seed,
15847            b"idxshard",
15848            0,
15849            BlockKind::IndexShardData,
15850            &mut next_block_index,
15851            &crypto_header,
15852            &volume_header,
15853        );
15854        let shard1 = encrypt_test_object(
15855            &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
15856            &subkeys.index_shard_key,
15857            &subkeys.index_nonce_seed,
15858            b"idxshard",
15859            1,
15860            BlockKind::IndexShardData,
15861            &mut next_block_index,
15862            &crypto_header,
15863            &volume_header,
15864        );
15865        insert_records(&mut blocks, &shard0.records);
15866        insert_records(&mut blocks, &shard1.records);
15867
15868        let index_root = IndexRoot {
15869            header: IndexRootHeader {
15870                frame_count: 2,
15871                envelope_count: 1,
15872                file_count: 2,
15873                payload_block_count: payload.extent.data_block_count as u64,
15874                tar_total_size: tar_stream.len() as u64,
15875                content_sha256: sha256_bytes(&tar_stream),
15876                ..IndexRootHeader::empty()
15877            },
15878            shards: vec![
15879                ShardEntry {
15880                    shard_index: 0,
15881                    first_block_index: shard0.extent.first_block_index,
15882                    data_block_count: shard0.extent.data_block_count,
15883                    parity_block_count: 0,
15884                    encrypted_size: shard0.extent.encrypted_size,
15885                    decompressed_size: shard0_plaintext.len() as u32,
15886                    file_count: 1,
15887                    first_path_hash: first0,
15888                    last_path_hash: last0,
15889                },
15890                ShardEntry {
15891                    shard_index: 1,
15892                    first_block_index: shard1.extent.first_block_index,
15893                    data_block_count: shard1.extent.data_block_count,
15894                    parity_block_count: 0,
15895                    encrypted_size: shard1.extent.encrypted_size,
15896                    decompressed_size: shard1_plaintext.len() as u32,
15897                    file_count: 1,
15898                    first_path_hash: first1,
15899                    last_path_hash: last1,
15900                },
15901            ],
15902            directory_hint_shards: Vec::new(),
15903        };
15904
15905        let index_root_plaintext = index_root.to_bytes();
15906        let index_root_object = encrypt_test_object(
15907            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
15908            &subkeys.index_root_key,
15909            &subkeys.index_nonce_seed,
15910            b"idxroot",
15911            0,
15912            BlockKind::IndexRootData,
15913            &mut next_block_index,
15914            &crypto_header,
15915            &volume_header,
15916        );
15917        insert_records(&mut blocks, &index_root_object.records);
15918
15919        let archive_uuid = volume_header.archive_uuid;
15920        let session_id = volume_header.session_id;
15921        let opened = OpenedArchive {
15922            options: ReaderOptions::default(),
15923            observed_archive_bytes: 1_000_000,
15924            observed_volume_count: 1,
15925            subkeys,
15926            blocks,
15927            lazy_blocks: None,
15928            crypto_header_bytes: Vec::new(),
15929            volume_header,
15930            crypto_header,
15931            manifest_footer: ManifestFooter {
15932                archive_uuid,
15933                session_id,
15934                volume_index: 0,
15935                is_authoritative: 1,
15936                total_volumes: 1,
15937                index_root_first_block: index_root_object.extent.first_block_index,
15938                index_root_data_block_count: index_root_object.extent.data_block_count,
15939                index_root_parity_block_count: 0,
15940                index_root_encrypted_size: index_root_object.extent.encrypted_size,
15941                index_root_decompressed_size: index_root_plaintext.len() as u32,
15942                manifest_hmac: [0u8; 32],
15943            },
15944            volume_trailer: Some(VolumeTrailer {
15945                archive_uuid,
15946                session_id,
15947                volume_index: 0,
15948                block_count: next_block_index,
15949                bytes_written: 0,
15950                manifest_footer_offset: 0,
15951                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
15952                closed_at_ns: 0,
15953                root_auth_footer_offset: 0,
15954                root_auth_footer_length: 0,
15955                root_auth_flags: 0,
15956                trailer_hmac: [0u8; 32],
15957            }),
15958            root_auth_footer: None,
15959            index_root,
15960            payload_dictionary: None,
15961        };
15962
15963        opened.verify().unwrap();
15964    }
15965
15966    #[test]
15967    fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
15968        let options = WriterOptions {
15969            index_root_fec_parity_shards: 0,
15970            ..single_stream_options()
15971        };
15972        let archive = write_archive(
15973            &[RegularFile::new("only.txt", b"only payload")],
15974            &master_key(),
15975            options,
15976        )
15977        .unwrap();
15978        let opened = open_archive(&archive.bytes, &master_key()).unwrap();
15979        assert!(opened.index_root.directory_hint_shards.is_empty());
15980
15981        let mut root = opened.index_root.clone();
15982        root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
15983        root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
15984        let root_plaintext = root.to_bytes();
15985        IndexRoot::parse(
15986            &root_plaintext,
15987            false,
15988            metadata_limits(&opened.crypto_header),
15989        )
15990        .unwrap();
15991        assert_eq!(
15992            root_plaintext.len() as u32,
15993            opened.manifest_footer.index_root_decompressed_size
15994        );
15995
15996        let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
15997        let mut next_block_index = opened.manifest_footer.index_root_first_block;
15998        let replacement = encrypt_test_object(
15999            &compressed_root,
16000            &opened.subkeys.index_root_key,
16001            &opened.subkeys.index_nonce_seed,
16002            b"idxroot",
16003            0,
16004            BlockKind::IndexRootData,
16005            &mut next_block_index,
16006            &opened.crypto_header,
16007            &opened.volume_header,
16008        );
16009        assert_eq!(
16010            replacement.extent.first_block_index,
16011            opened.manifest_footer.index_root_first_block
16012        );
16013        assert_eq!(
16014            replacement.extent.data_block_count,
16015            opened.manifest_footer.index_root_data_block_count
16016        );
16017        assert_eq!(
16018            replacement.extent.encrypted_size,
16019            opened.manifest_footer.index_root_encrypted_size
16020        );
16021
16022        let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
16023        let crypto_end = volume_header.crypto_header_offset as usize
16024            + volume_header.crypto_header_length as usize;
16025        let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
16026        let mut malformed = archive.bytes.clone();
16027        for record in replacement.records {
16028            let offset = crypto_end + record.block_index as usize * record_len;
16029            malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
16030        }
16031
16032        let reopened = open_archive(&malformed, &master_key()).unwrap();
16033        assert_eq!(
16034            reopened.index_root.header.file_count,
16035            DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
16036        );
16037        assert!(reopened.index_root.directory_hint_shards.is_empty());
16038
16039        assert_eq!(
16040            reopened.verify().unwrap_err(),
16041            FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
16042        );
16043    }
16044
16045    #[test]
16046    fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
16047        let mut map = DirectoryHintMap::new();
16048        add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
16049        add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
16050
16051        assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
16052        assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
16053        assert_eq!(
16054            map.get(b"foo/bar".as_slice()),
16055            Some(&BTreeSet::from([2, 4]))
16056        );
16057        assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
16058        assert!(!map.contains_key(b"foobar".as_slice()));
16059    }
16060
16061    #[test]
16062    fn directory_hint_validation_requires_exact_global_map() {
16063        let mut expected = DirectoryHintMap::new();
16064        add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
16065        add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
16066        let rows = sorted_directory_hint_rows(&expected);
16067        let table = directory_hint_table_from_rows(7, &rows, 2);
16068
16069        validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
16070            .unwrap();
16071
16072        let mut missing_root = expected.clone();
16073        missing_root.remove(&Vec::new());
16074        let missing_root_rows = sorted_directory_hint_rows(&missing_root);
16075        let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
16076        assert_eq!(
16077            validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
16078                .unwrap_err(),
16079            FormatError::InvalidArchive("directory hint map does not match decoded files")
16080        );
16081
16082        let mut expected_missing_directory_entry = expected.clone();
16083        expected_missing_directory_entry
16084            .get_mut(b"foo".as_slice())
16085            .unwrap()
16086            .remove(&1);
16087        assert_eq!(
16088            validate_directory_hint_tables_against_expected(
16089                std::slice::from_ref(&table),
16090                &expected_missing_directory_entry,
16091            )
16092            .unwrap_err(),
16093            FormatError::InvalidArchive("directory hint map does not match decoded files")
16094        );
16095
16096        let mut extra = expected.clone();
16097        extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
16098        let extra_rows = sorted_directory_hint_rows(&extra);
16099        let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
16100        assert_eq!(
16101            validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
16102            FormatError::InvalidArchive("directory hint map does not match decoded files")
16103        );
16104    }
16105
16106    #[test]
16107    fn directory_hint_validation_rejects_global_order_mismatch() {
16108        let mut expected = DirectoryHintMap::new();
16109        expected.insert(Vec::new(), BTreeSet::from([0]));
16110        expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
16111        let rows = sorted_directory_hint_rows(&expected);
16112        let first = directory_hint_table_from_rows(8, &rows[..1], 1);
16113        let second = directory_hint_table_from_rows(9, &rows[1..], 1);
16114
16115        assert_eq!(
16116            validate_directory_hint_tables_against_expected(&[second, first], &expected)
16117                .unwrap_err(),
16118            FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
16119        );
16120    }
16121
16122    #[test]
16123    fn object_extent_rejects_parity_above_class_cap() {
16124        let crypto_header = CryptoHeaderFixed {
16125            length: 0,
16126            compression_algo: CompressionAlgo::ZstdFramed,
16127            aead_algo: AeadAlgo::AesGcmSiv256,
16128            fec_algo: FecAlgo::ReedSolomonGF16,
16129            kdf_algo: KdfAlgo::Raw,
16130            chunk_size: 1024,
16131            envelope_target_size: 4096,
16132            block_size: 4096,
16133            fec_data_shards: 1,
16134            fec_parity_shards: 1,
16135            index_fec_data_shards: 1,
16136            index_fec_parity_shards: 1,
16137            index_root_fec_data_shards: 1,
16138            index_root_fec_parity_shards: 1,
16139            stripe_width: 1,
16140            volume_loss_tolerance: 0,
16141            bit_rot_buffer_pct: 0,
16142            has_dictionary: 0,
16143            max_path_length: 4096,
16144            expected_volume_size: 0,
16145        };
16146        let extent = ObjectExtent {
16147            first_block_index: 0,
16148            data_block_count: 1,
16149            parity_block_count: 2,
16150            encrypted_size: 4096,
16151        };
16152
16153        assert_eq!(
16154            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
16155            FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
16156        );
16157    }
16158
16159    #[test]
16160    fn object_extent_rejects_parity_below_recoverability_requirement() {
16161        let crypto_header = CryptoHeaderFixed {
16162            length: 0,
16163            compression_algo: CompressionAlgo::ZstdFramed,
16164            aead_algo: AeadAlgo::AesGcmSiv256,
16165            fec_algo: FecAlgo::ReedSolomonGF16,
16166            kdf_algo: KdfAlgo::Raw,
16167            chunk_size: 1024,
16168            envelope_target_size: 4096,
16169            block_size: 4096,
16170            fec_data_shards: 1,
16171            fec_parity_shards: 1,
16172            index_fec_data_shards: 1,
16173            index_fec_parity_shards: 1,
16174            index_root_fec_data_shards: 1,
16175            index_root_fec_parity_shards: 1,
16176            stripe_width: 2,
16177            volume_loss_tolerance: 1,
16178            bit_rot_buffer_pct: 0,
16179            has_dictionary: 0,
16180            max_path_length: 4096,
16181            expected_volume_size: 0,
16182        };
16183        let extent = ObjectExtent {
16184            first_block_index: 0,
16185            data_block_count: 1,
16186            parity_block_count: 0,
16187            encrypted_size: 4096,
16188        };
16189
16190        assert_eq!(
16191            validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
16192            FormatError::InvalidArchive(
16193                "encrypted object parity does not match v41 compute_parity"
16194            )
16195        );
16196    }
16197
16198    #[test]
16199    fn encrypted_object_extent_matrix_rejects_overlaps() {
16200        let (opened, _) = multi_envelope_reader_fixture();
16201        let loaded_shard = opened
16202            .load_index_shard(&opened.index_root.shards[0])
16203            .unwrap();
16204        let base_envelopes = loaded_shard
16205            .envelopes
16206            .iter()
16207            .map(|entry| (entry.envelope_index, entry.clone()))
16208            .collect::<BTreeMap<_, _>>();
16209        let payload_start = loaded_shard.envelopes[0].first_block_index;
16210        let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
16211
16212        let mut payload_overlap = base_envelopes.clone();
16213        payload_overlap
16214            .get_mut(&loaded_shard.envelopes[1].envelope_index)
16215            .unwrap()
16216            .first_block_index = payload_start;
16217        assert_eq!(
16218            opened
16219                .validate_encrypted_object_block_ranges(&payload_overlap)
16220                .unwrap_err(),
16221            overlap
16222        );
16223
16224        let mut shard_overlap = opened.clone();
16225        let shard = shard_overlap.index_root.shards[0].clone();
16226        shard_overlap.index_root.shards.push(ShardEntry {
16227            shard_index: 1,
16228            ..shard
16229        });
16230        assert_eq!(
16231            shard_overlap
16232                .validate_encrypted_object_block_ranges(&base_envelopes)
16233                .unwrap_err(),
16234            overlap
16235        );
16236
16237        let mut dictionary_overlap = opened.clone();
16238        dictionary_overlap.crypto_header.has_dictionary = 1;
16239        dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
16240        dictionary_overlap
16241            .index_root
16242            .header
16243            .dictionary_data_block_count = 1;
16244        dictionary_overlap
16245            .index_root
16246            .header
16247            .dictionary_parity_block_count = 0;
16248        dictionary_overlap
16249            .index_root
16250            .header
16251            .dictionary_encrypted_size = 4096;
16252        dictionary_overlap
16253            .index_root
16254            .header
16255            .dictionary_decompressed_size = 128;
16256        assert_eq!(
16257            dictionary_overlap
16258                .validate_encrypted_object_block_ranges(&base_envelopes)
16259                .unwrap_err(),
16260            overlap
16261        );
16262
16263        let mut hint_overlap = opened.clone();
16264        hint_overlap
16265            .index_root
16266            .directory_hint_shards
16267            .push(DirectoryHintShardEntry {
16268                hint_shard_index: 0,
16269                first_dir_hash: [0; 8],
16270                last_dir_hash: [0; 8],
16271                first_block_index: payload_start,
16272                data_block_count: 1,
16273                parity_block_count: 0,
16274                encrypted_size: 4096,
16275                decompressed_size: 128,
16276                entry_count: 1,
16277            });
16278        assert_eq!(
16279            hint_overlap
16280                .validate_encrypted_object_block_ranges(&base_envelopes)
16281                .unwrap_err(),
16282            overlap
16283        );
16284    }
16285
16286    #[test]
16287    fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
16288        let volume_header = test_volume_header();
16289        let crypto_header = test_crypto_header();
16290        let subkeys = Subkeys::derive(
16291            &master_key(),
16292            &volume_header.archive_uuid,
16293            &volume_header.session_id,
16294        )
16295        .unwrap();
16296        let mut next_block_index = 0u64;
16297
16298        let index_root_payload = b"index root metadata object";
16299        let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
16300        assert_metadata_object_from_compressed(
16301            &{
16302                let mut bytes = index_root_compressed.clone();
16303                bytes.push(0);
16304                bytes
16305            },
16306            index_root_payload.len(),
16307            &subkeys,
16308            &volume_header,
16309            &crypto_header,
16310            &subkeys.index_root_key,
16311            &subkeys.index_nonce_seed,
16312            b"idxroot",
16313            0,
16314            BlockKind::IndexRootData,
16315            BlockKind::IndexRootParity,
16316            crypto_header.index_root_fec_data_shards,
16317            crypto_header.index_root_fec_parity_shards,
16318            &mut next_block_index,
16319            FormatError::TrailingBytesAfterZstdFrame,
16320        );
16321        assert_metadata_object_from_compressed(
16322            &index_root_compressed,
16323            index_root_payload.len() + 1,
16324            &subkeys,
16325            &volume_header,
16326            &crypto_header,
16327            &subkeys.index_root_key,
16328            &subkeys.index_nonce_seed,
16329            b"idxroot",
16330            0,
16331            BlockKind::IndexRootData,
16332            BlockKind::IndexRootParity,
16333            crypto_header.index_root_fec_data_shards,
16334            crypto_header.index_root_fec_parity_shards,
16335            &mut next_block_index,
16336            FormatError::ZstdDecompressedSizeMismatch {
16337                expected: index_root_payload.len() + 1,
16338                actual: index_root_payload.len(),
16339            },
16340        );
16341
16342        let index_shard_payload = b"index shard metadata object";
16343        let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
16344        assert_metadata_object_from_compressed(
16345            &{
16346                let mut bytes = index_shard_compressed.clone();
16347                bytes.push(0);
16348                bytes
16349            },
16350            index_shard_payload.len(),
16351            &subkeys,
16352            &volume_header,
16353            &crypto_header,
16354            &subkeys.index_shard_key,
16355            &subkeys.index_nonce_seed,
16356            b"idxshard",
16357            1,
16358            BlockKind::IndexShardData,
16359            BlockKind::IndexShardParity,
16360            crypto_header.index_fec_data_shards,
16361            crypto_header.index_fec_parity_shards,
16362            &mut next_block_index,
16363            FormatError::TrailingBytesAfterZstdFrame,
16364        );
16365        assert_metadata_object_from_compressed(
16366            &index_shard_compressed,
16367            index_shard_payload.len() + 1,
16368            &subkeys,
16369            &volume_header,
16370            &crypto_header,
16371            &subkeys.index_shard_key,
16372            &subkeys.index_nonce_seed,
16373            b"idxshard",
16374            1,
16375            BlockKind::IndexShardData,
16376            BlockKind::IndexShardParity,
16377            crypto_header.index_fec_data_shards,
16378            crypto_header.index_fec_parity_shards,
16379            &mut next_block_index,
16380            FormatError::ZstdDecompressedSizeMismatch {
16381                expected: index_shard_payload.len() + 1,
16382                actual: index_shard_payload.len(),
16383            },
16384        );
16385
16386        let directory_hint_payload = b"directory hint metadata object";
16387        let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
16388        assert_metadata_object_from_compressed(
16389            &{
16390                let mut bytes = directory_hint_compressed.clone();
16391                bytes.push(0);
16392                bytes
16393            },
16394            directory_hint_payload.len(),
16395            &subkeys,
16396            &volume_header,
16397            &crypto_header,
16398            &subkeys.dir_hint_key,
16399            &subkeys.index_nonce_seed,
16400            b"dirhint",
16401            0,
16402            BlockKind::DirectoryHintData,
16403            BlockKind::DirectoryHintParity,
16404            crypto_header.index_fec_data_shards,
16405            crypto_header.index_fec_parity_shards,
16406            &mut next_block_index,
16407            FormatError::TrailingBytesAfterZstdFrame,
16408        );
16409        assert_metadata_object_from_compressed(
16410            &directory_hint_compressed,
16411            directory_hint_payload.len() + 1,
16412            &subkeys,
16413            &volume_header,
16414            &crypto_header,
16415            &subkeys.dir_hint_key,
16416            &subkeys.index_nonce_seed,
16417            b"dirhint",
16418            0,
16419            BlockKind::DirectoryHintData,
16420            BlockKind::DirectoryHintParity,
16421            crypto_header.index_fec_data_shards,
16422            crypto_header.index_fec_parity_shards,
16423            &mut next_block_index,
16424            FormatError::ZstdDecompressedSizeMismatch {
16425                expected: directory_hint_payload.len() + 1,
16426                actual: directory_hint_payload.len(),
16427            },
16428        );
16429
16430        let dictionary_payload = b"dictionary metadata object";
16431        let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
16432        assert_metadata_object_from_compressed(
16433            &{
16434                let mut bytes = dictionary_compressed.clone();
16435                bytes.push(0);
16436                bytes
16437            },
16438            dictionary_payload.len(),
16439            &subkeys,
16440            &volume_header,
16441            &crypto_header,
16442            &subkeys.dictionary_key,
16443            &subkeys.index_nonce_seed,
16444            b"dict",
16445            0,
16446            BlockKind::DictionaryData,
16447            BlockKind::DictionaryParity,
16448            crypto_header.index_root_fec_data_shards,
16449            crypto_header.index_root_fec_parity_shards,
16450            &mut next_block_index,
16451            FormatError::TrailingBytesAfterZstdFrame,
16452        );
16453        assert_metadata_object_from_compressed(
16454            &dictionary_compressed,
16455            dictionary_payload.len() + 1,
16456            &subkeys,
16457            &volume_header,
16458            &crypto_header,
16459            &subkeys.dictionary_key,
16460            &subkeys.index_nonce_seed,
16461            b"dict",
16462            0,
16463            BlockKind::DictionaryData,
16464            BlockKind::DictionaryParity,
16465            crypto_header.index_root_fec_data_shards,
16466            crypto_header.index_root_fec_parity_shards,
16467            &mut next_block_index,
16468            FormatError::ZstdDecompressedSizeMismatch {
16469                expected: dictionary_payload.len() + 1,
16470                actual: dictionary_payload.len(),
16471            },
16472        );
16473    }
16474
16475    #[test]
16476    fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
16477        let volume_header = test_volume_header();
16478        let crypto_header = test_crypto_header();
16479        let subkeys = Subkeys::derive(
16480            &master_key(),
16481            &volume_header.archive_uuid,
16482            &volume_header.session_id,
16483        )
16484        .unwrap();
16485        let mut next_block_index = 0u64;
16486
16487        let index_root_payload = b"index root metadata object";
16488        let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
16489            index_root_payload,
16490            &subkeys,
16491            &volume_header,
16492            &crypto_header,
16493            &subkeys.index_root_key,
16494            &subkeys.index_nonce_seed,
16495            b"idxroot",
16496            0,
16497            BlockKind::IndexRootData,
16498            &mut next_block_index,
16499        );
16500        let mut index_root_extent = index_root_extent;
16501        index_root_extent.encrypted_size = index_root_extent
16502            .encrypted_size
16503            .saturating_add(crypto_header.block_size);
16504        assert_eq!(
16505            load_metadata_object_from_parts(
16506                &index_root_records,
16507                ObjectLoadContext::index_root(
16508                    &volume_header,
16509                    &crypto_header,
16510                    &subkeys,
16511                    index_root_extent,
16512                ),
16513                index_root_payload.len() as u32,
16514            )
16515            .unwrap_err(),
16516            FormatError::InvalidArchive(
16517                "encrypted object size is not data_block_count * block_size"
16518            )
16519        );
16520
16521        let index_shard_payload = b"index shard metadata object";
16522        let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
16523            index_shard_payload,
16524            &subkeys,
16525            &volume_header,
16526            &crypto_header,
16527            &subkeys.index_shard_key,
16528            &subkeys.index_nonce_seed,
16529            b"idxshard",
16530            1,
16531            BlockKind::IndexShardData,
16532            &mut next_block_index,
16533        );
16534        let mut index_shard_extent = index_shard_extent;
16535        index_shard_extent.encrypted_size = index_shard_extent
16536            .encrypted_size
16537            .saturating_add(crypto_header.block_size);
16538        assert_eq!(
16539            load_metadata_object_from_parts(
16540                &index_shard_records,
16541                ObjectLoadContext {
16542                    volume_header: &volume_header,
16543                    crypto_header: &crypto_header,
16544                    extent: index_shard_extent,
16545                    data_kind: BlockKind::IndexShardData,
16546                    parity_kind: BlockKind::IndexShardParity,
16547                    key: &subkeys.index_shard_key,
16548                    nonce_seed: &subkeys.index_nonce_seed,
16549                    domain: b"idxshard",
16550                    counter: 1,
16551                    class_data_shard_max: crypto_header.index_fec_data_shards,
16552                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
16553                },
16554                index_shard_payload.len() as u32,
16555            )
16556            .unwrap_err(),
16557            FormatError::InvalidArchive(
16558                "encrypted object size is not data_block_count * block_size"
16559            )
16560        );
16561
16562        let directory_hint_payload = b"directory hint metadata object";
16563        let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
16564            directory_hint_payload,
16565            &subkeys,
16566            &volume_header,
16567            &crypto_header,
16568            &subkeys.dir_hint_key,
16569            &subkeys.index_nonce_seed,
16570            b"dirhint",
16571            0,
16572            BlockKind::DirectoryHintData,
16573            &mut next_block_index,
16574        );
16575        let mut directory_hint_extent = directory_hint_extent;
16576        directory_hint_extent.encrypted_size = directory_hint_extent
16577            .encrypted_size
16578            .saturating_add(crypto_header.block_size);
16579        assert_eq!(
16580            load_metadata_object_from_parts(
16581                &directory_hint_records,
16582                ObjectLoadContext {
16583                    volume_header: &volume_header,
16584                    crypto_header: &crypto_header,
16585                    extent: directory_hint_extent,
16586                    data_kind: BlockKind::DirectoryHintData,
16587                    parity_kind: BlockKind::DirectoryHintParity,
16588                    key: &subkeys.dir_hint_key,
16589                    nonce_seed: &subkeys.index_nonce_seed,
16590                    domain: b"dirhint",
16591                    counter: 0,
16592                    class_data_shard_max: crypto_header.index_fec_data_shards,
16593                    class_parity_shard_max: crypto_header.index_fec_parity_shards,
16594                },
16595                directory_hint_payload.len() as u32,
16596            )
16597            .unwrap_err(),
16598            FormatError::InvalidArchive(
16599                "encrypted object size is not data_block_count * block_size"
16600            )
16601        );
16602
16603        let dictionary_payload = b"dictionary metadata object";
16604        let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
16605            dictionary_payload,
16606            &subkeys,
16607            &volume_header,
16608            &crypto_header,
16609            &subkeys.dictionary_key,
16610            &subkeys.index_nonce_seed,
16611            b"dict",
16612            0,
16613            BlockKind::DictionaryData,
16614            &mut next_block_index,
16615        );
16616        let mut dictionary_extent = dictionary_extent;
16617        dictionary_extent.encrypted_size = dictionary_extent
16618            .encrypted_size
16619            .saturating_add(crypto_header.block_size);
16620        assert_eq!(
16621            load_metadata_object_from_parts(
16622                &dictionary_records,
16623                ObjectLoadContext {
16624                    volume_header: &volume_header,
16625                    crypto_header: &crypto_header,
16626                    extent: dictionary_extent,
16627                    data_kind: BlockKind::DictionaryData,
16628                    parity_kind: BlockKind::DictionaryParity,
16629                    key: &subkeys.dictionary_key,
16630                    nonce_seed: &subkeys.index_nonce_seed,
16631                    domain: b"dict",
16632                    counter: 0,
16633                    class_data_shard_max: crypto_header.index_root_fec_data_shards,
16634                    class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
16635                },
16636                dictionary_payload.len() as u32,
16637            )
16638            .unwrap_err(),
16639            FormatError::InvalidArchive(
16640                "encrypted object size is not data_block_count * block_size"
16641            )
16642        );
16643    }
16644
16645    #[test]
16646    fn opens_complete_multi_volume_archive() {
16647        let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
16648        let archive = write_archive(
16649            &files,
16650            &master_key(),
16651            WriterOptions {
16652                stripe_width: 2,
16653                volume_loss_tolerance: 1,
16654                ..single_stream_options()
16655            },
16656        )
16657        .unwrap();
16658        assert_eq!(archive.volumes.len(), 2);
16659
16660        let volume_refs = archive
16661            .volumes
16662            .iter()
16663            .map(Vec::as_slice)
16664            .collect::<Vec<_>>();
16665        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16666
16667        assert_eq!(opened.volume_header.stripe_width, 2);
16668        assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
16669        assert_eq!(
16670            opened.extract_file("alpha.txt").unwrap(),
16671            Some(b"hello from volume stripes".to_vec())
16672        );
16673        opened.verify().unwrap();
16674    }
16675
16676    #[test]
16677    fn recovers_from_one_missing_volume_when_parity_allows() {
16678        let files = [RegularFile::new("alpha.txt", b"recover me")];
16679        let archive = write_archive(
16680            &files,
16681            &master_key(),
16682            WriterOptions {
16683                stripe_width: 2,
16684                volume_loss_tolerance: 1,
16685                ..single_stream_options()
16686            },
16687        )
16688        .unwrap();
16689
16690        let recovered =
16691            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
16692        assert_eq!(
16693            recovered.extract_file("alpha.txt").unwrap(),
16694            Some(b"recover me".to_vec())
16695        );
16696        recovered.verify().unwrap();
16697    }
16698
16699    #[test]
16700    fn recovers_from_crc_corrupted_block_when_parity_allows() {
16701        let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
16702        let archive = write_archive(
16703            &files,
16704            &master_key(),
16705            WriterOptions {
16706                stripe_width: 2,
16707                volume_loss_tolerance: 1,
16708                ..single_stream_options()
16709            },
16710        )
16711        .unwrap();
16712        let mut volumes = archive.volumes.clone();
16713        corrupt_first_block_record_payload(&mut volumes[0]);
16714
16715        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16716        let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16717
16718        assert_eq!(
16719            recovered.extract_file("alpha.txt").unwrap(),
16720            Some(b"repair corrupt block".to_vec())
16721        );
16722        recovered.verify().unwrap();
16723    }
16724
16725    #[test]
16726    fn rejects_multi_volume_count_mismatch_without_tolerance() {
16727        let files = [RegularFile::new("alpha.txt", b"count check")];
16728        let archive = write_archive(
16729            &files,
16730            &master_key(),
16731            WriterOptions {
16732                stripe_width: 3,
16733                volume_loss_tolerance: 0,
16734                ..single_stream_options()
16735            },
16736        )
16737        .unwrap();
16738
16739        assert_eq!(
16740            open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
16741            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
16742        );
16743    }
16744
16745    #[test]
16746    fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
16747        let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
16748        let archive = write_archive(
16749            &files,
16750            &master_key(),
16751            WriterOptions {
16752                stripe_width: 2,
16753                volume_loss_tolerance: 1,
16754                ..single_stream_options()
16755            },
16756        )
16757        .unwrap();
16758
16759        let mut bad_first = archive.volumes[0].clone();
16760        rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
16761            footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
16762        });
16763
16764        open_archive_volumes(
16765            &[bad_first.as_slice(), archive.volumes[1].as_slice()],
16766            &master_key(),
16767        )
16768        .unwrap();
16769    }
16770
16771    #[test]
16772    fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
16773        let files = [RegularFile::new("alpha.txt", b"repair meta root")];
16774        let archive = write_archive(
16775            &files,
16776            &master_key(),
16777            WriterOptions {
16778                stripe_width: 2,
16779                volume_loss_tolerance: 1,
16780                ..single_stream_options()
16781            },
16782        )
16783        .unwrap();
16784        let mut volumes = archive.volumes.clone();
16785
16786        let mut corrupted = false;
16787        for volume in &mut volumes {
16788            if let Some(slot) =
16789                block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
16790            {
16791                corrupt_block_record_payload_at_slot(volume, *slot);
16792                corrupted = true;
16793                break;
16794            }
16795        }
16796        assert!(corrupted, "expected an IndexRootData record");
16797
16798        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16799        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16800        assert_eq!(
16801            opened.extract_file("alpha.txt").unwrap(),
16802            Some(b"repair meta root".to_vec())
16803        );
16804        opened.verify().unwrap();
16805    }
16806
16807    #[test]
16808    fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
16809        let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
16810        let archive = write_archive(
16811            &files,
16812            &master_key(),
16813            WriterOptions {
16814                stripe_width: 2,
16815                volume_loss_tolerance: 1,
16816                ..single_stream_options()
16817            },
16818        )
16819        .unwrap();
16820        let mut volumes = archive.volumes.clone();
16821
16822        let mut corrupted = false;
16823        for volume in &mut volumes {
16824            if let Some(slot) =
16825                block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
16826            {
16827                corrupt_block_record_payload_at_slot(volume, *slot);
16828                corrupted = true;
16829                break;
16830            }
16831        }
16832        assert!(corrupted, "expected an IndexShardData record");
16833
16834        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16835        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16836        assert_eq!(
16837            opened.extract_file("alpha.txt").unwrap(),
16838            Some(b"repair meta shard".to_vec())
16839        );
16840        opened.verify().unwrap();
16841    }
16842
16843    #[test]
16844    fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
16845        let files = [RegularFile::new(
16846            "alpha.txt",
16847            b"bitrot parity is not volume loss",
16848        )];
16849        let archive = write_archive(
16850            &files,
16851            &master_key(),
16852            WriterOptions {
16853                stripe_width: 2,
16854                volume_loss_tolerance: 0,
16855                bit_rot_buffer_pct: 1,
16856                ..single_stream_options()
16857            },
16858        )
16859        .unwrap();
16860
16861        assert_eq!(
16862            open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
16863            FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
16864        );
16865    }
16866
16867    #[test]
16868    fn repairs_crc_erasure_only_within_parity_budget() {
16869        let payload = pseudo_random_bytes(12_000);
16870        let archive = write_archive(
16871            &[RegularFile::new("rot.bin", &payload)],
16872            &master_key(),
16873            small_block_recovery_options(),
16874        )
16875        .unwrap();
16876        let payload_slots = first_payload_data_run_slots(&archive.bytes);
16877        assert!(
16878            payload_slots.len() >= 2,
16879            "fixture must contain a multi-block payload object"
16880        );
16881
16882        let mut one_erasure = archive.bytes.clone();
16883        corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
16884        let repaired = open_archive(&one_erasure, &master_key()).unwrap();
16885        assert_eq!(
16886            repaired.extract_file("rot.bin").unwrap(),
16887            Some(payload.clone())
16888        );
16889
16890        let mut two_erasures = archive.bytes.clone();
16891        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
16892        corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
16893        let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
16894        assert_eq!(
16895            unrepaired.extract_file("rot.bin").unwrap_err(),
16896            FormatError::FecTooFewAvailableShards
16897        );
16898    }
16899
16900    #[test]
16901    fn verify_rejects_missing_required_object_block_extent() {
16902        let (mut opened, missing_block) = multi_envelope_reader_fixture();
16903        assert!(opened.blocks.remove(&missing_block).is_some());
16904
16905        assert_eq!(
16906            opened.verify().unwrap_err(),
16907            FormatError::FecTooFewAvailableShards
16908        );
16909    }
16910
16911    #[test]
16912    fn parity_crc_erasure_does_not_hide_authenticated_data() {
16913        let payload = pseudo_random_bytes(12_000);
16914        let archive = write_archive(
16915            &[RegularFile::new("parity-erasure.bin", &payload)],
16916            &master_key(),
16917            parity_rich_recovery_options(),
16918        )
16919        .unwrap();
16920        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
16921        let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
16922        assert!(
16923            parity_slots.len() >= 2,
16924            "fixture must contain redundant parity shards"
16925        );
16926        let mut corrupted = archive.bytes;
16927        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
16928        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
16929
16930        let opened = open_archive(&corrupted, &master_key()).unwrap();
16931        assert_eq!(
16932            opened.extract_file("parity-erasure.bin").unwrap(),
16933            Some(payload)
16934        );
16935        opened.verify().unwrap();
16936    }
16937
16938    #[test]
16939    fn repair_patches_restore_crc_erased_payload_block() {
16940        let payload = pseudo_random_bytes(12_000);
16941        let archive = write_archive(
16942            &[RegularFile::new("rot.bin", &payload)],
16943            &master_key(),
16944            small_block_recovery_options(),
16945        )
16946        .unwrap();
16947        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
16948        let mut corrupted = archive.bytes.clone();
16949        corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
16950
16951        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
16952        opened.verify().unwrap();
16953        let patches = opened.repair_patches().unwrap();
16954        assert_eq!(patches.len(), 1);
16955        apply_repair_patches(&mut corrupted, &patches);
16956
16957        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
16958        repaired.verify().unwrap();
16959        assert!(repaired.repair_patches().unwrap().is_empty());
16960    }
16961
16962    #[test]
16963    fn repair_patches_restore_crc_erased_payload_parity_block() {
16964        let payload = pseudo_random_bytes(12_000);
16965        let archive = write_archive(
16966            &[RegularFile::new("parity-erasure.bin", &payload)],
16967            &master_key(),
16968            parity_rich_recovery_options(),
16969        )
16970        .unwrap();
16971        let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
16972        let mut corrupted = archive.bytes.clone();
16973        corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
16974
16975        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
16976        opened.verify().unwrap();
16977        let patches = opened.repair_patches().unwrap();
16978        assert_eq!(patches.len(), 1);
16979        apply_repair_patches(&mut corrupted, &patches);
16980
16981        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
16982        repaired.verify().unwrap();
16983        assert!(repaired.repair_patches().unwrap().is_empty());
16984    }
16985
16986    #[test]
16987    fn recovers_physical_odd_block_size_from_cmra_authority() {
16988        let archive = write_archive(
16989            &[RegularFile::new("odd-block.txt", b"payload")],
16990            &master_key(),
16991            small_block_recovery_options(),
16992        )
16993        .unwrap();
16994        let mut malformed = archive.bytes;
16995        let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
16996        let block_size_offset = volume_header.crypto_header_offset as usize + 24;
16997        malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
16998
16999        let opened = open_archive(&malformed, &master_key()).unwrap();
17000        assert_ne!(opened.crypto_header.block_size, 4097);
17001        assert_eq!(
17002            opened.extract_file("odd-block.txt").unwrap(),
17003            Some(b"payload".to_vec())
17004        );
17005        opened.verify().unwrap();
17006    }
17007
17008    #[test]
17009    fn repairs_structurally_malformed_payload_block_slots() {
17010        let payload = pseudo_random_bytes(12_000);
17011        let archive = write_archive(
17012            &[RegularFile::new("structural-block.bin", &payload)],
17013            &master_key(),
17014            small_block_recovery_options(),
17015        )
17016        .unwrap();
17017        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
17018
17019        let mut bad_magic = archive.bytes.clone();
17020        corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
17021        assert_eq!(
17022            open_archive(&bad_magic, &master_key())
17023                .unwrap()
17024                .extract_file("structural-block.bin")
17025                .unwrap(),
17026            Some(payload.clone())
17027        );
17028
17029        let mut bad_reserved = archive.bytes;
17030        corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
17031        assert_eq!(
17032            open_archive(&bad_reserved, &master_key())
17033                .unwrap()
17034                .extract_file("structural-block.bin")
17035                .unwrap(),
17036            Some(payload)
17037        );
17038    }
17039
17040    #[test]
17041    fn repair_patches_restore_structurally_malformed_payload_block_slot() {
17042        let payload = pseudo_random_bytes(12_000);
17043        let archive = write_archive(
17044            &[RegularFile::new("structural-patch.bin", &payload)],
17045            &master_key(),
17046            small_block_recovery_options(),
17047        )
17048        .unwrap();
17049        let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
17050        let mut corrupted = archive.bytes.clone();
17051        corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
17052
17053        let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
17054        opened.verify().unwrap();
17055        assert_eq!(
17056            opened.extract_file("structural-patch.bin").unwrap(),
17057            Some(payload)
17058        );
17059        let patches = opened.repair_patches().unwrap();
17060        assert_eq!(patches.len(), 1);
17061        apply_repair_patches(&mut corrupted, &patches);
17062
17063        let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
17064        repaired.verify().unwrap();
17065        assert!(repaired.repair_patches().unwrap().is_empty());
17066    }
17067
17068    #[test]
17069    fn repairs_structurally_malformed_index_root_block_slot() {
17070        let archive = write_archive(
17071            &[RegularFile::new(
17072                "structural-index-root.txt",
17073                b"metadata repair",
17074            )],
17075            &master_key(),
17076            small_block_recovery_options(),
17077        )
17078        .unwrap();
17079        let index_root_slot =
17080            first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
17081        let mut corrupted = archive.bytes;
17082        corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
17083
17084        let opened = open_archive(&corrupted, &master_key()).unwrap();
17085        assert_eq!(
17086            opened.extract_file("structural-index-root.txt").unwrap(),
17087            Some(b"metadata repair".to_vec())
17088        );
17089        opened.verify().unwrap();
17090    }
17091
17092    #[test]
17093    fn rejects_parity_block_with_last_data_flag() {
17094        let archive = write_archive(
17095            &[RegularFile::new("parity-flag.txt", b"payload")],
17096            &master_key(),
17097            small_block_recovery_options(),
17098        )
17099        .unwrap();
17100        let parity_slot =
17101            first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
17102        let mut malformed = archive.bytes;
17103        mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
17104            record.flags = 0x01;
17105        });
17106
17107        assert_eq!(
17108            open_archive(&malformed, &master_key()).unwrap_err(),
17109            FormatError::ParityBlockHasLastDataFlag
17110        );
17111    }
17112
17113    #[test]
17114    fn rejects_missing_and_duplicate_payload_last_data_flags() {
17115        let payload = pseudo_random_bytes(12_000);
17116        let archive = write_archive(
17117            &[RegularFile::new("flags.bin", &payload)],
17118            &master_key(),
17119            small_block_recovery_options(),
17120        )
17121        .unwrap();
17122        let payload_slots = first_payload_data_run_slots(&archive.bytes);
17123        assert!(
17124            payload_slots.len() >= 2,
17125            "fixture must contain a multi-block payload object"
17126        );
17127
17128        let mut duplicate_last = archive.bytes.clone();
17129        mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
17130            record.flags = 0x01;
17131        });
17132        let opened = open_archive(&duplicate_last, &master_key()).unwrap();
17133        assert_eq!(
17134            opened.extract_file("flags.bin").unwrap_err(),
17135            FormatError::InvalidArchive("object last-data flag is not on the final data block")
17136        );
17137
17138        let mut missing_last = archive.bytes;
17139        mutate_block_record_at_slot(
17140            &mut missing_last,
17141            *payload_slots.last().unwrap(),
17142            |record| {
17143                record.flags = 0;
17144            },
17145        );
17146        let opened = open_archive(&missing_last, &master_key()).unwrap();
17147        assert_eq!(
17148            opened.extract_file("flags.bin").unwrap_err(),
17149            FormatError::InvalidArchive("object last-data flag is not on the final data block")
17150        );
17151    }
17152
17153    #[test]
17154    fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
17155        let files = [RegularFile::new(
17156            "footer-copy.txt",
17157            b"survives one bad footer",
17158        )];
17159        let archive = write_archive(
17160            &files,
17161            &master_key(),
17162            WriterOptions {
17163                stripe_width: 2,
17164                volume_loss_tolerance: 1,
17165                ..single_stream_options()
17166            },
17167        )
17168        .unwrap();
17169        let mut volumes = archive.volumes.clone();
17170        corrupt_manifest_footer_hmac(&mut volumes[0]);
17171
17172        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
17173        let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
17174        assert_eq!(opened.manifest_footer.volume_index, 0);
17175        assert_eq!(opened.volume_header.volume_index, 0);
17176        assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
17177        assert_eq!(
17178            opened.extract_file("footer-copy.txt").unwrap(),
17179            Some(b"survives one bad footer".to_vec())
17180        );
17181        opened.verify().unwrap();
17182    }
17183
17184    #[test]
17185    fn manifest_footer_corruption_requires_trusted_sidecar() {
17186        let archive = write_archive(
17187            &[RegularFile::new("footer.txt", b"sidecar authority")],
17188            &master_key(),
17189            single_stream_options(),
17190        )
17191        .unwrap();
17192        let manifest_offset = terminal_material_offset(&archive.bytes);
17193        let mut corrupted = archive.bytes.clone();
17194        corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
17195        corrupt_v41_terminal_recovery(&mut corrupted);
17196
17197        assert!(open_archive(&corrupted, &master_key()).is_err());
17198
17199        let opened =
17200            open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
17201                .unwrap();
17202        assert!(opened.volume_trailer.is_none());
17203        assert_eq!(
17204            opened.extract_file("footer.txt").unwrap(),
17205            Some(b"sidecar authority".to_vec())
17206        );
17207        opened.verify().unwrap();
17208    }
17209
17210    #[test]
17211    fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
17212        let archive = write_archive(
17213            &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
17214            &master_key(),
17215            single_stream_options(),
17216        )
17217        .unwrap();
17218        let strict_options = ReaderOptions {
17219            max_trailing_garbage_scan: 0,
17220            ..ReaderOptions::default()
17221        };
17222
17223        let manifest_offset = terminal_material_offset(&archive.bytes);
17224        for offset in [
17225            manifest_offset + 71,
17226            manifest_offset + MANIFEST_HMAC_COVERED_LEN,
17227        ] {
17228            let mut corrupted = archive.bytes.clone();
17229            corrupted[offset] ^= 0x01;
17230            open_archive(&corrupted, &master_key()).unwrap();
17231        }
17232
17233        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
17234        for offset in [
17235            trailer_offset + 75,
17236            trailer_offset + TRAILER_HMAC_COVERED_LEN,
17237        ] {
17238            let mut corrupted = archive.bytes.clone();
17239            corrupted[offset] ^= 0x01;
17240            OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
17241        }
17242
17243        let mut covered_sidecar = archive.bootstrap_sidecar.clone();
17244        let mut header =
17245            BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
17246                .unwrap();
17247        header.manifest_footer_offset += 1;
17248        covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
17249        assert_eq!(
17250            open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
17251                .unwrap_err(),
17252            FormatError::HmacMismatch {
17253                structure: "BootstrapSidecarHeader"
17254            }
17255        );
17256
17257        let mut tag_sidecar = archive.bootstrap_sidecar.clone();
17258        let mut header =
17259            BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17260        header.sidecar_hmac[0] ^= 1;
17261        tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
17262        assert_eq!(
17263            open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
17264                .unwrap_err(),
17265            FormatError::HmacMismatch {
17266                structure: "BootstrapSidecarHeader"
17267            }
17268        );
17269
17270        let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
17271        let header =
17272            BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
17273                .unwrap();
17274        let mut header_bytes = header.to_bytes();
17275        header_bytes[124] ^= 0x01;
17276        let crc = crc32c::crc32c(&header_bytes[..124]);
17277        header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
17278        non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
17279        let opened = open_archive_with_bootstrap_sidecar(
17280            &archive.bytes,
17281            &non_covered_sidecar,
17282            &master_key(),
17283        )
17284        .unwrap();
17285        assert_eq!(
17286            opened.extract_file("hmac-boundary.txt").unwrap(),
17287            Some(b"boundary bytes".to_vec())
17288        );
17289    }
17290
17291    #[test]
17292    fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
17293        let archive = write_archive(
17294            &[RegularFile::new("volume-index.txt", b"identity")],
17295            &master_key(),
17296            single_stream_options(),
17297        )
17298        .unwrap();
17299
17300        let mut bad_trailer = archive.bytes.clone();
17301        rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
17302            trailer.volume_index = 1;
17303        });
17304        open_archive(&bad_trailer, &master_key()).unwrap();
17305
17306        let mut bad_manifest = archive.bytes;
17307        rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
17308            footer.volume_index = 1;
17309        });
17310        open_archive(&bad_manifest, &master_key()).unwrap();
17311    }
17312
17313    #[test]
17314    fn rejects_same_key_header_terminal_material_splice() {
17315        let first = write_archive(
17316            &[RegularFile::new("splice.txt", b"same shape")],
17317            &master_key(),
17318            single_stream_options(),
17319        )
17320        .unwrap();
17321        let second = write_archive(
17322            &[RegularFile::new("splice.txt", b"same shape")],
17323            &master_key(),
17324            single_stream_options(),
17325        )
17326        .unwrap();
17327        assert_ne!(first.archive_uuid, second.archive_uuid);
17328        assert_eq!(
17329            terminal_material_offset(&first.bytes),
17330            terminal_material_offset(&second.bytes)
17331        );
17332        assert_eq!(first.bytes.len(), second.bytes.len());
17333
17334        let terminal_offset = terminal_material_offset(&first.bytes);
17335        let mut spliced = first.bytes.clone();
17336        spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
17337
17338        assert_eq!(
17339            open_archive(&spliced, &master_key()).unwrap_err(),
17340            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
17341        );
17342    }
17343
17344    #[test]
17345    fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
17346        let kdf_params = crate::crypto::KdfParams::Argon2id {
17347            t_cost: 1,
17348            m_cost_kib: 8,
17349            parallelism: 1,
17350            salt: b"0123456789abcdef".to_vec(),
17351        };
17352        let archive = write_archive_with_kdf(
17353            &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
17354            &master_key(),
17355            single_stream_options(),
17356            &kdf_params,
17357        )
17358        .unwrap();
17359        let mut mutated = archive.bytes.clone();
17360        let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
17361        let subkeys = Subkeys::derive(
17362            &master_key(),
17363            &volume_header.archive_uuid,
17364            &volume_header.session_id,
17365        )
17366        .unwrap();
17367
17368        rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
17369            let crypto_region = image
17370                .regions
17371                .iter_mut()
17372                .find(|region| region.region_type == 2)
17373                .unwrap();
17374            let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
17375            let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
17376            crypto_region.bytes[salt_start] ^= 0x01;
17377            let hmac = compute_hmac(
17378                HmacDomain::CryptoHeader,
17379                &subkeys.mac_key,
17380                &volume_header.archive_uuid,
17381                &volume_header.session_id,
17382                &crypto_region.bytes[..hmac_offset],
17383            );
17384            crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
17385        });
17386
17387        let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17388        let locator = final_recovery_locator(&mutated);
17389        let crypto_start = volume_header.crypto_header_offset as usize;
17390        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17391        let parsed_crypto = CryptoHeader::parse(
17392            &mutated[crypto_start..crypto_end],
17393            volume_header.crypto_header_length,
17394        )
17395        .unwrap();
17396        assert_eq!(
17397            parse_locator_cmra_candidate(
17398                &mutated,
17399                final_offset,
17400                locator,
17401                KeyHoldingTerminalContext {
17402                    subkeys: &subkeys,
17403                    volume_header: &volume_header,
17404                    crypto_header: &parsed_crypto.fixed,
17405                    crypto_header_bytes: &mutated[crypto_start..crypto_end],
17406                },
17407            )
17408            .unwrap_err(),
17409            FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
17410        );
17411        assert!(open_archive(&mutated, &master_key()).is_err());
17412    }
17413
17414    #[test]
17415    fn recovers_physical_crypto_header_splice_from_cmra_authority() {
17416        let base = WriterOptions {
17417            archive_uuid: Some([0x11; 16]),
17418            session_id: Some([0x22; 16]),
17419            ..small_block_recovery_options()
17420        };
17421        let same_archive = WriterOptions {
17422            archive_uuid: Some([0x11; 16]),
17423            session_id: Some([0x33; 16]),
17424            ..small_block_recovery_options()
17425        };
17426
17427        let first = write_archive(
17428            &[RegularFile::new("splice.txt", b"same shape")],
17429            &master_key(),
17430            base,
17431        )
17432        .unwrap();
17433        let second = write_archive(
17434            &[RegularFile::new("splice.txt", b"same shape")],
17435            &master_key(),
17436            same_archive,
17437        )
17438        .unwrap();
17439
17440        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
17441        let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
17442        let crypto_start = volume_header.crypto_header_offset as usize;
17443        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17444        let second_crypto_end = second_volume_header.crypto_header_offset as usize
17445            + second_volume_header.crypto_header_length as usize;
17446        assert_eq!(crypto_end, second_crypto_end);
17447
17448        let mut spliced = first.bytes.clone();
17449        spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
17450
17451        let opened = open_archive(&spliced, &master_key()).unwrap();
17452        assert_eq!(
17453            opened.crypto_header_bytes,
17454            first.bytes[crypto_start..crypto_end].to_vec()
17455        );
17456        assert_eq!(
17457            opened.extract_file("splice.txt").unwrap(),
17458            Some(b"same shape".to_vec())
17459        );
17460        opened.verify().unwrap();
17461    }
17462
17463    #[test]
17464    fn rejects_same_key_object_splice_with_session_mismatch() {
17465        let first = write_archive(
17466            &[RegularFile::new("splice.txt", b"same shape")],
17467            &master_key(),
17468            WriterOptions {
17469                archive_uuid: Some([0x11; 16]),
17470                session_id: Some([0x22; 16]),
17471                ..single_stream_options()
17472            },
17473        )
17474        .unwrap();
17475        let second = write_archive(
17476            &[RegularFile::new("splice.txt", b"same shape")],
17477            &master_key(),
17478            WriterOptions {
17479                archive_uuid: Some([0x11; 16]),
17480                session_id: Some([0x33; 16]),
17481                ..single_stream_options()
17482            },
17483        )
17484        .unwrap();
17485
17486        let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
17487        let crypto_end = volume_header.crypto_header_offset as usize
17488            + volume_header.crypto_header_length as usize;
17489        let terminal_offset = terminal_material_offset(&first.bytes);
17490        let second_terminal_offset = terminal_material_offset(&second.bytes);
17491        assert_eq!(terminal_offset, second_terminal_offset);
17492
17493        let mut spliced = first.bytes.clone();
17494        spliced[crypto_end..terminal_offset]
17495            .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
17496
17497        assert_eq!(
17498            open_archive(&spliced, &master_key()).unwrap_err(),
17499            FormatError::AeadFailure
17500        );
17501    }
17502
17503    #[test]
17504    fn rejects_authenticated_trailer_pointer_and_count_mutations() {
17505        let archive = write_archive(
17506            &[RegularFile::new(
17507                "trailer-range.txt",
17508                b"authenticated ranges",
17509            )],
17510            &master_key(),
17511            single_stream_options(),
17512        )
17513        .unwrap();
17514        let strict_options = ReaderOptions {
17515            max_trailing_garbage_scan: 0,
17516            ..ReaderOptions::default()
17517        };
17518        let bytes = archive.bytes;
17519        let manifest_offset = terminal_material_offset(&bytes);
17520        let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
17521
17522        let mut wrong_footer_length = bytes.clone();
17523        rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
17524            trailer.manifest_footer_length = 42;
17525        });
17526        OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
17527            .unwrap();
17528
17529        for (label, offset) in [
17530            (
17531                "offset before trailer by 1",
17532                manifest_offset.saturating_sub(1),
17533            ),
17534            ("offset after trailer", manifest_offset + 1),
17535            ("offset at stream start", 0),
17536            ("offset at trailer", trailer_offset),
17537            ("offset beyond trailer", trailer_offset + 4),
17538        ] {
17539            let mut wrong_footer_offset = bytes.clone();
17540            rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
17541                trailer.manifest_footer_offset = offset as u64;
17542            });
17543            open_archive(&wrong_footer_offset, &master_key())
17544                .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
17545        }
17546
17547        let mut wrong_bytes_written = bytes.clone();
17548        rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
17549            trailer.bytes_written += 1;
17550        });
17551        open_archive(&wrong_bytes_written, &master_key()).unwrap();
17552
17553        let mut wrong_block_count = bytes.clone();
17554        rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
17555            trailer.block_count += 1;
17556        });
17557        open_archive(&wrong_block_count, &master_key()).unwrap();
17558
17559        let mut wrong_footer_offset = bytes.clone();
17560        rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
17561            trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
17562        });
17563        open_archive(&wrong_footer_offset, &master_key()).unwrap();
17564    }
17565
17566    #[test]
17567    fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
17568        let archive = write_archive(
17569            &[RegularFile::new(
17570                "trailer-trailing-scan.txt",
17571                b"trailer scan boundaries",
17572            )],
17573            &master_key(),
17574            single_stream_options(),
17575        )
17576        .unwrap();
17577        let options = ReaderOptions {
17578            max_trailing_garbage_scan: 8,
17579            ..ReaderOptions::default()
17580        };
17581
17582        let mut within_scan = archive.bytes.clone();
17583        within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
17584        let opened =
17585            OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
17586        assert_eq!(
17587            opened.extract_file("trailer-trailing-scan.txt").unwrap(),
17588            Some(b"trailer scan boundaries".to_vec())
17589        );
17590
17591        let mut beyond_scan = archive.bytes.clone();
17592        beyond_scan.resize(
17593            beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
17594            0xAA,
17595        );
17596        assert_eq!(
17597            OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
17598            FormatError::InvalidArchive("no valid v41 CMRA candidate found")
17599        );
17600    }
17601
17602    #[test]
17603    fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
17604        let archive = write_archive(
17605            &[RegularFile::new("index-root-size.txt", b"extent size")],
17606            &master_key(),
17607            single_stream_options(),
17608        )
17609        .unwrap();
17610        let mut malformed = archive.bytes;
17611        let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
17612            .expect("archive should contain IndexRootData");
17613        mutate_block_record_at_slot(&mut malformed, slot, |record| {
17614            record.payload[0] ^= 0x55;
17615        });
17616
17617        assert_eq!(
17618            open_archive(&malformed, &master_key()).unwrap_err(),
17619            FormatError::AeadFailure
17620        );
17621    }
17622
17623    #[test]
17624    fn rejects_block_record_at_wrong_stripe_position() {
17625        let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
17626        let archive = write_archive(
17627            &files,
17628            &master_key(),
17629            WriterOptions {
17630                stripe_width: 2,
17631                volume_loss_tolerance: 1,
17632                ..single_stream_options()
17633            },
17634        )
17635        .unwrap();
17636        let mut volumes = archive.volumes.clone();
17637        mutate_first_block_record(&mut volumes[0], |record| {
17638            record.block_index += 2;
17639        });
17640
17641        let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
17642        assert_eq!(
17643            open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
17644            FormatError::InvalidArchive("BlockRecord index does not match volume position")
17645        );
17646    }
17647
17648    #[test]
17649    fn rejects_decreasing_block_record_index_in_required_region() {
17650        let archive = write_archive(
17651            &[RegularFile::new("alpha.txt", b"decreasing block index")],
17652            &master_key(),
17653            single_stream_options(),
17654        )
17655        .unwrap();
17656        assert!(block_record_slots(&archive.bytes).len() >= 2);
17657
17658        let mut malformed = archive.bytes;
17659        mutate_block_record_at_slot(&mut malformed, 1, |record| {
17660            record.block_index = 0;
17661        });
17662
17663        assert_eq!(
17664            open_archive(&malformed, &master_key()).unwrap_err(),
17665            FormatError::InvalidArchive("BlockRecord index does not match volume position")
17666        );
17667    }
17668
17669    #[test]
17670    fn rejects_duplicate_authenticated_volume_indexes() {
17671        let files = [RegularFile::new("alpha.txt", b"duplicates")];
17672        let archive = write_archive(
17673            &files,
17674            &master_key(),
17675            WriterOptions {
17676                stripe_width: 2,
17677                volume_loss_tolerance: 1,
17678                ..single_stream_options()
17679            },
17680        )
17681        .unwrap();
17682
17683        assert_eq!(
17684            open_archive_volumes(
17685                &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
17686                &master_key()
17687            )
17688            .unwrap_err(),
17689            FormatError::InvalidArchive("duplicate authenticated volume index")
17690        );
17691    }
17692
17693    #[test]
17694    fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
17695        let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
17696        let archive = write_archive(
17697            &files,
17698            &master_key(),
17699            WriterOptions {
17700                stripe_width: 2,
17701                volume_loss_tolerance: 1,
17702                ..single_stream_options()
17703            },
17704        )
17705        .unwrap();
17706        let mut conflicting = archive.volumes[0].clone();
17707        corrupt_first_block_record_payload(&mut conflicting);
17708
17709        assert_eq!(
17710            open_archive_volumes(
17711                &[archive.volumes[0].as_slice(), conflicting.as_slice()],
17712                &master_key()
17713            )
17714            .unwrap_err(),
17715            FormatError::InvalidArchive("duplicate authenticated volume index")
17716        );
17717    }
17718
17719    fn directory_hint_table_from_rows(
17720        hint_shard_index: u64,
17721        rows: &[(Vec<u8>, Vec<u32>)],
17722        shard_count: u32,
17723    ) -> DirectoryHintTable {
17724        let mut entries = Vec::new();
17725        let mut shard_row_indexes = Vec::new();
17726        let mut string_pool = Vec::new();
17727
17728        for (path, rows) in rows {
17729            let path_offset = if path.is_empty() {
17730                0
17731            } else {
17732                let offset = string_pool.len() as u64;
17733                string_pool.extend_from_slice(path);
17734                offset
17735            };
17736            let shard_list_start_index = shard_row_indexes.len() as u32;
17737            shard_row_indexes.extend_from_slice(rows);
17738            entries.push(DirectoryHintEntry {
17739                dir_hash: hash_prefix(path),
17740                path_offset,
17741                path_length: path.len() as u32,
17742                shard_list_start_index,
17743                shard_count: rows.len() as u32,
17744            });
17745        }
17746
17747        let table_bytes =
17748            directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
17749        let locating = DirectoryHintShardEntry {
17750            hint_shard_index,
17751            first_dir_hash: hash_prefix(&rows.first().unwrap().0),
17752            last_dir_hash: hash_prefix(&rows.last().unwrap().0),
17753            first_block_index: 0,
17754            data_block_count: 1,
17755            parity_block_count: 0,
17756            encrypted_size: 4096,
17757            decompressed_size: table_bytes.len() as u32,
17758            entry_count: rows.len() as u64,
17759        };
17760        DirectoryHintTable::parse(
17761            &table_bytes,
17762            &locating,
17763            shard_count,
17764            MetadataLimits::default(),
17765        )
17766        .unwrap()
17767    }
17768
17769    fn directory_hint_table_bytes(
17770        hint_shard_index: u64,
17771        entries: Vec<DirectoryHintEntry>,
17772        shard_row_indexes: Vec<u32>,
17773        string_pool: Vec<u8>,
17774    ) -> Vec<u8> {
17775        let header_len = DirectoryHintTableHeader {
17776            version: 1,
17777            hint_shard_index,
17778            entry_count: 0,
17779            entry_table_offset: 0,
17780            shard_list_offset: 0,
17781            string_pool_offset: 0,
17782            string_pool_size: 0,
17783        }
17784        .to_bytes()
17785        .len();
17786        let entry_len = entries
17787            .first()
17788            .map(|entry| entry.to_bytes().len())
17789            .unwrap_or(0);
17790        let shard_list_offset = if entries.is_empty() {
17791            0
17792        } else {
17793            header_len + entries.len() * entry_len
17794        };
17795        let string_pool_offset = if string_pool.is_empty() {
17796            0
17797        } else {
17798            shard_list_offset + shard_row_indexes.len() * 4
17799        };
17800
17801        let header = DirectoryHintTableHeader {
17802            version: 1,
17803            hint_shard_index,
17804            entry_count: entries.len() as u64,
17805            entry_table_offset: if entries.is_empty() {
17806                0
17807            } else {
17808                header_len as u64
17809            },
17810            shard_list_offset: shard_list_offset as u64,
17811            string_pool_offset: string_pool_offset as u64,
17812            string_pool_size: string_pool.len() as u64,
17813        };
17814
17815        let mut out = Vec::new();
17816        out.extend_from_slice(&header.to_bytes());
17817        for entry in entries {
17818            out.extend_from_slice(&entry.to_bytes());
17819        }
17820        for row in shard_row_indexes {
17821            out.extend_from_slice(&row.to_le_bytes());
17822        }
17823        out.extend_from_slice(&string_pool);
17824        out
17825    }
17826
17827    fn corrupt_first_block_record_payload(volume: &mut [u8]) {
17828        let (record_offset, _) = first_block_record(volume);
17829        volume[record_offset + 16] ^= 0x55;
17830    }
17831
17832    fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
17833        let (record_offset, _) = block_record_at_slot(volume, slot);
17834        volume[record_offset + 16] ^= 0x55;
17835    }
17836
17837    fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
17838        for patch in patches {
17839            let offset = patch.record_offset as usize;
17840            let end = offset + patch.record_bytes.len();
17841            volume[offset..end].copy_from_slice(&patch.record_bytes);
17842        }
17843    }
17844
17845    fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
17846        let (record_offset, _) = block_record_at_slot(volume, slot);
17847        volume[record_offset] ^= 0x55;
17848    }
17849
17850    fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
17851        let (record_offset, _) = block_record_at_slot(volume, slot);
17852        volume[record_offset + 14] = 0x01;
17853    }
17854
17855    fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
17856        let manifest_offset = terminal_material_offset(volume);
17857        volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
17858    }
17859
17860    fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
17861        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17862        CriticalRecoveryLocator::parse(
17863            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
17864        )
17865        .unwrap()
17866    }
17867
17868    fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
17869        let locator = final_recovery_locator(volume);
17870        let tuple = CmraDecoderTuple::from(locator);
17871        assert!(parity_shard_count < tuple.parity_shard_count);
17872        let cmra_offset = locator.cmra_offset as usize;
17873        let shard_size = tuple.shard_size as usize;
17874        let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
17875        let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
17876        let mut header = CriticalMetadataRecoveryHeader::parse(
17877            &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
17878        )
17879        .unwrap();
17880        header.parity_shard_count = parity_shard_count;
17881
17882        let mut cmra =
17883            Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
17884        cmra.extend_from_slice(&header.to_bytes());
17885        let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
17886        for row in 0..kept_rows {
17887            let start = rows_start + row * row_len;
17888            cmra.extend_from_slice(&volume[start..start + row_len]);
17889        }
17890
17891        let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
17892        out.extend_from_slice(&volume[..cmra_offset]);
17893        out.extend_from_slice(&cmra);
17894        let mut mirror = locator;
17895        mirror.locator_sequence = 1;
17896        mirror.cmra_length = cmra.len() as u32;
17897        mirror.cmra_parity_shard_count = parity_shard_count;
17898        out.extend_from_slice(&mirror.to_bytes());
17899        let final_locator = CriticalRecoveryLocator {
17900            volume_format_rev: locator.volume_format_rev,
17901            locator_sequence: 0,
17902            ..mirror
17903        };
17904        out.extend_from_slice(&final_locator.to_bytes());
17905        out
17906    }
17907
17908    fn rewrite_public_cmra_image(
17909        volume: &mut [u8],
17910        mutate: impl FnOnce(&mut CriticalMetadataImage),
17911    ) {
17912        rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
17913    }
17914
17915    fn rewrite_root_auth_footer_revision_bytes(bytes: &mut [u8], revision: u16) {
17916        bytes[72..74].copy_from_slice(&revision.to_le_bytes());
17917        let crc_offset = bytes.len() - 4;
17918        let crc = crc32c::crc32c(&bytes[..crc_offset]);
17919        bytes[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
17920    }
17921
17922    fn rewrite_cmra_image(
17923        volume: &mut [u8],
17924        mode: CmraRecoveryMode,
17925        mutate: impl FnOnce(&mut CriticalMetadataImage),
17926    ) {
17927        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17928        let locator = final_recovery_locator(volume);
17929        let tuple = CmraDecoderTuple::from(locator);
17930        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
17931        let mut image = recovered.image;
17932        mutate(&mut image);
17933        refresh_critical_image_region_digests(&mut image);
17934        let image_bytes = image.to_bytes().unwrap();
17935        assert_eq!(image_bytes.len(), tuple.image_length as usize);
17936
17937        let shard_size = tuple.shard_size as usize;
17938        let data_shard_count = tuple.data_shard_count as usize;
17939        let parity_shard_count = tuple.parity_shard_count as usize;
17940        assert!(image_bytes.len() <= data_shard_count * shard_size);
17941
17942        let mut data_shards = Vec::with_capacity(data_shard_count);
17943        for idx in 0..data_shard_count {
17944            let start = idx * shard_size;
17945            let end = (start + shard_size).min(image_bytes.len());
17946            let mut payload = vec![0u8; shard_size];
17947            if start < image_bytes.len() {
17948                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
17949            }
17950            data_shards.push(payload);
17951        }
17952        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
17953        let image_sha256 = sha256_bytes(&image_bytes);
17954
17955        let header = CriticalMetadataRecoveryHeader {
17956            shard_size: tuple.shard_size,
17957            data_shard_count: tuple.data_shard_count,
17958            parity_shard_count: tuple.parity_shard_count,
17959            image_length: tuple.image_length,
17960            archive_uuid_hint: locator.archive_uuid_hint,
17961            session_id_hint: locator.session_id_hint,
17962            volume_index_hint: locator.volume_index_hint,
17963            image_sha256,
17964            header_crc32c: 0,
17965        };
17966        let mut cmra = Vec::new();
17967        cmra.extend_from_slice(&header.to_bytes());
17968        for (idx, payload) in data_shards.into_iter().enumerate() {
17969            let payload_len = if idx + 1 == data_shard_count {
17970                image_bytes.len() - idx * shard_size
17971            } else {
17972                shard_size
17973            };
17974            cmra.extend_from_slice(
17975                &CriticalMetadataRecoveryShard {
17976                    shard_index: idx as u16,
17977                    shard_role: 0,
17978                    shard_payload_length: payload_len as u32,
17979                    payload,
17980                    shard_crc32c: 0,
17981                }
17982                .to_bytes(shard_size)
17983                .unwrap(),
17984            );
17985        }
17986        for (idx, payload) in parity_shards.into_iter().enumerate() {
17987            cmra.extend_from_slice(
17988                &CriticalMetadataRecoveryShard {
17989                    shard_index: (data_shard_count + idx) as u16,
17990                    shard_role: 1,
17991                    shard_payload_length: shard_size as u32,
17992                    payload,
17993                    shard_crc32c: 0,
17994                }
17995                .to_bytes(shard_size)
17996                .unwrap(),
17997            );
17998        }
17999        assert_eq!(cmra.len() as u64, recovered.cmra_length);
18000        let cmra_offset = locator.cmra_offset as usize;
18001        volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
18002
18003        rewrite_locator_image_sha(volume, final_offset, image_sha256);
18004        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
18005        rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
18006    }
18007
18008    fn rewrite_cmra_image_variable_len(
18009        volume: &[u8],
18010        mode: CmraRecoveryMode,
18011        mutate: impl FnOnce(&mut CriticalMetadataImage),
18012    ) -> Vec<u8> {
18013        let locator = final_recovery_locator(volume);
18014        let tuple = CmraDecoderTuple::from(locator);
18015        let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
18016        let mut image = recovered.image;
18017        mutate(&mut image);
18018        refresh_critical_image_region_digests(&mut image);
18019        let image_bytes = image.to_bytes().unwrap();
18020
18021        let shard_size = tuple.shard_size as usize;
18022        let data_shard_count = image_bytes.len().div_ceil(shard_size);
18023        let parity_shard_count = tuple.parity_shard_count as usize;
18024        assert!(data_shard_count > 0);
18025        assert!(image_bytes.len() <= data_shard_count * shard_size);
18026
18027        let mut data_shards = Vec::with_capacity(data_shard_count);
18028        for idx in 0..data_shard_count {
18029            let start = idx * shard_size;
18030            let end = (start + shard_size).min(image_bytes.len());
18031            let mut payload = vec![0u8; shard_size];
18032            if start < image_bytes.len() {
18033                payload[..end - start].copy_from_slice(&image_bytes[start..end]);
18034            }
18035            data_shards.push(payload);
18036        }
18037        let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
18038        let image_sha256 = sha256_bytes(&image_bytes);
18039        let data_shard_count_u16 = u16::try_from(data_shard_count).unwrap();
18040        let image_length_u32 = u32::try_from(image_bytes.len()).unwrap();
18041
18042        let header = CriticalMetadataRecoveryHeader {
18043            shard_size: tuple.shard_size,
18044            data_shard_count: data_shard_count_u16,
18045            parity_shard_count: tuple.parity_shard_count,
18046            image_length: image_length_u32,
18047            archive_uuid_hint: locator.archive_uuid_hint,
18048            session_id_hint: locator.session_id_hint,
18049            volume_index_hint: locator.volume_index_hint,
18050            image_sha256,
18051            header_crc32c: 0,
18052        };
18053        let mut cmra = Vec::new();
18054        cmra.extend_from_slice(&header.to_bytes());
18055        for (idx, payload) in data_shards.into_iter().enumerate() {
18056            let payload_len = if idx + 1 == data_shard_count {
18057                image_bytes.len() - idx * shard_size
18058            } else {
18059                shard_size
18060            };
18061            cmra.extend_from_slice(
18062                &CriticalMetadataRecoveryShard {
18063                    shard_index: idx as u16,
18064                    shard_role: 0,
18065                    shard_payload_length: payload_len as u32,
18066                    payload,
18067                    shard_crc32c: 0,
18068                }
18069                .to_bytes(shard_size)
18070                .unwrap(),
18071            );
18072        }
18073        for (idx, payload) in parity_shards.into_iter().enumerate() {
18074            cmra.extend_from_slice(
18075                &CriticalMetadataRecoveryShard {
18076                    shard_index: (data_shard_count + idx) as u16,
18077                    shard_role: 1,
18078                    shard_payload_length: shard_size as u32,
18079                    payload,
18080                    shard_crc32c: 0,
18081                }
18082                .to_bytes(shard_size)
18083                .unwrap(),
18084            );
18085        }
18086
18087        let locator_base = CriticalRecoveryLocator {
18088            volume_format_rev: image.volume_format_rev,
18089            cmra_offset: locator.cmra_offset,
18090            cmra_length: cmra.len() as u32,
18091            volume_trailer_offset: locator.volume_trailer_offset,
18092            body_bytes_before_cmra: locator.body_bytes_before_cmra,
18093            archive_uuid_hint: locator.archive_uuid_hint,
18094            session_id_hint: locator.session_id_hint,
18095            volume_index_hint: locator.volume_index_hint,
18096            locator_sequence: 1,
18097            cmra_shard_size: tuple.shard_size,
18098            cmra_data_shard_count: data_shard_count_u16,
18099            cmra_parity_shard_count: tuple.parity_shard_count,
18100            cmra_image_length: image_length_u32,
18101            cmra_image_sha256: image_sha256,
18102            locator_crc32c: 0,
18103        };
18104
18105        let cmra_offset = locator.cmra_offset as usize;
18106        let mut out = Vec::new();
18107        out.extend_from_slice(&volume[..cmra_offset]);
18108        out.extend_from_slice(&cmra);
18109        out.extend_from_slice(&locator_base.to_bytes());
18110        out.extend_from_slice(
18111            &CriticalRecoveryLocator {
18112                locator_sequence: 0,
18113                ..locator_base
18114            }
18115            .to_bytes(),
18116        );
18117        out
18118    }
18119
18120    fn rewrite_recovery_locator(
18121        volume: &mut [u8],
18122        offset: usize,
18123        mutate: impl FnOnce(&mut CriticalRecoveryLocator),
18124    ) {
18125        let mut locator =
18126            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
18127                .unwrap();
18128        mutate(&mut locator);
18129        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
18130    }
18131
18132    fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
18133        image.volume_header_sha256 = sha256_bytes(
18134            &image
18135                .regions
18136                .iter()
18137                .find(|region| region.region_type == 1)
18138                .unwrap()
18139                .bytes,
18140        );
18141        image.crypto_header_sha256 = sha256_bytes(
18142            &image
18143                .regions
18144                .iter()
18145                .find(|region| region.region_type == 2)
18146                .unwrap()
18147                .bytes,
18148        );
18149        image.key_wrap_table_sha256 = image
18150            .regions
18151            .iter()
18152            .find(|region| region.region_type == 6)
18153            .map(|region| sha256_bytes(&region.bytes))
18154            .unwrap_or([0u8; 32]);
18155        image.manifest_footer_sha256 = sha256_bytes(
18156            &image
18157                .regions
18158                .iter()
18159                .find(|region| region.region_type == 3)
18160                .unwrap()
18161                .bytes,
18162        );
18163        image.root_auth_footer_sha256 = image
18164            .regions
18165            .iter()
18166            .find(|region| region.region_type == 4)
18167            .map(|region| sha256_bytes(&region.bytes))
18168            .unwrap_or([0u8; 32]);
18169        image.volume_trailer_sha256 = sha256_bytes(
18170            &image
18171                .regions
18172                .iter()
18173                .find(|region| region.region_type == 5)
18174                .unwrap()
18175                .bytes,
18176        );
18177    }
18178
18179    fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
18180        let mut locator =
18181            CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
18182                .unwrap();
18183        locator.cmra_image_sha256 = image_sha256;
18184        volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
18185    }
18186
18187    fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
18188        let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
18189        let final_locator = CriticalRecoveryLocator::parse(
18190            &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
18191        )
18192        .unwrap();
18193        let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
18194        volume[final_locator.cmra_offset as usize] ^= 0x55;
18195        volume[mirror_offset] ^= 0x55;
18196        volume[final_offset] ^= 0x55;
18197    }
18198
18199    fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
18200        let (record_offset, record_len) = first_block_record(volume);
18201        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18202        let mut record = BlockRecord::parse(
18203            &volume[record_offset..record_offset + record_len],
18204            block_size,
18205        )
18206        .unwrap();
18207        mutate(&mut record);
18208        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
18209    }
18210
18211    fn mutate_block_record_at_slot(
18212        volume: &mut [u8],
18213        slot: usize,
18214        mutate: impl FnOnce(&mut BlockRecord),
18215    ) {
18216        let (record_offset, record_len) = block_record_at_slot(volume, slot);
18217        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18218        let mut record = BlockRecord::parse(
18219            &volume[record_offset..record_offset + record_len],
18220            block_size,
18221        )
18222        .unwrap();
18223        mutate(&mut record);
18224        volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
18225    }
18226
18227    fn first_block_record(volume: &[u8]) -> (usize, usize) {
18228        block_record_at_slot(volume, 0)
18229    }
18230
18231    fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
18232        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18233        let crypto_start = volume_header.crypto_header_offset as usize;
18234        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18235        let crypto_header = CryptoHeader::parse(
18236            &volume[crypto_start..crypto_end],
18237            volume_header.crypto_header_length,
18238        )
18239        .unwrap();
18240        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
18241        let record_offset = crypto_end + slot * record_len;
18242        assert!(volume.len() >= record_offset + record_len);
18243        (record_offset, record_len)
18244    }
18245
18246    fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
18247        block_record_slots(volume)
18248            .into_iter()
18249            .enumerate()
18250            .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
18251    }
18252
18253    fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
18254        block_record_slots(volume)
18255            .into_iter()
18256            .enumerate()
18257            .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
18258            .collect()
18259    }
18260
18261    fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
18262        let mut slots = Vec::new();
18263        for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
18264            if record.kind == BlockKind::PayloadData {
18265                slots.push(slot);
18266            } else if !slots.is_empty() {
18267                break;
18268            }
18269        }
18270        slots
18271    }
18272
18273    fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
18274        envelope_entries_for_path(opened, path)
18275            .into_iter()
18276            .map(|entry| entry.envelope_index)
18277            .collect()
18278    }
18279
18280    fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
18281        let normalized =
18282            normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
18283        let located = opened.locate_index_file(&normalized).unwrap().unwrap();
18284        let file = &located.shard.files[located.file_index];
18285        frame_range_for_file(&located.shard, file)
18286            .unwrap()
18287            .iter()
18288            .map(|frame| {
18289                located
18290                    .shard
18291                    .envelopes
18292                    .iter()
18293                    .find(|entry| entry.envelope_index == frame.envelope_index)
18294                    .unwrap()
18295                    .clone()
18296            })
18297            .collect()
18298    }
18299
18300    fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
18301        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18302        let crypto_start = volume_header.crypto_header_offset as usize;
18303        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18304        let crypto_header = CryptoHeader::parse(
18305            &volume[crypto_start..crypto_end],
18306            volume_header.crypto_header_length,
18307        )
18308        .unwrap();
18309        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
18310        let manifest_offset = terminal_material_offset(volume);
18311        assert_eq!((manifest_offset - crypto_end) % record_len, 0);
18312        let record_count = (manifest_offset - crypto_end) / record_len;
18313        (0..record_count)
18314            .map(|slot| {
18315                let offset = crypto_end + slot * record_len;
18316                let record = BlockRecord::parse(
18317                    &volume[offset..offset + record_len],
18318                    record_len - BLOCK_RECORD_FRAMING_LEN,
18319                )
18320                .unwrap();
18321                (offset, record_len, record)
18322            })
18323            .collect()
18324    }
18325
18326    fn rewrite_manifest_footer(
18327        volume: &mut [u8],
18328        master_key: &MasterKey,
18329        mutate: impl FnOnce(&mut ManifestFooter),
18330    ) {
18331        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18332        let offset = terminal_material_offset(volume);
18333        let mut footer =
18334            ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
18335        mutate(&mut footer);
18336        footer.manifest_hmac = [0u8; 32];
18337        let mut footer_bytes = footer.to_bytes();
18338        let subkeys = Subkeys::derive(
18339            master_key,
18340            &volume_header.archive_uuid,
18341            &volume_header.session_id,
18342        )
18343        .unwrap();
18344        footer.manifest_hmac = compute_hmac(
18345            HmacDomain::ManifestFooter,
18346            &subkeys.mac_key,
18347            &volume_header.archive_uuid,
18348            &volume_header.session_id,
18349            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
18350        );
18351        footer_bytes = footer.to_bytes();
18352        volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
18353    }
18354
18355    fn rewrite_volume_trailer(
18356        volume: &mut [u8],
18357        master_key: &MasterKey,
18358        mutate: impl FnOnce(&mut VolumeTrailer),
18359    ) {
18360        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18361        let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
18362        let mut trailer =
18363            VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
18364        mutate(&mut trailer);
18365        trailer.trailer_hmac = [0u8; 32];
18366        let mut trailer_bytes = trailer.to_bytes();
18367        let subkeys = Subkeys::derive(
18368            master_key,
18369            &volume_header.archive_uuid,
18370            &volume_header.session_id,
18371        )
18372        .unwrap();
18373        trailer.trailer_hmac = compute_hmac(
18374            HmacDomain::VolumeTrailer,
18375            &subkeys.mac_key,
18376            &volume_header.archive_uuid,
18377            &volume_header.session_id,
18378            &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
18379        );
18380        trailer_bytes = trailer.to_bytes();
18381        volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
18382    }
18383
18384    fn rewrite_sidecar_header(
18385        sidecar: &mut [u8],
18386        master_key: &MasterKey,
18387        mutate: impl FnOnce(&mut BootstrapSidecarHeader),
18388    ) {
18389        let mut header =
18390            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18391        mutate(&mut header);
18392        write_signed_sidecar_header(sidecar, master_key, &mut header);
18393    }
18394
18395    fn write_signed_sidecar_header(
18396        sidecar: &mut [u8],
18397        master_key: &MasterKey,
18398        header: &mut BootstrapSidecarHeader,
18399    ) {
18400        header.sidecar_hmac = [0u8; 32];
18401        let mut header_bytes = header.to_bytes();
18402        let subkeys =
18403            Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
18404        header.sidecar_hmac = compute_hmac(
18405            HmacDomain::BootstrapSidecar,
18406            &subkeys.mac_key,
18407            &header.archive_uuid,
18408            &header.session_id,
18409            &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
18410        );
18411        header_bytes = header.to_bytes();
18412        sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
18413    }
18414
18415    fn sparse_bootstrap_sidecar(
18416        source: &[u8],
18417        master_key: &MasterKey,
18418        include_manifest: bool,
18419        include_index_root: bool,
18420        include_dictionary: bool,
18421    ) -> Vec<u8> {
18422        let source_header =
18423            BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18424        let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
18425        let mut header = BootstrapSidecarHeader {
18426            archive_uuid: source_header.archive_uuid,
18427            session_id: source_header.session_id,
18428            flags: 0,
18429            manifest_footer_offset: 0,
18430            manifest_footer_length: 0,
18431            index_root_records_offset: 0,
18432            index_root_records_length: 0,
18433            dictionary_records_offset: 0,
18434            dictionary_records_length: 0,
18435            sidecar_hmac: [0u8; 32],
18436            header_crc32c: 0,
18437        };
18438
18439        if include_manifest {
18440            assert!(source_header.has_manifest_footer());
18441            let (offset, length) = append_sidecar_section(
18442                source,
18443                &mut sidecar,
18444                source_header.manifest_footer_offset,
18445                source_header.manifest_footer_length as u64,
18446            );
18447            header.flags |= 0x01;
18448            header.manifest_footer_offset = offset;
18449            header.manifest_footer_length = length as u32;
18450        }
18451        if include_index_root {
18452            assert!(source_header.has_index_root_records());
18453            let (offset, length) = append_sidecar_section(
18454                source,
18455                &mut sidecar,
18456                source_header.index_root_records_offset,
18457                source_header.index_root_records_length,
18458            );
18459            header.flags |= 0x02;
18460            header.index_root_records_offset = offset;
18461            header.index_root_records_length = length;
18462        }
18463        if include_dictionary {
18464            assert!(source_header.has_dictionary_records());
18465            let (offset, length) = append_sidecar_section(
18466                source,
18467                &mut sidecar,
18468                source_header.dictionary_records_offset,
18469                source_header.dictionary_records_length,
18470            );
18471            header.flags |= 0x04;
18472            header.dictionary_records_offset = offset;
18473            header.dictionary_records_length = length;
18474        }
18475
18476        write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
18477        sidecar
18478    }
18479
18480    fn append_sidecar_section(
18481        source: &[u8],
18482        sidecar: &mut Vec<u8>,
18483        source_offset: u64,
18484        length: u64,
18485    ) -> (u64, u64) {
18486        let source_offset = source_offset as usize;
18487        let length = length as usize;
18488        let offset = sidecar.len() as u64;
18489        sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
18490        (offset, length as u64)
18491    }
18492
18493    fn mutate_sidecar_manifest(
18494        sidecar: &mut [u8],
18495        master_key: &MasterKey,
18496        mutate: impl FnOnce(&mut ManifestFooter),
18497    ) {
18498        let header =
18499            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18500        let offset = header.manifest_footer_offset as usize;
18501        let mut footer =
18502            ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
18503        mutate(&mut footer);
18504        footer.manifest_hmac = [0u8; 32];
18505        let mut footer_bytes = footer.to_bytes();
18506        let subkeys =
18507            Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
18508        footer.manifest_hmac = compute_hmac(
18509            HmacDomain::ManifestFooter,
18510            &subkeys.mac_key,
18511            &footer.archive_uuid,
18512            &footer.session_id,
18513            &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
18514        );
18515        footer_bytes = footer.to_bytes();
18516        sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
18517    }
18518
18519    fn mutate_sidecar_index_record(
18520        sidecar: &mut [u8],
18521        record_index: usize,
18522        mutate: impl FnOnce(&mut BlockRecord),
18523    ) {
18524        let header =
18525            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18526        let record_len = sidecar_record_len(sidecar);
18527        let offset = header.index_root_records_offset as usize + record_index * record_len;
18528        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18529        let mut record =
18530            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
18531        mutate(&mut record);
18532        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
18533    }
18534
18535    fn mutate_sidecar_dictionary_record(
18536        sidecar: &mut [u8],
18537        record_index: usize,
18538        mutate: impl FnOnce(&mut BlockRecord),
18539    ) {
18540        let header =
18541            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18542        let record_len = sidecar_record_len(sidecar);
18543        let offset = header.dictionary_records_offset as usize + record_index * record_len;
18544        let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
18545        let mut record =
18546            BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
18547        mutate(&mut record);
18548        sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
18549    }
18550
18551    fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
18552        let header =
18553            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18554        let record_len = sidecar_record_len(sidecar);
18555        let left_offset = header.index_root_records_offset as usize + left * record_len;
18556        let right_offset = header.index_root_records_offset as usize + right * record_len;
18557        for idx in 0..record_len {
18558            sidecar.swap(left_offset + idx, right_offset + idx);
18559        }
18560    }
18561
18562    fn sidecar_record_len(sidecar: &[u8]) -> usize {
18563        let header =
18564            BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
18565        let footer_offset = header.manifest_footer_offset as usize;
18566        let footer =
18567            ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
18568                .unwrap();
18569        let index_record_count = footer.index_root_data_block_count as usize
18570            + footer.index_root_parity_block_count as usize;
18571        header.index_root_records_length as usize / index_record_count
18572    }
18573
18574    fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
18575        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18576        assert_eq!(volume_header.volume_index, 0);
18577        assert_eq!(volume_header.stripe_width, 1);
18578        let crypto_start = volume_header.crypto_header_offset as usize;
18579        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18580        let crypto_header = CryptoHeader::parse(
18581            &volume[crypto_start..crypto_end],
18582            volume_header.crypto_header_length,
18583        )
18584        .unwrap();
18585        let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
18586        let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
18587        for offset in 0..record_count {
18588            let block_index = extent.first_block_index + offset;
18589            let record_offset = crypto_end + block_index as usize * record_len;
18590            volume[record_offset + 16] ^= 0x55;
18591        }
18592    }
18593
18594    fn terminal_material_offset(volume: &[u8]) -> usize {
18595        let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
18596        let crypto_start = volume_header.crypto_header_offset as usize;
18597        let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
18598        let crypto_header = CryptoHeader::parse(
18599            &volume[crypto_start..crypto_end],
18600            volume_header.crypto_header_length,
18601        )
18602        .unwrap();
18603        let (_, offset, _) = parse_stream_block_prefix(
18604            volume,
18605            crypto_end,
18606            crypto_header.fixed.block_size as usize,
18607            &volume_header,
18608        )
18609        .unwrap();
18610        offset
18611    }
18612
18613    #[derive(Debug)]
18614    struct TestObject {
18615        extent: ObjectExtent,
18616        records: Vec<BlockRecord>,
18617    }
18618
18619    #[derive(Debug)]
18620    struct TestFileMeta {
18621        path: Vec<u8>,
18622        frame_index: u64,
18623        tar_stream_offset: u64,
18624        member_group_size: u64,
18625        file_data_size: u64,
18626    }
18627
18628    fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
18629        let volume_header = test_volume_header();
18630        let crypto_header = test_crypto_header();
18631        let subkeys = Subkeys::derive(
18632            &master_key(),
18633            &volume_header.archive_uuid,
18634            &volume_header.session_id,
18635        )
18636        .unwrap();
18637        let mut next_block_index = 0u64;
18638        let mut blocks = BTreeMap::new();
18639
18640        let healthy = test_member(b"healthy.txt", b"healthy payload\n");
18641        let broken = test_member(b"broken.txt", b"broken payload\n");
18642        let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
18643
18644        let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
18645        let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
18646
18647        let healthy_payload = encrypt_test_object(
18648            &healthy_frame,
18649            &subkeys.enc_key,
18650            &subkeys.nonce_seed,
18651            b"envelope",
18652            0,
18653            BlockKind::PayloadData,
18654            &mut next_block_index,
18655            &crypto_header,
18656            &volume_header,
18657        );
18658        let broken_payload = encrypt_test_object(
18659            &broken_frame,
18660            &subkeys.enc_key,
18661            &subkeys.nonce_seed,
18662            b"envelope",
18663            1,
18664            BlockKind::PayloadData,
18665            &mut next_block_index,
18666            &crypto_header,
18667            &volume_header,
18668        );
18669        let broken_payload_block = broken_payload.extent.first_block_index;
18670        insert_records(&mut blocks, &healthy_payload.records);
18671        insert_records(&mut blocks, &broken_payload.records);
18672
18673        let frames = vec![
18674            FrameEntry {
18675                frame_index: 0,
18676                envelope_index: 0,
18677                offset_in_envelope: 0,
18678                compressed_size: healthy_frame.len() as u32,
18679                decompressed_size: healthy.len() as u32,
18680                flags: 0x0000_0003,
18681                tar_stream_offset: 0,
18682            },
18683            FrameEntry {
18684                frame_index: 1,
18685                envelope_index: 1,
18686                offset_in_envelope: 0,
18687                compressed_size: broken_frame.len() as u32,
18688                decompressed_size: broken.len() as u32,
18689                flags: 0x0000_0003,
18690                tar_stream_offset: healthy.len() as u64,
18691            },
18692        ];
18693        let envelopes = vec![
18694            EnvelopeEntry {
18695                envelope_index: 0,
18696                first_block_index: healthy_payload.extent.first_block_index,
18697                data_block_count: healthy_payload.extent.data_block_count,
18698                parity_block_count: 0,
18699                encrypted_size: healthy_payload.extent.encrypted_size,
18700                plaintext_size: healthy_frame.len() as u32,
18701                first_frame_index: 0,
18702                frame_count: 1,
18703            },
18704            EnvelopeEntry {
18705                envelope_index: 1,
18706                first_block_index: broken_payload.extent.first_block_index,
18707                data_block_count: broken_payload.extent.data_block_count,
18708                parity_block_count: 0,
18709                encrypted_size: broken_payload.extent.encrypted_size,
18710                plaintext_size: broken_frame.len() as u32,
18711                first_frame_index: 1,
18712                frame_count: 1,
18713            },
18714        ];
18715        let files = vec![
18716            TestFileMeta {
18717                path: b"healthy.txt".to_vec(),
18718                frame_index: 0,
18719                tar_stream_offset: 0,
18720                member_group_size: healthy.len() as u64,
18721                file_data_size: b"healthy payload\n".len() as u64,
18722            },
18723            TestFileMeta {
18724                path: b"broken.txt".to_vec(),
18725                frame_index: 1,
18726                tar_stream_offset: healthy.len() as u64,
18727                member_group_size: broken.len() as u64,
18728                file_data_size: b"broken payload\n".len() as u64,
18729            },
18730        ];
18731
18732        let (index_shard_plaintext, first_path_hash, last_path_hash) =
18733            build_test_index_shard(&files, &frames, &envelopes);
18734        let index_shard = encrypt_test_object(
18735            &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
18736            &subkeys.index_shard_key,
18737            &subkeys.index_nonce_seed,
18738            b"idxshard",
18739            0,
18740            BlockKind::IndexShardData,
18741            &mut next_block_index,
18742            &crypto_header,
18743            &volume_header,
18744        );
18745        insert_records(&mut blocks, &index_shard.records);
18746
18747        let shard_entry = ShardEntry {
18748            shard_index: 0,
18749            first_block_index: index_shard.extent.first_block_index,
18750            data_block_count: index_shard.extent.data_block_count,
18751            parity_block_count: 0,
18752            encrypted_size: index_shard.extent.encrypted_size,
18753            decompressed_size: index_shard_plaintext.len() as u32,
18754            file_count: files.len() as u32,
18755            first_path_hash,
18756            last_path_hash,
18757        };
18758        let mut root_header = IndexRootHeader::empty();
18759        root_header.frame_count = frames.len() as u64;
18760        root_header.envelope_count = envelopes.len() as u64;
18761        root_header.file_count = files.len() as u64;
18762        root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
18763            + broken_payload.extent.data_block_count as u64;
18764        root_header.tar_total_size = tar_stream.len() as u64;
18765        root_header.content_sha256 = sha256_bytes(&tar_stream);
18766        let index_root = IndexRoot {
18767            header: root_header,
18768            shards: vec![shard_entry],
18769            directory_hint_shards: Vec::new(),
18770        };
18771
18772        let index_root_plaintext = index_root.to_bytes();
18773        let index_root_object = encrypt_test_object(
18774            &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
18775            &subkeys.index_root_key,
18776            &subkeys.index_nonce_seed,
18777            b"idxroot",
18778            0,
18779            BlockKind::IndexRootData,
18780            &mut next_block_index,
18781            &crypto_header,
18782            &volume_header,
18783        );
18784        insert_records(&mut blocks, &index_root_object.records);
18785
18786        let archive_uuid = volume_header.archive_uuid;
18787        let session_id = volume_header.session_id;
18788        let opened = OpenedArchive {
18789            options: ReaderOptions::default(),
18790            observed_archive_bytes: 1_000_000,
18791            observed_volume_count: 1,
18792            subkeys,
18793            blocks,
18794            lazy_blocks: None,
18795            crypto_header_bytes: Vec::new(),
18796            volume_header,
18797            crypto_header,
18798            manifest_footer: ManifestFooter {
18799                archive_uuid,
18800                session_id,
18801                volume_index: 0,
18802                is_authoritative: 1,
18803                total_volumes: 1,
18804                index_root_first_block: index_root_object.extent.first_block_index,
18805                index_root_data_block_count: index_root_object.extent.data_block_count,
18806                index_root_parity_block_count: 0,
18807                index_root_encrypted_size: index_root_object.extent.encrypted_size,
18808                index_root_decompressed_size: index_root_plaintext.len() as u32,
18809                manifest_hmac: [0u8; 32],
18810            },
18811            volume_trailer: Some(VolumeTrailer {
18812                archive_uuid,
18813                session_id,
18814                volume_index: 0,
18815                block_count: next_block_index,
18816                bytes_written: 0,
18817                manifest_footer_offset: 0,
18818                manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
18819                closed_at_ns: 0,
18820                root_auth_footer_offset: 0,
18821                root_auth_footer_length: 0,
18822                root_auth_flags: 0,
18823                trailer_hmac: [0u8; 32],
18824            }),
18825            root_auth_footer: None,
18826            index_root,
18827            payload_dictionary: None,
18828        };
18829        (opened, broken_payload_block)
18830    }
18831
18832    fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
18833        let locating = opened.index_root.shards[0].clone();
18834        let mut shard = opened.load_index_shard(&locating).unwrap();
18835        mutate(&mut shard);
18836        let plaintext = shard.to_bytes();
18837        let mut next_block_index = opened
18838            .blocks
18839            .keys()
18840            .last()
18841            .copied()
18842            .map(|index| index + 1)
18843            .unwrap_or(0);
18844        let replacement = encrypt_test_object(
18845            &compress_zstd_frame(&plaintext, 1).unwrap(),
18846            &opened.subkeys.index_shard_key,
18847            &opened.subkeys.index_nonce_seed,
18848            b"idxshard",
18849            locating.shard_index,
18850            BlockKind::IndexShardData,
18851            &mut next_block_index,
18852            &opened.crypto_header,
18853            &opened.volume_header,
18854        );
18855        insert_records(&mut opened.blocks, &replacement.records);
18856        opened.index_root.shards[0] = ShardEntry {
18857            shard_index: locating.shard_index,
18858            first_block_index: replacement.extent.first_block_index,
18859            data_block_count: replacement.extent.data_block_count,
18860            parity_block_count: 0,
18861            encrypted_size: replacement.extent.encrypted_size,
18862            decompressed_size: plaintext.len() as u32,
18863            file_count: shard.files.len() as u32,
18864            first_path_hash: shard.files.first().unwrap().path_hash,
18865            last_path_hash: shard.files.last().unwrap().path_hash,
18866        };
18867    }
18868
18869    fn rewrite_as_single_healthy_file(
18870        opened: &mut OpenedArchive,
18871        mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
18872    ) {
18873        let healthy_path = b"healthy.txt";
18874        let healthy_payload = b"healthy payload\n";
18875        let healthy_member = test_member(healthy_path, healthy_payload);
18876        replace_first_index_shard(opened, |shard| {
18877            let file_index = (0..shard.files.len())
18878                .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
18879                .unwrap();
18880            let mut file = shard.files[file_index].clone();
18881            let frame = shard
18882                .frames
18883                .iter()
18884                .find(|entry| entry.frame_index == 0)
18885                .unwrap()
18886                .clone();
18887            let envelope = shard
18888                .envelopes
18889                .iter()
18890                .find(|entry| entry.envelope_index == 0)
18891                .unwrap()
18892                .clone();
18893            let mut path = healthy_path.to_vec();
18894
18895            file.path_offset = 0;
18896            file.path_length = path.len() as u32;
18897            file.first_frame_index = 0;
18898            file.frame_count = 1;
18899            file.offset_in_first_frame_plaintext = 0;
18900            file.tar_member_group_size = healthy_member.len() as u64;
18901            file.file_data_size = healthy_payload.len() as u64;
18902            file.flags = crate::entry_metadata::EXTENDED_METADATA_V1;
18903            mutate(&mut file, &mut path);
18904            file.path_offset = 0;
18905            file.path_length = path.len() as u32;
18906            file.path_hash = hash_prefix(&path);
18907
18908            shard.files = vec![file];
18909            shard.frames = vec![frame];
18910            shard.envelopes = vec![envelope];
18911            shard.string_pool = path;
18912        });
18913
18914        opened.index_root.header.file_count = 1;
18915        opened.index_root.header.frame_count = 1;
18916        opened.index_root.header.envelope_count = 1;
18917        opened.index_root.header.payload_block_count = 1;
18918        opened.index_root.header.tar_total_size = healthy_member.len() as u64;
18919        opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
18920    }
18921
18922    fn test_volume_header() -> VolumeHeader {
18923        VolumeHeader {
18924            format_version: FORMAT_VERSION,
18925            volume_format_rev: VOLUME_FORMAT_REV,
18926            volume_index: 0,
18927            stripe_width: 1,
18928            archive_uuid: [0x31; 16],
18929            session_id: [0x42; 16],
18930            crypto_header_offset: VOLUME_HEADER_LEN as u32,
18931            crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
18932            header_crc32c: 0,
18933        }
18934    }
18935
18936    fn test_crypto_header() -> CryptoHeaderFixed {
18937        CryptoHeaderFixed {
18938            length: CRYPTO_HEADER_FIXED_LEN as u32,
18939            compression_algo: CompressionAlgo::ZstdFramed,
18940            aead_algo: AeadAlgo::AesGcmSiv256,
18941            fec_algo: FecAlgo::ReedSolomonGF16,
18942            kdf_algo: KdfAlgo::Raw,
18943            chunk_size: 4096,
18944            envelope_target_size: 8192,
18945            block_size: 4096,
18946            fec_data_shards: 4,
18947            fec_parity_shards: 0,
18948            index_fec_data_shards: 4,
18949            index_fec_parity_shards: 0,
18950            index_root_fec_data_shards: 4,
18951            index_root_fec_parity_shards: 0,
18952            stripe_width: 1,
18953            volume_loss_tolerance: 0,
18954            bit_rot_buffer_pct: 0,
18955            has_dictionary: 0,
18956            max_path_length: 4096,
18957            expected_volume_size: 0,
18958        }
18959    }
18960
18961    #[allow(clippy::too_many_arguments)]
18962    fn encrypt_test_object(
18963        plaintext: &[u8],
18964        key: &[u8; 32],
18965        nonce_seed: &[u8; 32],
18966        domain: &[u8],
18967        counter: u64,
18968        data_kind: BlockKind,
18969        next_block_index: &mut u64,
18970        crypto_header: &CryptoHeaderFixed,
18971        volume_header: &VolumeHeader,
18972    ) -> TestObject {
18973        let block_size = crypto_header.block_size as usize;
18974        let encrypted = encrypt_padded_aead_object(
18975            AeadObjectContext {
18976                algo: crypto_header.aead_algo,
18977                key,
18978                nonce_seed,
18979                domain,
18980                archive_uuid: &volume_header.archive_uuid,
18981                session_id: &volume_header.session_id,
18982                counter,
18983            },
18984            block_size,
18985            plaintext,
18986        )
18987        .unwrap();
18988        assert_eq!(encrypted.len() % block_size, 0);
18989
18990        let first_block_index = *next_block_index;
18991        let data_block_count = encrypted.len() / block_size;
18992        let records = encrypted
18993            .chunks(block_size)
18994            .enumerate()
18995            .map(|(index, payload)| BlockRecord {
18996                block_index: first_block_index + index as u64,
18997                kind: data_kind,
18998                flags: if index + 1 == data_block_count {
18999                    0x01
19000                } else {
19001                    0
19002                },
19003                payload: payload.to_vec(),
19004                record_crc32c: 0,
19005            })
19006            .collect::<Vec<_>>();
19007        *next_block_index += data_block_count as u64;
19008
19009        TestObject {
19010            extent: ObjectExtent {
19011                first_block_index,
19012                data_block_count: data_block_count as u32,
19013                parity_block_count: 0,
19014                encrypted_size: encrypted.len() as u32,
19015            },
19016            records,
19017        }
19018    }
19019
19020    fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
19021        for record in records {
19022            assert!(blocks.insert(record.block_index, record.clone()).is_none());
19023        }
19024    }
19025
19026    #[allow(clippy::too_many_arguments)]
19027    fn build_metadata_object_from_payload(
19028        payload: &[u8],
19029        _subkeys: &Subkeys,
19030        volume_header: &VolumeHeader,
19031        crypto_header: &CryptoHeaderFixed,
19032        key: &[u8; 32],
19033        nonce_seed: &[u8; 32],
19034        domain: &[u8],
19035        counter: u64,
19036        data_kind: BlockKind,
19037        next_block_index: &mut u64,
19038    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
19039        let compressed = compress_zstd_frame(payload, 1).unwrap();
19040        build_metadata_object_from_compressed(
19041            &compressed,
19042            key,
19043            nonce_seed,
19044            domain,
19045            counter,
19046            data_kind,
19047            next_block_index,
19048            crypto_header,
19049            volume_header,
19050        )
19051    }
19052
19053    #[allow(clippy::too_many_arguments)]
19054    fn build_metadata_object_from_compressed(
19055        compressed: &[u8],
19056        key: &[u8; 32],
19057        nonce_seed: &[u8; 32],
19058        domain: &[u8],
19059        counter: u64,
19060        data_kind: BlockKind,
19061        next_block_index: &mut u64,
19062        crypto_header: &CryptoHeaderFixed,
19063        volume_header: &VolumeHeader,
19064    ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
19065        let object = encrypt_test_object(
19066            compressed,
19067            key,
19068            nonce_seed,
19069            domain,
19070            counter,
19071            data_kind,
19072            next_block_index,
19073            crypto_header,
19074            volume_header,
19075        );
19076
19077        let mut blocks = BTreeMap::new();
19078        for record in object.records {
19079            blocks.insert(record.block_index, record);
19080        }
19081        (object.extent, blocks)
19082    }
19083
19084    #[allow(clippy::too_many_arguments)]
19085    fn assert_metadata_object_from_compressed(
19086        compressed: &[u8],
19087        decompressed_size: usize,
19088        _subkeys: &Subkeys,
19089        volume_header: &VolumeHeader,
19090        crypto_header: &CryptoHeaderFixed,
19091        key: &[u8; 32],
19092        nonce_seed: &[u8; 32],
19093        domain: &[u8],
19094        counter: u64,
19095        data_kind: BlockKind,
19096        parity_kind: BlockKind,
19097        class_data_shards: u16,
19098        class_parity_shards: u16,
19099        next_block_index: &mut u64,
19100        expected: FormatError,
19101    ) {
19102        let (extent, blocks) = build_metadata_object_from_compressed(
19103            compressed,
19104            key,
19105            nonce_seed,
19106            domain,
19107            counter,
19108            data_kind,
19109            next_block_index,
19110            crypto_header,
19111            volume_header,
19112        );
19113        let error = load_metadata_object_from_parts(
19114            &blocks,
19115            ObjectLoadContext {
19116                volume_header,
19117                crypto_header,
19118                extent,
19119                data_kind,
19120                parity_kind,
19121                key,
19122                nonce_seed,
19123                domain,
19124                counter,
19125                class_data_shard_max: class_data_shards,
19126                class_parity_shard_max: class_parity_shards,
19127            },
19128            decompressed_size as u32,
19129        )
19130        .unwrap_err();
19131        assert_eq!(error, expected);
19132    }
19133
19134    fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
19135        let record = blocks.get_mut(&block_index).unwrap();
19136        assert_eq!(record.kind, BlockKind::PayloadData);
19137        record.payload[0] ^= 0x55;
19138    }
19139
19140    fn build_test_index_shard(
19141        files: &[TestFileMeta],
19142        frames: &[FrameEntry],
19143        envelopes: &[EnvelopeEntry],
19144    ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
19145        let mut sorted = files
19146            .iter()
19147            .map(|file| (hash_prefix(&file.path), file))
19148            .collect::<Vec<_>>();
19149        sorted.sort_by(|left, right| {
19150            (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
19151                right.0,
19152                right.1.path.as_slice(),
19153                right.1.tar_stream_offset,
19154            ))
19155        });
19156
19157        let mut string_pool = Vec::new();
19158        let mut file_entries = Vec::with_capacity(sorted.len());
19159        for (path_hash, file) in &sorted {
19160            let path_offset = string_pool.len() as u32;
19161            string_pool.extend_from_slice(&file.path);
19162            file_entries.push(FileEntry {
19163                path_hash: *path_hash,
19164                path_offset,
19165                path_length: file.path.len() as u32,
19166                first_frame_index: file.frame_index,
19167                frame_count: 1,
19168                offset_in_first_frame_plaintext: 0,
19169                tar_member_group_size: file.member_group_size,
19170                file_data_size: file.file_data_size,
19171                flags: crate::entry_metadata::EXTENDED_METADATA_V1,
19172            });
19173        }
19174
19175        let header = IndexShardHeader {
19176            version: 1,
19177            shard_index: 0,
19178            file_count: file_entries.len() as u32,
19179            frame_count: frames.len() as u32,
19180            envelope_count: envelopes.len() as u32,
19181            file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
19182            frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
19183                as u32,
19184            envelope_table_offset: (INDEX_SHARD_HEADER_LEN
19185                + file_entries.len() * FILE_ENTRY_LEN
19186                + frames.len() * FRAME_ENTRY_LEN) as u32,
19187            string_pool_offset: (INDEX_SHARD_HEADER_LEN
19188                + file_entries.len() * FILE_ENTRY_LEN
19189                + frames.len() * FRAME_ENTRY_LEN
19190                + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
19191            string_pool_size: string_pool.len() as u32,
19192        };
19193
19194        let mut bytes = Vec::new();
19195        bytes.extend_from_slice(&header.to_bytes());
19196        for entry in &file_entries {
19197            bytes.extend_from_slice(&entry.to_bytes());
19198        }
19199        for entry in frames {
19200            bytes.extend_from_slice(&entry.to_bytes());
19201        }
19202        for entry in envelopes {
19203            bytes.extend_from_slice(&entry.to_bytes());
19204        }
19205        bytes.extend_from_slice(&string_pool);
19206
19207        (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
19208    }
19209
19210    fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
19211        let records =
19212            crate::entry_metadata::portable_primary_pax(path, 0o644, "other", false).unwrap();
19213        let pax = crate::entry_metadata::encode_canonical_pax(&records).unwrap();
19214        let mut out = Vec::new();
19215        out.extend_from_slice(&test_tar_header(
19216            b"TZAP-PAX/PRIMARY",
19217            pax.len() as u64,
19218            0,
19219            b'x',
19220        ));
19221        out.extend_from_slice(&pax);
19222        out.resize(out.len() + padding_to_512(pax.len()), 0);
19223        out.extend_from_slice(&test_tar_header(path, data.len() as u64, 0o644, b'0'));
19224        out.extend_from_slice(data);
19225        out.resize(out.len() + padding_to_512(data.len()), 0);
19226        out
19227    }
19228
19229    fn test_tar_header(path: &[u8], size: u64, mode: u64, typeflag: u8) -> [u8; 512] {
19230        let mut header = [0u8; 512];
19231        header[..path.len()].copy_from_slice(path);
19232        write_test_tar_octal(&mut header[100..108], mode);
19233        write_test_tar_octal(&mut header[108..116], 0);
19234        write_test_tar_octal(&mut header[116..124], 0);
19235        write_test_tar_octal(&mut header[124..136], size);
19236        write_test_tar_octal(&mut header[136..148], 0);
19237        header[148..156].fill(b' ');
19238        header[156] = typeflag;
19239        header[257..263].copy_from_slice(b"ustar\0");
19240        header[263..265].copy_from_slice(b"00");
19241        let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
19242        write_test_tar_checksum(&mut header[148..156], checksum);
19243        header
19244    }
19245
19246    fn write_test_tar_octal(field: &mut [u8], value: u64) {
19247        let digits = format!("{value:o}");
19248        field.fill(0);
19249        let start = field.len() - 1 - digits.len();
19250        field[..start].fill(b'0');
19251        field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
19252    }
19253
19254    fn write_test_tar_checksum(field: &mut [u8], value: u64) {
19255        let digits = format!("{value:06o}");
19256        field[0..6].copy_from_slice(digits.as_bytes());
19257        field[6] = 0;
19258        field[7] = b' ';
19259    }
19260
19261    fn padding_to_512(len: usize) -> usize {
19262        let remainder = len % 512;
19263        if remainder == 0 {
19264            0
19265        } else {
19266            512 - remainder
19267        }
19268    }
19269}