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