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