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(¤t) {
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}