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