Skip to main content

tzap_core/
non_seekable_reader.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::io::{ErrorKind, Read};
4use std::path::{Path, PathBuf};
5
6use sha2::{Digest, Sha256};
7
8use crate::compression::validate_exact_zstd_frame;
9use crate::crypto::{
10    decrypt_padded_aead_object, verify_integrity_tag, AeadObjectContext, HmacDomain, MasterKey,
11    Subkeys,
12};
13use crate::fec::repair_data_gf16;
14use crate::format::{
15    BlockKind, ExtractError, FormatError, BLOCK_RECORD_FRAMING_LEN, VOLUME_HEADER_LEN,
16};
17use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
18use crate::reader::{
19    block_record_error_is_recoverable_erasure, expected_stream_block_index,
20    manifest_bootstrap_fields_match, observed_archive_size, parse_non_seekable_bootstrap_material,
21    parse_terminal_material_read_at, recipient_wrap_subkeys_from_table, required_object_parity,
22    startup_key_wrap_table, total_extraction_size_cap, v41_terminal_tail_cap,
23    validate_crypto_class_parity_exactness, validate_reader_options, ArchiveEntry,
24    ArchiveIndexEntry, ArchiveReadAt, KeyHoldingTerminalContext, NonSeekableBootstrapMaterial,
25    OpenedArchive, ReaderOptions, RecipientWrapCandidateMasterKey, RecipientWrapRecordContext,
26    StreamedArchiveOpenParts,
27};
28use crate::tar_model::{
29    NoopTarStreamObserver, SafeExtractionOptions, TarStreamFilesystemRestoreObserver,
30    TarStreamMemberSummary, TarStreamObserver, TarStreamSummary, TarStreamSummaryValidator,
31};
32use crate::wire::{
33    BlockRecord, CryptoHeader, CryptoHeaderFixed, ExtensionTlv, RootAuthFooterV1, VolumeHeader,
34};
35
36const DEFAULT_MAX_RETAINED_METADATA_BYTES: usize = 128 * 1024 * 1024;
37const DEFAULT_MAX_INCOMPLETE_TAR_GROUP_BYTES: usize = 1024 * 1024;
38const DEFAULT_MAX_STREAMED_MEMBER_COUNT: u64 = 1_000_000;
39
40fn parse_volume_format_dispatch(volume_header: &VolumeHeader) -> Result<(), FormatError> {
41    let revision = volume_header.parse_volume_format_revision()?;
42    match revision {
43        crate::format::VolumeFormatRevision::V44 => Ok(()),
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SequentialRootAuthStatus {
49    Absent,
50    WireValidOnly,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct SequentialVerifyReport {
55    pub archive_uuid: [u8; 16],
56    pub session_id: [u8; 16],
57    pub volume_format_rev: u16,
58    pub volume_index: u32,
59    pub total_volumes: u32,
60    pub file_count: u64,
61    pub payload_block_count: u64,
62    pub tar_total_size: u64,
63    pub content_sha256: [u8; 32],
64    pub root_auth: SequentialRootAuthStatus,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct SequentialExtractReport {
69    pub verification: SequentialVerifyReport,
70    pub extracted_member_count: u64,
71    pub degraded_metadata_count: u64,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct SequentialListReport {
76    pub verification: SequentialVerifyReport,
77    pub entries: Vec<ArchiveEntry>,
78    pub index_entries: Vec<ArchiveIndexEntry>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct NonSeekableReaderOptions {
83    pub reader: ReaderOptions,
84    pub max_terminal_tail_size: usize,
85    pub max_retained_metadata_bytes: usize,
86    pub max_incomplete_tar_group_bytes: usize,
87    pub max_streamed_member_count: u64,
88}
89
90impl Default for NonSeekableReaderOptions {
91    fn default() -> Self {
92        Self {
93            reader: ReaderOptions::default(),
94            max_terminal_tail_size: v41_terminal_tail_cap()
95                .expect("v41 terminal tail cap must fit usize"),
96            max_retained_metadata_bytes: DEFAULT_MAX_RETAINED_METADATA_BYTES,
97            max_incomplete_tar_group_bytes: DEFAULT_MAX_INCOMPLETE_TAR_GROUP_BYTES,
98            max_streamed_member_count: DEFAULT_MAX_STREAMED_MEMBER_COUNT,
99        }
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub(crate) struct StreamedEnvelopeSummary {
105    pub(crate) envelope_index: u64,
106    pub(crate) first_block_index: u64,
107    pub(crate) data_block_count: u32,
108    pub(crate) parity_block_count: u32,
109    pub(crate) encrypted_size: u32,
110    pub(crate) plaintext_size: u32,
111    pub(crate) first_frame_index: u64,
112    pub(crate) frame_count: u32,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub(crate) struct StreamedFrameSummary {
117    pub(crate) frame_index: u64,
118    pub(crate) envelope_index: u64,
119    pub(crate) offset_in_envelope: u32,
120    pub(crate) compressed_size: u32,
121    pub(crate) decompressed_size: u32,
122    pub(crate) tar_stream_offset: u64,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub(crate) struct StreamedPayloadSummary {
127    pub(crate) tar: TarStreamSummary,
128    pub(crate) content_sha256: [u8; 32],
129    pub(crate) envelopes: Vec<StreamedEnvelopeSummary>,
130    pub(crate) frames: Vec<StreamedFrameSummary>,
131}
132
133impl StreamedPayloadSummary {
134    pub(crate) fn envelope_map(
135        &self,
136    ) -> Result<BTreeMap<u64, &StreamedEnvelopeSummary>, FormatError> {
137        let mut map = BTreeMap::new();
138        for envelope in &self.envelopes {
139            if map.insert(envelope.envelope_index, envelope).is_some() {
140                return Err(FormatError::InvalidArchive(
141                    "duplicate streamed payload envelope",
142                ));
143            }
144        }
145        Ok(map)
146    }
147
148    pub(crate) fn frame_map(&self) -> Result<BTreeMap<u64, &StreamedFrameSummary>, FormatError> {
149        let mut map = BTreeMap::new();
150        for frame in &self.frames {
151            if map.insert(frame.frame_index, frame).is_some() {
152                return Err(FormatError::InvalidArchive(
153                    "duplicate streamed payload frame",
154                ));
155            }
156        }
157        Ok(map)
158    }
159
160    pub(crate) fn member_start_map(
161        &self,
162    ) -> Result<BTreeMap<u64, &TarStreamMemberSummary>, FormatError> {
163        let mut map = BTreeMap::new();
164        for member in &self.tar.members {
165            if map.insert(member.group_start, member).is_some() {
166                return Err(FormatError::InvalidArchive(
167                    "duplicate streamed tar member start",
168                ));
169            }
170        }
171        Ok(map)
172    }
173
174    pub(crate) fn frame_flags(&self, frame: &StreamedFrameSummary) -> Result<u32, FormatError> {
175        let frame_end = frame
176            .tar_stream_offset
177            .checked_add(frame.decompressed_size as u64)
178            .ok_or(FormatError::InvalidArchive("streamed frame range overflow"))?;
179        let mut flags = 0u32;
180        for member in &self.tar.members {
181            if member.group_start == frame.tar_stream_offset {
182                flags |= 0x0000_0001;
183            }
184            let member_end = member.group_start.checked_add(member.group_size).ok_or(
185                FormatError::InvalidArchive("streamed tar member range overflow"),
186            )?;
187            if member_end == frame_end {
188                flags |= 0x0000_0002;
189            }
190        }
191        Ok(flags)
192    }
193}
194
195pub fn verify_non_seekable_stream<R: Read>(
196    reader: R,
197    master_key: &MasterKey,
198) -> Result<SequentialVerifyReport, FormatError> {
199    verify_non_seekable_stream_with_options(reader, master_key, NonSeekableReaderOptions::default())
200}
201
202pub fn verify_non_seekable_stream_with_options<R: Read>(
203    reader: R,
204    master_key: &MasterKey,
205    options: NonSeekableReaderOptions,
206) -> Result<SequentialVerifyReport, FormatError> {
207    Ok(run_non_seekable_stream(
208        reader,
209        NonSeekableKeySource::MasterKey(Some(master_key)),
210        options,
211        NoopTarStreamObserver,
212        None,
213    )?
214    .verification)
215}
216
217pub fn verify_non_seekable_stream_with_recipient_wrap_resolver_options<R, F>(
218    reader: R,
219    mut resolver: F,
220    options: NonSeekableReaderOptions,
221) -> Result<SequentialVerifyReport, FormatError>
222where
223    R: Read,
224    F: FnMut(
225        RecipientWrapRecordContext<'_>,
226    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
227{
228    Ok(run_non_seekable_stream(
229        reader,
230        NonSeekableKeySource::RecipientWrap(&mut resolver),
231        options,
232        NoopTarStreamObserver,
233        None,
234    )?
235    .verification)
236}
237
238pub fn verify_unencrypted_non_seekable_stream_with_options<R: Read>(
239    reader: R,
240    options: NonSeekableReaderOptions,
241) -> Result<SequentialVerifyReport, FormatError> {
242    Ok(run_non_seekable_stream(
243        reader,
244        NonSeekableKeySource::MasterKey(None),
245        options,
246        NoopTarStreamObserver,
247        None,
248    )?
249    .verification)
250}
251
252pub fn verify_non_seekable_stream_with_bootstrap_sidecar<R: Read>(
253    reader: R,
254    bootstrap_sidecar: &[u8],
255    master_key: &MasterKey,
256    options: NonSeekableReaderOptions,
257) -> Result<SequentialVerifyReport, FormatError> {
258    Ok(run_non_seekable_stream(
259        reader,
260        NonSeekableKeySource::MasterKey(Some(master_key)),
261        options,
262        NoopTarStreamObserver,
263        Some(bootstrap_sidecar),
264    )?
265    .verification)
266}
267
268pub fn verify_non_seekable_stream_with_recipient_wrap_resolver_and_bootstrap_sidecar<R, F>(
269    reader: R,
270    bootstrap_sidecar: &[u8],
271    mut resolver: F,
272    options: NonSeekableReaderOptions,
273) -> Result<SequentialVerifyReport, FormatError>
274where
275    R: Read,
276    F: FnMut(
277        RecipientWrapRecordContext<'_>,
278    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
279{
280    Ok(run_non_seekable_stream(
281        reader,
282        NonSeekableKeySource::RecipientWrap(&mut resolver),
283        options,
284        NoopTarStreamObserver,
285        Some(bootstrap_sidecar),
286    )?
287    .verification)
288}
289
290pub fn verify_unencrypted_non_seekable_stream_with_bootstrap_sidecar<R: Read>(
291    reader: R,
292    bootstrap_sidecar: &[u8],
293    options: NonSeekableReaderOptions,
294) -> Result<SequentialVerifyReport, FormatError> {
295    Ok(run_non_seekable_stream(
296        reader,
297        NonSeekableKeySource::MasterKey(None),
298        options,
299        NoopTarStreamObserver,
300        Some(bootstrap_sidecar),
301    )?
302    .verification)
303}
304
305pub fn extract_non_seekable_stream_to_dir<R: Read>(
306    reader: R,
307    master_key: &MasterKey,
308    output_dir: &Path,
309    options: NonSeekableReaderOptions,
310    extraction: SafeExtractionOptions,
311) -> Result<SequentialExtractReport, ExtractError> {
312    let staging = StagedExtraction::new(output_dir)?;
313    let observer = TarStreamFilesystemRestoreObserver::new(
314        staging.root(),
315        SafeExtractionOptions {
316            overwrite_existing: true,
317        },
318    );
319    let outcome = run_non_seekable_stream(
320        reader,
321        NonSeekableKeySource::MasterKey(Some(master_key)),
322        options,
323        observer,
324        None,
325    )?;
326    staging.commit(extraction)?;
327    Ok(SequentialExtractReport {
328        verification: outcome.verification,
329        extracted_member_count: outcome
330            .streamed_payload
331            .tar
332            .members
333            .len()
334            .try_into()
335            .map_err(|_| FormatError::InvalidArchive("extracted member count overflow"))?,
336        degraded_metadata_count: degraded_metadata_count(&outcome.streamed_payload)?,
337    })
338}
339
340pub fn extract_non_seekable_stream_to_dir_with_recipient_wrap_resolver<R, F>(
341    reader: R,
342    mut resolver: F,
343    output_dir: &Path,
344    options: NonSeekableReaderOptions,
345    extraction: SafeExtractionOptions,
346) -> Result<SequentialExtractReport, ExtractError>
347where
348    R: Read,
349    F: FnMut(
350        RecipientWrapRecordContext<'_>,
351    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
352{
353    let staging = StagedExtraction::new(output_dir)?;
354    let observer = TarStreamFilesystemRestoreObserver::new(
355        staging.root(),
356        SafeExtractionOptions {
357            overwrite_existing: true,
358        },
359    );
360    let outcome = run_non_seekable_stream(
361        reader,
362        NonSeekableKeySource::RecipientWrap(&mut resolver),
363        options,
364        observer,
365        None,
366    )?;
367    staging.commit(extraction)?;
368    Ok(SequentialExtractReport {
369        verification: outcome.verification,
370        extracted_member_count: outcome
371            .streamed_payload
372            .tar
373            .members
374            .len()
375            .try_into()
376            .map_err(|_| FormatError::InvalidArchive("extracted member count overflow"))?,
377        degraded_metadata_count: degraded_metadata_count(&outcome.streamed_payload)?,
378    })
379}
380
381pub fn extract_unencrypted_non_seekable_stream_to_dir<R: Read>(
382    reader: R,
383    output_dir: &Path,
384    options: NonSeekableReaderOptions,
385    extraction: SafeExtractionOptions,
386) -> Result<SequentialExtractReport, ExtractError> {
387    let staging = StagedExtraction::new(output_dir)?;
388    let observer = TarStreamFilesystemRestoreObserver::new(
389        staging.root(),
390        SafeExtractionOptions {
391            overwrite_existing: true,
392        },
393    );
394    let outcome = run_non_seekable_stream(
395        reader,
396        NonSeekableKeySource::MasterKey(None),
397        options,
398        observer,
399        None,
400    )?;
401    staging.commit(extraction)?;
402    Ok(SequentialExtractReport {
403        verification: outcome.verification,
404        extracted_member_count: outcome
405            .streamed_payload
406            .tar
407            .members
408            .len()
409            .try_into()
410            .map_err(|_| FormatError::InvalidArchive("extracted member count overflow"))?,
411        degraded_metadata_count: degraded_metadata_count(&outcome.streamed_payload)?,
412    })
413}
414
415pub fn extract_non_seekable_stream_to_dir_with_bootstrap_sidecar<R: Read>(
416    reader: R,
417    bootstrap_sidecar: &[u8],
418    master_key: &MasterKey,
419    output_dir: &Path,
420    options: NonSeekableReaderOptions,
421    extraction: SafeExtractionOptions,
422) -> Result<SequentialExtractReport, ExtractError> {
423    let staging = StagedExtraction::new(output_dir)?;
424    let observer = TarStreamFilesystemRestoreObserver::new(
425        staging.root(),
426        SafeExtractionOptions {
427            overwrite_existing: true,
428        },
429    );
430    let outcome = run_non_seekable_stream(
431        reader,
432        NonSeekableKeySource::MasterKey(Some(master_key)),
433        options,
434        observer,
435        Some(bootstrap_sidecar),
436    )?;
437    staging.commit(extraction)?;
438    Ok(SequentialExtractReport {
439        verification: outcome.verification,
440        extracted_member_count: outcome
441            .streamed_payload
442            .tar
443            .members
444            .len()
445            .try_into()
446            .map_err(|_| FormatError::InvalidArchive("extracted member count overflow"))?,
447        degraded_metadata_count: degraded_metadata_count(&outcome.streamed_payload)?,
448    })
449}
450
451pub fn extract_non_seekable_stream_to_dir_with_recipient_wrap_resolver_and_bootstrap_sidecar<R, F>(
452    reader: R,
453    bootstrap_sidecar: &[u8],
454    mut resolver: F,
455    output_dir: &Path,
456    options: NonSeekableReaderOptions,
457    extraction: SafeExtractionOptions,
458) -> Result<SequentialExtractReport, ExtractError>
459where
460    R: Read,
461    F: FnMut(
462        RecipientWrapRecordContext<'_>,
463    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
464{
465    let staging = StagedExtraction::new(output_dir)?;
466    let observer = TarStreamFilesystemRestoreObserver::new(
467        staging.root(),
468        SafeExtractionOptions {
469            overwrite_existing: true,
470        },
471    );
472    let outcome = run_non_seekable_stream(
473        reader,
474        NonSeekableKeySource::RecipientWrap(&mut resolver),
475        options,
476        observer,
477        Some(bootstrap_sidecar),
478    )?;
479    staging.commit(extraction)?;
480    Ok(SequentialExtractReport {
481        verification: outcome.verification,
482        extracted_member_count: outcome
483            .streamed_payload
484            .tar
485            .members
486            .len()
487            .try_into()
488            .map_err(|_| FormatError::InvalidArchive("extracted member count overflow"))?,
489        degraded_metadata_count: degraded_metadata_count(&outcome.streamed_payload)?,
490    })
491}
492
493pub fn extract_unencrypted_non_seekable_stream_to_dir_with_bootstrap_sidecar<R: Read>(
494    reader: R,
495    bootstrap_sidecar: &[u8],
496    output_dir: &Path,
497    options: NonSeekableReaderOptions,
498    extraction: SafeExtractionOptions,
499) -> Result<SequentialExtractReport, ExtractError> {
500    let staging = StagedExtraction::new(output_dir)?;
501    let observer = TarStreamFilesystemRestoreObserver::new(
502        staging.root(),
503        SafeExtractionOptions {
504            overwrite_existing: true,
505        },
506    );
507    let outcome = run_non_seekable_stream(
508        reader,
509        NonSeekableKeySource::MasterKey(None),
510        options,
511        observer,
512        Some(bootstrap_sidecar),
513    )?;
514    staging.commit(extraction)?;
515    Ok(SequentialExtractReport {
516        verification: outcome.verification,
517        extracted_member_count: outcome
518            .streamed_payload
519            .tar
520            .members
521            .len()
522            .try_into()
523            .map_err(|_| FormatError::InvalidArchive("extracted member count overflow"))?,
524        degraded_metadata_count: degraded_metadata_count(&outcome.streamed_payload)?,
525    })
526}
527
528pub fn list_non_seekable_stream<R: Read>(
529    reader: R,
530    master_key: &MasterKey,
531    options: NonSeekableReaderOptions,
532) -> Result<SequentialListReport, FormatError> {
533    let outcome = run_non_seekable_stream(
534        reader,
535        NonSeekableKeySource::MasterKey(Some(master_key)),
536        options,
537        NoopTarStreamObserver,
538        None,
539    )?;
540    sequential_list_report(outcome)
541}
542
543pub fn list_non_seekable_stream_with_recipient_wrap_resolver<R, F>(
544    reader: R,
545    mut resolver: F,
546    options: NonSeekableReaderOptions,
547) -> Result<SequentialListReport, FormatError>
548where
549    R: Read,
550    F: FnMut(
551        RecipientWrapRecordContext<'_>,
552    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
553{
554    let outcome = run_non_seekable_stream(
555        reader,
556        NonSeekableKeySource::RecipientWrap(&mut resolver),
557        options,
558        NoopTarStreamObserver,
559        None,
560    )?;
561    sequential_list_report(outcome)
562}
563
564pub fn list_unencrypted_non_seekable_stream<R: Read>(
565    reader: R,
566    options: NonSeekableReaderOptions,
567) -> Result<SequentialListReport, FormatError> {
568    let outcome = run_non_seekable_stream(
569        reader,
570        NonSeekableKeySource::MasterKey(None),
571        options,
572        NoopTarStreamObserver,
573        None,
574    )?;
575    sequential_list_report(outcome)
576}
577
578pub fn list_non_seekable_stream_with_bootstrap_sidecar<R: Read>(
579    reader: R,
580    bootstrap_sidecar: &[u8],
581    master_key: &MasterKey,
582    options: NonSeekableReaderOptions,
583) -> Result<SequentialListReport, FormatError> {
584    let outcome = run_non_seekable_stream(
585        reader,
586        NonSeekableKeySource::MasterKey(Some(master_key)),
587        options,
588        NoopTarStreamObserver,
589        Some(bootstrap_sidecar),
590    )?;
591    sequential_list_report(outcome)
592}
593
594pub fn list_non_seekable_stream_with_recipient_wrap_resolver_and_bootstrap_sidecar<R, F>(
595    reader: R,
596    bootstrap_sidecar: &[u8],
597    mut resolver: F,
598    options: NonSeekableReaderOptions,
599) -> Result<SequentialListReport, FormatError>
600where
601    R: Read,
602    F: FnMut(
603        RecipientWrapRecordContext<'_>,
604    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
605{
606    let outcome = run_non_seekable_stream(
607        reader,
608        NonSeekableKeySource::RecipientWrap(&mut resolver),
609        options,
610        NoopTarStreamObserver,
611        Some(bootstrap_sidecar),
612    )?;
613    sequential_list_report(outcome)
614}
615
616pub fn list_unencrypted_non_seekable_stream_with_bootstrap_sidecar<R: Read>(
617    reader: R,
618    bootstrap_sidecar: &[u8],
619    options: NonSeekableReaderOptions,
620) -> Result<SequentialListReport, FormatError> {
621    let outcome = run_non_seekable_stream(
622        reader,
623        NonSeekableKeySource::MasterKey(None),
624        options,
625        NoopTarStreamObserver,
626        Some(bootstrap_sidecar),
627    )?;
628    sequential_list_report(outcome)
629}
630
631struct SequentialStreamOutcome {
632    opened: OpenedArchive,
633    streamed_payload: StreamedPayloadSummary,
634    verification: SequentialVerifyReport,
635}
636
637fn sequential_list_report(
638    outcome: SequentialStreamOutcome,
639) -> Result<SequentialListReport, FormatError> {
640    let index_entries = outcome.opened.list_index_entries()?;
641    let entries = streamed_list_entries(&index_entries, &outcome.streamed_payload)?;
642    Ok(SequentialListReport {
643        verification: outcome.verification,
644        entries,
645        index_entries,
646    })
647}
648
649type RecipientWrapResolver<'a> = dyn FnMut(
650        RecipientWrapRecordContext<'_>,
651    ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
652    + 'a;
653
654enum NonSeekableKeySource<'a> {
655    MasterKey(Option<&'a MasterKey>),
656    RecipientWrap(&'a mut RecipientWrapResolver<'a>),
657}
658
659fn run_non_seekable_stream<R, O>(
660    mut reader: R,
661    key_source: NonSeekableKeySource<'_>,
662    options: NonSeekableReaderOptions,
663    observer: O,
664    bootstrap_sidecar: Option<&[u8]>,
665) -> Result<SequentialStreamOutcome, FormatError>
666where
667    R: Read,
668    O: TarStreamObserver,
669{
670    validate_reader_options(options.reader)?;
671    let mut volume_header_bytes = [0u8; VOLUME_HEADER_LEN];
672    read_exact_stream(&mut reader, &mut volume_header_bytes, "VolumeHeader")?;
673    let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
674    parse_volume_format_dispatch(&volume_header)?;
675
676    let crypto_len = usize::try_from(volume_header.crypto_header_length)
677        .map_err(|_| FormatError::InvalidArchive("CryptoHeader length overflow"))?;
678    let mut crypto_header_bytes = vec![0u8; crypto_len];
679    read_exact_stream(&mut reader, &mut crypto_header_bytes, "CryptoHeader")?;
680    let parsed_crypto =
681        CryptoHeader::parse(&crypto_header_bytes, volume_header.crypto_header_length)?;
682    let crypto_header = parsed_crypto.fixed.clone();
683    let mut stream_cursor = checked_u64_add(
684        VOLUME_HEADER_LEN as u64,
685        volume_header.crypto_header_length as u64,
686    )?;
687    let startup_key_wrap_table = startup_key_wrap_table(
688        &volume_header,
689        &parsed_crypto.kdf_params,
690        |start, length| {
691            if stream_cursor != start {
692                return Err(FormatError::InvalidArchive(
693                    "KeyWrapTableV1 does not start at stream cursor",
694                ));
695            }
696            let mut key_wrap_table_bytes = vec![0u8; length];
697            read_exact_stream(&mut reader, &mut key_wrap_table_bytes, "KeyWrapTableV1")?;
698            stream_cursor = checked_u64_add(stream_cursor, length as u64)?;
699            Ok(key_wrap_table_bytes)
700        },
701    )?;
702    let subkeys = match (
703        crypto_header.aead_algo.is_encrypted(),
704        &parsed_crypto.kdf_params,
705        key_source,
706    ) {
707        (false, _, NonSeekableKeySource::RecipientWrap(_)) => {
708            return Err(FormatError::KeyMaterialMismatch);
709        }
710        (false, _, NonSeekableKeySource::MasterKey(_)) => Subkeys::unencrypted_placeholder(),
711        (
712            true,
713            crate::crypto::KdfParams::RecipientWrap { .. },
714            NonSeekableKeySource::RecipientWrap(resolver),
715        ) => {
716            let table = startup_key_wrap_table
717                .as_ref()
718                .ok_or(FormatError::KeyMaterialMismatch)?;
719            recipient_wrap_subkeys_from_table(
720                &volume_header,
721                &parsed_crypto,
722                &table.table,
723                resolver,
724            )?
725        }
726        (
727            true,
728            crate::crypto::KdfParams::RecipientWrap { .. },
729            NonSeekableKeySource::MasterKey(_),
730        ) => {
731            return Err(FormatError::KeyMaterialMismatch);
732        }
733        (true, _, NonSeekableKeySource::MasterKey(master_key)) => Subkeys::derive(
734            master_key.ok_or(FormatError::KeyMaterialMismatch)?,
735            &volume_header.archive_uuid,
736            &volume_header.session_id,
737        )?,
738        (true, _, NonSeekableKeySource::RecipientWrap(_)) => {
739            return Err(FormatError::KeyMaterialMismatch);
740        }
741    };
742    verify_integrity_tag(
743        HmacDomain::CryptoHeader,
744        crypto_header.aead_algo,
745        volume_header.volume_format_rev,
746        Some(&subkeys.mac_key),
747        &volume_header.archive_uuid,
748        &volume_header.session_id,
749        parsed_crypto.hmac_covered_bytes,
750        &parsed_crypto.header_hmac,
751    )?;
752    parsed_crypto.validate_extension_semantics()?;
753    reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
754    validate_crypto_class_parity_exactness(&crypto_header)?;
755    let block_records_start = startup_key_wrap_table
756        .as_ref()
757        .map(|table| table.block_records_start)
758        .unwrap_or(stream_cursor);
759    let bootstrap = bootstrap_sidecar
760        .map(|sidecar| {
761            parse_non_seekable_bootstrap_material(sidecar, &volume_header, &crypto_header, &subkeys)
762        })
763        .transpose()?;
764    validate_sequential_verify_supported_volume(
765        &volume_header,
766        &crypto_header,
767        &parsed_crypto.extensions,
768        bootstrap.as_ref(),
769    )?;
770
771    let block_size = crypto_header.block_size as usize;
772    let record_len = block_size
773        .checked_add(BLOCK_RECORD_FRAMING_LEN)
774        .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
775    let mut stream_offset = block_records_start;
776    let mut observed_block_count = 0u64;
777    let mut metadata_seen = false;
778    let mut pending = PendingLiveEnvelope::default();
779    let mut next_envelope_index = 0u64;
780    let mut retained_metadata_bytes = 0usize;
781    let mut metadata_blocks = BTreeMap::new();
782    let mut payload = StreamedPayloadCollector::with_observer(
783        &crypto_header,
784        options,
785        observer,
786        bootstrap
787            .as_ref()
788            .and_then(|material| material.payload_dictionary.clone()),
789    )?;
790
791    let terminal_tail = loop {
792        let mut magic = [0u8; 4];
793        read_exact_stream(&mut reader, &mut magic, "BlockRecord or terminal tail")?;
794        if magic != *b"TZBK" {
795            let tail_start = stream_offset;
796            stream_offset = checked_u64_add(stream_offset, magic.len() as u64)?;
797            let mut tail = TerminalTailBuffer::new(tail_start, options.max_terminal_tail_size);
798            tail.append(&magic)?;
799            let mut buf = [0u8; 64 * 1024];
800            loop {
801                let read = read_stream_chunk(&mut reader, &mut buf)?;
802                if read == 0 {
803                    break;
804                }
805                tail.append(&buf[..read])?;
806                stream_offset = checked_u64_add(stream_offset, read as u64)?;
807            }
808            break tail.finish(stream_offset);
809        }
810
811        let expected_block_index =
812            expected_stream_block_index(&volume_header, observed_block_count)?;
813        let mut raw = vec![0u8; record_len];
814        raw[..4].copy_from_slice(&magic);
815        read_exact_stream(&mut reader, &mut raw[4..], "BlockRecord")?;
816        observed_block_count = checked_u64_add(observed_block_count, 1)?;
817        stream_offset = checked_u64_add(stream_offset, record_len as u64)?;
818
819        match BlockRecord::parse(&raw, block_size) {
820            Ok(record) => {
821                if record.block_index != expected_block_index {
822                    return Err(FormatError::InvalidArchive(
823                        "BlockRecord index does not match stream position",
824                    ));
825                }
826                handle_live_record(
827                    record,
828                    &mut pending,
829                    LiveStreamContext {
830                        payload: &mut payload,
831                        subkeys: &subkeys,
832                        volume_header: &volume_header,
833                        crypto_header: &crypto_header,
834                        next_envelope_index: &mut next_envelope_index,
835                        metadata_seen: &mut metadata_seen,
836                        metadata_blocks: &mut metadata_blocks,
837                        retained_metadata_bytes: &mut retained_metadata_bytes,
838                        max_retained_metadata_bytes: options.max_retained_metadata_bytes,
839                    },
840                )?;
841            }
842            Err(err) if block_record_error_is_recoverable_erasure(&err) => {
843                handle_live_erasure(
844                    &mut pending,
845                    LiveStreamContext {
846                        payload: &mut payload,
847                        subkeys: &subkeys,
848                        volume_header: &volume_header,
849                        crypto_header: &crypto_header,
850                        next_envelope_index: &mut next_envelope_index,
851                        metadata_seen: &mut metadata_seen,
852                        metadata_blocks: &mut metadata_blocks,
853                        retained_metadata_bytes: &mut retained_metadata_bytes,
854                        max_retained_metadata_bytes: options.max_retained_metadata_bytes,
855                    },
856                    expected_block_index,
857                )?;
858            }
859            Err(err) => return Err(err),
860        }
861    };
862
863    if !pending.is_empty() {
864        finalize_live_envelope(
865            &mut pending,
866            &mut payload,
867            &subkeys,
868            &volume_header,
869            &crypto_header,
870            &mut next_envelope_index,
871        )?;
872    }
873
874    let terminal = parse_terminal_material_read_at(
875        &terminal_tail,
876        terminal_tail.stream_len,
877        terminal_tail.start_offset,
878        observed_block_count,
879        KeyHoldingTerminalContext {
880            subkeys: &subkeys,
881            volume_header: &volume_header,
882            crypto_header: &crypto_header,
883            crypto_header_bytes: &crypto_header_bytes,
884        },
885    )?;
886    if let Some(bootstrap) = &bootstrap {
887        if !manifest_bootstrap_fields_match(&terminal.manifest_footer, &bootstrap.manifest_footer) {
888            return Err(FormatError::InvalidArchive(
889                "bootstrap sidecar conflicts with terminal ManifestFooter",
890            ));
891        }
892    }
893    let observed_archive_bytes = observed_archive_size([terminal_tail.stream_len])?;
894    let streamed_payload = payload.finish()?;
895    if streamed_payload.tar.total_extraction_size
896        > total_extraction_size_cap(options.reader, observed_archive_bytes)
897    {
898        return Err(FormatError::ReaderUnsupported(
899            "total extraction size exceeds configured cap",
900        ));
901    }
902
903    let root_auth = root_auth_status(terminal.root_auth_footer.as_ref());
904    let opened = OpenedArchive::from_streamed_parts(StreamedArchiveOpenParts {
905        options: options.reader,
906        observed_archive_bytes,
907        subkeys,
908        blocks: metadata_blocks,
909        crypto_header_bytes,
910        volume_header,
911        crypto_header,
912        manifest_footer: terminal.manifest_footer,
913        volume_trailer: terminal.volume_trailer,
914        root_auth_footer: terminal.root_auth_footer,
915    })?;
916    opened.verify_streamed_payload_summary(&streamed_payload)?;
917
918    let verification = SequentialVerifyReport {
919        archive_uuid: opened.volume_header.archive_uuid,
920        session_id: opened.volume_header.session_id,
921        volume_format_rev: opened.volume_header.volume_format_rev,
922        volume_index: opened.volume_header.volume_index,
923        total_volumes: opened.manifest_footer.total_volumes,
924        file_count: opened.index_root.header.file_count,
925        payload_block_count: opened.index_root.header.payload_block_count,
926        tar_total_size: opened.index_root.header.tar_total_size,
927        content_sha256: opened.index_root.header.content_sha256,
928        root_auth,
929    };
930
931    Ok(SequentialStreamOutcome {
932        opened,
933        streamed_payload,
934        verification,
935    })
936}
937
938fn degraded_metadata_count(payload: &StreamedPayloadSummary) -> Result<u64, FormatError> {
939    payload.tar.members.iter().try_fold(0u64, |count, member| {
940        count
941            .checked_add(member.diagnostics.len() as u64)
942            .ok_or(FormatError::InvalidArchive(
943                "degraded metadata count overflow",
944            ))
945    })
946}
947
948fn streamed_list_entries(
949    index_entries: &[ArchiveIndexEntry],
950    payload: &StreamedPayloadSummary,
951) -> Result<Vec<ArchiveEntry>, FormatError> {
952    let mut latest_by_path = BTreeMap::<Vec<u8>, &TarStreamMemberSummary>::new();
953    for member in &payload.tar.members {
954        let replace = latest_by_path
955            .get(&member.path)
956            .map(|existing| member.group_start > existing.group_start)
957            .unwrap_or(true);
958        if replace {
959            latest_by_path.insert(member.path.clone(), member);
960        }
961    }
962
963    index_entries
964        .iter()
965        .map(|entry| {
966            let member =
967                latest_by_path
968                    .get(entry.path.as_bytes())
969                    .ok_or(FormatError::InvalidArchive(
970                        "streamed tar member missing from final index",
971                    ))?;
972            Ok(ArchiveEntry {
973                path: entry.path.clone(),
974                file_data_size: entry.file_data_size,
975                kind: entry.kind,
976                mode: entry.mode,
977                mtime: entry.mtime,
978                diagnostics: member.diagnostics.clone(),
979            })
980        })
981        .collect()
982}
983
984struct StagedExtraction {
985    tempdir: tempfile::TempDir,
986    root: PathBuf,
987    output_dir: PathBuf,
988}
989
990impl StagedExtraction {
991    fn new(output_dir: &Path) -> Result<Self, ExtractError> {
992        let parent = output_dir
993            .parent()
994            .filter(|path| !path.as_os_str().is_empty())
995            .unwrap_or_else(|| Path::new("."));
996        let tempdir = tempfile::Builder::new()
997            .prefix(".tzap-nonseekable-")
998            .tempdir_in(parent)
999            .map_err(ExtractError::Output)?;
1000        let root = tempdir.path().join("root");
1001        fs::create_dir(&root).map_err(ExtractError::Output)?;
1002        Ok(Self {
1003            tempdir,
1004            root,
1005            output_dir: output_dir.to_path_buf(),
1006        })
1007    }
1008
1009    fn root(&self) -> &Path {
1010        &self.root
1011    }
1012
1013    fn commit(self, options: SafeExtractionOptions) -> Result<(), ExtractError> {
1014        match fs::symlink_metadata(&self.output_dir) {
1015            Ok(metadata) => {
1016                if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
1017                    return Err(FormatError::UnsafeArchivePath.into());
1018                }
1019                preflight_staged_merge(&self.root, &self.output_dir, options)?;
1020                merge_staged_dir(&self.root, &self.output_dir, options)?;
1021            }
1022            Err(error) if error.kind() == ErrorKind::NotFound => {
1023                if let Some(parent) = self
1024                    .output_dir
1025                    .parent()
1026                    .filter(|path| !path.as_os_str().is_empty())
1027                {
1028                    fs::create_dir_all(parent).map_err(ExtractError::Output)?;
1029                }
1030                fs::rename(&self.root, &self.output_dir).map_err(ExtractError::Output)?;
1031            }
1032            Err(_) => {
1033                return Err(FormatError::FilesystemExtractionFailed(
1034                    "failed to inspect extraction directory",
1035                )
1036                .into());
1037            }
1038        }
1039        drop(self.tempdir);
1040        Ok(())
1041    }
1042}
1043
1044fn preflight_staged_merge(
1045    staged_root: &Path,
1046    output_root: &Path,
1047    options: SafeExtractionOptions,
1048) -> Result<(), FormatError> {
1049    for entry in read_dir_sorted(staged_root)? {
1050        let staged_path = entry.path();
1051        let relative = staged_path
1052            .strip_prefix(staged_root)
1053            .map_err(|_| FormatError::UnsafeArchivePath)?;
1054        preflight_staged_entry(&staged_path, output_root, relative, options)?;
1055    }
1056    Ok(())
1057}
1058
1059fn preflight_staged_entry(
1060    staged_path: &Path,
1061    output_root: &Path,
1062    relative: &Path,
1063    options: SafeExtractionOptions,
1064) -> Result<(), FormatError> {
1065    let final_path = output_root.join(relative);
1066    let staged_metadata = fs::symlink_metadata(staged_path).map_err(|_| {
1067        FormatError::FilesystemExtractionFailed("failed to inspect staged extraction output")
1068    })?;
1069    if let Some(parent) = relative
1070        .parent()
1071        .filter(|path| !path.as_os_str().is_empty())
1072    {
1073        preflight_relative_parent_chain(output_root, parent)?;
1074    }
1075    match fs::symlink_metadata(&final_path) {
1076        Ok(final_metadata) => {
1077            let final_type = final_metadata.file_type();
1078            if final_type.is_symlink() {
1079                return Err(FormatError::UnsafeArchivePath);
1080            }
1081            if staged_metadata.file_type().is_dir() {
1082                if !final_type.is_dir() {
1083                    return Err(FormatError::UnsafeOverwrite);
1084                }
1085            } else if final_type.is_dir() || !options.overwrite_existing {
1086                return Err(FormatError::UnsafeOverwrite);
1087            }
1088        }
1089        Err(error) if error.kind() == ErrorKind::NotFound => {}
1090        Err(_) => {
1091            return Err(FormatError::FilesystemExtractionFailed(
1092                "failed to inspect extraction destination",
1093            ));
1094        }
1095    }
1096    if staged_metadata.file_type().is_dir() {
1097        for entry in read_dir_sorted(staged_path)? {
1098            let child_relative = relative.join(entry.file_name());
1099            preflight_staged_entry(&entry.path(), output_root, &child_relative, options)?;
1100        }
1101    }
1102    Ok(())
1103}
1104
1105fn preflight_relative_parent_chain(root: &Path, parent: &Path) -> Result<(), FormatError> {
1106    let mut current = root.to_path_buf();
1107    for component in parent.components() {
1108        current.push(component.as_os_str());
1109        match fs::symlink_metadata(&current) {
1110            Ok(metadata) => {
1111                let file_type = metadata.file_type();
1112                if file_type.is_symlink() || !file_type.is_dir() {
1113                    return Err(FormatError::UnsafeArchivePath);
1114                }
1115            }
1116            Err(error) if error.kind() == ErrorKind::NotFound => {}
1117            Err(_) => {
1118                return Err(FormatError::FilesystemExtractionFailed(
1119                    "failed to inspect extraction destination",
1120                ));
1121            }
1122        }
1123    }
1124    Ok(())
1125}
1126
1127fn merge_staged_dir(
1128    staged_dir: &Path,
1129    final_dir: &Path,
1130    options: SafeExtractionOptions,
1131) -> Result<(), ExtractError> {
1132    fs::create_dir_all(final_dir).map_err(ExtractError::Output)?;
1133    for entry in read_dir_sorted(staged_dir)? {
1134        let staged_path = entry.path();
1135        let final_path = final_dir.join(entry.file_name());
1136        let metadata = fs::symlink_metadata(&staged_path).map_err(|_| {
1137            FormatError::FilesystemExtractionFailed("failed to inspect staged extraction output")
1138        })?;
1139        if metadata.file_type().is_dir() {
1140            merge_staged_dir(&staged_path, &final_path, options)?;
1141            continue;
1142        }
1143        if options.overwrite_existing && fs::symlink_metadata(&final_path).is_ok() {
1144            fs::remove_file(&final_path).map_err(ExtractError::Output)?;
1145        }
1146        fs::rename(&staged_path, &final_path).map_err(ExtractError::Output)?;
1147    }
1148    Ok(())
1149}
1150
1151fn read_dir_sorted(path: &Path) -> Result<Vec<fs::DirEntry>, FormatError> {
1152    let mut entries = fs::read_dir(path)
1153        .map_err(|_| FormatError::FilesystemExtractionFailed("failed to read directory"))?
1154        .collect::<Result<Vec<_>, _>>()
1155        .map_err(|_| FormatError::FilesystemExtractionFailed("failed to read directory"))?;
1156    entries.sort_by_key(|entry| entry.file_name());
1157    Ok(entries)
1158}
1159
1160fn validate_sequential_verify_supported_volume(
1161    volume_header: &VolumeHeader,
1162    crypto_header: &CryptoHeaderFixed,
1163    extensions: &[ExtensionTlv<'_>],
1164    bootstrap: Option<&NonSeekableBootstrapMaterial>,
1165) -> Result<(), FormatError> {
1166    reject_unsupported_raw_stream_profile(extensions)?;
1167    if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
1168        return Err(FormatError::ReaderUnsupported(
1169            "sequential reader supports only single-volume archive input",
1170        ));
1171    }
1172    if crypto_header.stripe_width != volume_header.stripe_width {
1173        return Err(FormatError::InvalidArchive(
1174            "VolumeHeader and CryptoHeader stripe_width differ",
1175        ));
1176    }
1177    if crypto_header.has_dictionary != 0
1178        && bootstrap
1179            .and_then(|material| material.payload_dictionary.as_ref())
1180            .is_none()
1181    {
1182        return Err(FormatError::ReaderUnsupported(
1183            "dictionary bootstrap required for non-seekable sequential verification",
1184        ));
1185    }
1186    Ok(())
1187}
1188
1189struct LiveStreamContext<'a, O: TarStreamObserver> {
1190    payload: &'a mut StreamedPayloadCollector<O>,
1191    subkeys: &'a Subkeys,
1192    volume_header: &'a VolumeHeader,
1193    crypto_header: &'a CryptoHeaderFixed,
1194    next_envelope_index: &'a mut u64,
1195    metadata_seen: &'a mut bool,
1196    metadata_blocks: &'a mut BTreeMap<u64, BlockRecord>,
1197    retained_metadata_bytes: &'a mut usize,
1198    max_retained_metadata_bytes: usize,
1199}
1200
1201fn handle_live_record<O: TarStreamObserver>(
1202    record: BlockRecord,
1203    pending: &mut PendingLiveEnvelope,
1204    context: LiveStreamContext<'_, O>,
1205) -> Result<(), FormatError> {
1206    match record.kind {
1207        BlockKind::PayloadData => {
1208            if *context.metadata_seen {
1209                return Err(FormatError::InvalidArchive(
1210                    "payload BlockRecord appears after metadata",
1211                ));
1212            }
1213            if pending.awaiting_tentative_parity {
1214                return Err(FormatError::InvalidArchive(
1215                    "sequential payload envelope boundary is ambiguous after CRC erasure",
1216                ));
1217            }
1218            if pending.saw_last_data {
1219                finalize_live_envelope(
1220                    pending,
1221                    &mut *context.payload,
1222                    context.subkeys,
1223                    context.volume_header,
1224                    context.crypto_header,
1225                    &mut *context.next_envelope_index,
1226                )?;
1227            }
1228            pending.note_block(record.block_index);
1229            let is_last_data = record.is_last_data();
1230            pending.data_shards.push(Some(record.payload));
1231            if is_last_data {
1232                pending.saw_last_data = true;
1233            }
1234            if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
1235                return Err(FormatError::InvalidArchive(
1236                    "sequential payload envelope exceeds data-shard cap",
1237                ));
1238            }
1239        }
1240        BlockKind::PayloadParity => {
1241            if *context.metadata_seen {
1242                return Err(FormatError::InvalidArchive(
1243                    "payload parity BlockRecord appears after metadata",
1244                ));
1245            }
1246            if pending.awaiting_tentative_parity {
1247                pending.awaiting_tentative_parity = false;
1248                pending.saw_last_data = true;
1249            } else if pending.data_shards.is_empty() || !pending.saw_last_data {
1250                return Err(FormatError::InvalidArchive(
1251                    "payload parity appears before envelope data is complete",
1252                ));
1253            }
1254            pending.note_block(record.block_index);
1255            pending.parity_shards.push(Some(record.payload));
1256            if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
1257                return Err(FormatError::InvalidArchive(
1258                    "sequential payload envelope exceeds parity-shard cap",
1259                ));
1260            }
1261        }
1262        _ => {
1263            if !pending.is_empty() {
1264                finalize_live_envelope(
1265                    pending,
1266                    &mut *context.payload,
1267                    context.subkeys,
1268                    context.volume_header,
1269                    context.crypto_header,
1270                    &mut *context.next_envelope_index,
1271                )?;
1272            }
1273            *context.metadata_seen = true;
1274            retain_metadata_record(
1275                &mut *context.metadata_blocks,
1276                record,
1277                &mut *context.retained_metadata_bytes,
1278                context.max_retained_metadata_bytes,
1279            )?;
1280        }
1281    }
1282    Ok(())
1283}
1284
1285fn retain_metadata_record(
1286    metadata_blocks: &mut BTreeMap<u64, BlockRecord>,
1287    record: BlockRecord,
1288    retained_metadata_bytes: &mut usize,
1289    max_retained_metadata_bytes: usize,
1290) -> Result<(), FormatError> {
1291    let retained = record
1292        .payload
1293        .len()
1294        .checked_add(BLOCK_RECORD_FRAMING_LEN)
1295        .ok_or(FormatError::InvalidArchive("metadata retention overflow"))?;
1296    *retained_metadata_bytes = retained_metadata_bytes
1297        .checked_add(retained)
1298        .ok_or(FormatError::InvalidArchive("metadata retention overflow"))?;
1299    if *retained_metadata_bytes > max_retained_metadata_bytes {
1300        return Err(FormatError::ReaderUnsupported(
1301            "retained metadata exceeds configured streaming cap",
1302        ));
1303    }
1304    if metadata_blocks.insert(record.block_index, record).is_some() {
1305        return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1306    }
1307    Ok(())
1308}
1309
1310fn handle_live_erasure<O: TarStreamObserver>(
1311    pending: &mut PendingLiveEnvelope,
1312    context: LiveStreamContext<'_, O>,
1313    expected_block_index: u64,
1314) -> Result<(), FormatError> {
1315    if *context.metadata_seen {
1316        return Ok(());
1317    }
1318    if pending.saw_last_data
1319        && pending.parity_shards.len()
1320            >= required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?
1321                as usize
1322    {
1323        finalize_live_envelope(
1324            pending,
1325            &mut *context.payload,
1326            context.subkeys,
1327            context.volume_header,
1328            context.crypto_header,
1329            &mut *context.next_envelope_index,
1330        )?;
1331        *context.metadata_seen = true;
1332        return Ok(());
1333    }
1334    if pending.saw_last_data {
1335        return Err(FormatError::BadCrc {
1336            structure: "BlockRecord",
1337        });
1338    }
1339    if !sequential_payload_parity_is_guaranteed(context.crypto_header) {
1340        return Err(FormatError::BadCrc {
1341            structure: "BlockRecord",
1342        });
1343    }
1344    pending.note_block(expected_block_index);
1345    pending.data_shards.push(None);
1346    pending.awaiting_tentative_parity = true;
1347    if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
1348        return Err(FormatError::InvalidArchive(
1349            "sequential payload envelope exceeds data-shard cap",
1350        ));
1351    }
1352    Ok(())
1353}
1354
1355fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
1356    crypto_header.fec_parity_shards > 0
1357        && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
1358}
1359
1360fn finalize_live_envelope<O: TarStreamObserver>(
1361    pending: &mut PendingLiveEnvelope,
1362    payload: &mut StreamedPayloadCollector<O>,
1363    subkeys: &Subkeys,
1364    volume_header: &VolumeHeader,
1365    crypto_header: &CryptoHeaderFixed,
1366    next_envelope_index: &mut u64,
1367) -> Result<(), FormatError> {
1368    if !pending.saw_last_data {
1369        return Err(FormatError::InvalidArchive(
1370            "sequential payload envelope is missing last-data flag",
1371        ));
1372    }
1373    if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
1374        return Err(FormatError::InvalidArchive(
1375            "sequential payload envelope exceeds data-shard cap",
1376        ));
1377    }
1378    if pending.parity_shards.len() > crypto_header.fec_parity_shards as usize {
1379        return Err(FormatError::InvalidArchive(
1380            "sequential payload envelope exceeds parity-shard cap",
1381        ));
1382    }
1383    let required_parity = required_object_parity(pending.data_shards.len() as u64, crypto_header)?;
1384    if pending.parity_shards.len() < required_parity as usize {
1385        return Err(FormatError::InvalidArchive(
1386            "sequential payload envelope has insufficient parity for recovery settings",
1387        ));
1388    }
1389    let first_block_index = pending
1390        .first_block_index
1391        .ok_or(FormatError::InvalidArchive(
1392            "sequential payload envelope is missing first block",
1393        ))?;
1394
1395    let repaired = repair_data_gf16(
1396        &pending.data_shards,
1397        &pending.parity_shards,
1398        crypto_header.block_size as usize,
1399    )?;
1400    let mut encrypted = Vec::with_capacity(repaired.len() * crypto_header.block_size as usize);
1401    for shard in repaired {
1402        encrypted.extend_from_slice(&shard);
1403    }
1404    let plaintext = decrypt_padded_aead_object(
1405        AeadObjectContext {
1406            algo: crypto_header.aead_algo,
1407            key: &subkeys.enc_key,
1408            nonce_seed: &subkeys.nonce_seed,
1409            domain: b"envelope",
1410            archive_uuid: &volume_header.archive_uuid,
1411            session_id: &volume_header.session_id,
1412            counter: *next_envelope_index,
1413        },
1414        &encrypted,
1415    )?;
1416    payload.decode_envelope(
1417        *next_envelope_index,
1418        first_block_index,
1419        pending.data_shards.len(),
1420        pending.parity_shards.len(),
1421        crypto_header.block_size as usize,
1422        &plaintext,
1423    )?;
1424    *next_envelope_index = checked_u64_add(*next_envelope_index, 1)?;
1425    *pending = PendingLiveEnvelope::default();
1426    Ok(())
1427}
1428
1429#[derive(Debug, Default)]
1430struct PendingLiveEnvelope {
1431    first_block_index: Option<u64>,
1432    data_shards: Vec<Option<Vec<u8>>>,
1433    parity_shards: Vec<Option<Vec<u8>>>,
1434    saw_last_data: bool,
1435    awaiting_tentative_parity: bool,
1436}
1437
1438impl PendingLiveEnvelope {
1439    fn is_empty(&self) -> bool {
1440        self.data_shards.is_empty() && self.parity_shards.is_empty()
1441    }
1442
1443    fn note_block(&mut self, block_index: u64) {
1444        if self.first_block_index.is_none() {
1445            self.first_block_index = Some(block_index);
1446        }
1447    }
1448}
1449
1450struct StreamedPayloadCollector<O = NoopTarStreamObserver> {
1451    tar: TarStreamSummaryValidator<O>,
1452    hasher: Sha256,
1453    max_tar_stream_size: usize,
1454    payload_dictionary: Option<Vec<u8>>,
1455    envelopes: Vec<StreamedEnvelopeSummary>,
1456    frames: Vec<StreamedFrameSummary>,
1457}
1458
1459impl<O: TarStreamObserver> StreamedPayloadCollector<O> {
1460    fn with_observer(
1461        crypto_header: &CryptoHeaderFixed,
1462        options: NonSeekableReaderOptions,
1463        observer: O,
1464        payload_dictionary: Option<Vec<u8>>,
1465    ) -> Result<Self, FormatError> {
1466        Ok(Self {
1467            tar: TarStreamSummaryValidator::with_observer(
1468                crypto_header.max_path_length,
1469                options.reader.max_total_extraction_size,
1470                options.max_incomplete_tar_group_bytes,
1471                options.max_streamed_member_count,
1472                observer,
1473            ),
1474            hasher: Sha256::new(),
1475            max_tar_stream_size: options.reader.max_verify_tar_size,
1476            payload_dictionary,
1477            envelopes: Vec::new(),
1478            frames: Vec::new(),
1479        })
1480    }
1481
1482    fn decode_envelope(
1483        &mut self,
1484        envelope_index: u64,
1485        first_block_index: u64,
1486        data_block_count: usize,
1487        parity_block_count: usize,
1488        block_size: usize,
1489        plaintext: &[u8],
1490    ) -> Result<(), FormatError> {
1491        if plaintext.is_empty() {
1492            return Err(FormatError::InvalidArchive(
1493                "payload envelope plaintext has no frames",
1494            ));
1495        }
1496        let first_frame_index = u64::try_from(self.frames.len())
1497            .map_err(|_| FormatError::InvalidArchive("FrameEntry count overflow"))?;
1498        let mut cursor = 0usize;
1499        while cursor < plaintext.len() {
1500            let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
1501                .map_err(|_| FormatError::InvalidZstdFrame)?;
1502            if frame_len == 0 {
1503                return Err(FormatError::InvalidZstdFrame);
1504            }
1505            let end = checked_usize_add(cursor, frame_len)?;
1506            validate_exact_zstd_frame(&plaintext[cursor..end])?;
1507            self.decode_frame(
1508                envelope_index,
1509                u32_len(cursor, "FrameEntry.offset_in_envelope")?,
1510                &plaintext[cursor..end],
1511            )?;
1512            cursor = end;
1513        }
1514        let frame_count = u32_len(
1515            self.frames.len() - first_frame_index as usize,
1516            "EnvelopeEntry.frame_count",
1517        )?;
1518        if frame_count == 0 {
1519            return Err(FormatError::InvalidArchive(
1520                "payload envelope plaintext has no frames",
1521            ));
1522        }
1523        let encrypted_size =
1524            data_block_count
1525                .checked_mul(block_size)
1526                .ok_or(FormatError::InvalidArchive(
1527                    "EnvelopeEntry encrypted size overflow",
1528                ))?;
1529        self.envelopes.push(StreamedEnvelopeSummary {
1530            envelope_index,
1531            first_block_index,
1532            data_block_count: u32_len(data_block_count, "EnvelopeEntry.data_block_count")?,
1533            parity_block_count: u32_len(parity_block_count, "EnvelopeEntry.parity_block_count")?,
1534            encrypted_size: u32_len(encrypted_size, "EnvelopeEntry.encrypted_size")?,
1535            plaintext_size: u32_len(plaintext.len(), "EnvelopeEntry.plaintext_size")?,
1536            first_frame_index,
1537            frame_count,
1538        });
1539        Ok(())
1540    }
1541
1542    fn decode_frame(
1543        &mut self,
1544        envelope_index: u64,
1545        offset_in_envelope: u32,
1546        compressed: &[u8],
1547    ) -> Result<(), FormatError> {
1548        let frame_index = u64::try_from(self.frames.len())
1549            .map_err(|_| FormatError::InvalidArchive("FrameEntry count overflow"))?;
1550        let tar_stream_offset = self.tar.tar_total_size();
1551        let decompressed_size = if let Some(dictionary) = &self.payload_dictionary {
1552            let mut decoder = zstd::stream::Decoder::with_dictionary(compressed, dictionary)
1553                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
1554            self.decode_zstd_frame_body(&mut decoder)?
1555        } else {
1556            let mut decoder = zstd::stream::Decoder::new(compressed)
1557                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
1558            self.decode_zstd_frame_body(&mut decoder)?
1559        };
1560        if decompressed_size == 0 {
1561            return Err(FormatError::InvalidArchive(
1562                "zstd payload frame decompressed to zero bytes",
1563            ));
1564        }
1565        self.frames.push(StreamedFrameSummary {
1566            frame_index,
1567            envelope_index,
1568            offset_in_envelope,
1569            compressed_size: u32_len(compressed.len(), "FrameEntry.compressed_size")?,
1570            decompressed_size: u32_len(
1571                usize::try_from(decompressed_size)
1572                    .map_err(|_| FormatError::InvalidArchive("FrameEntry size overflow"))?,
1573                "FrameEntry.decompressed_size",
1574            )?,
1575            tar_stream_offset,
1576        });
1577        Ok(())
1578    }
1579
1580    fn decode_zstd_frame_body<D: Read>(&mut self, decoder: &mut D) -> Result<u64, FormatError> {
1581        let mut decompressed_size = 0u64;
1582        let mut buf = [0u8; 64 * 1024];
1583        loop {
1584            let read = decoder
1585                .read(&mut buf)
1586                .map_err(|_| FormatError::ZstdDecompressionFailure)?;
1587            if read == 0 {
1588                break;
1589            }
1590            let next_tar_size = self.tar.tar_total_size().checked_add(read as u64).ok_or(
1591                FormatError::ReaderUnsupported(
1592                    "sequential tar stream exceeds configured verification cap",
1593                ),
1594            )?;
1595            if next_tar_size > self.max_tar_stream_size as u64 {
1596                return Err(FormatError::ReaderUnsupported(
1597                    "sequential tar stream exceeds configured verification cap",
1598                ));
1599            }
1600            self.hasher.update(&buf[..read]);
1601            self.tar.observe(&buf[..read])?;
1602            decompressed_size = checked_u64_add(decompressed_size, read as u64)?;
1603        }
1604        Ok(decompressed_size)
1605    }
1606
1607    fn finish(self) -> Result<StreamedPayloadSummary, FormatError> {
1608        let content_sha256 = self.hasher.finalize();
1609        let mut digest = [0u8; 32];
1610        digest.copy_from_slice(&content_sha256);
1611        Ok(StreamedPayloadSummary {
1612            tar: self.tar.finish()?,
1613            content_sha256: digest,
1614            envelopes: self.envelopes,
1615            frames: self.frames,
1616        })
1617    }
1618}
1619
1620struct TerminalTailBuffer {
1621    start_offset: u64,
1622    cap: usize,
1623    bytes: Vec<u8>,
1624}
1625
1626impl TerminalTailBuffer {
1627    fn new(start_offset: u64, cap: usize) -> Self {
1628        Self {
1629            start_offset,
1630            cap,
1631            bytes: Vec::new(),
1632        }
1633    }
1634
1635    fn append(&mut self, bytes: &[u8]) -> Result<(), FormatError> {
1636        let next_len = self
1637            .bytes
1638            .len()
1639            .checked_add(bytes.len())
1640            .ok_or(FormatError::InvalidArchive("terminal tail size overflow"))?;
1641        if next_len > self.cap {
1642            return Err(FormatError::ReaderUnsupported(
1643                "terminal tail exceeds configured cap",
1644            ));
1645        }
1646        self.bytes.extend_from_slice(bytes);
1647        Ok(())
1648    }
1649
1650    fn finish(self, stream_len: u64) -> TerminalTailReadAt {
1651        TerminalTailReadAt {
1652            start_offset: self.start_offset,
1653            stream_len,
1654            bytes: self.bytes,
1655        }
1656    }
1657}
1658
1659struct TerminalTailReadAt {
1660    start_offset: u64,
1661    stream_len: u64,
1662    bytes: Vec<u8>,
1663}
1664
1665impl ArchiveReadAt for TerminalTailReadAt {
1666    fn len(&self) -> Result<u64, FormatError> {
1667        Ok(self.stream_len)
1668    }
1669
1670    fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
1671        let end = offset
1672            .checked_add(buf.len() as u64)
1673            .ok_or(FormatError::InvalidArchive("terminal tail range overflow"))?;
1674        let tail_end = self
1675            .start_offset
1676            .checked_add(self.bytes.len() as u64)
1677            .ok_or(FormatError::InvalidArchive("terminal tail range overflow"))?;
1678        if offset < self.start_offset || end > tail_end {
1679            return Err(FormatError::InvalidLength {
1680                structure: "terminal tail",
1681                expected: usize::try_from(end.saturating_sub(self.start_offset))
1682                    .unwrap_or(usize::MAX),
1683                actual: self.bytes.len(),
1684            });
1685        }
1686        let start = usize::try_from(offset - self.start_offset)
1687            .map_err(|_| FormatError::InvalidArchive("terminal tail range overflow"))?;
1688        let end = start
1689            .checked_add(buf.len())
1690            .ok_or(FormatError::InvalidArchive("terminal tail range overflow"))?;
1691        buf.copy_from_slice(&self.bytes[start..end]);
1692        Ok(())
1693    }
1694}
1695
1696fn root_auth_status(footer: Option<&RootAuthFooterV1>) -> SequentialRootAuthStatus {
1697    if footer.is_some() {
1698        SequentialRootAuthStatus::WireValidOnly
1699    } else {
1700        SequentialRootAuthStatus::Absent
1701    }
1702}
1703
1704fn read_exact_stream<R: Read>(
1705    reader: &mut R,
1706    mut buf: &mut [u8],
1707    structure: &'static str,
1708) -> Result<(), FormatError> {
1709    let expected = buf.len();
1710    let mut actual = 0usize;
1711    while !buf.is_empty() {
1712        match reader.read(buf) {
1713            Ok(0) => {
1714                return Err(FormatError::InvalidLength {
1715                    structure,
1716                    expected,
1717                    actual,
1718                })
1719            }
1720            Ok(read) => {
1721                actual = checked_usize_add(actual, read)?;
1722                let (_, rest) = buf.split_at_mut(read);
1723                buf = rest;
1724            }
1725            Err(err) if err.kind() == ErrorKind::Interrupted => {}
1726            Err(_) => return Err(FormatError::InvalidArchive("archive read failed")),
1727        }
1728    }
1729    Ok(())
1730}
1731
1732fn read_stream_chunk<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize, FormatError> {
1733    loop {
1734        match reader.read(buf) {
1735            Ok(read) => return Ok(read),
1736            Err(err) if err.kind() == ErrorKind::Interrupted => {}
1737            Err(_) => return Err(FormatError::InvalidArchive("archive read failed")),
1738        }
1739    }
1740}
1741
1742fn checked_u64_add(lhs: u64, rhs: u64) -> Result<u64, FormatError> {
1743    lhs.checked_add(rhs)
1744        .ok_or(FormatError::InvalidArchive("stream arithmetic overflow"))
1745}
1746
1747fn checked_usize_add(lhs: usize, rhs: usize) -> Result<usize, FormatError> {
1748    lhs.checked_add(rhs)
1749        .ok_or(FormatError::InvalidArchive("stream arithmetic overflow"))
1750}
1751
1752fn u32_len(value: usize, structure: &'static str) -> Result<u32, FormatError> {
1753    u32::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
1754}