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