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