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, ArchiveReadAt,
24 KeyHoldingTerminalContext, NonSeekableBootstrapMaterial, OpenedArchive, ReaderOptions,
25 RecipientWrapCandidateMasterKey, RecipientWrapRecordContext, StreamedArchiveOpenParts,
26};
27use crate::tar_model::{
28 NoopTarStreamObserver, SafeExtractionOptions, TarStreamFilesystemRestoreObserver,
29 TarStreamMemberSummary, TarStreamObserver, TarStreamSummary, TarStreamSummaryValidator,
30};
31use crate::wire::{
32 BlockRecord, CryptoHeader, CryptoHeaderFixed, ExtensionTlv, RootAuthFooterV1, VolumeHeader,
33};
34
35const DEFAULT_MAX_RETAINED_METADATA_BYTES: usize = 128 * 1024 * 1024;
36const DEFAULT_MAX_INCOMPLETE_TAR_GROUP_BYTES: usize = 1024 * 1024;
37const DEFAULT_MAX_STREAMED_MEMBER_COUNT: u64 = 1_000_000;
38
39fn parse_volume_format_dispatch(volume_header: &VolumeHeader) -> Result<(), FormatError> {
40 let revision = volume_header.parse_volume_format_revision()?;
41 match revision {
42 crate::format::VolumeFormatRevision::V43 | crate::format::VolumeFormatRevision::V44 => {
43 Ok(())
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum SequentialRootAuthStatus {
50 Absent,
51 WireValidOnly,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SequentialVerifyReport {
56 pub archive_uuid: [u8; 16],
57 pub session_id: [u8; 16],
58 pub volume_format_rev: u16,
59 pub volume_index: u32,
60 pub total_volumes: u32,
61 pub file_count: u64,
62 pub payload_block_count: u64,
63 pub tar_total_size: u64,
64 pub content_sha256: [u8; 32],
65 pub root_auth: SequentialRootAuthStatus,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct SequentialExtractReport {
70 pub verification: SequentialVerifyReport,
71 pub extracted_member_count: u64,
72 pub degraded_metadata_count: u64,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct SequentialListReport {
77 pub verification: SequentialVerifyReport,
78 pub entries: Vec<ArchiveEntry>,
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 let entries = streamed_list_entries(&outcome.opened, &outcome.streamed_payload)?;
541 Ok(SequentialListReport {
542 verification: outcome.verification,
543 entries,
544 })
545}
546
547pub fn list_non_seekable_stream_with_recipient_wrap_resolver<R, F>(
548 reader: R,
549 mut resolver: F,
550 options: NonSeekableReaderOptions,
551) -> Result<SequentialListReport, FormatError>
552where
553 R: Read,
554 F: FnMut(
555 RecipientWrapRecordContext<'_>,
556 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
557{
558 let outcome = run_non_seekable_stream(
559 reader,
560 NonSeekableKeySource::RecipientWrap(&mut resolver),
561 options,
562 NoopTarStreamObserver,
563 None,
564 )?;
565 let entries = streamed_list_entries(&outcome.opened, &outcome.streamed_payload)?;
566 Ok(SequentialListReport {
567 verification: outcome.verification,
568 entries,
569 })
570}
571
572pub fn list_unencrypted_non_seekable_stream<R: Read>(
573 reader: R,
574 options: NonSeekableReaderOptions,
575) -> Result<SequentialListReport, FormatError> {
576 let outcome = run_non_seekable_stream(
577 reader,
578 NonSeekableKeySource::MasterKey(None),
579 options,
580 NoopTarStreamObserver,
581 None,
582 )?;
583 let entries = streamed_list_entries(&outcome.opened, &outcome.streamed_payload)?;
584 Ok(SequentialListReport {
585 verification: outcome.verification,
586 entries,
587 })
588}
589
590pub fn list_non_seekable_stream_with_bootstrap_sidecar<R: Read>(
591 reader: R,
592 bootstrap_sidecar: &[u8],
593 master_key: &MasterKey,
594 options: NonSeekableReaderOptions,
595) -> Result<SequentialListReport, FormatError> {
596 let outcome = run_non_seekable_stream(
597 reader,
598 NonSeekableKeySource::MasterKey(Some(master_key)),
599 options,
600 NoopTarStreamObserver,
601 Some(bootstrap_sidecar),
602 )?;
603 let entries = streamed_list_entries(&outcome.opened, &outcome.streamed_payload)?;
604 Ok(SequentialListReport {
605 verification: outcome.verification,
606 entries,
607 })
608}
609
610pub fn list_non_seekable_stream_with_recipient_wrap_resolver_and_bootstrap_sidecar<R, F>(
611 reader: R,
612 bootstrap_sidecar: &[u8],
613 mut resolver: F,
614 options: NonSeekableReaderOptions,
615) -> Result<SequentialListReport, FormatError>
616where
617 R: Read,
618 F: FnMut(
619 RecipientWrapRecordContext<'_>,
620 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
621{
622 let outcome = run_non_seekable_stream(
623 reader,
624 NonSeekableKeySource::RecipientWrap(&mut resolver),
625 options,
626 NoopTarStreamObserver,
627 Some(bootstrap_sidecar),
628 )?;
629 let entries = streamed_list_entries(&outcome.opened, &outcome.streamed_payload)?;
630 Ok(SequentialListReport {
631 verification: outcome.verification,
632 entries,
633 })
634}
635
636pub fn list_unencrypted_non_seekable_stream_with_bootstrap_sidecar<R: Read>(
637 reader: R,
638 bootstrap_sidecar: &[u8],
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 Some(bootstrap_sidecar),
647 )?;
648 let entries = streamed_list_entries(&outcome.opened, &outcome.streamed_payload)?;
649 Ok(SequentialListReport {
650 verification: outcome.verification,
651 entries,
652 })
653}
654
655struct SequentialStreamOutcome {
656 opened: OpenedArchive,
657 streamed_payload: StreamedPayloadSummary,
658 verification: SequentialVerifyReport,
659}
660
661type RecipientWrapResolver<'a> = dyn FnMut(
662 RecipientWrapRecordContext<'_>,
663 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
664 + 'a;
665
666enum NonSeekableKeySource<'a> {
667 MasterKey(Option<&'a MasterKey>),
668 RecipientWrap(&'a mut RecipientWrapResolver<'a>),
669}
670
671fn run_non_seekable_stream<R, O>(
672 mut reader: R,
673 key_source: NonSeekableKeySource<'_>,
674 options: NonSeekableReaderOptions,
675 observer: O,
676 bootstrap_sidecar: Option<&[u8]>,
677) -> Result<SequentialStreamOutcome, FormatError>
678where
679 R: Read,
680 O: TarStreamObserver,
681{
682 validate_reader_options(options.reader)?;
683 let mut volume_header_bytes = [0u8; VOLUME_HEADER_LEN];
684 read_exact_stream(&mut reader, &mut volume_header_bytes, "VolumeHeader")?;
685 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
686 parse_volume_format_dispatch(&volume_header)?;
687
688 let crypto_len = usize::try_from(volume_header.crypto_header_length)
689 .map_err(|_| FormatError::InvalidArchive("CryptoHeader length overflow"))?;
690 let mut crypto_header_bytes = vec![0u8; crypto_len];
691 read_exact_stream(&mut reader, &mut crypto_header_bytes, "CryptoHeader")?;
692 let parsed_crypto =
693 CryptoHeader::parse(&crypto_header_bytes, volume_header.crypto_header_length)?;
694 let crypto_header = parsed_crypto.fixed.clone();
695 let mut stream_cursor = checked_u64_add(
696 VOLUME_HEADER_LEN as u64,
697 volume_header.crypto_header_length as u64,
698 )?;
699 let startup_key_wrap_table = startup_key_wrap_table(
700 &volume_header,
701 &parsed_crypto.kdf_params,
702 |start, length| {
703 if stream_cursor != start {
704 return Err(FormatError::InvalidArchive(
705 "KeyWrapTableV1 does not start at stream cursor",
706 ));
707 }
708 let mut key_wrap_table_bytes = vec![0u8; length];
709 read_exact_stream(&mut reader, &mut key_wrap_table_bytes, "KeyWrapTableV1")?;
710 stream_cursor = checked_u64_add(stream_cursor, length as u64)?;
711 Ok(key_wrap_table_bytes)
712 },
713 )?;
714 let subkeys = match (
715 crypto_header.aead_algo.is_encrypted(),
716 &parsed_crypto.kdf_params,
717 key_source,
718 ) {
719 (false, _, NonSeekableKeySource::RecipientWrap(_)) => {
720 return Err(FormatError::KeyMaterialMismatch);
721 }
722 (false, _, NonSeekableKeySource::MasterKey(_)) => Subkeys::unencrypted_placeholder(),
723 (
724 true,
725 crate::crypto::KdfParams::RecipientWrap { .. },
726 NonSeekableKeySource::RecipientWrap(resolver),
727 ) => {
728 let table = startup_key_wrap_table
729 .as_ref()
730 .ok_or(FormatError::KeyMaterialMismatch)?;
731 recipient_wrap_subkeys_from_table(
732 &volume_header,
733 &parsed_crypto,
734 &table.table,
735 resolver,
736 )?
737 }
738 (
739 true,
740 crate::crypto::KdfParams::RecipientWrap { .. },
741 NonSeekableKeySource::MasterKey(_),
742 ) => {
743 return Err(FormatError::KeyMaterialMismatch);
744 }
745 (true, _, NonSeekableKeySource::MasterKey(master_key)) => Subkeys::derive(
746 master_key.ok_or(FormatError::KeyMaterialMismatch)?,
747 &volume_header.archive_uuid,
748 &volume_header.session_id,
749 )?,
750 (true, _, NonSeekableKeySource::RecipientWrap(_)) => {
751 return Err(FormatError::KeyMaterialMismatch);
752 }
753 };
754 verify_integrity_tag(
755 HmacDomain::CryptoHeader,
756 crypto_header.aead_algo,
757 volume_header.volume_format_rev,
758 Some(&subkeys.mac_key),
759 &volume_header.archive_uuid,
760 &volume_header.session_id,
761 parsed_crypto.hmac_covered_bytes,
762 &parsed_crypto.header_hmac,
763 )?;
764 parsed_crypto.validate_extension_semantics()?;
765 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
766 validate_crypto_class_parity_exactness(&crypto_header)?;
767 let block_records_start = startup_key_wrap_table
768 .as_ref()
769 .map(|table| table.block_records_start)
770 .unwrap_or(stream_cursor);
771 let bootstrap = bootstrap_sidecar
772 .map(|sidecar| {
773 parse_non_seekable_bootstrap_material(sidecar, &volume_header, &crypto_header, &subkeys)
774 })
775 .transpose()?;
776 validate_sequential_verify_supported_volume(
777 &volume_header,
778 &crypto_header,
779 &parsed_crypto.extensions,
780 bootstrap.as_ref(),
781 )?;
782
783 let block_size = crypto_header.block_size as usize;
784 let record_len = block_size
785 .checked_add(BLOCK_RECORD_FRAMING_LEN)
786 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
787 let mut stream_offset = block_records_start;
788 let mut observed_block_count = 0u64;
789 let mut metadata_seen = false;
790 let mut pending = PendingLiveEnvelope::default();
791 let mut next_envelope_index = 0u64;
792 let mut retained_metadata_bytes = 0usize;
793 let mut metadata_blocks = BTreeMap::new();
794 let mut payload = StreamedPayloadCollector::with_observer(
795 &crypto_header,
796 options,
797 observer,
798 bootstrap
799 .as_ref()
800 .and_then(|material| material.payload_dictionary.clone()),
801 )?;
802
803 let terminal_tail = loop {
804 let mut magic = [0u8; 4];
805 read_exact_stream(&mut reader, &mut magic, "BlockRecord or terminal tail")?;
806 if magic != *b"TZBK" {
807 let tail_start = stream_offset;
808 stream_offset = checked_u64_add(stream_offset, magic.len() as u64)?;
809 let mut tail = TerminalTailBuffer::new(tail_start, options.max_terminal_tail_size);
810 tail.append(&magic)?;
811 let mut buf = [0u8; 64 * 1024];
812 loop {
813 let read = read_stream_chunk(&mut reader, &mut buf)?;
814 if read == 0 {
815 break;
816 }
817 tail.append(&buf[..read])?;
818 stream_offset = checked_u64_add(stream_offset, read as u64)?;
819 }
820 break tail.finish(stream_offset);
821 }
822
823 let expected_block_index =
824 expected_stream_block_index(&volume_header, observed_block_count)?;
825 let mut raw = vec![0u8; record_len];
826 raw[..4].copy_from_slice(&magic);
827 read_exact_stream(&mut reader, &mut raw[4..], "BlockRecord")?;
828 observed_block_count = checked_u64_add(observed_block_count, 1)?;
829 stream_offset = checked_u64_add(stream_offset, record_len as u64)?;
830
831 match BlockRecord::parse(&raw, block_size) {
832 Ok(record) => {
833 if record.block_index != expected_block_index {
834 return Err(FormatError::InvalidArchive(
835 "BlockRecord index does not match stream position",
836 ));
837 }
838 handle_live_record(
839 record,
840 &mut pending,
841 LiveStreamContext {
842 payload: &mut payload,
843 subkeys: &subkeys,
844 volume_header: &volume_header,
845 crypto_header: &crypto_header,
846 next_envelope_index: &mut next_envelope_index,
847 metadata_seen: &mut metadata_seen,
848 metadata_blocks: &mut metadata_blocks,
849 retained_metadata_bytes: &mut retained_metadata_bytes,
850 max_retained_metadata_bytes: options.max_retained_metadata_bytes,
851 },
852 )?;
853 }
854 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
855 handle_live_erasure(
856 &mut pending,
857 LiveStreamContext {
858 payload: &mut payload,
859 subkeys: &subkeys,
860 volume_header: &volume_header,
861 crypto_header: &crypto_header,
862 next_envelope_index: &mut next_envelope_index,
863 metadata_seen: &mut metadata_seen,
864 metadata_blocks: &mut metadata_blocks,
865 retained_metadata_bytes: &mut retained_metadata_bytes,
866 max_retained_metadata_bytes: options.max_retained_metadata_bytes,
867 },
868 expected_block_index,
869 )?;
870 }
871 Err(err) => return Err(err),
872 }
873 };
874
875 if !pending.is_empty() {
876 finalize_live_envelope(
877 &mut pending,
878 &mut payload,
879 &subkeys,
880 &volume_header,
881 &crypto_header,
882 &mut next_envelope_index,
883 )?;
884 }
885
886 let terminal = parse_terminal_material_read_at(
887 &terminal_tail,
888 terminal_tail.stream_len,
889 terminal_tail.start_offset,
890 observed_block_count,
891 KeyHoldingTerminalContext {
892 subkeys: &subkeys,
893 volume_header: &volume_header,
894 crypto_header: &crypto_header,
895 crypto_header_bytes: &crypto_header_bytes,
896 },
897 )?;
898 if let Some(bootstrap) = &bootstrap {
899 if !manifest_bootstrap_fields_match(&terminal.manifest_footer, &bootstrap.manifest_footer) {
900 return Err(FormatError::InvalidArchive(
901 "bootstrap sidecar conflicts with terminal ManifestFooter",
902 ));
903 }
904 }
905 let observed_archive_bytes = observed_archive_size([terminal_tail.stream_len])?;
906 let streamed_payload = payload.finish()?;
907 if streamed_payload.tar.total_extraction_size
908 > total_extraction_size_cap(options.reader, observed_archive_bytes)
909 {
910 return Err(FormatError::ReaderUnsupported(
911 "total extraction size exceeds configured cap",
912 ));
913 }
914
915 let root_auth = root_auth_status(terminal.root_auth_footer.as_ref());
916 let opened = OpenedArchive::from_streamed_parts(StreamedArchiveOpenParts {
917 options: options.reader,
918 observed_archive_bytes,
919 subkeys,
920 blocks: metadata_blocks,
921 crypto_header_bytes,
922 volume_header,
923 crypto_header,
924 manifest_footer: terminal.manifest_footer,
925 volume_trailer: terminal.volume_trailer,
926 root_auth_footer: terminal.root_auth_footer,
927 })?;
928 opened.verify_streamed_payload_summary(&streamed_payload)?;
929
930 let verification = SequentialVerifyReport {
931 archive_uuid: opened.volume_header.archive_uuid,
932 session_id: opened.volume_header.session_id,
933 volume_format_rev: opened.volume_header.volume_format_rev,
934 volume_index: opened.volume_header.volume_index,
935 total_volumes: opened.manifest_footer.total_volumes,
936 file_count: opened.index_root.header.file_count,
937 payload_block_count: opened.index_root.header.payload_block_count,
938 tar_total_size: opened.index_root.header.tar_total_size,
939 content_sha256: opened.index_root.header.content_sha256,
940 root_auth,
941 };
942
943 Ok(SequentialStreamOutcome {
944 opened,
945 streamed_payload,
946 verification,
947 })
948}
949
950fn degraded_metadata_count(payload: &StreamedPayloadSummary) -> Result<u64, FormatError> {
951 payload.tar.members.iter().try_fold(0u64, |count, member| {
952 count
953 .checked_add(member.diagnostics.len() as u64)
954 .ok_or(FormatError::InvalidArchive(
955 "degraded metadata count overflow",
956 ))
957 })
958}
959
960fn streamed_list_entries(
961 opened: &OpenedArchive,
962 payload: &StreamedPayloadSummary,
963) -> Result<Vec<ArchiveEntry>, FormatError> {
964 let mut latest_by_path = BTreeMap::<Vec<u8>, &TarStreamMemberSummary>::new();
965 for member in &payload.tar.members {
966 let replace = latest_by_path
967 .get(&member.path)
968 .map(|existing| member.group_start > existing.group_start)
969 .unwrap_or(true);
970 if replace {
971 latest_by_path.insert(member.path.clone(), member);
972 }
973 }
974
975 opened
976 .list_index_entries()?
977 .into_iter()
978 .map(|entry| {
979 let member =
980 latest_by_path
981 .get(entry.path.as_bytes())
982 .ok_or(FormatError::InvalidArchive(
983 "streamed tar member missing from final index",
984 ))?;
985 Ok(ArchiveEntry {
986 path: entry.path,
987 file_data_size: entry.file_data_size,
988 kind: member.kind,
989 mode: member.mode,
990 mtime: member.mtime,
991 diagnostics: member.diagnostics.clone(),
992 })
993 })
994 .collect()
995}
996
997struct StagedExtraction {
998 tempdir: tempfile::TempDir,
999 root: PathBuf,
1000 output_dir: PathBuf,
1001}
1002
1003impl StagedExtraction {
1004 fn new(output_dir: &Path) -> Result<Self, ExtractError> {
1005 let parent = output_dir
1006 .parent()
1007 .filter(|path| !path.as_os_str().is_empty())
1008 .unwrap_or_else(|| Path::new("."));
1009 let tempdir = tempfile::Builder::new()
1010 .prefix(".tzap-nonseekable-")
1011 .tempdir_in(parent)
1012 .map_err(ExtractError::Output)?;
1013 let root = tempdir.path().join("root");
1014 fs::create_dir(&root).map_err(ExtractError::Output)?;
1015 Ok(Self {
1016 tempdir,
1017 root,
1018 output_dir: output_dir.to_path_buf(),
1019 })
1020 }
1021
1022 fn root(&self) -> &Path {
1023 &self.root
1024 }
1025
1026 fn commit(self, options: SafeExtractionOptions) -> Result<(), ExtractError> {
1027 match fs::symlink_metadata(&self.output_dir) {
1028 Ok(metadata) => {
1029 if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
1030 return Err(FormatError::UnsafeArchivePath.into());
1031 }
1032 preflight_staged_merge(&self.root, &self.output_dir, options)?;
1033 merge_staged_dir(&self.root, &self.output_dir, options)?;
1034 }
1035 Err(error) if error.kind() == ErrorKind::NotFound => {
1036 if let Some(parent) = self
1037 .output_dir
1038 .parent()
1039 .filter(|path| !path.as_os_str().is_empty())
1040 {
1041 fs::create_dir_all(parent).map_err(ExtractError::Output)?;
1042 }
1043 fs::rename(&self.root, &self.output_dir).map_err(ExtractError::Output)?;
1044 }
1045 Err(_) => {
1046 return Err(FormatError::FilesystemExtractionFailed(
1047 "failed to inspect extraction directory",
1048 )
1049 .into());
1050 }
1051 }
1052 drop(self.tempdir);
1053 Ok(())
1054 }
1055}
1056
1057fn preflight_staged_merge(
1058 staged_root: &Path,
1059 output_root: &Path,
1060 options: SafeExtractionOptions,
1061) -> Result<(), FormatError> {
1062 for entry in read_dir_sorted(staged_root)? {
1063 let staged_path = entry.path();
1064 let relative = staged_path
1065 .strip_prefix(staged_root)
1066 .map_err(|_| FormatError::UnsafeArchivePath)?;
1067 preflight_staged_entry(&staged_path, output_root, relative, options)?;
1068 }
1069 Ok(())
1070}
1071
1072fn preflight_staged_entry(
1073 staged_path: &Path,
1074 output_root: &Path,
1075 relative: &Path,
1076 options: SafeExtractionOptions,
1077) -> Result<(), FormatError> {
1078 let final_path = output_root.join(relative);
1079 let staged_metadata = fs::symlink_metadata(staged_path).map_err(|_| {
1080 FormatError::FilesystemExtractionFailed("failed to inspect staged extraction output")
1081 })?;
1082 if let Some(parent) = relative
1083 .parent()
1084 .filter(|path| !path.as_os_str().is_empty())
1085 {
1086 preflight_relative_parent_chain(output_root, parent)?;
1087 }
1088 match fs::symlink_metadata(&final_path) {
1089 Ok(final_metadata) => {
1090 let final_type = final_metadata.file_type();
1091 if final_type.is_symlink() {
1092 return Err(FormatError::UnsafeArchivePath);
1093 }
1094 if staged_metadata.file_type().is_dir() {
1095 if !final_type.is_dir() {
1096 return Err(FormatError::UnsafeOverwrite);
1097 }
1098 } else if final_type.is_dir() || !options.overwrite_existing {
1099 return Err(FormatError::UnsafeOverwrite);
1100 }
1101 }
1102 Err(error) if error.kind() == ErrorKind::NotFound => {}
1103 Err(_) => {
1104 return Err(FormatError::FilesystemExtractionFailed(
1105 "failed to inspect extraction destination",
1106 ));
1107 }
1108 }
1109 if staged_metadata.file_type().is_dir() {
1110 for entry in read_dir_sorted(staged_path)? {
1111 let child_relative = relative.join(entry.file_name());
1112 preflight_staged_entry(&entry.path(), output_root, &child_relative, options)?;
1113 }
1114 }
1115 Ok(())
1116}
1117
1118fn preflight_relative_parent_chain(root: &Path, parent: &Path) -> Result<(), FormatError> {
1119 let mut current = root.to_path_buf();
1120 for component in parent.components() {
1121 current.push(component.as_os_str());
1122 match fs::symlink_metadata(¤t) {
1123 Ok(metadata) => {
1124 let file_type = metadata.file_type();
1125 if file_type.is_symlink() || !file_type.is_dir() {
1126 return Err(FormatError::UnsafeArchivePath);
1127 }
1128 }
1129 Err(error) if error.kind() == ErrorKind::NotFound => {}
1130 Err(_) => {
1131 return Err(FormatError::FilesystemExtractionFailed(
1132 "failed to inspect extraction destination",
1133 ));
1134 }
1135 }
1136 }
1137 Ok(())
1138}
1139
1140fn merge_staged_dir(
1141 staged_dir: &Path,
1142 final_dir: &Path,
1143 options: SafeExtractionOptions,
1144) -> Result<(), ExtractError> {
1145 fs::create_dir_all(final_dir).map_err(ExtractError::Output)?;
1146 for entry in read_dir_sorted(staged_dir)? {
1147 let staged_path = entry.path();
1148 let final_path = final_dir.join(entry.file_name());
1149 let metadata = fs::symlink_metadata(&staged_path).map_err(|_| {
1150 FormatError::FilesystemExtractionFailed("failed to inspect staged extraction output")
1151 })?;
1152 if metadata.file_type().is_dir() {
1153 merge_staged_dir(&staged_path, &final_path, options)?;
1154 continue;
1155 }
1156 if options.overwrite_existing && fs::symlink_metadata(&final_path).is_ok() {
1157 fs::remove_file(&final_path).map_err(ExtractError::Output)?;
1158 }
1159 fs::rename(&staged_path, &final_path).map_err(ExtractError::Output)?;
1160 }
1161 Ok(())
1162}
1163
1164fn read_dir_sorted(path: &Path) -> Result<Vec<fs::DirEntry>, FormatError> {
1165 let mut entries = fs::read_dir(path)
1166 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to read directory"))?
1167 .collect::<Result<Vec<_>, _>>()
1168 .map_err(|_| FormatError::FilesystemExtractionFailed("failed to read directory"))?;
1169 entries.sort_by_key(|entry| entry.file_name());
1170 Ok(entries)
1171}
1172
1173fn validate_sequential_verify_supported_volume(
1174 volume_header: &VolumeHeader,
1175 crypto_header: &CryptoHeaderFixed,
1176 extensions: &[ExtensionTlv<'_>],
1177 bootstrap: Option<&NonSeekableBootstrapMaterial>,
1178) -> Result<(), FormatError> {
1179 reject_unsupported_raw_stream_profile(extensions)?;
1180 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
1181 return Err(FormatError::ReaderUnsupported(
1182 "sequential reader supports only single-volume archive input",
1183 ));
1184 }
1185 if crypto_header.stripe_width != volume_header.stripe_width {
1186 return Err(FormatError::InvalidArchive(
1187 "VolumeHeader and CryptoHeader stripe_width differ",
1188 ));
1189 }
1190 if crypto_header.has_dictionary != 0
1191 && bootstrap
1192 .and_then(|material| material.payload_dictionary.as_ref())
1193 .is_none()
1194 {
1195 return Err(FormatError::ReaderUnsupported(
1196 "dictionary bootstrap required for non-seekable sequential verification",
1197 ));
1198 }
1199 Ok(())
1200}
1201
1202struct LiveStreamContext<'a, O: TarStreamObserver> {
1203 payload: &'a mut StreamedPayloadCollector<O>,
1204 subkeys: &'a Subkeys,
1205 volume_header: &'a VolumeHeader,
1206 crypto_header: &'a CryptoHeaderFixed,
1207 next_envelope_index: &'a mut u64,
1208 metadata_seen: &'a mut bool,
1209 metadata_blocks: &'a mut BTreeMap<u64, BlockRecord>,
1210 retained_metadata_bytes: &'a mut usize,
1211 max_retained_metadata_bytes: usize,
1212}
1213
1214fn handle_live_record<O: TarStreamObserver>(
1215 record: BlockRecord,
1216 pending: &mut PendingLiveEnvelope,
1217 context: LiveStreamContext<'_, O>,
1218) -> Result<(), FormatError> {
1219 match record.kind {
1220 BlockKind::PayloadData => {
1221 if *context.metadata_seen {
1222 return Err(FormatError::InvalidArchive(
1223 "payload BlockRecord appears after metadata",
1224 ));
1225 }
1226 if pending.awaiting_tentative_parity {
1227 return Err(FormatError::InvalidArchive(
1228 "sequential payload envelope boundary is ambiguous after CRC erasure",
1229 ));
1230 }
1231 if pending.saw_last_data {
1232 finalize_live_envelope(
1233 pending,
1234 &mut *context.payload,
1235 context.subkeys,
1236 context.volume_header,
1237 context.crypto_header,
1238 &mut *context.next_envelope_index,
1239 )?;
1240 }
1241 pending.note_block(record.block_index);
1242 let is_last_data = record.is_last_data();
1243 pending.data_shards.push(Some(record.payload));
1244 if is_last_data {
1245 pending.saw_last_data = true;
1246 }
1247 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
1248 return Err(FormatError::InvalidArchive(
1249 "sequential payload envelope exceeds data-shard cap",
1250 ));
1251 }
1252 }
1253 BlockKind::PayloadParity => {
1254 if *context.metadata_seen {
1255 return Err(FormatError::InvalidArchive(
1256 "payload parity BlockRecord appears after metadata",
1257 ));
1258 }
1259 if pending.awaiting_tentative_parity {
1260 pending.awaiting_tentative_parity = false;
1261 pending.saw_last_data = true;
1262 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
1263 return Err(FormatError::InvalidArchive(
1264 "payload parity appears before envelope data is complete",
1265 ));
1266 }
1267 pending.note_block(record.block_index);
1268 pending.parity_shards.push(Some(record.payload));
1269 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
1270 return Err(FormatError::InvalidArchive(
1271 "sequential payload envelope exceeds parity-shard cap",
1272 ));
1273 }
1274 }
1275 _ => {
1276 if !pending.is_empty() {
1277 finalize_live_envelope(
1278 pending,
1279 &mut *context.payload,
1280 context.subkeys,
1281 context.volume_header,
1282 context.crypto_header,
1283 &mut *context.next_envelope_index,
1284 )?;
1285 }
1286 *context.metadata_seen = true;
1287 retain_metadata_record(
1288 &mut *context.metadata_blocks,
1289 record,
1290 &mut *context.retained_metadata_bytes,
1291 context.max_retained_metadata_bytes,
1292 )?;
1293 }
1294 }
1295 Ok(())
1296}
1297
1298fn retain_metadata_record(
1299 metadata_blocks: &mut BTreeMap<u64, BlockRecord>,
1300 record: BlockRecord,
1301 retained_metadata_bytes: &mut usize,
1302 max_retained_metadata_bytes: usize,
1303) -> Result<(), FormatError> {
1304 let retained = record
1305 .payload
1306 .len()
1307 .checked_add(BLOCK_RECORD_FRAMING_LEN)
1308 .ok_or(FormatError::InvalidArchive("metadata retention overflow"))?;
1309 *retained_metadata_bytes = retained_metadata_bytes
1310 .checked_add(retained)
1311 .ok_or(FormatError::InvalidArchive("metadata retention overflow"))?;
1312 if *retained_metadata_bytes > max_retained_metadata_bytes {
1313 return Err(FormatError::ReaderUnsupported(
1314 "retained metadata exceeds configured streaming cap",
1315 ));
1316 }
1317 if metadata_blocks.insert(record.block_index, record).is_some() {
1318 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1319 }
1320 Ok(())
1321}
1322
1323fn handle_live_erasure<O: TarStreamObserver>(
1324 pending: &mut PendingLiveEnvelope,
1325 context: LiveStreamContext<'_, O>,
1326 expected_block_index: u64,
1327) -> Result<(), FormatError> {
1328 if *context.metadata_seen {
1329 return Ok(());
1330 }
1331 if pending.saw_last_data
1332 && pending.parity_shards.len()
1333 >= required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?
1334 as usize
1335 {
1336 finalize_live_envelope(
1337 pending,
1338 &mut *context.payload,
1339 context.subkeys,
1340 context.volume_header,
1341 context.crypto_header,
1342 &mut *context.next_envelope_index,
1343 )?;
1344 *context.metadata_seen = true;
1345 return Ok(());
1346 }
1347 if pending.saw_last_data {
1348 return Err(FormatError::BadCrc {
1349 structure: "BlockRecord",
1350 });
1351 }
1352 if !sequential_payload_parity_is_guaranteed(context.crypto_header) {
1353 return Err(FormatError::BadCrc {
1354 structure: "BlockRecord",
1355 });
1356 }
1357 pending.note_block(expected_block_index);
1358 pending.data_shards.push(None);
1359 pending.awaiting_tentative_parity = true;
1360 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
1361 return Err(FormatError::InvalidArchive(
1362 "sequential payload envelope exceeds data-shard cap",
1363 ));
1364 }
1365 Ok(())
1366}
1367
1368fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
1369 crypto_header.fec_parity_shards > 0
1370 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
1371}
1372
1373fn finalize_live_envelope<O: TarStreamObserver>(
1374 pending: &mut PendingLiveEnvelope,
1375 payload: &mut StreamedPayloadCollector<O>,
1376 subkeys: &Subkeys,
1377 volume_header: &VolumeHeader,
1378 crypto_header: &CryptoHeaderFixed,
1379 next_envelope_index: &mut u64,
1380) -> Result<(), FormatError> {
1381 if !pending.saw_last_data {
1382 return Err(FormatError::InvalidArchive(
1383 "sequential payload envelope is missing last-data flag",
1384 ));
1385 }
1386 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
1387 return Err(FormatError::InvalidArchive(
1388 "sequential payload envelope exceeds data-shard cap",
1389 ));
1390 }
1391 if pending.parity_shards.len() > crypto_header.fec_parity_shards as usize {
1392 return Err(FormatError::InvalidArchive(
1393 "sequential payload envelope exceeds parity-shard cap",
1394 ));
1395 }
1396 let required_parity = required_object_parity(pending.data_shards.len() as u64, crypto_header)?;
1397 if pending.parity_shards.len() < required_parity as usize {
1398 return Err(FormatError::InvalidArchive(
1399 "sequential payload envelope has insufficient parity for recovery settings",
1400 ));
1401 }
1402 let first_block_index = pending
1403 .first_block_index
1404 .ok_or(FormatError::InvalidArchive(
1405 "sequential payload envelope is missing first block",
1406 ))?;
1407
1408 let repaired = repair_data_gf16(
1409 &pending.data_shards,
1410 &pending.parity_shards,
1411 crypto_header.block_size as usize,
1412 )?;
1413 let mut encrypted = Vec::with_capacity(repaired.len() * crypto_header.block_size as usize);
1414 for shard in repaired {
1415 encrypted.extend_from_slice(&shard);
1416 }
1417 let plaintext = decrypt_padded_aead_object(
1418 AeadObjectContext {
1419 algo: crypto_header.aead_algo,
1420 key: &subkeys.enc_key,
1421 nonce_seed: &subkeys.nonce_seed,
1422 domain: b"envelope",
1423 archive_uuid: &volume_header.archive_uuid,
1424 session_id: &volume_header.session_id,
1425 counter: *next_envelope_index,
1426 },
1427 &encrypted,
1428 )?;
1429 payload.decode_envelope(
1430 *next_envelope_index,
1431 first_block_index,
1432 pending.data_shards.len(),
1433 pending.parity_shards.len(),
1434 crypto_header.block_size as usize,
1435 &plaintext,
1436 )?;
1437 *next_envelope_index = checked_u64_add(*next_envelope_index, 1)?;
1438 *pending = PendingLiveEnvelope::default();
1439 Ok(())
1440}
1441
1442#[derive(Debug, Default)]
1443struct PendingLiveEnvelope {
1444 first_block_index: Option<u64>,
1445 data_shards: Vec<Option<Vec<u8>>>,
1446 parity_shards: Vec<Option<Vec<u8>>>,
1447 saw_last_data: bool,
1448 awaiting_tentative_parity: bool,
1449}
1450
1451impl PendingLiveEnvelope {
1452 fn is_empty(&self) -> bool {
1453 self.data_shards.is_empty() && self.parity_shards.is_empty()
1454 }
1455
1456 fn note_block(&mut self, block_index: u64) {
1457 if self.first_block_index.is_none() {
1458 self.first_block_index = Some(block_index);
1459 }
1460 }
1461}
1462
1463struct StreamedPayloadCollector<O = NoopTarStreamObserver> {
1464 tar: TarStreamSummaryValidator<O>,
1465 hasher: Sha256,
1466 max_tar_stream_size: usize,
1467 payload_dictionary: Option<Vec<u8>>,
1468 envelopes: Vec<StreamedEnvelopeSummary>,
1469 frames: Vec<StreamedFrameSummary>,
1470}
1471
1472impl<O: TarStreamObserver> StreamedPayloadCollector<O> {
1473 fn with_observer(
1474 crypto_header: &CryptoHeaderFixed,
1475 options: NonSeekableReaderOptions,
1476 observer: O,
1477 payload_dictionary: Option<Vec<u8>>,
1478 ) -> Result<Self, FormatError> {
1479 Ok(Self {
1480 tar: TarStreamSummaryValidator::with_observer(
1481 crypto_header.max_path_length,
1482 options.reader.max_total_extraction_size,
1483 options.max_incomplete_tar_group_bytes,
1484 options.max_streamed_member_count,
1485 observer,
1486 ),
1487 hasher: Sha256::new(),
1488 max_tar_stream_size: options.reader.max_verify_tar_size,
1489 payload_dictionary,
1490 envelopes: Vec::new(),
1491 frames: Vec::new(),
1492 })
1493 }
1494
1495 fn decode_envelope(
1496 &mut self,
1497 envelope_index: u64,
1498 first_block_index: u64,
1499 data_block_count: usize,
1500 parity_block_count: usize,
1501 block_size: usize,
1502 plaintext: &[u8],
1503 ) -> Result<(), FormatError> {
1504 if plaintext.is_empty() {
1505 return Err(FormatError::InvalidArchive(
1506 "payload envelope plaintext has no frames",
1507 ));
1508 }
1509 let first_frame_index = u64::try_from(self.frames.len())
1510 .map_err(|_| FormatError::InvalidArchive("FrameEntry count overflow"))?;
1511 let mut cursor = 0usize;
1512 while cursor < plaintext.len() {
1513 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
1514 .map_err(|_| FormatError::InvalidZstdFrame)?;
1515 if frame_len == 0 {
1516 return Err(FormatError::InvalidZstdFrame);
1517 }
1518 let end = checked_usize_add(cursor, frame_len)?;
1519 validate_exact_zstd_frame(&plaintext[cursor..end])?;
1520 self.decode_frame(
1521 envelope_index,
1522 u32_len(cursor, "FrameEntry.offset_in_envelope")?,
1523 &plaintext[cursor..end],
1524 )?;
1525 cursor = end;
1526 }
1527 let frame_count = u32_len(
1528 self.frames.len() - first_frame_index as usize,
1529 "EnvelopeEntry.frame_count",
1530 )?;
1531 if frame_count == 0 {
1532 return Err(FormatError::InvalidArchive(
1533 "payload envelope plaintext has no frames",
1534 ));
1535 }
1536 let encrypted_size =
1537 data_block_count
1538 .checked_mul(block_size)
1539 .ok_or(FormatError::InvalidArchive(
1540 "EnvelopeEntry encrypted size overflow",
1541 ))?;
1542 self.envelopes.push(StreamedEnvelopeSummary {
1543 envelope_index,
1544 first_block_index,
1545 data_block_count: u32_len(data_block_count, "EnvelopeEntry.data_block_count")?,
1546 parity_block_count: u32_len(parity_block_count, "EnvelopeEntry.parity_block_count")?,
1547 encrypted_size: u32_len(encrypted_size, "EnvelopeEntry.encrypted_size")?,
1548 plaintext_size: u32_len(plaintext.len(), "EnvelopeEntry.plaintext_size")?,
1549 first_frame_index,
1550 frame_count,
1551 });
1552 Ok(())
1553 }
1554
1555 fn decode_frame(
1556 &mut self,
1557 envelope_index: u64,
1558 offset_in_envelope: u32,
1559 compressed: &[u8],
1560 ) -> Result<(), FormatError> {
1561 let frame_index = u64::try_from(self.frames.len())
1562 .map_err(|_| FormatError::InvalidArchive("FrameEntry count overflow"))?;
1563 let tar_stream_offset = self.tar.tar_total_size();
1564 let decompressed_size = if let Some(dictionary) = &self.payload_dictionary {
1565 let mut decoder = zstd::stream::Decoder::with_dictionary(compressed, dictionary)
1566 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
1567 self.decode_zstd_frame_body(&mut decoder)?
1568 } else {
1569 let mut decoder = zstd::stream::Decoder::new(compressed)
1570 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
1571 self.decode_zstd_frame_body(&mut decoder)?
1572 };
1573 if decompressed_size == 0 {
1574 return Err(FormatError::InvalidArchive(
1575 "zstd payload frame decompressed to zero bytes",
1576 ));
1577 }
1578 self.frames.push(StreamedFrameSummary {
1579 frame_index,
1580 envelope_index,
1581 offset_in_envelope,
1582 compressed_size: u32_len(compressed.len(), "FrameEntry.compressed_size")?,
1583 decompressed_size: u32_len(
1584 usize::try_from(decompressed_size)
1585 .map_err(|_| FormatError::InvalidArchive("FrameEntry size overflow"))?,
1586 "FrameEntry.decompressed_size",
1587 )?,
1588 tar_stream_offset,
1589 });
1590 Ok(())
1591 }
1592
1593 fn decode_zstd_frame_body<D: Read>(&mut self, decoder: &mut D) -> Result<u64, FormatError> {
1594 let mut decompressed_size = 0u64;
1595 let mut buf = [0u8; 64 * 1024];
1596 loop {
1597 let read = decoder
1598 .read(&mut buf)
1599 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
1600 if read == 0 {
1601 break;
1602 }
1603 let next_tar_size = self.tar.tar_total_size().checked_add(read as u64).ok_or(
1604 FormatError::ReaderUnsupported(
1605 "sequential tar stream exceeds configured verification cap",
1606 ),
1607 )?;
1608 if next_tar_size > self.max_tar_stream_size as u64 {
1609 return Err(FormatError::ReaderUnsupported(
1610 "sequential tar stream exceeds configured verification cap",
1611 ));
1612 }
1613 self.hasher.update(&buf[..read]);
1614 self.tar.observe(&buf[..read])?;
1615 decompressed_size = checked_u64_add(decompressed_size, read as u64)?;
1616 }
1617 Ok(decompressed_size)
1618 }
1619
1620 fn finish(self) -> Result<StreamedPayloadSummary, FormatError> {
1621 let content_sha256 = self.hasher.finalize();
1622 let mut digest = [0u8; 32];
1623 digest.copy_from_slice(&content_sha256);
1624 Ok(StreamedPayloadSummary {
1625 tar: self.tar.finish()?,
1626 content_sha256: digest,
1627 envelopes: self.envelopes,
1628 frames: self.frames,
1629 })
1630 }
1631}
1632
1633struct TerminalTailBuffer {
1634 start_offset: u64,
1635 cap: usize,
1636 bytes: Vec<u8>,
1637}
1638
1639impl TerminalTailBuffer {
1640 fn new(start_offset: u64, cap: usize) -> Self {
1641 Self {
1642 start_offset,
1643 cap,
1644 bytes: Vec::new(),
1645 }
1646 }
1647
1648 fn append(&mut self, bytes: &[u8]) -> Result<(), FormatError> {
1649 let next_len = self
1650 .bytes
1651 .len()
1652 .checked_add(bytes.len())
1653 .ok_or(FormatError::InvalidArchive("terminal tail size overflow"))?;
1654 if next_len > self.cap {
1655 return Err(FormatError::ReaderUnsupported(
1656 "terminal tail exceeds configured cap",
1657 ));
1658 }
1659 self.bytes.extend_from_slice(bytes);
1660 Ok(())
1661 }
1662
1663 fn finish(self, stream_len: u64) -> TerminalTailReadAt {
1664 TerminalTailReadAt {
1665 start_offset: self.start_offset,
1666 stream_len,
1667 bytes: self.bytes,
1668 }
1669 }
1670}
1671
1672struct TerminalTailReadAt {
1673 start_offset: u64,
1674 stream_len: u64,
1675 bytes: Vec<u8>,
1676}
1677
1678impl ArchiveReadAt for TerminalTailReadAt {
1679 fn len(&self) -> Result<u64, FormatError> {
1680 Ok(self.stream_len)
1681 }
1682
1683 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
1684 let end = offset
1685 .checked_add(buf.len() as u64)
1686 .ok_or(FormatError::InvalidArchive("terminal tail range overflow"))?;
1687 let tail_end = self
1688 .start_offset
1689 .checked_add(self.bytes.len() as u64)
1690 .ok_or(FormatError::InvalidArchive("terminal tail range overflow"))?;
1691 if offset < self.start_offset || end > tail_end {
1692 return Err(FormatError::InvalidLength {
1693 structure: "terminal tail",
1694 expected: usize::try_from(end.saturating_sub(self.start_offset))
1695 .unwrap_or(usize::MAX),
1696 actual: self.bytes.len(),
1697 });
1698 }
1699 let start = usize::try_from(offset - self.start_offset)
1700 .map_err(|_| FormatError::InvalidArchive("terminal tail range overflow"))?;
1701 let end = start
1702 .checked_add(buf.len())
1703 .ok_or(FormatError::InvalidArchive("terminal tail range overflow"))?;
1704 buf.copy_from_slice(&self.bytes[start..end]);
1705 Ok(())
1706 }
1707}
1708
1709fn root_auth_status(footer: Option<&RootAuthFooterV1>) -> SequentialRootAuthStatus {
1710 if footer.is_some() {
1711 SequentialRootAuthStatus::WireValidOnly
1712 } else {
1713 SequentialRootAuthStatus::Absent
1714 }
1715}
1716
1717fn read_exact_stream<R: Read>(
1718 reader: &mut R,
1719 mut buf: &mut [u8],
1720 structure: &'static str,
1721) -> Result<(), FormatError> {
1722 let expected = buf.len();
1723 let mut actual = 0usize;
1724 while !buf.is_empty() {
1725 match reader.read(buf) {
1726 Ok(0) => {
1727 return Err(FormatError::InvalidLength {
1728 structure,
1729 expected,
1730 actual,
1731 })
1732 }
1733 Ok(read) => {
1734 actual = checked_usize_add(actual, read)?;
1735 let (_, rest) = buf.split_at_mut(read);
1736 buf = rest;
1737 }
1738 Err(err) if err.kind() == ErrorKind::Interrupted => {}
1739 Err(_) => return Err(FormatError::InvalidArchive("archive read failed")),
1740 }
1741 }
1742 Ok(())
1743}
1744
1745fn read_stream_chunk<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize, FormatError> {
1746 loop {
1747 match reader.read(buf) {
1748 Ok(read) => return Ok(read),
1749 Err(err) if err.kind() == ErrorKind::Interrupted => {}
1750 Err(_) => return Err(FormatError::InvalidArchive("archive read failed")),
1751 }
1752 }
1753}
1754
1755fn checked_u64_add(lhs: u64, rhs: u64) -> Result<u64, FormatError> {
1756 lhs.checked_add(rhs)
1757 .ok_or(FormatError::InvalidArchive("stream arithmetic overflow"))
1758}
1759
1760fn checked_usize_add(lhs: usize, rhs: usize) -> Result<usize, FormatError> {
1761 lhs.checked_add(rhs)
1762 .ok_or(FormatError::InvalidArchive("stream arithmetic overflow"))
1763}
1764
1765fn u32_len(value: usize, structure: &'static str) -> Result<u32, FormatError> {
1766 u32::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
1767}