1use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
2use std::fs::File;
3use std::io::{Read, Write};
4use std::sync::Arc;
5use std::thread;
6
7use sha2::{Digest, Sha256};
8
9use crate::compression::{decompress_exact_zstd_frame, validate_exact_zstd_frame};
10use crate::crypto::{
11 decrypt_padded_aead_object, verify_integrity_tag, AeadObjectContext, HmacDomain, KdfParams,
12 MasterKey, Subkeys,
13};
14use crate::fec::{encode_parity_gf16, repair_data_gf16};
15use crate::format::{
16 AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, VolumeFormatRevision,
17 BLOCK_RECORD_FRAMING_LEN, BOOTSTRAP_SIDECAR_HEADER_LEN, CRITICAL_METADATA_IMAGE_FIXED_LEN,
18 CRITICAL_METADATA_RECOVERY_HEADER_LEN, CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN,
19 CRITICAL_RECOVERY_LOCATOR_LEN, CRYPTO_HEADER_HMAC_LEN, IMAGE_CRC_LEN, LOCATOR_PAIR_LEN,
20 MANIFEST_FOOTER_LEN, MASTER_KEY_LEN, READER_MAX_CMRA_PARITY_PCT, READER_MAX_CRYPTO_HEADER_LEN,
21 READER_MAX_KEY_WRAP_TABLE_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN,
22 VOLUME_FORMAT_REV_44, VOLUME_HEADER_LEN, VOLUME_TRAILER_LEN,
23};
24use crate::metadata::{
25 hash_prefix, normalize_lookup_file_path, DirectoryHintShardEntry, DirectoryHintTable,
26 EnvelopeEntry, FileEntry, FrameEntry, IndexRoot, IndexShard, MetadataLimits, ShardEntry,
27};
28use crate::non_seekable_reader::{
29 StreamedEnvelopeSummary, StreamedFrameSummary, StreamedPayloadSummary,
30};
31use crate::raw_stream_profile::reject_unsupported_raw_stream_profile;
32use crate::root_auth::{
33 archive_root_for_revision, critical_metadata_digest, data_block_merkle_root_for_revision,
34 fec_layout_digest_for_revision, index_digest_for_revision,
35 root_auth_descriptor_digest_for_revision, signer_identity_digest, ArchiveRootInputs,
36 CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
37};
38use crate::tar_model::{
39 parse_tar_member_group, restore_streaming_tar_member_group,
40 stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
41 MetadataDiagnostic, NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
42 TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
43 TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
44};
45use crate::wire::{
46 compute_key_wrap_table_digest, BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage,
47 CriticalMetadataRecoveryHeader, CriticalMetadataRecoveryShard, CriticalRecoveryLocator,
48 CryptoHeader, CryptoHeaderFixed, ExtensionTlv, KeyWrapTableV1, ManifestFooter,
49 RootAuthFooterV1, VolumeHeader, VolumeTrailer,
50};
51
52const TRAILER_HMAC_COVERED_LEN: usize = 96;
53const MANIFEST_HMAC_COVERED_LEN: usize = 104;
54const SIDECAR_HMAC_COVERED_LEN: usize = 92;
55const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
56const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
57const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
58const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
59
60fn default_jobs() -> usize {
61 std::thread::available_parallelism()
62 .map(|jobs| jobs.get())
63 .unwrap_or(1)
64}
65
66pub trait ArchiveReadAt: Send + Sync + 'static {
67 fn len(&self) -> Result<u64, FormatError>;
68 fn is_empty(&self) -> Result<bool, FormatError> {
69 Ok(self.len()? == 0)
70 }
71 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
72}
73
74pub type RecipientWrapCandidateMasterKey = [u8; MASTER_KEY_LEN];
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct RecipientWrapArchiveIdentity {
78 pub archive_uuid: [u8; 16],
79 pub session_id: [u8; 16],
80 pub format_version: u16,
81 pub volume_format_rev: u16,
82}
83
84#[derive(Debug, Clone, Copy)]
85pub struct RecipientWrapRecordContext<'a> {
86 pub archive_identity: RecipientWrapArchiveIdentity,
87 pub record: &'a crate::wire::RecipientRecordV1,
88}
89
90impl ArchiveReadAt for File {
91 fn len(&self) -> Result<u64, FormatError> {
92 self.metadata()
93 .map(|metadata| metadata.len())
94 .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
95 }
96
97 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
98 file_read_exact_at(self, offset, buf)
99 }
100}
101
102#[cfg(unix)]
103fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
104 use std::os::unix::fs::FileExt;
105
106 file_read_exact_at_with(offset, buf, |chunk, offset| file.read_at(chunk, offset))
107}
108
109#[cfg(windows)]
110fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
111 use std::os::windows::fs::FileExt;
112
113 file_read_exact_at_with(offset, buf, |chunk, offset| file.seek_read(chunk, offset))
114}
115
116#[cfg(any(unix, windows))]
117fn file_read_exact_at_with<F>(
118 mut offset: u64,
119 mut buf: &mut [u8],
120 mut read_at: F,
121) -> Result<(), FormatError>
122where
123 F: FnMut(&mut [u8], u64) -> std::io::Result<usize>,
124{
125 while !buf.is_empty() {
126 let read =
127 read_at(buf, offset).map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
128 if read == 0 {
129 return Err(FormatError::InvalidArchive("archive read failed"));
130 }
131 offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
132 let rest = std::mem::take(&mut buf).split_at_mut(read).1;
133 buf = rest;
134 }
135 Ok(())
136}
137
138#[cfg(not(any(unix, windows)))]
139fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
140 let mut file = file
141 .try_clone()
142 .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
143 std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
144 .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
145 file.read_exact(buf)
146 .map_err(|_| FormatError::InvalidArchive("archive read failed"))
147}
148
149impl ArchiveReadAt for Vec<u8> {
150 fn len(&self) -> Result<u64, FormatError> {
151 u64::try_from(self.len())
152 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
153 }
154
155 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
156 let offset = to_usize(offset, "archive")?;
157 let end = checked_add(offset, buf.len(), "archive")?;
158 let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
159 structure: "archive",
160 expected: end,
161 actual: self.len(),
162 })?;
163 buf.copy_from_slice(source);
164 Ok(())
165 }
166}
167
168impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
169 fn len(&self) -> Result<u64, FormatError> {
170 (**self).len()
171 }
172
173 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
174 (**self).read_exact_at(offset, buf)
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct ReaderOptions {
180 pub max_trailing_garbage_scan: usize,
181 pub max_verify_tar_size: usize,
182 pub max_total_extraction_size: u64,
183 pub jobs: usize,
184}
185
186impl Default for ReaderOptions {
187 fn default() -> Self {
188 Self {
189 max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
190 max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
191 max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
192 jobs: default_jobs(),
193 }
194 }
195}
196
197pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
198 if options.jobs == 0 {
199 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
200 }
201 Ok(())
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct ArchiveEntry {
206 pub path: String,
207 pub file_data_size: u64,
208 pub kind: TarEntryKind,
209 pub mode: u32,
210 pub mtime: u64,
211 pub diagnostics: Vec<MetadataDiagnostic>,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct ArchiveIndexEntry {
216 pub path: String,
217 pub name: String,
218 pub file_data_size: u64,
219 pub kind: TarEntryKind,
220 pub mode: u32,
221 pub mtime: u64,
222 pub path_hash: [u8; 8],
223 pub tar_member_group_size: u64,
224 pub first_frame_index: u64,
225 pub frame_count: u32,
226 pub offset_in_first_frame_plaintext: u32,
227 pub layout: ArchiveIndexEntryLayout,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct ArchiveIndexEntryLayout {
232 pub compressed_size: u64,
233 pub decompressed_frame_size: u64,
234 pub envelope_count: u32,
235 pub first_envelope_index: Option<u64>,
236 pub last_envelope_index: Option<u64>,
237 pub first_payload_block_index: Option<u64>,
238 pub payload_data_block_count: u64,
239 pub payload_parity_block_count: u64,
240 pub payload_encrypted_size: u64,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct ExtractedArchiveMember {
245 pub path: String,
246 pub kind: TarEntryKind,
247 pub data: Vec<u8>,
248 pub link_target: Option<String>,
249 pub diagnostics: Vec<MetadataDiagnostic>,
250}
251
252pub trait ArchiveExtractProgressSink {
258 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
260}
261
262impl<F> ArchiveExtractProgressSink for F
263where
264 F: FnMut(&str, u64),
265{
266 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
267 self(archive_path, bytes);
268 }
269}
270
271#[derive(Debug, Clone)]
272pub struct OpenedArchive {
273 options: ReaderOptions,
274 observed_archive_bytes: u64,
275 observed_volume_count: u32,
276 subkeys: Subkeys,
277 blocks: BTreeMap<u64, BlockRecord>,
278 lazy_blocks: Option<Arc<SeekableBlockSource>>,
279 crypto_header_bytes: Vec<u8>,
280 pub volume_header: VolumeHeader,
281 pub crypto_header: CryptoHeaderFixed,
282 pub manifest_footer: ManifestFooter,
283 pub volume_trailer: Option<VolumeTrailer>,
284 pub root_auth_footer: Option<RootAuthFooterV1>,
285 pub index_root: IndexRoot,
286 payload_dictionary: Option<Vec<u8>>,
287}
288
289#[derive(Debug)]
290pub struct ArchiveContentVerification<'a> {
291 archive: &'a OpenedArchive,
292 mode: ContentVerificationMode,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296enum ContentVerificationMode {
297 Full,
298 Fast,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct ArchiveRepairPatch {
303 pub volume_index: u32,
304 pub block_index: u64,
305 pub record_offset: u64,
306 pub record_bytes: Vec<u8>,
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum RootAuthDiagnostic {
311 RootAuthContentVerified,
312 RootAuthDeferredFullArchiveScanRequired,
313 AuthenticatedMetadataNotRootSigned,
314 RecoveryMarginNotRootAuthenticated,
315 ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
316 RecoveryMarginChecked,
317 RecoveryMarginFailed,
318 RecoveryMarginUnchecked,
319}
320
321impl RootAuthDiagnostic {
322 pub const fn label(self) -> &'static str {
323 match self {
324 Self::RootAuthContentVerified => "root_auth_content_verified",
325 Self::RootAuthDeferredFullArchiveScanRequired => {
326 "root_auth_deferred_full_archive_scan_required"
327 }
328 Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
329 Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
330 Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
331 "replicated_global_copy_unchecked_due_to_volume_loss"
332 }
333 Self::RecoveryMarginChecked => "recovery_margin_checked",
334 Self::RecoveryMarginFailed => "recovery_margin_failed",
335 Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
336 }
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum PublicNoKeyDiagnostic {
342 PublicDataBlockCommitmentVerified,
343 PublicPhysicalCompletenessUnverified,
344 PublicRecoveryMarginUnchecked,
345}
346
347impl PublicNoKeyDiagnostic {
348 pub const fn label(self) -> &'static str {
349 match self {
350 Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
351 Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
352 Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
353 }
354 }
355}
356
357#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct RootAuthVerification {
359 pub format_version: u16,
360 pub volume_format_rev: u16,
361 pub archive_root: [u8; 32],
362 pub authenticator_id: u16,
363 pub signer_identity_type: u16,
364 pub signer_identity_bytes: Vec<u8>,
365 pub total_data_block_count: u64,
366 pub diagnostics: Vec<RootAuthDiagnostic>,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct PublicNoKeyVerification {
371 pub format_version: u16,
372 pub volume_format_rev: u16,
373 pub archive_root: [u8; 32],
374 pub authenticator_id: u16,
375 pub signer_identity_type: u16,
376 pub signer_identity_bytes: Vec<u8>,
377 pub total_data_block_count: u64,
378 pub diagnostics: Vec<PublicNoKeyDiagnostic>,
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq)]
382struct RootAuthMaterial {
383 critical_metadata_digest: [u8; 32],
384 index_digest: [u8; 32],
385 fec_layout_digest: [u8; 32],
386 data_block_merkle_root: [u8; 32],
387 signer_identity_digest: [u8; 32],
388 archive_root: [u8; 32],
389 total_data_block_count: u64,
390}
391
392#[derive(Debug, Clone, Copy)]
393struct ObjectExtent {
394 first_block_index: u64,
395 data_block_count: u32,
396 parity_block_count: u32,
397 encrypted_size: u32,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401enum ParityReadPolicy {
402 Always,
403 RepairOnly,
404}
405
406pub(crate) struct StreamedArchiveOpenParts {
407 pub(crate) options: ReaderOptions,
408 pub(crate) observed_archive_bytes: u64,
409 pub(crate) subkeys: Subkeys,
410 pub(crate) blocks: BTreeMap<u64, BlockRecord>,
411 pub(crate) crypto_header_bytes: Vec<u8>,
412 pub(crate) volume_header: VolumeHeader,
413 pub(crate) crypto_header: CryptoHeaderFixed,
414 pub(crate) manifest_footer: ManifestFooter,
415 pub(crate) volume_trailer: VolumeTrailer,
416 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
417}
418
419#[derive(Clone, Copy)]
420struct WinningIndexEntry {
421 start: u64,
422 file_data_size: u64,
423 mtime: u64,
424 shard_index: usize,
425 file_index: usize,
426}
427
428struct LocatedIndexFile {
429 shard: IndexShard,
430 file_index: usize,
431 start: u64,
432}
433
434struct ExtractProgressWriter<'a, W> {
435 inner: &'a mut W,
436 archive_path: &'a str,
437 file_data_size: u64,
438 reported_bytes: u64,
439 progress: &'a mut dyn ArchiveExtractProgressSink,
440}
441
442impl<'a, W> ExtractProgressWriter<'a, W> {
443 fn new(
444 inner: &'a mut W,
445 archive_path: &'a str,
446 file_data_size: u64,
447 progress: &'a mut dyn ArchiveExtractProgressSink,
448 ) -> Self {
449 Self {
450 inner,
451 archive_path,
452 file_data_size,
453 reported_bytes: 0,
454 progress,
455 }
456 }
457
458 fn report(&mut self, bytes: u64) {
459 if bytes == 0 || self.file_data_size == 0 {
460 return;
461 }
462 let capped_next = self
463 .reported_bytes
464 .saturating_add(bytes)
465 .min(self.file_data_size);
466 let delta = capped_next.saturating_sub(self.reported_bytes);
467 if delta == 0 {
468 return;
469 }
470 self.reported_bytes = capped_next;
471 self.progress.file_bytes_extracted(self.archive_path, delta);
472 }
473}
474
475impl<W: Write> Write for ExtractProgressWriter<'_, W> {
476 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
477 let written = self.inner.write(buf)?;
478 self.report(written as u64);
479 Ok(written)
480 }
481
482 fn flush(&mut self) -> std::io::Result<()> {
483 self.inner.flush()
484 }
485}
486
487struct DecodedTarMemberGroupReader<'a> {
488 archive: &'a OpenedArchive,
489 shard: &'a IndexShard,
490 file: &'a FileEntry,
491 decompressor: zstd::bulk::Decompressor<'static>,
492 next_frame_offset: u64,
493 cached_envelope_index: Option<u64>,
494 cached_envelope_plaintext: Vec<u8>,
495 current_frame: Vec<u8>,
496 current_frame_offset: usize,
497 remaining_group_bytes: u64,
498}
499
500struct SeekableVolumeSource {
501 reader: Arc<dyn ArchiveReadAt>,
502 volume_index: u32,
503 block_records_start: u64,
504 block_count: u64,
505 record_len: u64,
506 block_size: usize,
507}
508
509impl std::fmt::Debug for SeekableVolumeSource {
510 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511 f.debug_struct("SeekableVolumeSource")
512 .field("volume_index", &self.volume_index)
513 .field("block_records_start", &self.block_records_start)
514 .field("block_count", &self.block_count)
515 .field("record_len", &self.record_len)
516 .field("block_size", &self.block_size)
517 .finish_non_exhaustive()
518 }
519}
520
521#[derive(Debug)]
522struct SeekableBlockSource {
523 stripe_width: u32,
524 volumes: Vec<Option<SeekableVolumeSource>>,
525}
526
527trait BlockProvider {
528 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
529}
530
531struct OpenedBlockProvider<'a> {
532 memory_blocks: &'a BTreeMap<u64, BlockRecord>,
533 lazy_blocks: Option<&'a SeekableBlockSource>,
534}
535
536impl SeekableBlockSource {
537 fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
538 if self.stripe_width == 0 {
539 return Err(FormatError::ZeroStripeWidth);
540 }
541 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
542 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
543 let Some(volume) = self
544 .volumes
545 .get(volume_index as usize)
546 .and_then(Option::as_ref)
547 else {
548 return Err(FormatError::InvalidArchive(
549 "repair output requires all archive volumes",
550 ));
551 };
552 let slot = block_index / self.stripe_width as u64;
553 if slot >= volume.block_count {
554 return Err(FormatError::InvalidArchive(
555 "BlockRecord global coverage has a gap",
556 ));
557 }
558 Ok((volume_index, volume.record_offset(slot)?))
559 }
560
561 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
562 if self.stripe_width == 0 {
563 return Err(FormatError::ZeroStripeWidth);
564 }
565 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
566 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
567 let Some(volume) = self
568 .volumes
569 .get(volume_index as usize)
570 .and_then(Option::as_ref)
571 else {
572 return Ok(None);
573 };
574 let slot = block_index / self.stripe_width as u64;
575 if slot >= volume.block_count {
576 return Ok(None);
577 }
578 match volume.read_slot(slot, block_index) {
579 Ok(record) => Ok(Some(record)),
580 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
581 Err(err) => Err(err),
582 }
583 }
584
585 fn is_complete_volume_set(&self) -> bool {
586 self.volumes.iter().all(Option::is_some)
587 }
588
589 fn total_block_count(&self) -> Result<u64, FormatError> {
590 self.volumes
591 .iter()
592 .map(|volume| {
593 volume
594 .as_ref()
595 .map(|volume| volume.block_count)
596 .ok_or(FormatError::InvalidArchive(
597 "missing volume in complete set",
598 ))
599 })
600 .try_fold(0u64, |sum, count| {
601 checked_u64_add(sum, count?, "BlockRecord count overflow")
602 })
603 }
604}
605
606impl SeekableVolumeSource {
607 fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
608 self.block_records_start
609 .checked_add(checked_u64_mul(
610 slot,
611 self.record_len,
612 "BlockRecord offset overflow",
613 )?)
614 .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
615 }
616
617 fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
618 let record_offset = self.record_offset(slot)?;
619 let raw = read_at_vec_unchecked(
620 self.reader.as_ref(),
621 record_offset,
622 usize::try_from(self.record_len)
623 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
624 )?;
625 let record = BlockRecord::parse(&raw, self.block_size)?;
626 if record.block_index != expected_block_index {
627 return Err(FormatError::InvalidArchive(
628 "BlockRecord index does not match volume position",
629 ));
630 }
631 Ok(record)
632 }
633}
634
635impl BlockProvider for BTreeMap<u64, BlockRecord> {
636 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
637 Ok(self.get(&block_index).cloned())
638 }
639}
640
641impl BlockProvider for OpenedBlockProvider<'_> {
642 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
643 if let Some(record) = self.memory_blocks.get(&block_index) {
644 return Ok(Some(record.clone()));
645 }
646 match self.lazy_blocks {
647 Some(source) => source.block(block_index),
648 None => Ok(None),
649 }
650 }
651}
652
653fn subkeys_for_open(
654 master_key: Option<&MasterKey>,
655 aead_algo: AeadAlgo,
656 archive_uuid: &[u8; 16],
657 session_id: &[u8; 16],
658) -> Result<Subkeys, FormatError> {
659 if aead_algo.is_encrypted() {
660 Subkeys::derive(
661 master_key.ok_or(FormatError::KeyMaterialMismatch)?,
662 archive_uuid,
663 session_id,
664 )
665 } else {
666 Ok(Subkeys::unencrypted_placeholder())
667 }
668}
669
670type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
671pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
672const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
673 "fast full extract requires unique archive paths";
674
675fn parse_volume_format_dispatch(
676 volume_header: &VolumeHeader,
677) -> Result<VolumeFormatRevision, FormatError> {
678 let revision = volume_header.parse_volume_format_revision()?;
679 match revision {
680 VolumeFormatRevision::V44 => Ok(revision),
681 }
682}
683
684#[derive(Debug)]
685struct PayloadIndexTables {
686 shards: Vec<IndexShard>,
687 file_count: u64,
688 frames: BTreeMap<u64, FrameEntry>,
689 envelopes: BTreeMap<u64, EnvelopeEntry>,
690}
691
692pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
693 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
694}
695
696pub fn open_archive_with_recipient_wrap_resolver<F>(
697 bytes: &[u8],
698 resolver: F,
699) -> Result<OpenedArchive, FormatError>
700where
701 F: FnMut(
702 RecipientWrapRecordContext<'_>,
703 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
704{
705 OpenedArchive::open_with_recipient_wrap_resolver_options(
706 bytes,
707 resolver,
708 ReaderOptions::default(),
709 )
710}
711
712pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
713 require_unencrypted_volume_profile(bytes)?;
714 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
715 OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
716}
717
718pub fn open_archive_volumes(
719 volumes: &[&[u8]],
720 master_key: &MasterKey,
721) -> Result<OpenedArchive, FormatError> {
722 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
723}
724
725pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
726 for volume in volumes {
727 require_unencrypted_volume_profile(volume)?;
728 }
729 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
730 OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
731}
732
733pub fn open_archive_with_bootstrap_sidecar(
734 bytes: &[u8],
735 bootstrap_sidecar: &[u8],
736 master_key: &MasterKey,
737) -> Result<OpenedArchive, FormatError> {
738 OpenedArchive::open_with_bootstrap_sidecar_options(
739 bytes,
740 bootstrap_sidecar,
741 master_key,
742 ReaderOptions::default(),
743 )
744}
745
746fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
747 if bytes.len() < VOLUME_HEADER_LEN {
748 return Err(FormatError::InvalidLength {
749 structure: "archive",
750 expected: VOLUME_HEADER_LEN,
751 actual: bytes.len(),
752 });
753 }
754 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
755 parse_volume_format_dispatch(&volume_header)?;
756 let crypto_start = volume_header.crypto_header_offset as usize;
757 let crypto_len = volume_header.crypto_header_length as usize;
758 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
759 let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
760 if crypto_header.fixed.aead_algo == AeadAlgo::None
761 && crypto_header.fixed.kdf_algo == KdfAlgo::None
762 {
763 Ok(())
764 } else {
765 Err(FormatError::KeyMaterialMismatch)
766 }
767}
768
769pub fn open_seekable_archive<R: ArchiveReadAt>(
770 reader: R,
771 master_key: &MasterKey,
772) -> Result<OpenedArchive, FormatError> {
773 OpenedArchive::open_seekable_volumes_with_options(
774 vec![reader],
775 master_key,
776 ReaderOptions::default(),
777 )
778}
779
780pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
781 readers: Vec<R>,
782 master_key: &MasterKey,
783) -> Result<OpenedArchive, FormatError> {
784 OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
785}
786
787pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
788 reader: R,
789 bootstrap_sidecar: &[u8],
790 master_key: &MasterKey,
791) -> Result<OpenedArchive, FormatError> {
792 open_seekable_archive_with_bootstrap_sidecar_options(
793 reader,
794 bootstrap_sidecar,
795 master_key,
796 ReaderOptions::default(),
797 )
798}
799
800pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
801 reader: R,
802 bootstrap_sidecar: &[u8],
803 master_key: &MasterKey,
804 options: ReaderOptions,
805) -> Result<OpenedArchive, FormatError> {
806 OpenedArchive::open_seekable_volumes_with_options_for_mode(
807 vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
808 master_key,
809 options,
810 Some(bootstrap_sidecar),
811 )
812}
813
814pub fn open_seekable_archive_with_recipient_wrap_resolver_options<R, F>(
815 reader: R,
816 resolver: F,
817 options: ReaderOptions,
818) -> Result<OpenedArchive, FormatError>
819where
820 R: ArchiveReadAt,
821 F: FnMut(
822 RecipientWrapRecordContext<'_>,
823 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
824{
825 OpenedArchive::open_seekable_with_recipient_wrap_resolver_options(reader, resolver, options)
826}
827
828pub fn open_seekable_archive_volumes_with_recipient_wrap_resolver_options<R, F>(
829 readers: Vec<R>,
830 resolver: F,
831 options: ReaderOptions,
832) -> Result<OpenedArchive, FormatError>
833where
834 R: ArchiveReadAt,
835 F: FnMut(
836 RecipientWrapRecordContext<'_>,
837 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
838{
839 OpenedArchive::open_seekable_volumes_with_recipient_wrap_resolver_options(
840 readers, resolver, options,
841 )
842}
843
844pub fn open_non_seekable_archive(
845 bytes: &[u8],
846 master_key: &MasterKey,
847 bootstrap_sidecar: Option<&[u8]>,
848) -> Result<OpenedArchive, FormatError> {
849 match bootstrap_sidecar {
850 Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
851 bytes,
852 sidecar,
853 master_key,
854 ReaderOptions::default(),
855 BootstrapSidecarUse::NonSeekableRandomAccess,
856 ),
857 None => Err(FormatError::ReaderUnsupported(
858 "non-seekable random access requires a bootstrap sidecar",
859 )),
860 }
861}
862
863pub fn public_no_key_verify_archive_with<F>(
864 bytes: &[u8],
865 verifier: F,
866) -> Result<PublicNoKeyVerification, FormatError>
867where
868 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
869{
870 public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
871}
872
873pub fn public_no_key_verify_volumes_with<F>(
874 volumes: &[&[u8]],
875 verifier: F,
876) -> Result<PublicNoKeyVerification, FormatError>
877where
878 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
879{
880 public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
881}
882
883pub fn sequential_extract_tar_stream(
889 bytes: &[u8],
890 master_key: &MasterKey,
891) -> Result<Vec<u8>, FormatError> {
892 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
893}
894
895impl OpenedArchive {
896 fn block_provider(&self) -> OpenedBlockProvider<'_> {
897 OpenedBlockProvider {
898 memory_blocks: &self.blocks,
899 lazy_blocks: self.lazy_blocks.as_deref(),
900 }
901 }
902
903 fn missing_volume_count(&self) -> u32 {
904 self.crypto_header
905 .stripe_width
906 .saturating_sub(self.observed_volume_count)
907 }
908
909 fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
910 let mut diagnostics = vec![
911 RootAuthDiagnostic::RootAuthContentVerified,
912 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
913 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
914 ];
915 if self.missing_volume_count() > 0 {
916 diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
917 }
918 diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
919 diagnostics
920 }
921
922 pub fn open_with_options(
923 bytes: &[u8],
924 master_key: &MasterKey,
925 options: ReaderOptions,
926 ) -> Result<Self, FormatError> {
927 Self::open_volumes_with_options(&[bytes], master_key, options)
928 }
929
930 pub fn open_with_recipient_wrap_resolver_options<F>(
931 bytes: &[u8],
932 mut resolver: F,
933 options: ReaderOptions,
934 ) -> Result<Self, FormatError>
935 where
936 F: FnMut(
937 RecipientWrapRecordContext<'_>,
938 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
939 {
940 validate_reader_options(options)?;
941 let observed_archive_bytes = observed_archive_size(std::iter::once(bytes.len() as u64))?;
942 let parsed =
943 parse_seekable_volume_with_recipient_wrap_resolver(bytes, &mut resolver, options)?;
944 let ParsedSeekableVolume {
945 volume_header,
946 crypto_header,
947 crypto_header_bytes,
948 key_wrap_table_bytes: _,
949 subkeys,
950 manifest_footer,
951 manifest_footer_error,
952 root_auth_footer,
953 root_auth_footer_bytes: _,
954 volume_trailer,
955 blocks,
956 erased_block_indices,
957 } = parsed;
958 let manifest_footer = match manifest_footer {
959 Some(footer) => footer,
960 None => {
961 return Err(manifest_footer_error.unwrap_or(FormatError::InvalidArchive(
962 "no authenticated ManifestFooter found",
963 )));
964 }
965 };
966 let observed_volume_count = 1;
967 let missing_volume_count = crypto_header
968 .stripe_width
969 .checked_sub(observed_volume_count)
970 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
971 if missing_volume_count > crypto_header.volume_loss_tolerance as u32 {
972 return Err(FormatError::InvalidArchive(
973 "missing volume count exceeds volume_loss_tolerance",
974 ));
975 }
976 if missing_volume_count == 0 {
977 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
978 }
979
980 let limits = metadata_limits(&crypto_header);
981 let index_root_plaintext = load_metadata_object_from_parts(
982 &blocks,
983 ObjectLoadContext::index_root(
984 &volume_header,
985 &crypto_header,
986 &subkeys,
987 ObjectExtent {
988 first_block_index: manifest_footer.index_root_first_block,
989 data_block_count: manifest_footer.index_root_data_block_count,
990 parity_block_count: manifest_footer.index_root_parity_block_count,
991 encrypted_size: manifest_footer.index_root_encrypted_size,
992 },
993 ),
994 manifest_footer.index_root_decompressed_size,
995 )?;
996 let index_root = IndexRoot::parse(
997 &index_root_plaintext,
998 crypto_header.has_dictionary != 0,
999 limits,
1000 )?;
1001 let payload_dictionary = load_archive_dictionary(
1002 &blocks,
1003 &subkeys,
1004 &volume_header,
1005 &crypto_header,
1006 &index_root,
1007 )?;
1008
1009 Ok(Self {
1010 options,
1011 observed_archive_bytes,
1012 observed_volume_count,
1013 subkeys,
1014 blocks,
1015 lazy_blocks: None,
1016 crypto_header_bytes,
1017 volume_header,
1018 crypto_header,
1019 manifest_footer,
1020 volume_trailer: Some(volume_trailer),
1021 root_auth_footer,
1022 index_root,
1023 payload_dictionary,
1024 })
1025 }
1026
1027 pub fn open_seekable_with_recipient_wrap_resolver_options<R, F>(
1028 reader: R,
1029 mut resolver: F,
1030 options: ReaderOptions,
1031 ) -> Result<Self, FormatError>
1032 where
1033 R: ArchiveReadAt,
1034 F: FnMut(
1035 RecipientWrapRecordContext<'_>,
1036 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1037 {
1038 validate_reader_options(options)?;
1039 let reader = Arc::new(reader) as Arc<dyn ArchiveReadAt>;
1040 let observed_len = reader.len()?;
1041 let observed_archive_bytes = observed_archive_size([observed_len])?;
1042 let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1043 reader.clone(),
1044 &mut resolver,
1045 options,
1046 )?;
1047 let manifest_footer = match parsed.manifest_footer.take() {
1048 Some(footer) => footer,
1049 None => {
1050 return Err(parsed.manifest_footer_error.take().unwrap_or(
1051 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1052 ));
1053 }
1054 };
1055 let observed_volume_count = 1;
1056 let missing_volume_count = parsed
1057 .crypto_header
1058 .stripe_width
1059 .checked_sub(observed_volume_count)
1060 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1061 if missing_volume_count > parsed.crypto_header.volume_loss_tolerance as u32 {
1062 return Err(FormatError::InvalidArchive(
1063 "missing volume count exceeds volume_loss_tolerance",
1064 ));
1065 }
1066
1067 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1068 let mut lazy_volume_slots = Vec::new();
1069 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1070 let slot = parsed.volume_header.volume_index as usize;
1071 if slot >= lazy_volume_slots.len() {
1072 return Err(FormatError::InvalidArchive(
1073 "authenticated volume index exceeds stripe_width",
1074 ));
1075 }
1076 lazy_volume_slots[slot] = Some(SeekableVolumeSource {
1077 reader: parsed.reader.clone(),
1078 volume_index: parsed.volume_header.volume_index,
1079 block_records_start: parsed.block_records_start,
1080 block_count: parsed.volume_trailer.block_count,
1081 record_len,
1082 block_size: parsed.crypto_header.block_size as usize,
1083 });
1084 let lazy_source = Arc::new(SeekableBlockSource {
1085 stripe_width: parsed.crypto_header.stripe_width,
1086 volumes: lazy_volume_slots,
1087 });
1088 let blocks = BTreeMap::new();
1089 let block_provider = OpenedBlockProvider {
1090 memory_blocks: &blocks,
1091 lazy_blocks: Some(lazy_source.as_ref()),
1092 };
1093 let limits = metadata_limits(&parsed.crypto_header);
1094 let index_root_plaintext = load_metadata_object_from_parts(
1095 &block_provider,
1096 ObjectLoadContext::index_root(
1097 &parsed.volume_header,
1098 &parsed.crypto_header,
1099 &parsed.subkeys,
1100 index_root_extent_from_manifest(&manifest_footer),
1101 ),
1102 manifest_footer.index_root_decompressed_size,
1103 )?;
1104 let index_root = IndexRoot::parse(
1105 &index_root_plaintext,
1106 parsed.crypto_header.has_dictionary != 0,
1107 limits,
1108 )?;
1109 let block_provider = OpenedBlockProvider {
1110 memory_blocks: &blocks,
1111 lazy_blocks: Some(lazy_source.as_ref()),
1112 };
1113 let payload_dictionary = load_archive_dictionary(
1114 &block_provider,
1115 &parsed.subkeys,
1116 &parsed.volume_header,
1117 &parsed.crypto_header,
1118 &index_root,
1119 )?;
1120
1121 Ok(Self {
1122 options,
1123 observed_archive_bytes,
1124 observed_volume_count,
1125 subkeys: parsed.subkeys,
1126 blocks,
1127 lazy_blocks: Some(lazy_source),
1128 crypto_header_bytes: parsed.crypto_header_bytes,
1129 volume_header: parsed.volume_header,
1130 crypto_header: parsed.crypto_header,
1131 manifest_footer,
1132 volume_trailer: Some(parsed.volume_trailer),
1133 root_auth_footer: parsed.root_auth_footer,
1134 index_root,
1135 payload_dictionary,
1136 })
1137 }
1138
1139 pub fn open_seekable_volumes_with_recipient_wrap_resolver_options<R, F>(
1140 readers: Vec<R>,
1141 mut resolver: F,
1142 options: ReaderOptions,
1143 ) -> Result<Self, FormatError>
1144 where
1145 R: ArchiveReadAt,
1146 F: FnMut(
1147 RecipientWrapRecordContext<'_>,
1148 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
1149 {
1150 validate_reader_options(options)?;
1151 if readers.is_empty() {
1152 return Err(FormatError::InvalidArchive("no volumes supplied"));
1153 }
1154 let readers = readers
1155 .into_iter()
1156 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1157 .collect::<Vec<_>>();
1158 let observed_archive_bytes = observed_archive_size(
1159 readers
1160 .iter()
1161 .map(|reader| reader.len())
1162 .collect::<Result<Vec<_>, _>>()?,
1163 )?;
1164 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1165 let mut manifest_authority: Option<ManifestFooter> = None;
1166 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1167 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1168 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1169 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1170 let mut saw_root_auth_absent = false;
1171 let mut first_manifest_footer_error: Option<FormatError> = None;
1172 let mut seen_volume_indexes = BTreeSet::new();
1173 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1174
1175 for reader in readers {
1176 let mut parsed = parse_seekable_read_at_volume_with_recipient_wrap_resolver(
1177 reader,
1178 &mut resolver,
1179 options,
1180 )?;
1181 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1182 return Err(FormatError::InvalidArchive(
1183 "duplicate authenticated volume index",
1184 ));
1185 }
1186
1187 if let Some(first) = &first {
1188 validate_volume_set_member_metadata(
1189 &first.volume_header,
1190 &first.crypto_header,
1191 &first.crypto_header_bytes,
1192 &parsed.volume_header,
1193 &parsed.crypto_header,
1194 &parsed.crypto_header_bytes,
1195 )?;
1196 validate_key_wrap_table_bytes_match(
1197 &first.key_wrap_table_bytes,
1198 &parsed.key_wrap_table_bytes,
1199 )?;
1200 } else {
1201 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1202 }
1203
1204 if let Some(footer) = &parsed.manifest_footer {
1205 if let Some(authority) = &manifest_authority {
1206 if !manifest_bootstrap_fields_match(authority, footer) {
1207 return Err(FormatError::InvalidArchive(
1208 "ManifestFooter bootstrap fields differ",
1209 ));
1210 }
1211 } else {
1212 manifest_authority = Some(footer.clone());
1213 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1214 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1215 }
1216 } else if first_manifest_footer_error.is_none() {
1217 first_manifest_footer_error = parsed.manifest_footer_error.take();
1218 }
1219
1220 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1221 (Some(footer), Some(bytes)) => {
1222 if saw_root_auth_absent {
1223 return Err(FormatError::InvalidArchive(
1224 "root-auth footer presence differs across volumes",
1225 ));
1226 }
1227 if let Some(authority_bytes) = &root_auth_authority_bytes {
1228 if authority_bytes != bytes {
1229 return Err(FormatError::InvalidArchive(
1230 "RootAuthFooter copies differ",
1231 ));
1232 }
1233 } else {
1234 root_auth_authority = Some(footer.clone());
1235 root_auth_authority_bytes = Some(bytes.clone());
1236 }
1237 }
1238 (None, None) => {
1239 if root_auth_authority_bytes.is_some() {
1240 return Err(FormatError::InvalidArchive(
1241 "root-auth footer presence differs across volumes",
1242 ));
1243 }
1244 saw_root_auth_absent = true;
1245 }
1246 _ => {
1247 return Err(FormatError::InvalidArchive(
1248 "root-auth footer terminal state is inconsistent",
1249 ));
1250 }
1251 }
1252
1253 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1254 let source = SeekableVolumeSource {
1255 reader: parsed.reader.clone(),
1256 volume_index: parsed.volume_header.volume_index,
1257 block_records_start: parsed.block_records_start,
1258 block_count: parsed.volume_trailer.block_count,
1259 record_len,
1260 block_size: parsed.crypto_header.block_size as usize,
1261 };
1262 let slot = parsed.volume_header.volume_index as usize;
1263 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1264 {
1265 return Err(FormatError::InvalidArchive(
1266 "duplicate authenticated volume index",
1267 ));
1268 }
1269
1270 if first.is_none() {
1271 first = Some(parsed);
1272 }
1273 }
1274
1275 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1276 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1277 Some(err) => err,
1278 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1279 })?;
1280 let authority_volume_header = manifest_authority_volume_header.ok_or(
1281 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1282 )?;
1283 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1284 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1285 )?;
1286 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1287 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1288 let missing_volume_count = first
1289 .crypto_header
1290 .stripe_width
1291 .checked_sub(observed_volume_count)
1292 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1293 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1294 return Err(FormatError::InvalidArchive(
1295 "missing volume count exceeds volume_loss_tolerance",
1296 ));
1297 }
1298
1299 let blocks = BTreeMap::new();
1300 let lazy_source = Arc::new(SeekableBlockSource {
1301 stripe_width: first.crypto_header.stripe_width,
1302 volumes: lazy_volume_slots,
1303 });
1304 let block_provider = OpenedBlockProvider {
1305 memory_blocks: &blocks,
1306 lazy_blocks: Some(lazy_source.as_ref()),
1307 };
1308 let limits = metadata_limits(&first.crypto_header);
1309 let index_root_plaintext = load_metadata_object_from_parts(
1310 &block_provider,
1311 ObjectLoadContext::index_root(
1312 &first.volume_header,
1313 &first.crypto_header,
1314 &first.subkeys,
1315 index_root_extent_from_manifest(&manifest_footer),
1316 ),
1317 manifest_footer.index_root_decompressed_size,
1318 )?;
1319 let index_root = IndexRoot::parse(
1320 &index_root_plaintext,
1321 first.crypto_header.has_dictionary != 0,
1322 limits,
1323 )?;
1324 let block_provider = OpenedBlockProvider {
1325 memory_blocks: &blocks,
1326 lazy_blocks: Some(lazy_source.as_ref()),
1327 };
1328 let payload_dictionary = load_archive_dictionary(
1329 &block_provider,
1330 &first.subkeys,
1331 &first.volume_header,
1332 &first.crypto_header,
1333 &index_root,
1334 )?;
1335
1336 Ok(Self {
1337 options,
1338 observed_archive_bytes,
1339 observed_volume_count,
1340 subkeys: first.subkeys,
1341 blocks,
1342 lazy_blocks: Some(lazy_source),
1343 crypto_header_bytes: first.crypto_header_bytes,
1344 volume_header: authority_volume_header,
1345 crypto_header: first.crypto_header,
1346 manifest_footer,
1347 volume_trailer: Some(authority_volume_trailer),
1348 root_auth_footer: root_auth_authority,
1349 index_root,
1350 payload_dictionary,
1351 })
1352 }
1353
1354 pub fn open_volumes_with_options(
1355 volumes: &[&[u8]],
1356 master_key: &MasterKey,
1357 options: ReaderOptions,
1358 ) -> Result<Self, FormatError> {
1359 validate_reader_options(options)?;
1360 if volumes.is_empty() {
1361 return Err(FormatError::InvalidArchive("no volumes supplied"));
1362 }
1363
1364 let observed_archive_bytes =
1365 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
1366 let mut first: Option<ParsedSeekableVolume> = None;
1367 let mut manifest_authority: Option<ManifestFooter> = None;
1368 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1369 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1370 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1371 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1372 let mut saw_root_auth_absent = false;
1373 let mut first_manifest_footer_error: Option<FormatError> = None;
1374 let mut seen_volume_indexes = BTreeSet::new();
1375 let mut blocks = BTreeMap::new();
1376 let mut erased_block_indices = BTreeSet::new();
1377
1378 for volume_bytes in volumes {
1379 let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
1380 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1381 return Err(FormatError::InvalidArchive(
1382 "duplicate authenticated volume index",
1383 ));
1384 }
1385
1386 if let Some(first) = &first {
1387 validate_volume_set_member(first, &parsed)?;
1388 }
1389
1390 if let Some(footer) = &parsed.manifest_footer {
1391 if let Some(authority) = &manifest_authority {
1392 if !manifest_bootstrap_fields_match(authority, footer) {
1393 return Err(FormatError::InvalidArchive(
1394 "ManifestFooter bootstrap fields differ",
1395 ));
1396 }
1397 } else {
1398 manifest_authority = Some(footer.clone());
1399 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1400 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1401 }
1402 } else if first_manifest_footer_error.is_none() {
1403 first_manifest_footer_error = parsed.manifest_footer_error.take();
1404 }
1405
1406 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1407 (Some(footer), Some(bytes)) => {
1408 if saw_root_auth_absent {
1409 return Err(FormatError::InvalidArchive(
1410 "root-auth footer presence differs across volumes",
1411 ));
1412 }
1413 if let Some(authority_bytes) = &root_auth_authority_bytes {
1414 if authority_bytes != bytes {
1415 return Err(FormatError::InvalidArchive(
1416 "RootAuthFooter copies differ",
1417 ));
1418 }
1419 } else {
1420 root_auth_authority = Some(footer.clone());
1421 root_auth_authority_bytes = Some(bytes.clone());
1422 }
1423 }
1424 (None, None) => {
1425 if root_auth_authority_bytes.is_some() {
1426 return Err(FormatError::InvalidArchive(
1427 "root-auth footer presence differs across volumes",
1428 ));
1429 }
1430 saw_root_auth_absent = true;
1431 }
1432 _ => {
1433 return Err(FormatError::InvalidArchive(
1434 "root-auth footer terminal state is inconsistent",
1435 ));
1436 }
1437 }
1438
1439 for (block_index, record) in &parsed.blocks {
1440 if blocks.insert(*block_index, record.clone()).is_some() {
1441 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
1442 }
1443 }
1444 for block_index in &parsed.erased_block_indices {
1445 erased_block_indices.insert(*block_index);
1446 }
1447
1448 if first.is_none() {
1449 first = Some(parsed);
1450 }
1451 }
1452
1453 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1454 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1455 Some(err) => err,
1456 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1457 })?;
1458 let authority_volume_header = manifest_authority_volume_header.ok_or(
1459 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1460 )?;
1461 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1462 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1463 )?;
1464 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1465 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1466 let missing_volume_count = first
1467 .crypto_header
1468 .stripe_width
1469 .checked_sub(observed_volume_count)
1470 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1471 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1472 return Err(FormatError::InvalidArchive(
1473 "missing volume count exceeds volume_loss_tolerance",
1474 ));
1475 }
1476 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
1477 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
1478 }
1479
1480 let limits = metadata_limits(&first.crypto_header);
1481 let index_root_plaintext = load_metadata_object_from_parts(
1482 &blocks,
1483 ObjectLoadContext::index_root(
1484 &first.volume_header,
1485 &first.crypto_header,
1486 &first.subkeys,
1487 ObjectExtent {
1488 first_block_index: manifest_footer.index_root_first_block,
1489 data_block_count: manifest_footer.index_root_data_block_count,
1490 parity_block_count: manifest_footer.index_root_parity_block_count,
1491 encrypted_size: manifest_footer.index_root_encrypted_size,
1492 },
1493 ),
1494 manifest_footer.index_root_decompressed_size,
1495 )?;
1496 let index_root = IndexRoot::parse(
1497 &index_root_plaintext,
1498 first.crypto_header.has_dictionary != 0,
1499 limits,
1500 )?;
1501 let payload_dictionary = load_archive_dictionary(
1502 &blocks,
1503 &first.subkeys,
1504 &first.volume_header,
1505 &first.crypto_header,
1506 &index_root,
1507 )?;
1508
1509 Ok(Self {
1510 options,
1511 observed_archive_bytes,
1512 observed_volume_count,
1513 subkeys: first.subkeys,
1514 blocks,
1515 lazy_blocks: None,
1516 crypto_header_bytes: first.crypto_header_bytes,
1517 volume_header: authority_volume_header,
1518 crypto_header: first.crypto_header,
1519 manifest_footer,
1520 volume_trailer: Some(authority_volume_trailer),
1521 root_auth_footer: root_auth_authority,
1522 index_root,
1523 payload_dictionary,
1524 })
1525 }
1526
1527 pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
1528 readers: Vec<R>,
1529 master_key: &MasterKey,
1530 options: ReaderOptions,
1531 ) -> Result<Self, FormatError> {
1532 let readers = readers
1533 .into_iter()
1534 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1535 .collect::<Vec<_>>();
1536 Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1537 }
1538
1539 fn open_seekable_volumes_with_options_for_mode(
1540 readers: Vec<Arc<dyn ArchiveReadAt>>,
1541 master_key: &MasterKey,
1542 options: ReaderOptions,
1543 bootstrap_sidecar: Option<&[u8]>,
1544 ) -> Result<Self, FormatError> {
1545 validate_reader_options(options)?;
1546 if readers.is_empty() {
1547 return Err(FormatError::InvalidArchive("no volumes supplied"));
1548 }
1549 if bootstrap_sidecar.is_some() && readers.len() > 1 {
1550 return Err(FormatError::ReaderUnsupported(
1551 "multi-volume inputs with bootstrap sidecar are not supported",
1552 ));
1553 }
1554
1555 let observed_archive_bytes = observed_archive_size(
1556 readers
1557 .iter()
1558 .map(|reader| reader.len())
1559 .collect::<Result<Vec<_>, _>>()?
1560 .into_iter()
1561 .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1562 )?;
1563 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1564 let mut manifest_authority: Option<ManifestFooter> = None;
1565 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1566 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1567 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1568 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1569 let mut saw_root_auth_absent = false;
1570 let mut first_manifest_footer_error: Option<FormatError> = None;
1571 let mut seen_volume_indexes = BTreeSet::new();
1572 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1573
1574 for reader in readers {
1575 let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1576 if bootstrap_sidecar.is_some() {
1577 validate_bootstrap_single_volume_input(
1578 &parsed.volume_header,
1579 &parsed.crypto_header,
1580 )?;
1581 }
1582 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1583 return Err(FormatError::InvalidArchive(
1584 "duplicate authenticated volume index",
1585 ));
1586 }
1587
1588 if let Some(first) = &first {
1589 validate_volume_set_member_metadata(
1590 &first.volume_header,
1591 &first.crypto_header,
1592 &first.crypto_header_bytes,
1593 &parsed.volume_header,
1594 &parsed.crypto_header,
1595 &parsed.crypto_header_bytes,
1596 )?;
1597 validate_key_wrap_table_bytes_match(
1598 &first.key_wrap_table_bytes,
1599 &parsed.key_wrap_table_bytes,
1600 )?;
1601 } else {
1602 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1603 }
1604
1605 if let Some(footer) = &parsed.manifest_footer {
1606 if let Some(authority) = &manifest_authority {
1607 if !manifest_bootstrap_fields_match(authority, footer) {
1608 return Err(FormatError::InvalidArchive(
1609 "ManifestFooter bootstrap fields differ",
1610 ));
1611 }
1612 } else {
1613 manifest_authority = Some(footer.clone());
1614 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1615 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1616 }
1617 } else if first_manifest_footer_error.is_none() {
1618 first_manifest_footer_error = parsed.manifest_footer_error.take();
1619 }
1620
1621 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1622 (Some(footer), Some(bytes)) => {
1623 if saw_root_auth_absent {
1624 return Err(FormatError::InvalidArchive(
1625 "root-auth footer presence differs across volumes",
1626 ));
1627 }
1628 if let Some(authority_bytes) = &root_auth_authority_bytes {
1629 if authority_bytes != bytes {
1630 return Err(FormatError::InvalidArchive(
1631 "RootAuthFooter copies differ",
1632 ));
1633 }
1634 } else {
1635 root_auth_authority = Some(footer.clone());
1636 root_auth_authority_bytes = Some(bytes.clone());
1637 }
1638 }
1639 (None, None) => {
1640 if root_auth_authority_bytes.is_some() {
1641 return Err(FormatError::InvalidArchive(
1642 "root-auth footer presence differs across volumes",
1643 ));
1644 }
1645 saw_root_auth_absent = true;
1646 }
1647 _ => {
1648 return Err(FormatError::InvalidArchive(
1649 "root-auth footer terminal state is inconsistent",
1650 ));
1651 }
1652 }
1653
1654 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1655 let source = SeekableVolumeSource {
1656 reader: parsed.reader.clone(),
1657 volume_index: parsed.volume_header.volume_index,
1658 block_records_start: parsed.block_records_start,
1659 block_count: parsed.volume_trailer.block_count,
1660 record_len,
1661 block_size: parsed.crypto_header.block_size as usize,
1662 };
1663 let slot = parsed.volume_header.volume_index as usize;
1664 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1665 {
1666 return Err(FormatError::InvalidArchive(
1667 "duplicate authenticated volume index",
1668 ));
1669 }
1670
1671 if first.is_none() {
1672 first = Some(parsed);
1673 }
1674 }
1675
1676 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1677 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1678 Some(err) => err,
1679 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1680 })?;
1681 let authority_volume_header = manifest_authority_volume_header.ok_or(
1682 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1683 )?;
1684 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1685 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1686 )?;
1687 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1688 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1689 let missing_volume_count = first
1690 .crypto_header
1691 .stripe_width
1692 .checked_sub(observed_volume_count)
1693 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1694 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1695 return Err(FormatError::InvalidArchive(
1696 "missing volume count exceeds volume_loss_tolerance",
1697 ));
1698 }
1699
1700 let mut blocks = BTreeMap::new();
1701 let sidecar = if let Some(bytes) = bootstrap_sidecar {
1702 let sidecar = parse_bootstrap_sidecar(
1703 bytes,
1704 &first.volume_header,
1705 &first.crypto_header,
1706 &first.subkeys,
1707 )?;
1708 sidecar
1709 .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1710 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1711 if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1712 return Err(FormatError::InvalidArchive(
1713 "bootstrap sidecar conflicts with terminal ManifestFooter",
1714 ));
1715 }
1716 }
1717 Some((bytes, sidecar))
1718 } else {
1719 None
1720 };
1721
1722 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1723 if let Some((offset, length)) = sidecar.index_root_records_section {
1724 let index_root_records = parse_sidecar_block_records(
1725 sidecar_bytes,
1726 first.crypto_header.block_size as usize,
1727 SidecarBlockRecordsSection {
1728 offset,
1729 length,
1730 extent: index_root_extent_from_manifest(&manifest_footer),
1731 data_kind: BlockKind::IndexRootData,
1732 parity_kind: BlockKind::IndexRootParity,
1733 structure: "IndexRoot",
1734 },
1735 )?;
1736 insert_sidecar_records(&mut blocks, index_root_records)?;
1737 }
1738 }
1739
1740 let lazy_source = Arc::new(SeekableBlockSource {
1741 stripe_width: first.crypto_header.stripe_width,
1742 volumes: lazy_volume_slots,
1743 });
1744 let block_provider = OpenedBlockProvider {
1745 memory_blocks: &blocks,
1746 lazy_blocks: Some(lazy_source.as_ref()),
1747 };
1748 let limits = metadata_limits(&first.crypto_header);
1749 let index_root_plaintext = load_metadata_object_from_parts(
1750 &block_provider,
1751 ObjectLoadContext::index_root(
1752 &first.volume_header,
1753 &first.crypto_header,
1754 &first.subkeys,
1755 index_root_extent_from_manifest(&manifest_footer),
1756 ),
1757 manifest_footer.index_root_decompressed_size,
1758 )?;
1759 let index_root = IndexRoot::parse(
1760 &index_root_plaintext,
1761 first.crypto_header.has_dictionary != 0,
1762 limits,
1763 )?;
1764 if first.crypto_header.has_dictionary != 0 {
1765 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1766 if let Some((offset, length)) = sidecar.dictionary_records_section {
1767 let dictionary_records = parse_sidecar_block_records(
1768 sidecar_bytes,
1769 first.crypto_header.block_size as usize,
1770 SidecarBlockRecordsSection {
1771 offset,
1772 length,
1773 extent: dictionary_extent_from_index_root(&index_root)?,
1774 data_kind: BlockKind::DictionaryData,
1775 parity_kind: BlockKind::DictionaryParity,
1776 structure: "Dictionary",
1777 },
1778 )?;
1779 insert_sidecar_records(&mut blocks, dictionary_records)?;
1780 }
1781 }
1782 }
1783 let block_provider = OpenedBlockProvider {
1784 memory_blocks: &blocks,
1785 lazy_blocks: Some(lazy_source.as_ref()),
1786 };
1787 let payload_dictionary = load_archive_dictionary(
1788 &block_provider,
1789 &first.subkeys,
1790 &first.volume_header,
1791 &first.crypto_header,
1792 &index_root,
1793 )?;
1794
1795 Ok(Self {
1796 options,
1797 observed_archive_bytes,
1798 observed_volume_count,
1799 subkeys: first.subkeys,
1800 blocks,
1801 lazy_blocks: Some(lazy_source),
1802 crypto_header_bytes: first.crypto_header_bytes,
1803 volume_header: authority_volume_header,
1804 crypto_header: first.crypto_header,
1805 manifest_footer,
1806 volume_trailer: Some(authority_volume_trailer),
1807 root_auth_footer: root_auth_authority,
1808 index_root,
1809 payload_dictionary,
1810 })
1811 }
1812
1813 pub fn open_with_bootstrap_sidecar_options(
1814 bytes: &[u8],
1815 bootstrap_sidecar: &[u8],
1816 master_key: &MasterKey,
1817 options: ReaderOptions,
1818 ) -> Result<Self, FormatError> {
1819 Self::open_with_bootstrap_sidecar_options_for_mode(
1820 bytes,
1821 bootstrap_sidecar,
1822 master_key,
1823 options,
1824 BootstrapSidecarUse::SeekableAssist,
1825 )
1826 }
1827
1828 fn open_with_bootstrap_sidecar_options_for_mode(
1829 bytes: &[u8],
1830 bootstrap_sidecar: &[u8],
1831 master_key: &MasterKey,
1832 options: ReaderOptions,
1833 sidecar_use: BootstrapSidecarUse,
1834 ) -> Result<Self, FormatError> {
1835 let observed_archive_bytes =
1836 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1837 if bytes.len() < VOLUME_HEADER_LEN {
1838 return Err(FormatError::InvalidLength {
1839 structure: "archive",
1840 expected: VOLUME_HEADER_LEN,
1841 actual: bytes.len(),
1842 });
1843 }
1844
1845 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1846 parse_volume_format_dispatch(&volume_header)?;
1847 let crypto_start = volume_header.crypto_header_offset as usize;
1848 let crypto_len = volume_header.crypto_header_length as usize;
1849 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1850 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1851 let subkeys = subkeys_for_open(
1852 Some(master_key),
1853 parsed_crypto.fixed.aead_algo,
1854 &volume_header.archive_uuid,
1855 &volume_header.session_id,
1856 )?;
1857 verify_integrity_tag(
1858 HmacDomain::CryptoHeader,
1859 parsed_crypto.fixed.aead_algo,
1860 volume_header.volume_format_rev,
1861 Some(&subkeys.mac_key),
1862 &volume_header.archive_uuid,
1863 &volume_header.session_id,
1864 parsed_crypto.hmac_covered_bytes,
1865 &parsed_crypto.header_hmac,
1866 )?;
1867 parsed_crypto.validate_extension_semantics()?;
1868 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1869 validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1870 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1871
1872 let sidecar = parse_bootstrap_sidecar(
1873 bootstrap_sidecar,
1874 &volume_header,
1875 &parsed_crypto.fixed,
1876 &subkeys,
1877 )?;
1878 sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1879 let block_records_start = startup_block_records_start(
1880 &volume_header,
1881 &parsed_crypto.kdf_params,
1882 |start, length| {
1883 let start = to_usize(start, "KeyWrapTableV1")?;
1884 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
1885 },
1886 )?;
1887
1888 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1889 bytes,
1890 to_usize(block_records_start, "BlockRecord")?,
1891 parsed_crypto.fixed.block_size as usize,
1892 &volume_header,
1893 )?;
1894 let terminal_material = match sidecar_use {
1895 BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1896 bytes,
1897 terminal_offset,
1898 observed_block_count,
1899 KeyHoldingTerminalContext {
1900 subkeys: &subkeys,
1901 volume_header: &volume_header,
1902 crypto_header: &parsed_crypto.fixed,
1903 crypto_header_bytes: crypto_bytes,
1904 },
1905 options,
1906 )?),
1907 BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1908 bytes,
1909 terminal_offset,
1910 observed_block_count,
1911 KeyHoldingTerminalContext {
1912 subkeys: &subkeys,
1913 volume_header: &volume_header,
1914 crypto_header: &parsed_crypto.fixed,
1915 crypto_header_bytes: crypto_bytes,
1916 },
1917 options,
1918 )
1919 .ok(),
1920 };
1921 let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1922 let manifest_authority = match sidecar_use {
1923 BootstrapSidecarUse::SeekableAssist => {
1924 let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1925 "terminal ManifestFooter/VolumeTrailer is required",
1926 ))?;
1927 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1928 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1929 return Err(FormatError::InvalidArchive(
1930 "bootstrap sidecar conflicts with terminal ManifestFooter",
1931 ));
1932 }
1933 }
1934 terminal_manifest.clone()
1935 }
1936 BootstrapSidecarUse::NonSeekableRandomAccess => {
1937 let sidecar_manifest = sidecar
1938 .manifest_footer
1939 .as_ref()
1940 .ok_or(FormatError::ReaderUnsupported(
1941 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1942 ))?;
1943 if let Some(terminal_manifest) = terminal_manifest {
1944 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1945 return Err(FormatError::InvalidArchive(
1946 "bootstrap sidecar conflicts with terminal ManifestFooter",
1947 ));
1948 }
1949 }
1950 sidecar_manifest.clone()
1951 }
1952 };
1953 manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1954
1955 if let Some((offset, length)) = sidecar.index_root_records_section {
1956 let index_root_records = parse_sidecar_block_records(
1957 bootstrap_sidecar,
1958 parsed_crypto.fixed.block_size as usize,
1959 SidecarBlockRecordsSection {
1960 offset,
1961 length,
1962 extent: index_root_extent_from_manifest(&manifest_authority),
1963 data_kind: BlockKind::IndexRootData,
1964 parity_kind: BlockKind::IndexRootParity,
1965 structure: "IndexRoot",
1966 },
1967 )?;
1968 insert_sidecar_records(&mut blocks, index_root_records)?;
1969 }
1970
1971 let limits = metadata_limits(&parsed_crypto.fixed);
1972 let index_root_plaintext = load_metadata_object_from_parts(
1973 &blocks,
1974 ObjectLoadContext::index_root(
1975 &volume_header,
1976 &parsed_crypto.fixed,
1977 &subkeys,
1978 index_root_extent_from_manifest(&manifest_authority),
1979 ),
1980 manifest_authority.index_root_decompressed_size,
1981 )?;
1982 let index_root = IndexRoot::parse(
1983 &index_root_plaintext,
1984 parsed_crypto.fixed.has_dictionary != 0,
1985 limits,
1986 )?;
1987 if parsed_crypto.fixed.has_dictionary != 0 {
1988 if let Some((offset, length)) = sidecar.dictionary_records_section {
1989 let dictionary_records = parse_sidecar_block_records(
1990 bootstrap_sidecar,
1991 parsed_crypto.fixed.block_size as usize,
1992 SidecarBlockRecordsSection {
1993 offset,
1994 length,
1995 extent: dictionary_extent_from_index_root(&index_root)?,
1996 data_kind: BlockKind::DictionaryData,
1997 parity_kind: BlockKind::DictionaryParity,
1998 structure: "dictionary",
1999 },
2000 )?;
2001 insert_sidecar_records(&mut blocks, dictionary_records)?;
2002 }
2003 }
2004 let payload_dictionary = load_archive_dictionary(
2005 &blocks,
2006 &subkeys,
2007 &volume_header,
2008 &parsed_crypto.fixed,
2009 &index_root,
2010 )?;
2011
2012 Ok(Self {
2013 options,
2014 observed_archive_bytes,
2015 observed_volume_count: 1,
2016 subkeys,
2017 blocks,
2018 lazy_blocks: None,
2019 crypto_header_bytes: crypto_bytes.to_vec(),
2020 volume_header,
2021 crypto_header: parsed_crypto.fixed,
2022 manifest_footer: manifest_authority,
2023 volume_trailer: terminal_material
2024 .as_ref()
2025 .map(|(_, trailer, _)| trailer.clone()),
2026 root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
2027 index_root,
2028 payload_dictionary,
2029 })
2030 }
2031
2032 pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
2038 let shards = self.load_all_index_shards()?;
2039 final_index_entry_winners(&shards)?
2040 .into_iter()
2041 .map(|(path, winner)| {
2042 archive_index_entry_from_loaded_file_with_path(
2043 path,
2044 &shards[winner.shard_index],
2045 winner.file_index,
2046 )
2047 })
2048 .collect()
2049 }
2050
2051 pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
2053 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2054 self.locate_index_file(&normalized)?
2055 .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
2056 .transpose()
2057 }
2058
2059 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
2060 let shards = self.load_all_index_shards()?;
2061 final_index_entry_winners(&shards)?
2062 .into_iter()
2063 .map(|(path, winner)| {
2064 let shard = &shards[winner.shard_index];
2065 let member =
2066 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
2067 Ok(ArchiveEntry {
2068 path,
2069 file_data_size: winner.file_data_size,
2070 kind: member.kind,
2071 mode: member.mode,
2072 mtime: member.mtime,
2073 diagnostics: member.diagnostics,
2074 })
2075 })
2076 .collect()
2077 }
2078
2079 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
2086 self.extract_member(path)?
2087 .map(|member| {
2088 if member.kind != TarEntryKind::Regular {
2089 return Err(FormatError::ReaderUnsupported(
2090 "extract_file returns only regular file payloads",
2091 ));
2092 }
2093 Ok(member.data)
2094 })
2095 .transpose()
2096 }
2097
2098 pub fn extract_file_with_diagnostics(
2101 &self,
2102 path: &str,
2103 ) -> Result<Option<ExtractedRegularFile>, FormatError> {
2104 self.extract_member(path)?
2105 .map(|member| {
2106 if member.kind != TarEntryKind::Regular {
2107 return Err(FormatError::ReaderUnsupported(
2108 "extract_file_with_diagnostics returns only regular file payloads",
2109 ));
2110 }
2111 Ok((member.data, member.diagnostics))
2112 })
2113 .transpose()
2114 }
2115
2116 pub fn extract_file_to_writer<W: Write>(
2122 &self,
2123 path: &str,
2124 writer: &mut W,
2125 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2126 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2127 self.locate_index_file(&normalized)?
2128 .map(|located| {
2129 self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
2130 })
2131 .transpose()
2132 }
2133
2134 pub fn extract_file_to_writer_with_progress<W: Write>(
2137 &self,
2138 path: &str,
2139 writer: &mut W,
2140 progress: &mut dyn ArchiveExtractProgressSink,
2141 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
2142 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2143 self.locate_index_file(&normalized)?
2144 .map(|located| {
2145 self.stream_loaded_file_to_writer_with_progress(
2146 &located.shard,
2147 located.file_index,
2148 writer,
2149 progress,
2150 )
2151 })
2152 .transpose()
2153 }
2154
2155 pub fn extract_member(
2156 &self,
2157 path: &str,
2158 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
2159 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2160 self.locate_index_file(&normalized)?
2161 .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
2162 .transpose()
2163 }
2164
2165 pub fn extract_file_to(
2166 &self,
2167 path: &str,
2168 root: &std::path::Path,
2169 options: SafeExtractionOptions,
2170 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
2171 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
2172 self.locate_index_file(&normalized)?
2173 .map(|located| {
2174 self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
2175 })
2176 .transpose()
2177 }
2178
2179 pub fn extract_indexed_files_to(
2180 &self,
2181 root: &std::path::Path,
2182 options: SafeExtractionOptions,
2183 jobs: usize,
2184 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2185 if jobs == 0 {
2186 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
2187 }
2188
2189 let shards = self.load_all_index_shards()?;
2190 let mut entries = final_index_entry_winners(&shards)?
2191 .into_iter()
2192 .collect::<Vec<_>>();
2193 entries.sort_by_key(|(_, entry)| entry.start);
2194 self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
2195 }
2196
2197 pub fn verify(&self) -> Result<(), FormatError> {
2198 self.verify_content().map(|_| ())
2199 }
2200
2201 pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2202 self.verify_content_with_parity_policy(
2203 ParityReadPolicy::Always,
2204 ContentVerificationMode::Full,
2205 )
2206 }
2207
2208 pub fn verify_content_fast(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
2209 if self.fast_verify_defers_payload_semantics() {
2210 self.verify_payload_record_integrity_only()?;
2211 return Ok(ArchiveContentVerification {
2212 archive: self,
2213 mode: ContentVerificationMode::Fast,
2214 });
2215 }
2216 self.verify_content_with_parity_policy(
2217 ParityReadPolicy::RepairOnly,
2218 ContentVerificationMode::Fast,
2219 )
2220 }
2221
2222 pub fn fast_verify_defers_payload_semantics(&self) -> bool {
2223 self.root_auth_footer.is_none()
2224 && self.crypto_header.has_dictionary == 0
2225 && !self.crypto_header.aead_algo.is_encrypted()
2226 && self.crypto_header.fec_parity_shards == 0
2227 && self.crypto_header.index_fec_parity_shards == 0
2228 && self.crypto_header.index_root_fec_parity_shards == 0
2229 && self.manifest_footer.index_root_parity_block_count == 0
2230 }
2231
2232 fn verify_payload_record_integrity_only(&self) -> Result<(), FormatError> {
2233 let tables = self.load_payload_index_tables()?;
2234 let block_provider = self.block_provider();
2235 let block_size = self.crypto_header.block_size as u64;
2236 for envelope in tables.envelopes.values() {
2237 if envelope.parity_block_count != 0 {
2238 return Err(FormatError::InvalidArchive(
2239 "fast payload record scan requires zero parity",
2240 ));
2241 }
2242 let expected_encrypted_size = checked_u64_mul(
2243 envelope.data_block_count as u64,
2244 block_size,
2245 "payload envelope encrypted size",
2246 )?;
2247 if envelope.encrypted_size as u64 != expected_encrypted_size {
2248 return Err(FormatError::InvalidArchive(
2249 "payload envelope encrypted_size mismatch",
2250 ));
2251 }
2252 for offset in 0..envelope.data_block_count {
2253 let block_index =
2254 checked_u64_add(envelope.first_block_index, offset as u64, "payload")?;
2255 let record = block_provider
2256 .block(block_index)?
2257 .ok_or(FormatError::InvalidArchive("payload data block is missing"))?;
2258 if record.kind != BlockKind::PayloadData {
2259 return Err(FormatError::InvalidArchive(
2260 "payload data block has unexpected kind",
2261 ));
2262 }
2263 let should_be_last = offset + 1 == envelope.data_block_count;
2264 if record.is_last_data() != should_be_last {
2265 return Err(FormatError::InvalidArchive(
2266 "payload last-data flag is not on the final data block",
2267 ));
2268 }
2269 }
2270 }
2271 Ok(())
2272 }
2273
2274 fn verify_content_with_parity_policy(
2275 &self,
2276 parity_policy: ParityReadPolicy,
2277 mode: ContentVerificationMode,
2278 ) -> Result<ArchiveContentVerification<'_>, FormatError> {
2279 let tables = self.load_payload_index_tables()?;
2280 let streamed = self.scan_seekable_payload(
2281 &tables,
2282 u64::MAX,
2283 NoopTarStreamObserver,
2284 true,
2285 parity_policy,
2286 )?;
2287 self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
2288 Ok(ArchiveContentVerification {
2289 archive: self,
2290 mode,
2291 })
2292 }
2293
2294 pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
2295 let lazy_source = self
2296 .lazy_blocks
2297 .as_ref()
2298 .ok_or(FormatError::ReaderUnsupported(
2299 "repair output requires seekable archive input",
2300 ))?;
2301 if !lazy_source.is_complete_volume_set() {
2302 return Err(FormatError::ReaderUnsupported(
2303 "repair output requires all archive volumes",
2304 ));
2305 }
2306
2307 let shards = self.load_all_index_shards()?;
2308 let rows = self.root_auth_fec_layout_rows(&shards)?;
2309 let block_provider = self.block_provider();
2310 let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
2311 for row in rows.into_iter().filter(|row| row.present) {
2312 self.collect_repair_patches_for_object(
2313 &block_provider,
2314 lazy_source,
2315 row,
2316 &mut patches,
2317 )?;
2318 }
2319 Ok(patches.into_values().collect())
2320 }
2321
2322 pub fn extract_all_to(
2323 &self,
2324 root: &std::path::Path,
2325 options: SafeExtractionOptions,
2326 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2327 let tables = self.load_payload_index_tables()?;
2328 if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
2329 return Err(FormatError::ReaderUnsupported(
2330 FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
2331 ));
2332 }
2333
2334 let dry_run = self.scan_seekable_payload(
2335 &tables,
2336 total_extraction_size_cap(self.options, self.observed_archive_bytes),
2337 NoopTarStreamObserver,
2338 false,
2339 ParityReadPolicy::RepairOnly,
2340 )?;
2341 self.validate_streamed_payload_summary(&tables, &dry_run, true, false)?;
2342
2343 let observer = TarStreamFilesystemRestoreObserver::new(root, options);
2344 let streamed = self.scan_seekable_payload(
2345 &tables,
2346 total_extraction_size_cap(self.options, self.observed_archive_bytes),
2347 observer,
2348 false,
2349 ParityReadPolicy::RepairOnly,
2350 )?;
2351 streamed
2352 .tar
2353 .members
2354 .into_iter()
2355 .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
2356 .collect()
2357 }
2358
2359 fn collect_repair_patches_for_object(
2360 &self,
2361 blocks: &impl BlockProvider,
2362 source: &SeekableBlockSource,
2363 row: FecLayoutObjectRow,
2364 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2365 ) -> Result<(), FormatError> {
2366 let (data_kind, parity_kind, data_max, parity_max) =
2367 self.fec_object_class_shape(row.object_class)?;
2368 let extent = ObjectExtent {
2369 first_block_index: row.first_block_index,
2370 data_block_count: row.data_block_count,
2371 parity_block_count: row.parity_block_count,
2372 encrypted_size: row.encrypted_size,
2373 };
2374 validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
2375
2376 let block_size = self.crypto_header.block_size as usize;
2377 let data_count = extent.data_block_count as usize;
2378 let parity_count = extent.parity_block_count as usize;
2379 let mut data_shards = Vec::with_capacity(data_count);
2380 let mut parity_shards = Vec::with_capacity(parity_count);
2381
2382 for offset in 0..data_count {
2383 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2384 match blocks.block(block_index)? {
2385 Some(record) => {
2386 if record.kind != data_kind {
2387 return Err(FormatError::InvalidArchive(
2388 "object data block has unexpected kind",
2389 ));
2390 }
2391 let should_be_last = offset + 1 == data_count;
2392 if record.is_last_data() != should_be_last {
2393 return Err(FormatError::InvalidArchive(
2394 "object last-data flag is not on the final data block",
2395 ));
2396 }
2397 data_shards.push(Some(record.payload.clone()));
2398 }
2399 None => data_shards.push(None),
2400 }
2401 }
2402
2403 for offset in 0..parity_count {
2404 let block_index = checked_u64_add(
2405 extent.first_block_index,
2406 data_count as u64 + offset as u64,
2407 "object",
2408 )?;
2409 match blocks.block(block_index)? {
2410 Some(record) => {
2411 if record.kind != parity_kind {
2412 return Err(FormatError::InvalidArchive(
2413 "object parity block has unexpected kind",
2414 ));
2415 }
2416 if record.is_last_data() {
2417 return Err(FormatError::InvalidArchive(
2418 "object parity block has last-data flag",
2419 ));
2420 }
2421 parity_shards.push(Some(record.payload.clone()));
2422 }
2423 None => parity_shards.push(None),
2424 }
2425 }
2426
2427 let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
2428 for (offset, payload) in repaired_data.iter().enumerate() {
2429 if data_shards[offset].is_none() {
2430 let block_index =
2431 checked_u64_add(extent.first_block_index, offset as u64, "object")?;
2432 let flags = if offset + 1 == data_count { 0x01 } else { 0 };
2433 self.insert_repair_patch(
2434 patches,
2435 source,
2436 block_index,
2437 data_kind,
2438 flags,
2439 payload.clone(),
2440 )?;
2441 }
2442 }
2443
2444 if parity_count > 0 {
2445 let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
2446 for (offset, payload) in repaired_parity.into_iter().enumerate() {
2447 if parity_shards[offset].as_ref() != Some(&payload) {
2448 let block_index = checked_u64_add(
2449 extent.first_block_index,
2450 data_count as u64 + offset as u64,
2451 "object",
2452 )?;
2453 self.insert_repair_patch(
2454 patches,
2455 source,
2456 block_index,
2457 parity_kind,
2458 0,
2459 payload,
2460 )?;
2461 }
2462 }
2463 }
2464
2465 Ok(())
2466 }
2467
2468 fn insert_repair_patch(
2469 &self,
2470 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
2471 source: &SeekableBlockSource,
2472 block_index: u64,
2473 kind: BlockKind,
2474 flags: u8,
2475 payload: Vec<u8>,
2476 ) -> Result<(), FormatError> {
2477 let (volume_index, record_offset) = source.record_location(block_index)?;
2478 let record = BlockRecord {
2479 block_index,
2480 kind,
2481 flags,
2482 payload,
2483 record_crc32c: 0,
2484 };
2485 let patch = ArchiveRepairPatch {
2486 volume_index,
2487 block_index,
2488 record_offset,
2489 record_bytes: record.to_bytes(),
2490 };
2491 if let Some(existing) = patches.insert(block_index, patch.clone()) {
2492 if existing != patch {
2493 return Err(FormatError::InvalidArchive(
2494 "conflicting repair patch for BlockRecord",
2495 ));
2496 }
2497 }
2498 Ok(())
2499 }
2500
2501 fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
2502 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
2503 && self.index_root.directory_hint_shards.is_empty()
2504 {
2505 return Err(FormatError::InvalidArchive(
2506 "IndexRoot file_count requires directory hints",
2507 ));
2508 }
2509
2510 let shards = self.load_all_index_shards()?;
2511 let mut file_count = 0u64;
2512 let mut frames = BTreeMap::<u64, FrameEntry>::new();
2513 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2514
2515 for shard in &shards {
2516 file_count = file_count
2517 .checked_add(shard.files.len() as u64)
2518 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
2519 for frame in &shard.frames {
2520 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
2521 if existing != *frame {
2522 return Err(FormatError::InvalidArchive(
2523 "duplicate FrameEntry rows do not match",
2524 ));
2525 }
2526 }
2527 }
2528 for envelope in &shard.envelopes {
2529 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2530 {
2531 if existing != *envelope {
2532 return Err(FormatError::InvalidArchive(
2533 "duplicate EnvelopeEntry rows do not match",
2534 ));
2535 }
2536 }
2537 }
2538 }
2539 validate_global_file_table_order(&shards)?;
2540
2541 if file_count != self.index_root.header.file_count {
2542 return Err(FormatError::InvalidArchive(
2543 "IndexRoot file_count does not match decoded shards",
2544 ));
2545 }
2546 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
2547 verify_dense_keys(
2548 &envelopes,
2549 self.index_root.header.envelope_count,
2550 "EnvelopeEntry",
2551 )?;
2552 validate_envelope_frame_coverage(&frames, &envelopes)?;
2553 self.validate_encrypted_object_block_ranges(&envelopes)?;
2554
2555 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
2556 sum.checked_add(envelope.data_block_count as u64)
2557 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2558 })?;
2559 if payload_block_count != self.index_root.header.payload_block_count {
2560 return Err(FormatError::InvalidArchive(
2561 "IndexRoot payload_block_count does not match envelopes",
2562 ));
2563 }
2564
2565 Ok(PayloadIndexTables {
2566 shards,
2567 file_count,
2568 frames,
2569 envelopes,
2570 })
2571 }
2572
2573 fn scan_seekable_payload<O: TarStreamObserver>(
2574 &self,
2575 tables: &PayloadIndexTables,
2576 extraction_cap: u64,
2577 observer: O,
2578 hash_content: bool,
2579 parity_policy: ParityReadPolicy,
2580 ) -> Result<StreamedPayloadSummary, FormatError> {
2581 let mut tar = TarStreamSummaryValidator::with_observer(
2582 self.crypto_header.max_path_length,
2583 extraction_cap,
2584 usize::MAX,
2585 self.index_root.header.file_count,
2586 observer,
2587 );
2588 let mut content_hasher = hash_content.then(Sha256::new);
2589 let mut streamed_frames = Vec::with_capacity(tables.frames.len());
2590 let streamed_envelopes = tables
2591 .envelopes
2592 .values()
2593 .map(|envelope| StreamedEnvelopeSummary {
2594 envelope_index: envelope.envelope_index,
2595 first_block_index: envelope.first_block_index,
2596 data_block_count: envelope.data_block_count,
2597 parity_block_count: envelope.parity_block_count,
2598 encrypted_size: envelope.encrypted_size,
2599 plaintext_size: envelope.plaintext_size,
2600 first_frame_index: envelope.first_frame_index,
2601 frame_count: envelope.frame_count,
2602 })
2603 .collect::<Vec<_>>();
2604 let mut cached_envelope_index = None;
2605 let mut cached_envelope_plaintext = Vec::new();
2606 let mut decompressor = self.new_payload_decompressor()?;
2607
2608 for frame in tables.frames.values() {
2609 let envelope =
2610 tables
2611 .envelopes
2612 .get(&frame.envelope_index)
2613 .ok_or(FormatError::InvalidArchive(
2614 "FrameEntry references missing EnvelopeEntry",
2615 ))?;
2616 if cached_envelope_index != Some(envelope.envelope_index) {
2617 cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
2618 cached_envelope_index = Some(envelope.envelope_index);
2619 }
2620 let compressed = slice(
2621 &cached_envelope_plaintext,
2622 frame.offset_in_envelope as usize,
2623 frame.compressed_size as usize,
2624 "FrameEntry",
2625 )?;
2626 let tar_stream_offset = tar.tar_total_size();
2627 let decoded = self.decompress_payload_frame_with(
2628 &mut decompressor,
2629 compressed,
2630 frame.decompressed_size,
2631 )?;
2632 if decoded.is_empty() {
2633 return Err(FormatError::InvalidArchive(
2634 "zstd payload frame decompressed to zero bytes",
2635 ));
2636 }
2637 if let Some(hasher) = &mut content_hasher {
2638 hasher.update(&decoded);
2639 }
2640 tar.observe(&decoded)?;
2641 streamed_frames.push(StreamedFrameSummary {
2642 frame_index: frame.frame_index,
2643 envelope_index: frame.envelope_index,
2644 offset_in_envelope: frame.offset_in_envelope,
2645 compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2646 FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2647 })?,
2648 decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2649 FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2650 })?,
2651 tar_stream_offset,
2652 });
2653 }
2654
2655 let mut content_sha256 = [0u8; 32];
2656 if let Some(hasher) = content_hasher {
2657 let digest = hasher.finalize();
2658 content_sha256.copy_from_slice(&digest);
2659 }
2660 Ok(StreamedPayloadSummary {
2661 tar: tar.finish()?,
2662 content_sha256,
2663 envelopes: streamed_envelopes,
2664 frames: streamed_frames,
2665 })
2666 }
2667
2668 fn validate_streamed_payload_summary(
2669 &self,
2670 tables: &PayloadIndexTables,
2671 streamed: &StreamedPayloadSummary,
2672 enforce_total_extraction_cap: bool,
2673 enforce_content_sha256: bool,
2674 ) -> Result<(), FormatError> {
2675 if enforce_total_extraction_cap
2676 && streamed.tar.total_extraction_size
2677 > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2678 {
2679 return Err(FormatError::ReaderUnsupported(
2680 "total extraction size exceeds configured cap",
2681 ));
2682 }
2683
2684 let streamed_payload_block_count =
2685 streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2686 sum.checked_add(envelope.data_block_count as u64)
2687 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2688 })?;
2689 if streamed_payload_block_count != self.index_root.header.payload_block_count {
2690 return Err(FormatError::InvalidArchive(
2691 "streamed payload block count does not match IndexRoot",
2692 ));
2693 }
2694
2695 if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2696 return Err(FormatError::InvalidArchive(
2697 "IndexRoot tar_total_size does not match streamed tar stream",
2698 ));
2699 }
2700 if enforce_content_sha256
2701 && streamed.content_sha256 != self.index_root.header.content_sha256
2702 {
2703 return Err(FormatError::InvalidArchive(
2704 "IndexRoot content_sha256 does not match decoded tar stream",
2705 ));
2706 }
2707
2708 let streamed_envelopes = streamed.envelope_map()?;
2709 for envelope in tables.envelopes.values() {
2710 let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2711 FormatError::InvalidArchive(
2712 "metadata references missing streamed payload envelope",
2713 ),
2714 )?;
2715 if actual.first_block_index != envelope.first_block_index
2716 || actual.data_block_count != envelope.data_block_count
2717 || actual.parity_block_count != envelope.parity_block_count
2718 || actual.encrypted_size != envelope.encrypted_size
2719 || actual.plaintext_size != envelope.plaintext_size
2720 || actual.first_frame_index != envelope.first_frame_index
2721 || actual.frame_count != envelope.frame_count
2722 {
2723 return Err(FormatError::InvalidArchive(
2724 "EnvelopeEntry does not match streamed payload envelope",
2725 ));
2726 }
2727 }
2728
2729 let streamed_frames = streamed.frame_map()?;
2730 for frame in tables.frames.values() {
2731 let actual =
2732 streamed_frames
2733 .get(&frame.frame_index)
2734 .ok_or(FormatError::InvalidArchive(
2735 "metadata references missing streamed payload frame",
2736 ))?;
2737 if actual.envelope_index != frame.envelope_index
2738 || actual.offset_in_envelope != frame.offset_in_envelope
2739 || actual.compressed_size != frame.compressed_size
2740 || actual.decompressed_size != frame.decompressed_size
2741 || actual.tar_stream_offset != frame.tar_stream_offset
2742 || streamed.frame_flags(actual)? != frame.flags
2743 {
2744 return Err(FormatError::InvalidArchive(
2745 "FrameEntry does not match streamed payload frame",
2746 ));
2747 }
2748 }
2749
2750 let streamed_members = streamed.member_start_map()?;
2751 if streamed.tar.members.len() as u64 != tables.file_count {
2752 return Err(FormatError::InvalidArchive(
2753 "streamed tar member count does not match decoded shards",
2754 ));
2755 }
2756 let mut file_extents = Vec::new();
2757 let mut directory_hint_map = DirectoryHintMap::new();
2758 for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2759 let shard_row_index = u32::try_from(shard_row_index)
2760 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2761 for idx in 0..shard.files.len() {
2762 let file = &shard.files[idx];
2763 let start =
2764 shard
2765 .tar_member_group_start(idx)
2766 .ok_or(FormatError::InvalidArchive(
2767 "FileEntry tar member start is missing",
2768 ))?;
2769 file_extents.push((start, file.tar_member_group_size));
2770 let path = shard
2771 .file_path(idx)
2772 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2773 let member = streamed_members
2774 .get(&start)
2775 .ok_or(FormatError::InvalidArchive(
2776 "FileEntry tar member start is missing from streamed tar",
2777 ))?;
2778 if member.path != path {
2779 return Err(FormatError::InvalidArchive(
2780 "tar member path does not match FileEntry path",
2781 ));
2782 }
2783 if member.logical_size != file.file_data_size {
2784 return Err(FormatError::InvalidArchive(
2785 "tar member size does not match FileEntry file_data_size",
2786 ));
2787 }
2788 if member.group_size != file.tar_member_group_size {
2789 return Err(FormatError::InvalidArchive(
2790 "FileEntry does not match streamed tar member",
2791 ));
2792 }
2793 add_expected_directory_hint_rows(
2794 &mut directory_hint_map,
2795 shard_row_index,
2796 path,
2797 member.kind,
2798 );
2799 }
2800 }
2801 validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2802 if !self.index_root.directory_hint_shards.is_empty() {
2803 let hint_tables = self.load_all_directory_hint_tables()?;
2804 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2805 }
2806
2807 Ok(())
2808 }
2809
2810 pub(crate) fn from_streamed_parts(
2811 parts: StreamedArchiveOpenParts,
2812 ) -> Result<Self, FormatError> {
2813 let limits = metadata_limits(&parts.crypto_header);
2814 let index_root_plaintext = load_metadata_object_from_parts(
2815 &parts.blocks,
2816 ObjectLoadContext::index_root(
2817 &parts.volume_header,
2818 &parts.crypto_header,
2819 &parts.subkeys,
2820 ObjectExtent {
2821 first_block_index: parts.manifest_footer.index_root_first_block,
2822 data_block_count: parts.manifest_footer.index_root_data_block_count,
2823 parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2824 encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2825 },
2826 ),
2827 parts.manifest_footer.index_root_decompressed_size,
2828 )?;
2829 let index_root = IndexRoot::parse(
2830 &index_root_plaintext,
2831 parts.crypto_header.has_dictionary != 0,
2832 limits,
2833 )?;
2834 let payload_dictionary = load_archive_dictionary(
2835 &parts.blocks,
2836 &parts.subkeys,
2837 &parts.volume_header,
2838 &parts.crypto_header,
2839 &index_root,
2840 )?;
2841
2842 Ok(Self {
2843 options: parts.options,
2844 observed_archive_bytes: parts.observed_archive_bytes,
2845 observed_volume_count: 1,
2846 subkeys: parts.subkeys,
2847 blocks: parts.blocks,
2848 lazy_blocks: None,
2849 crypto_header_bytes: parts.crypto_header_bytes,
2850 volume_header: parts.volume_header,
2851 crypto_header: parts.crypto_header,
2852 manifest_footer: parts.manifest_footer,
2853 volume_trailer: Some(parts.volume_trailer),
2854 root_auth_footer: parts.root_auth_footer,
2855 index_root,
2856 payload_dictionary,
2857 })
2858 }
2859
2860 pub(crate) fn verify_streamed_payload_summary(
2861 &self,
2862 streamed: &StreamedPayloadSummary,
2863 ) -> Result<(), FormatError> {
2864 let tables = self.load_payload_index_tables()?;
2865 self.validate_streamed_payload_summary(&tables, streamed, true, true)
2866 }
2867
2868 pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2869 where
2870 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2871 {
2872 let content_verification = self.verify_content()?;
2873 self.verify_root_auth_with_verified_content(&content_verification, verifier)
2874 }
2875
2876 pub fn verify_root_auth_with_verified_content<F>(
2877 &self,
2878 content_verification: &ArchiveContentVerification<'_>,
2879 mut verifier: F,
2880 ) -> Result<RootAuthVerification, FormatError>
2881 where
2882 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2883 {
2884 if !std::ptr::eq(content_verification.archive, self) {
2885 return Err(FormatError::InvalidArchive(
2886 "content verification does not match archive",
2887 ));
2888 }
2889 if content_verification.mode != ContentVerificationMode::Full {
2890 return Err(FormatError::ReaderUnsupported(
2891 "RootAuth verification requires full archive content verification",
2892 ));
2893 }
2894 let footer = self
2895 .root_auth_footer
2896 .as_ref()
2897 .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2898 let material = self.recompute_root_auth_material(footer)?;
2899 if material.critical_metadata_digest != footer.critical_metadata_digest
2900 || material.index_digest != footer.index_digest
2901 || material.fec_layout_digest != footer.fec_layout_digest
2902 || material.data_block_merkle_root != footer.data_block_merkle_root
2903 || material.signer_identity_digest != footer.signer_identity_digest
2904 || material.archive_root != footer.archive_root
2905 || material.total_data_block_count != footer.total_data_block_count
2906 {
2907 return Err(FormatError::InvalidArchive(
2908 "RootAuthFooter commitments do not match recomputed archive root",
2909 ));
2910 }
2911 if !verifier(footer, &material.archive_root)? {
2912 return Err(FormatError::InvalidArchive(
2913 "root-auth authenticator verification failed",
2914 ));
2915 }
2916 Ok(RootAuthVerification {
2917 format_version: footer.format_version,
2918 volume_format_rev: footer.volume_format_rev,
2919 archive_root: material.archive_root,
2920 authenticator_id: footer.authenticator_id,
2921 signer_identity_type: footer.signer_identity_type,
2922 signer_identity_bytes: footer.signer_identity_bytes.clone(),
2923 total_data_block_count: footer.total_data_block_count,
2924 diagnostics: self.root_auth_success_diagnostics(),
2925 })
2926 }
2927
2928 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2929 parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2930 self.load_index_shard(entry)
2931 })
2932 }
2933
2934 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2935 let block_provider = self.block_provider();
2936 let plaintext = load_metadata_object_from_parts(
2937 &block_provider,
2938 ObjectLoadContext::index_shard(
2939 &self.volume_header,
2940 &self.crypto_header,
2941 &self.subkeys,
2942 entry,
2943 ),
2944 entry.decompressed_size,
2945 )?;
2946 IndexShard::parse(&plaintext, entry, self.metadata_limits())
2947 }
2948
2949 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2950 parallel_map_ref(
2951 &self.index_root.directory_hint_shards,
2952 self.options.jobs,
2953 |entry| self.load_directory_hint_table(entry),
2954 )
2955 }
2956
2957 fn load_directory_hint_table(
2958 &self,
2959 entry: &DirectoryHintShardEntry,
2960 ) -> Result<DirectoryHintTable, FormatError> {
2961 let block_provider = self.block_provider();
2962 let plaintext = load_metadata_object_from_parts(
2963 &block_provider,
2964 ObjectLoadContext::directory_hint(
2965 &self.volume_header,
2966 &self.crypto_header,
2967 &self.subkeys,
2968 entry,
2969 ),
2970 entry.decompressed_size,
2971 )?;
2972 DirectoryHintTable::parse(
2973 &plaintext,
2974 entry,
2975 self.index_root.header.shard_count,
2976 self.metadata_limits(),
2977 )
2978 }
2979
2980 fn load_payload_envelope(
2981 &self,
2982 envelope: &EnvelopeEntry,
2983 parity_policy: ParityReadPolicy,
2984 ) -> Result<Vec<u8>, FormatError> {
2985 let block_provider = self.block_provider();
2986 let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2987 &block_provider,
2988 ObjectLoadContext::payload(
2989 &self.volume_header,
2990 &self.crypto_header,
2991 &self.subkeys,
2992 envelope,
2993 ),
2994 parity_policy,
2995 )?;
2996 if plaintext.len() != envelope.plaintext_size as usize {
2997 return Err(FormatError::InvalidArchive(
2998 "payload envelope plaintext_size mismatch",
2999 ));
3000 }
3001 Ok(plaintext)
3002 }
3003
3004 fn locate_index_file(
3005 &self,
3006 normalized: &[u8],
3007 ) -> Result<Option<LocatedIndexFile>, FormatError> {
3008 let candidate_indexes = self
3009 .index_root
3010 .candidate_shards_for_path(normalized, self.metadata_limits())?;
3011 let mut winner: Option<LocatedIndexFile> = None;
3012
3013 for row_index in candidate_indexes {
3014 let locating =
3015 self.index_root
3016 .shards
3017 .get(row_index)
3018 .ok_or(FormatError::InvalidArchive(
3019 "candidate shard row is out of bounds",
3020 ))?;
3021 let shard = self.load_index_shard(locating)?;
3022 if let Some(file_index) = shard.lookup_file_index(normalized) {
3023 let start =
3024 shard
3025 .tar_member_group_start(file_index)
3026 .ok_or(FormatError::InvalidArchive(
3027 "FileEntry tar member start is missing",
3028 ))?;
3029 if winner
3030 .as_ref()
3031 .map(|existing| start > existing.start)
3032 .unwrap_or(true)
3033 {
3034 winner = Some(LocatedIndexFile {
3035 shard,
3036 file_index,
3037 start,
3038 });
3039 }
3040 }
3041 }
3042
3043 Ok(winner)
3044 }
3045
3046 fn extract_loaded_member(
3047 &self,
3048 shard: &IndexShard,
3049 file_index: usize,
3050 ) -> Result<ExtractedArchiveMember, FormatError> {
3051 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
3052 Ok(ExtractedArchiveMember {
3053 path: utf8_path(&member.path)?,
3054 kind: member.kind,
3055 data: member.data,
3056 link_target: member
3057 .link_target
3058 .map(|target| utf8_path(&target))
3059 .transpose()?,
3060 diagnostics: member.diagnostics,
3061 })
3062 }
3063
3064 fn extract_loaded_owned_tar_member(
3065 &self,
3066 shard: &IndexShard,
3067 file_index: usize,
3068 ) -> Result<OwnedTarMember, FormatError> {
3069 self.decode_loaded_owned_tar_member(shard, file_index, true)
3070 }
3071
3072 fn stream_loaded_file_to_writer<W: Write>(
3073 &self,
3074 shard: &IndexShard,
3075 file_index: usize,
3076 writer: &mut W,
3077 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3078 let file = shard
3079 .files
3080 .get(file_index)
3081 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3082 self.validate_total_extraction_size(file.file_data_size)?;
3083 let expected_path = shard
3084 .file_path(file_index)
3085 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3086 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3087 stream_regular_tar_member_group_to_writer(
3088 &mut reader,
3089 expected_path,
3090 file.file_data_size,
3091 file.tar_member_group_size,
3092 self.crypto_header.max_path_length,
3093 writer,
3094 )
3095 }
3096
3097 fn stream_loaded_file_to_writer_with_progress<W: Write>(
3098 &self,
3099 shard: &IndexShard,
3100 file_index: usize,
3101 writer: &mut W,
3102 progress: &mut dyn ArchiveExtractProgressSink,
3103 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
3104 let file = shard
3105 .files
3106 .get(file_index)
3107 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3108 self.validate_total_extraction_size(file.file_data_size)?;
3109 let expected_path = shard
3110 .file_path(file_index)
3111 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3112 let archive_path = utf8_path(expected_path)?;
3113 let mut progress_writer =
3114 ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
3115 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3116 stream_regular_tar_member_group_to_writer(
3117 &mut reader,
3118 expected_path,
3119 file.file_data_size,
3120 file.tar_member_group_size,
3121 self.crypto_header.max_path_length,
3122 &mut progress_writer,
3123 )
3124 }
3125
3126 fn stream_loaded_file_to_path(
3127 &self,
3128 shard: &IndexShard,
3129 file_index: usize,
3130 root: &std::path::Path,
3131 options: SafeExtractionOptions,
3132 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
3133 let file = shard
3134 .files
3135 .get(file_index)
3136 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3137 self.validate_total_extraction_size(file.file_data_size)?;
3138 let expected_path = shard
3139 .file_path(file_index)
3140 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3141 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
3142 restore_streaming_tar_member_group(
3143 root,
3144 expected_path,
3145 file.file_data_size,
3146 file.tar_member_group_size,
3147 self.crypto_header.max_path_length,
3148 options,
3149 &mut reader,
3150 )
3151 .map_err(format_error_from_extract_error)
3152 }
3153
3154 fn extract_winning_index_entries_to(
3155 &self,
3156 shards: &[IndexShard],
3157 entries: Vec<(String, WinningIndexEntry)>,
3158 root: &std::path::Path,
3159 options: SafeExtractionOptions,
3160 _jobs: usize,
3161 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
3162 if entries.is_empty() {
3163 return Ok(Vec::new());
3164 }
3165 entries
3166 .into_iter()
3167 .map(|(path, entry)| {
3168 let shard = shards
3169 .get(entry.shard_index)
3170 .ok_or(FormatError::InvalidArchive(
3171 "winning FileEntry shard is out of bounds",
3172 ))?;
3173 let diagnostics =
3174 self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
3175 Ok((path, diagnostics))
3176 })
3177 .collect()
3178 }
3179
3180 fn decode_loaded_owned_tar_member(
3181 &self,
3182 shard: &IndexShard,
3183 file_index: usize,
3184 enforce_extraction_cap: bool,
3185 ) -> Result<OwnedTarMember, FormatError> {
3186 let file = shard
3187 .files
3188 .get(file_index)
3189 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3190 if enforce_extraction_cap {
3191 self.validate_total_extraction_size(file.file_data_size)?;
3192 }
3193 let expected_path = shard
3194 .file_path(file_index)
3195 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
3196 let frames = frame_range_for_file(shard, file)?;
3197 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
3198 let mut decoded = Vec::new();
3199
3200 for frame in frames {
3201 let envelope = envelope_by_index(shard, frame.envelope_index)?;
3202 if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
3203 entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
3204 }
3205 let envelope_plaintext = envelope_cache
3206 .get(&envelope.envelope_index)
3207 .expect("inserted above");
3208 let compressed = slice(
3209 envelope_plaintext,
3210 frame.offset_in_envelope as usize,
3211 frame.compressed_size as usize,
3212 "FrameEntry",
3213 )?;
3214 decoded.extend_from_slice(
3215 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
3216 );
3217 }
3218
3219 let offset = file.offset_in_first_frame_plaintext as usize;
3220 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
3221 let group = slice(&decoded, offset, group_len, "FileEntry")?;
3222 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
3223 if member.path != expected_path {
3224 return Err(FormatError::InvalidArchive(
3225 "tar member path does not match FileEntry path",
3226 ));
3227 }
3228 if member.logical_size != file.file_data_size {
3229 return Err(FormatError::InvalidArchive(
3230 "tar member size does not match FileEntry file_data_size",
3231 ));
3232 }
3233 Ok(member.to_owned_member())
3234 }
3235
3236 fn metadata_limits(&self) -> MetadataLimits {
3237 metadata_limits(&self.crypto_header)
3238 }
3239
3240 fn recompute_root_auth_material(
3241 &self,
3242 footer: &RootAuthFooterV1,
3243 ) -> Result<RootAuthMaterial, FormatError> {
3244 if footer.format_version != self.volume_header.format_version {
3245 return Err(FormatError::InvalidArchive(
3246 "RootAuthFooter format_version differs from authenticated VolumeHeader",
3247 ));
3248 }
3249 if footer.volume_format_rev != self.volume_header.volume_format_rev {
3250 return Err(FormatError::InvalidArchive(
3251 "RootAuthFooter volume_format_rev differs from authenticated VolumeHeader",
3252 ));
3253 }
3254 let format_version = self.volume_header.format_version;
3255 let volume_format_rev = self.volume_header.volume_format_rev;
3256 let footer_length = footer.footer_length()?;
3257 let root_auth_descriptor_digest = root_auth_descriptor_digest_for_revision(
3258 format_version,
3259 volume_format_rev,
3260 footer.authenticator_id,
3261 footer.signer_identity_type,
3262 &footer.signer_identity_bytes,
3263 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3264 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3265 })?,
3266 footer_length,
3267 )?;
3268 let signer_identity_digest =
3269 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3270 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
3271 let crypto_pre_hmac_len = self
3272 .crypto_header_bytes
3273 .len()
3274 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
3275 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
3276 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
3277 archive_uuid: self.volume_header.archive_uuid,
3278 session_id: self.volume_header.session_id,
3279 format_version,
3280 volume_format_rev,
3281 stripe_width: self.crypto_header.stripe_width,
3282 total_volumes: self.manifest_footer.total_volumes,
3283 compression_algo: self.crypto_header.compression_algo,
3284 aead_algo: self.crypto_header.aead_algo,
3285 fec_algo: self.crypto_header.fec_algo,
3286 kdf_algo: self.crypto_header.kdf_algo,
3287 crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
3288 chunk_size: self.crypto_header.chunk_size,
3289 envelope_target_size: self.crypto_header.envelope_target_size,
3290 block_size: self.crypto_header.block_size,
3291 fec_data_shards: self.crypto_header.fec_data_shards,
3292 fec_parity_shards: self.crypto_header.fec_parity_shards,
3293 index_fec_data_shards: self.crypto_header.index_fec_data_shards,
3294 index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
3295 index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
3296 index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
3297 volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
3298 bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
3299 has_dictionary: self.crypto_header.has_dictionary,
3300 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
3301 index_root_first_block: self.manifest_footer.index_root_first_block,
3302 index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
3303 index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
3304 index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
3305 index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
3306 root_auth_descriptor_digest,
3307 })?;
3308 let index_root_plaintext = self.index_root.to_bytes();
3309 let index_digest =
3310 index_digest_for_revision(format_version, volume_format_rev, &index_root_plaintext)?;
3311 let shards = self.load_all_index_shards()?;
3312 let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
3313 let fec_layout_digest =
3314 fec_layout_digest_for_revision(format_version, volume_format_rev, &fec_layout_rows)?;
3315 let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
3316 let total_data_block_count = u64::try_from(data_leaves.len())
3317 .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
3318 let data_block_merkle_root =
3319 data_block_merkle_root_for_revision(format_version, volume_format_rev, &data_leaves)?;
3320 let archive_root = archive_root_for_revision(ArchiveRootInputs {
3321 archive_uuid: self.volume_header.archive_uuid,
3322 session_id: self.volume_header.session_id,
3323 format_version,
3324 volume_format_rev,
3325 compression_algo: self.crypto_header.compression_algo,
3326 aead_algo: self.crypto_header.aead_algo,
3327 fec_algo: self.crypto_header.fec_algo,
3328 kdf_algo: self.crypto_header.kdf_algo,
3329 critical_metadata_digest,
3330 index_digest,
3331 fec_layout_digest,
3332 total_data_block_count,
3333 data_block_merkle_root,
3334 root_auth_descriptor_digest,
3335 signer_identity_digest,
3336 })?;
3337 Ok(RootAuthMaterial {
3338 critical_metadata_digest,
3339 index_digest,
3340 fec_layout_digest,
3341 data_block_merkle_root,
3342 signer_identity_digest,
3343 archive_root,
3344 total_data_block_count,
3345 })
3346 }
3347
3348 fn root_auth_fec_layout_rows(
3349 &self,
3350 shards: &[IndexShard],
3351 ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
3352 let mut rows = Vec::new();
3353 rows.push(FecLayoutObjectRow {
3354 object_class: 1,
3355 present: true,
3356 object_id: 0,
3357 first_block_index: self.manifest_footer.index_root_first_block,
3358 data_block_count: self.manifest_footer.index_root_data_block_count,
3359 parity_block_count: self.manifest_footer.index_root_parity_block_count,
3360 encrypted_size: self.manifest_footer.index_root_encrypted_size,
3361 plain_size: self.manifest_footer.index_root_decompressed_size,
3362 });
3363 if self.crypto_header.has_dictionary != 0 {
3364 rows.push(FecLayoutObjectRow {
3365 object_class: 2,
3366 present: true,
3367 object_id: 0,
3368 first_block_index: self.index_root.header.dictionary_first_block,
3369 data_block_count: self.index_root.header.dictionary_data_block_count,
3370 parity_block_count: self.index_root.header.dictionary_parity_block_count,
3371 encrypted_size: self.index_root.header.dictionary_encrypted_size,
3372 plain_size: self.index_root.header.dictionary_decompressed_size,
3373 });
3374 } else {
3375 rows.push(FecLayoutObjectRow {
3376 object_class: 2,
3377 present: false,
3378 object_id: 0,
3379 first_block_index: 0,
3380 data_block_count: 0,
3381 parity_block_count: 0,
3382 encrypted_size: 0,
3383 plain_size: 0,
3384 });
3385 }
3386 for entry in &self.index_root.shards {
3387 rows.push(FecLayoutObjectRow {
3388 object_class: 3,
3389 present: true,
3390 object_id: entry.shard_index,
3391 first_block_index: entry.first_block_index,
3392 data_block_count: entry.data_block_count,
3393 parity_block_count: entry.parity_block_count,
3394 encrypted_size: entry.encrypted_size,
3395 plain_size: entry.decompressed_size,
3396 });
3397 }
3398 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
3399 for shard in shards {
3400 for envelope in &shard.envelopes {
3401 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
3402 {
3403 if existing != *envelope {
3404 return Err(FormatError::InvalidArchive(
3405 "duplicate EnvelopeEntry rows do not match",
3406 ));
3407 }
3408 }
3409 }
3410 }
3411 for envelope in envelopes.values() {
3412 rows.push(FecLayoutObjectRow {
3413 object_class: 4,
3414 present: true,
3415 object_id: envelope.envelope_index,
3416 first_block_index: envelope.first_block_index,
3417 data_block_count: envelope.data_block_count,
3418 parity_block_count: envelope.parity_block_count,
3419 encrypted_size: envelope.encrypted_size,
3420 plain_size: envelope.plaintext_size,
3421 });
3422 }
3423 for entry in &self.index_root.directory_hint_shards {
3424 rows.push(FecLayoutObjectRow {
3425 object_class: 5,
3426 present: true,
3427 object_id: entry.hint_shard_index,
3428 first_block_index: entry.first_block_index,
3429 data_block_count: entry.data_block_count,
3430 parity_block_count: entry.parity_block_count,
3431 encrypted_size: entry.encrypted_size,
3432 plain_size: entry.decompressed_size,
3433 });
3434 }
3435 Ok(rows)
3436 }
3437
3438 fn fec_object_class_shape(
3439 &self,
3440 object_class: u8,
3441 ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
3442 match object_class {
3443 1 => Ok((
3444 BlockKind::IndexRootData,
3445 BlockKind::IndexRootParity,
3446 self.crypto_header.index_root_fec_data_shards,
3447 self.crypto_header.index_root_fec_parity_shards,
3448 )),
3449 2 => Ok((
3450 BlockKind::DictionaryData,
3451 BlockKind::DictionaryParity,
3452 self.crypto_header.index_root_fec_data_shards,
3453 self.crypto_header.index_root_fec_parity_shards,
3454 )),
3455 3 => Ok((
3456 BlockKind::IndexShardData,
3457 BlockKind::IndexShardParity,
3458 self.crypto_header.index_fec_data_shards,
3459 self.crypto_header.index_fec_parity_shards,
3460 )),
3461 4 => Ok((
3462 BlockKind::PayloadData,
3463 BlockKind::PayloadParity,
3464 self.crypto_header.fec_data_shards,
3465 self.crypto_header.fec_parity_shards,
3466 )),
3467 5 => Ok((
3468 BlockKind::DirectoryHintData,
3469 BlockKind::DirectoryHintParity,
3470 self.crypto_header.index_fec_data_shards,
3471 self.crypto_header.index_fec_parity_shards,
3472 )),
3473 _ => Err(FormatError::InvalidArchive(
3474 "unknown root-auth FEC row class",
3475 )),
3476 }
3477 }
3478
3479 fn root_auth_data_block_leaves(
3480 &self,
3481 rows: &[FecLayoutObjectRow],
3482 ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
3483 let block_provider = self.block_provider();
3484 let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
3485 let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
3486 let row = **row;
3487 let (data_kind, parity_kind, data_max, parity_max) =
3488 self.fec_object_class_shape(row.object_class)?;
3489 let extent = ObjectExtent {
3490 first_block_index: row.first_block_index,
3491 data_block_count: row.data_block_count,
3492 parity_block_count: row.parity_block_count,
3493 encrypted_size: row.encrypted_size,
3494 };
3495 let repaired = load_repaired_object_data_shards_from_parts(
3496 &block_provider,
3497 &self.crypto_header,
3498 extent,
3499 data_kind,
3500 parity_kind,
3501 data_max,
3502 parity_max,
3503 )?;
3504 let mut leaves = Vec::new();
3505 for (offset, payload) in repaired.into_iter().enumerate() {
3506 leaves.push(DataBlockMerkleLeaf {
3507 block_index: checked_u64_add(
3508 row.first_block_index,
3509 offset as u64,
3510 "root-auth data block",
3511 )?,
3512 kind: data_kind,
3513 flags: if offset + 1 == row.data_block_count as usize {
3514 0x01
3515 } else {
3516 0
3517 },
3518 payload,
3519 });
3520 }
3521 Ok(leaves)
3522 })?;
3523 let mut leaves = Vec::new();
3524 for mut chunk in chunks {
3525 leaves.append(&mut chunk);
3526 }
3527 leaves.sort_by_key(|leaf| leaf.block_index);
3528 Ok(leaves)
3529 }
3530
3531 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
3532 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
3533 if logical_size > cap {
3534 return Err(FormatError::ReaderUnsupported(
3535 "total extraction size exceeds configured cap",
3536 ));
3537 }
3538 Ok(())
3539 }
3540
3541 fn decompress_payload_frame(
3542 &self,
3543 compressed: &[u8],
3544 decompressed_size: u32,
3545 ) -> Result<Vec<u8>, FormatError> {
3546 let mut decompressor = self.new_payload_decompressor()?;
3547 self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
3548 }
3549
3550 fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
3551 match &self.payload_dictionary {
3552 Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
3553 None => zstd::bulk::Decompressor::new(),
3554 }
3555 .map_err(|_| FormatError::ZstdDecompressionFailure)
3556 }
3557
3558 fn decompress_payload_frame_with(
3559 &self,
3560 decompressor: &mut zstd::bulk::Decompressor<'static>,
3561 compressed: &[u8],
3562 decompressed_size: u32,
3563 ) -> Result<Vec<u8>, FormatError> {
3564 validate_exact_zstd_frame(compressed)?;
3565 let expected = decompressed_size as usize;
3566 let decoded = decompressor
3567 .decompress(compressed, expected)
3568 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
3569 if decoded.len() != expected {
3570 return Err(FormatError::ZstdDecompressedSizeMismatch {
3571 expected,
3572 actual: decoded.len(),
3573 });
3574 }
3575 Ok(decoded)
3576 }
3577
3578 fn validate_encrypted_object_block_ranges(
3579 &self,
3580 envelopes: &BTreeMap<u64, EnvelopeEntry>,
3581 ) -> Result<(), FormatError> {
3582 let mut ranges = Vec::new();
3583 ranges.push(object_block_range(
3584 self.manifest_footer.index_root_first_block,
3585 self.manifest_footer.index_root_data_block_count,
3586 self.manifest_footer.index_root_parity_block_count,
3587 "IndexRoot",
3588 )?);
3589 for shard in &self.index_root.shards {
3590 ranges.push(object_block_range(
3591 shard.first_block_index,
3592 shard.data_block_count,
3593 shard.parity_block_count,
3594 "IndexShard",
3595 )?);
3596 }
3597 for hint in &self.index_root.directory_hint_shards {
3598 ranges.push(object_block_range(
3599 hint.first_block_index,
3600 hint.data_block_count,
3601 hint.parity_block_count,
3602 "DirectoryHintShardEntry",
3603 )?);
3604 }
3605 if self.crypto_header.has_dictionary != 0 {
3606 ranges.push(object_block_range(
3607 self.index_root.header.dictionary_first_block,
3608 self.index_root.header.dictionary_data_block_count,
3609 self.index_root.header.dictionary_parity_block_count,
3610 "dictionary",
3611 )?);
3612 }
3613 for envelope in envelopes.values() {
3614 ranges.push(object_block_range(
3615 envelope.first_block_index,
3616 envelope.data_block_count,
3617 envelope.parity_block_count,
3618 "EnvelopeEntry",
3619 )?);
3620 }
3621 validate_non_overlapping_object_ranges(&mut ranges)?;
3622 if let Some(source) = &self.lazy_blocks {
3623 if source.is_complete_volume_set() {
3624 validate_exact_coverage_ranges_u64(
3625 &mut ranges,
3626 source.total_block_count()?,
3627 "encrypted object block ranges do not cover complete archive exactly",
3628 )?;
3629 }
3630 }
3631 Ok(())
3632 }
3633}
3634
3635impl<'a> DecodedTarMemberGroupReader<'a> {
3636 fn new(
3637 archive: &'a OpenedArchive,
3638 shard: &'a IndexShard,
3639 file: &'a FileEntry,
3640 ) -> Result<Self, FormatError> {
3641 Ok(Self {
3642 archive,
3643 shard,
3644 file,
3645 decompressor: archive.new_payload_decompressor()?,
3646 next_frame_offset: 0,
3647 cached_envelope_index: None,
3648 cached_envelope_plaintext: Vec::new(),
3649 current_frame: Vec::new(),
3650 current_frame_offset: 0,
3651 remaining_group_bytes: file.tar_member_group_size,
3652 })
3653 }
3654
3655 fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3656 while self.current_frame_offset >= self.current_frame.len() {
3657 if self.next_frame_offset >= self.file.frame_count as u64 {
3658 return Err(
3659 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3660 );
3661 }
3662 let frame_index = self
3663 .file
3664 .first_frame_index
3665 .checked_add(self.next_frame_offset)
3666 .ok_or(FormatError::InvalidArchive(
3667 "FileEntry frame range overflow",
3668 ))?;
3669 let frame = frame_by_index(self.shard, frame_index)?;
3670 let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3671 if self.cached_envelope_index != Some(envelope.envelope_index) {
3672 self.cached_envelope_plaintext = self
3673 .archive
3674 .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3675 self.cached_envelope_index = Some(envelope.envelope_index);
3676 }
3677 let compressed = slice(
3678 &self.cached_envelope_plaintext,
3679 frame.offset_in_envelope as usize,
3680 frame.compressed_size as usize,
3681 "FrameEntry",
3682 )?;
3683 let decoded = self.archive.decompress_payload_frame_with(
3684 &mut self.decompressor,
3685 compressed,
3686 frame.decompressed_size,
3687 )?;
3688 let offset = if self.next_frame_offset == 0 {
3689 self.file.offset_in_first_frame_plaintext as usize
3690 } else {
3691 0
3692 };
3693 if offset > decoded.len() {
3694 return Err(FormatError::InvalidArchive(
3695 "offset in first frame is outside the first referenced frame",
3696 )
3697 .into());
3698 }
3699 self.next_frame_offset += 1;
3700 self.current_frame = decoded;
3701 self.current_frame_offset = offset;
3702 }
3703 Ok(())
3704 }
3705}
3706
3707impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3708 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3709 if buf.is_empty() {
3710 return Ok(0);
3711 }
3712 if self.remaining_group_bytes == 0 {
3713 return Ok(0);
3714 }
3715 self.ensure_frame_available()?;
3716 let available = self.current_frame.len() - self.current_frame_offset;
3717 let len = available
3718 .min(buf.len())
3719 .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3720 if len == 0 {
3721 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3722 }
3723 buf[..len].copy_from_slice(
3724 &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3725 );
3726 self.current_frame_offset += len;
3727 self.remaining_group_bytes -= len as u64;
3728 Ok(len)
3729 }
3730}
3731
3732fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3733 shard
3734 .frames
3735 .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3736 .map(|idx| &shard.frames[idx])
3737 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3738}
3739
3740fn envelope_by_index(
3741 shard: &IndexShard,
3742 envelope_index: u64,
3743) -> Result<&EnvelopeEntry, FormatError> {
3744 shard
3745 .envelopes
3746 .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3747 .map(|idx| &shard.envelopes[idx])
3748 .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3749}
3750
3751fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3752 match err {
3753 ExtractError::Format(err) => err,
3754 ExtractError::Output(_) => {
3755 FormatError::FilesystemExtractionFailed("failed to write regular file")
3756 }
3757 }
3758}
3759
3760fn final_index_entry_winners(
3761 shards: &[IndexShard],
3762) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3763 let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3764 for (shard_index, shard) in shards.iter().enumerate() {
3765 for (idx, file) in shard.files.iter().enumerate() {
3766 let path = utf8_path(
3767 shard
3768 .file_path(idx)
3769 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3770 )?;
3771 let start = shard
3772 .tar_member_group_start(idx)
3773 .ok_or(FormatError::InvalidArchive(
3774 "FileEntry tar member start is missing",
3775 ))?;
3776 if let Some(winner) = final_entries.get_mut(&path) {
3777 if start >= winner.start {
3778 winner.start = start;
3779 winner.file_data_size = file.file_data_size;
3780 winner.mtime = file.mtime;
3781 winner.shard_index = shard_index;
3782 winner.file_index = idx;
3783 }
3784 } else {
3785 final_entries.insert(
3786 path,
3787 WinningIndexEntry {
3788 start,
3789 file_data_size: file.file_data_size,
3790 mtime: file.mtime,
3791 shard_index,
3792 file_index: idx,
3793 },
3794 );
3795 }
3796 }
3797 }
3798 Ok(final_entries)
3799}
3800
3801fn archive_index_entry_from_loaded_file(
3802 shard: &IndexShard,
3803 file_index: usize,
3804) -> Result<ArchiveIndexEntry, FormatError> {
3805 let path = utf8_path(
3806 shard
3807 .file_path(file_index)
3808 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3809 )?;
3810 archive_index_entry_from_loaded_file_with_path(path, shard, file_index)
3811}
3812
3813fn archive_index_entry_from_loaded_file_with_path(
3814 path: String,
3815 shard: &IndexShard,
3816 file_index: usize,
3817) -> Result<ArchiveIndexEntry, FormatError> {
3818 let file = shard
3819 .files
3820 .get(file_index)
3821 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3822 let layout = archive_index_entry_layout(shard, file)?;
3823 Ok(ArchiveIndexEntry {
3824 name: archive_entry_name(&path),
3825 path,
3826 file_data_size: file.file_data_size,
3827 kind: file.kind,
3828 mode: file.mode,
3829 mtime: file.mtime,
3830 path_hash: file.path_hash,
3831 tar_member_group_size: file.tar_member_group_size,
3832 first_frame_index: file.first_frame_index,
3833 frame_count: file.frame_count,
3834 offset_in_first_frame_plaintext: file.offset_in_first_frame_plaintext,
3835 layout,
3836 })
3837}
3838
3839fn archive_index_entry_layout(
3840 shard: &IndexShard,
3841 file: &FileEntry,
3842) -> Result<ArchiveIndexEntryLayout, FormatError> {
3843 let frames = frame_range_for_file(shard, file)?;
3844 if let [frame] = frames {
3845 let envelope = envelope_by_index(shard, frame.envelope_index)?;
3846 return Ok(ArchiveIndexEntryLayout {
3847 compressed_size: frame.compressed_size as u64,
3848 decompressed_frame_size: frame.decompressed_size as u64,
3849 envelope_count: 1,
3850 first_envelope_index: Some(envelope.envelope_index),
3851 last_envelope_index: Some(envelope.envelope_index),
3852 first_payload_block_index: Some(envelope.first_block_index),
3853 payload_data_block_count: envelope.data_block_count as u64,
3854 payload_parity_block_count: envelope.parity_block_count as u64,
3855 payload_encrypted_size: envelope.encrypted_size as u64,
3856 });
3857 }
3858
3859 let mut compressed_size = 0u64;
3860 let mut decompressed_frame_size = 0u64;
3861 let mut envelope_indexes = BTreeSet::new();
3862
3863 for frame in frames {
3864 compressed_size = checked_u64_add(
3865 compressed_size,
3866 frame.compressed_size as u64,
3867 "ArchiveIndexEntry.compressed_size",
3868 )?;
3869 decompressed_frame_size = checked_u64_add(
3870 decompressed_frame_size,
3871 frame.decompressed_size as u64,
3872 "ArchiveIndexEntry.decompressed_frame_size",
3873 )?;
3874 envelope_indexes.insert(frame.envelope_index);
3875 }
3876
3877 let mut first_payload_block_index = None::<u64>;
3878 let mut payload_data_block_count = 0u64;
3879 let mut payload_parity_block_count = 0u64;
3880 let mut payload_encrypted_size = 0u64;
3881
3882 for envelope_index in &envelope_indexes {
3883 let envelope = envelope_by_index(shard, *envelope_index)?;
3884 first_payload_block_index = Some(
3885 first_payload_block_index
3886 .map(|existing| existing.min(envelope.first_block_index))
3887 .unwrap_or(envelope.first_block_index),
3888 );
3889 payload_data_block_count = checked_u64_add(
3890 payload_data_block_count,
3891 envelope.data_block_count as u64,
3892 "ArchiveIndexEntry.payload_data_block_count",
3893 )?;
3894 payload_parity_block_count = checked_u64_add(
3895 payload_parity_block_count,
3896 envelope.parity_block_count as u64,
3897 "ArchiveIndexEntry.payload_parity_block_count",
3898 )?;
3899 payload_encrypted_size = checked_u64_add(
3900 payload_encrypted_size,
3901 envelope.encrypted_size as u64,
3902 "ArchiveIndexEntry.payload_encrypted_size",
3903 )?;
3904 }
3905
3906 Ok(ArchiveIndexEntryLayout {
3907 compressed_size,
3908 decompressed_frame_size,
3909 envelope_count: u32::try_from(envelope_indexes.len()).map_err(|_| {
3910 FormatError::InvalidArchive("ArchiveIndexEntry envelope count overflow")
3911 })?,
3912 first_envelope_index: envelope_indexes.iter().next().copied(),
3913 last_envelope_index: envelope_indexes.iter().next_back().copied(),
3914 first_payload_block_index,
3915 payload_data_block_count,
3916 payload_parity_block_count,
3917 payload_encrypted_size,
3918 })
3919}
3920
3921fn archive_entry_name(path: &str) -> String {
3922 path.rsplit('/').next().unwrap_or(path).to_owned()
3923}
3924
3925#[derive(Debug)]
3926struct ParsedSeekableVolume {
3927 volume_header: VolumeHeader,
3928 crypto_header: CryptoHeaderFixed,
3929 crypto_header_bytes: Vec<u8>,
3930 key_wrap_table_bytes: Option<Vec<u8>>,
3931 subkeys: Subkeys,
3932 manifest_footer: Option<ManifestFooter>,
3933 manifest_footer_error: Option<FormatError>,
3934 root_auth_footer: Option<RootAuthFooterV1>,
3935 root_auth_footer_bytes: Option<Vec<u8>>,
3936 volume_trailer: VolumeTrailer,
3937 blocks: BTreeMap<u64, BlockRecord>,
3938 erased_block_indices: BTreeSet<u64>,
3939}
3940
3941struct ParsedSeekableReadAtVolume {
3942 reader: Arc<dyn ArchiveReadAt>,
3943 volume_header: VolumeHeader,
3944 crypto_header: CryptoHeaderFixed,
3945 crypto_header_bytes: Vec<u8>,
3946 key_wrap_table_bytes: Option<Vec<u8>>,
3947 subkeys: Subkeys,
3948 manifest_footer: Option<ManifestFooter>,
3949 manifest_footer_error: Option<FormatError>,
3950 root_auth_footer: Option<RootAuthFooterV1>,
3951 root_auth_footer_bytes: Option<Vec<u8>>,
3952 volume_trailer: VolumeTrailer,
3953 block_records_start: u64,
3954}
3955
3956struct ParsedOpenPrefix {
3957 volume_header: VolumeHeader,
3958 crypto_header: CryptoHeaderFixed,
3959 crypto_header_bytes: Vec<u8>,
3960 key_wrap_table_bytes: Option<Vec<u8>>,
3961 block_records_start: u64,
3962 subkeys: Subkeys,
3963}
3964
3965struct ParsedReadAtOpenPrefix {
3966 volume_header: VolumeHeader,
3967 crypto_header: CryptoHeaderFixed,
3968 crypto_header_bytes: Vec<u8>,
3969 key_wrap_table_bytes: Option<Vec<u8>>,
3970 block_records_start: u64,
3971 subkeys: Subkeys,
3972}
3973
3974pub(crate) struct StartupKeyWrapTable {
3975 pub(crate) table: KeyWrapTableV1,
3976 pub(crate) bytes: Vec<u8>,
3977 pub(crate) block_records_start: u64,
3978}
3979
3980pub(crate) fn startup_block_records_start(
3981 volume_header: &VolumeHeader,
3982 kdf_params: &KdfParams,
3983 read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3984) -> Result<u64, FormatError> {
3985 Ok(
3986 startup_key_wrap_table(volume_header, kdf_params, read_key_wrap_table)?
3987 .map(|startup| startup.block_records_start)
3988 .unwrap_or_else(|| {
3989 volume_header.crypto_header_offset as u64
3990 + volume_header.crypto_header_length as u64
3991 }),
3992 )
3993}
3994
3995pub(crate) fn startup_key_wrap_table(
3996 volume_header: &VolumeHeader,
3997 kdf_params: &KdfParams,
3998 mut read_key_wrap_table: impl FnMut(u64, usize) -> Result<Vec<u8>, FormatError>,
3999) -> Result<Option<StartupKeyWrapTable>, FormatError> {
4000 let crypto_end = checked_u64_add(
4001 volume_header.crypto_header_offset as u64,
4002 volume_header.crypto_header_length as u64,
4003 "CryptoHeader",
4004 )?;
4005 let &KdfParams::RecipientWrap {
4006 key_wrap_table_length,
4007 ..
4008 } = kdf_params
4009 else {
4010 return Ok(None);
4011 };
4012 if volume_header.volume_format_rev != VOLUME_FORMAT_REV_44 {
4013 return Err(FormatError::InvalidArchive(
4014 "RecipientWrap KdfParams require volume_format_rev 44",
4015 ));
4016 }
4017 let key_wrap_table_length_usize =
4018 to_usize(u64::from(key_wrap_table_length), "KeyWrapTableV1 length")?;
4019 let key_wrap_table_bytes = read_key_wrap_table(crypto_end, key_wrap_table_length_usize)?;
4020 Ok(Some(parse_startup_key_wrap_table_bytes(
4021 volume_header,
4022 kdf_params,
4023 key_wrap_table_bytes,
4024 )?))
4025}
4026
4027pub(crate) fn parse_startup_key_wrap_table_bytes(
4028 volume_header: &VolumeHeader,
4029 kdf_params: &KdfParams,
4030 key_wrap_table_bytes: Vec<u8>,
4031) -> Result<StartupKeyWrapTable, FormatError> {
4032 let crypto_end = checked_u64_add(
4033 volume_header.crypto_header_offset as u64,
4034 volume_header.crypto_header_length as u64,
4035 "CryptoHeader",
4036 )?;
4037 let KdfParams::RecipientWrap {
4038 key_wrap_table_length,
4039 key_wrap_table_record_count,
4040 key_wrap_table_digest,
4041 ..
4042 } = kdf_params
4043 else {
4044 return Err(FormatError::KeyMaterialMismatch);
4045 };
4046 let key_wrap_table = KeyWrapTableV1::parse(
4047 &key_wrap_table_bytes,
4048 &volume_header.archive_uuid,
4049 &volume_header.session_id,
4050 *key_wrap_table_length,
4051 *key_wrap_table_record_count,
4052 )?;
4053 if compute_key_wrap_table_digest(*key_wrap_table_length, &key_wrap_table_bytes)
4054 != *key_wrap_table_digest
4055 {
4056 return Err(FormatError::IntegrityDigestMismatch {
4057 structure: "KeyWrapTableV1",
4058 });
4059 }
4060 let block_records_start = checked_u64_add(
4061 crypto_end,
4062 key_wrap_table.table_length as u64,
4063 "KeyWrapTableV1",
4064 )?;
4065 Ok(StartupKeyWrapTable {
4066 table: key_wrap_table,
4067 bytes: key_wrap_table_bytes,
4068 block_records_start,
4069 })
4070}
4071
4072fn parse_seekable_volume(
4073 bytes: &[u8],
4074 master_key: &MasterKey,
4075 options: ReaderOptions,
4076) -> Result<ParsedSeekableVolume, FormatError> {
4077 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
4078 return Err(FormatError::InvalidLength {
4079 structure: "archive",
4080 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4081 actual: bytes.len(),
4082 });
4083 }
4084
4085 let prefix = match parse_open_prefix(bytes, master_key) {
4086 Ok(prefix) => prefix,
4087 Err(prefix_err) => {
4088 if matches!(
4089 prefix_err,
4090 FormatError::UnsupportedVolumeFormatRevision { .. }
4091 ) {
4092 return Err(prefix_err);
4093 }
4094 if matches!(prefix_err, FormatError::KeyMaterialMismatch)
4095 && prefix_uses_recipient_wrap(bytes)
4096 {
4097 return Err(prefix_err);
4098 }
4099 return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
4100 .or(Err(prefix_err));
4101 }
4102 };
4103 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4104 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4105 Ok(parsed) => Ok(parsed),
4106 Err(prefix_err) => {
4107 match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
4108 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4109 Ok(recovered)
4110 }
4111 Ok(_) | Err(_) => Err(prefix_err),
4112 }
4113 }
4114 }
4115}
4116
4117fn parse_seekable_volume_with_recipient_wrap_resolver<F>(
4118 bytes: &[u8],
4119 resolver: &mut F,
4120 options: ReaderOptions,
4121) -> Result<ParsedSeekableVolume, FormatError>
4122where
4123 F: FnMut(
4124 RecipientWrapRecordContext<'_>,
4125 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4126{
4127 let prefix = match parse_open_prefix_with_recipient_wrap_resolver(bytes, resolver) {
4128 Ok(prefix) => prefix,
4129 Err(prefix_err) => {
4130 if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4131 return Err(prefix_err);
4132 }
4133 return parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4134 bytes, resolver, options,
4135 )
4136 .or(Err(prefix_err));
4137 }
4138 };
4139 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4140 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
4141 Ok(parsed) => Ok(parsed),
4142 Err(prefix_err) => {
4143 match parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4144 bytes, resolver, options,
4145 ) {
4146 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4147 Ok(recovered)
4148 }
4149 Ok(_) | Err(_) => Err(prefix_err),
4150 }
4151 }
4152 }
4153}
4154
4155fn parse_seekable_volume_with_prefix(
4156 bytes: &[u8],
4157 prefix: ParsedOpenPrefix,
4158 options: ReaderOptions,
4159) -> Result<ParsedSeekableVolume, FormatError> {
4160 let ParsedOpenPrefix {
4161 volume_header,
4162 crypto_header,
4163 crypto_header_bytes,
4164 key_wrap_table_bytes,
4165 block_records_start,
4166 subkeys,
4167 } = prefix;
4168 let crypto_bytes = crypto_header_bytes.as_slice();
4169
4170 let terminal = locate_v41_terminal(
4171 bytes,
4172 KeyHoldingTerminalContext {
4173 subkeys: &subkeys,
4174 volume_header: &volume_header,
4175 crypto_header: &crypto_header,
4176 crypto_header_bytes: crypto_bytes,
4177 },
4178 options,
4179 )?;
4180 finish_parse_seekable_volume(
4181 bytes,
4182 volume_header,
4183 crypto_header,
4184 crypto_header_bytes,
4185 key_wrap_table_bytes,
4186 block_records_start,
4187 subkeys,
4188 terminal,
4189 )
4190}
4191
4192fn parse_seekable_volume_from_recovered_terminal(
4193 bytes: &[u8],
4194 master_key: &MasterKey,
4195 options: ReaderOptions,
4196) -> Result<ParsedSeekableVolume, FormatError> {
4197 let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
4198 parse_volume_format_dispatch(&authority.volume_header)?;
4199 let startup_key_wrap_table = startup_key_wrap_table(
4200 &authority.volume_header,
4201 &authority.kdf_params,
4202 |start, length| {
4203 let start = to_usize(start, "KeyWrapTableV1")?;
4204 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4205 },
4206 )?;
4207 let crypto_end = checked_u64_add(
4208 authority.volume_header.crypto_header_offset as u64,
4209 authority.volume_header.crypto_header_length as u64,
4210 "CryptoHeader",
4211 )?;
4212 let (key_wrap_table_bytes, block_records_start) = startup_key_wrap_table
4213 .map(|startup| (Some(startup.bytes), startup.block_records_start))
4214 .unwrap_or((None, crypto_end));
4215 finish_parse_seekable_volume(
4216 bytes,
4217 authority.volume_header,
4218 authority.crypto_header,
4219 authority.crypto_header_bytes,
4220 key_wrap_table_bytes,
4221 block_records_start,
4222 authority.subkeys,
4223 authority.terminal,
4224 )
4225}
4226
4227fn parse_seekable_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4228 bytes: &[u8],
4229 resolver: &mut F,
4230 options: ReaderOptions,
4231) -> Result<ParsedSeekableVolume, FormatError>
4232where
4233 F: FnMut(
4234 RecipientWrapRecordContext<'_>,
4235 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4236{
4237 let authority = locate_v41_recipient_wrap_terminal_authority(bytes, resolver, options)?;
4238 finish_parse_seekable_volume(
4239 bytes,
4240 authority.volume_header,
4241 authority.crypto_header,
4242 authority.crypto_header_bytes,
4243 Some(authority.key_wrap_table_bytes),
4244 authority.block_records_start,
4245 authority.subkeys,
4246 authority.terminal,
4247 )
4248}
4249
4250fn recipient_wrap_prefix_error_precludes_recovery(error: &FormatError) -> bool {
4251 matches!(
4252 error,
4253 FormatError::UnsupportedVolumeFormatRevision { .. }
4254 | FormatError::ReaderUnsupported(_)
4255 | FormatError::InvalidArchive(
4256 "VolumeHeader and CryptoHeader stripe_width differ"
4257 | "fec_parity_shards does not match v41 compute_parity"
4258 | "index_fec_parity_shards does not match v41 compute_parity"
4259 | "index_root_fec_parity_shards does not match v41 compute_parity"
4260 )
4261 )
4262}
4263
4264fn parse_open_prefix(
4265 bytes: &[u8],
4266 master_key: &MasterKey,
4267) -> Result<ParsedOpenPrefix, FormatError> {
4268 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4269 parse_volume_format_dispatch(&volume_header)?;
4270 let crypto_start = volume_header.crypto_header_offset as usize;
4271 let crypto_len = volume_header.crypto_header_length as usize;
4272 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4273 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4274 if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4275 return Err(FormatError::KeyMaterialMismatch);
4276 }
4277 let subkeys = subkeys_for_open(
4278 Some(master_key),
4279 parsed_crypto.fixed.aead_algo,
4280 &volume_header.archive_uuid,
4281 &volume_header.session_id,
4282 )?;
4283 verify_integrity_tag(
4284 HmacDomain::CryptoHeader,
4285 parsed_crypto.fixed.aead_algo,
4286 volume_header.volume_format_rev,
4287 Some(&subkeys.mac_key),
4288 &volume_header.archive_uuid,
4289 &volume_header.session_id,
4290 parsed_crypto.hmac_covered_bytes,
4291 &parsed_crypto.header_hmac,
4292 )?;
4293 parsed_crypto.validate_extension_semantics()?;
4294 validate_seekable_supported_volume(
4295 &volume_header,
4296 &parsed_crypto.fixed,
4297 &parsed_crypto.extensions,
4298 )?;
4299 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4300 let block_records_start = startup_block_records_start(
4301 &volume_header,
4302 &parsed_crypto.kdf_params,
4303 |start, length| {
4304 let start = to_usize(start, "KeyWrapTableV1")?;
4305 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4306 },
4307 )?;
4308 let crypto_header = parsed_crypto.fixed.clone();
4309 Ok(ParsedOpenPrefix {
4310 volume_header,
4311 crypto_header,
4312 crypto_header_bytes: crypto_bytes.to_vec(),
4313 key_wrap_table_bytes: None,
4314 block_records_start,
4315 subkeys,
4316 })
4317}
4318
4319fn prefix_uses_recipient_wrap(bytes: &[u8]) -> bool {
4320 let Ok(volume_header_bytes) = slice(bytes, 0, VOLUME_HEADER_LEN, "archive") else {
4321 return false;
4322 };
4323 let Ok(volume_header) = VolumeHeader::parse(volume_header_bytes) else {
4324 return false;
4325 };
4326 let crypto_start = volume_header.crypto_header_offset as usize;
4327 let crypto_len = volume_header.crypto_header_length as usize;
4328 let Ok(crypto_bytes) = slice(bytes, crypto_start, crypto_len, "CryptoHeader") else {
4329 return false;
4330 };
4331 let Ok(parsed_crypto) = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)
4332 else {
4333 return false;
4334 };
4335 matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4336}
4337
4338fn parse_open_prefix_with_recipient_wrap_resolver<F>(
4339 bytes: &[u8],
4340 resolver: &mut F,
4341) -> Result<ParsedOpenPrefix, FormatError>
4342where
4343 F: FnMut(
4344 RecipientWrapRecordContext<'_>,
4345 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4346{
4347 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
4348 parse_volume_format_dispatch(&volume_header)?;
4349 let crypto_start = volume_header.crypto_header_offset as usize;
4350 let crypto_len = volume_header.crypto_header_length as usize;
4351 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
4352 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
4353 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4354 || !parsed_crypto.fixed.aead_algo.is_encrypted()
4355 {
4356 return Err(FormatError::KeyMaterialMismatch);
4357 }
4358
4359 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4360 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4361
4362 let startup_key_wrap_table = startup_key_wrap_table(
4363 &volume_header,
4364 &parsed_crypto.kdf_params,
4365 |start, length| {
4366 let start = to_usize(start, "KeyWrapTableV1")?;
4367 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
4368 },
4369 )?
4370 .ok_or(FormatError::KeyMaterialMismatch)?;
4371 let key_wrap_table = startup_key_wrap_table.table;
4372 let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4373 let block_records_start = startup_key_wrap_table.block_records_start;
4374
4375 let subkeys = recipient_wrap_subkeys_from_table(
4376 &volume_header,
4377 &parsed_crypto,
4378 &key_wrap_table,
4379 resolver,
4380 )?;
4381 parsed_crypto.validate_extension_semantics()?;
4382 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4383
4384 let crypto_header = parsed_crypto.fixed.clone();
4385 Ok(ParsedOpenPrefix {
4386 volume_header,
4387 crypto_header,
4388 crypto_header_bytes: crypto_bytes.to_vec(),
4389 key_wrap_table_bytes,
4390 block_records_start,
4391 subkeys,
4392 })
4393}
4394
4395pub(crate) fn recipient_wrap_subkeys_from_table<F>(
4396 volume_header: &VolumeHeader,
4397 parsed_crypto: &CryptoHeader<'_>,
4398 key_wrap_table: &KeyWrapTableV1,
4399 resolver: &mut F,
4400) -> Result<Subkeys, FormatError>
4401where
4402 F: FnMut(
4403 RecipientWrapRecordContext<'_>,
4404 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>
4405 + ?Sized,
4406{
4407 let archive_identity = RecipientWrapArchiveIdentity {
4408 archive_uuid: volume_header.archive_uuid,
4409 session_id: volume_header.session_id,
4410 format_version: volume_header.format_version,
4411 volume_format_rev: volume_header.volume_format_rev,
4412 };
4413
4414 for record in &key_wrap_table.recipient_records {
4415 let candidates = resolver(RecipientWrapRecordContext {
4416 archive_identity,
4417 record,
4418 })?;
4419 for candidate in candidates {
4420 let master_key = MasterKey::from_raw_key(&candidate)?;
4421 let subkeys = subkeys_for_open(
4422 Some(&master_key),
4423 parsed_crypto.fixed.aead_algo,
4424 &volume_header.archive_uuid,
4425 &volume_header.session_id,
4426 )?;
4427 if verify_integrity_tag(
4428 HmacDomain::CryptoHeader,
4429 parsed_crypto.fixed.aead_algo,
4430 volume_header.volume_format_rev,
4431 Some(&subkeys.mac_key),
4432 &volume_header.archive_uuid,
4433 &volume_header.session_id,
4434 parsed_crypto.hmac_covered_bytes,
4435 &parsed_crypto.header_hmac,
4436 )
4437 .is_ok()
4438 {
4439 return Ok(subkeys);
4440 }
4441 }
4442 }
4443 Err(FormatError::KeyMaterialMismatch)
4444}
4445
4446#[allow(clippy::too_many_arguments)]
4447fn finish_parse_seekable_volume(
4448 bytes: &[u8],
4449 volume_header: VolumeHeader,
4450 crypto_header: CryptoHeaderFixed,
4451 crypto_header_bytes: Vec<u8>,
4452 key_wrap_table_bytes: Option<Vec<u8>>,
4453 block_records_start: u64,
4454 subkeys: Subkeys,
4455 terminal: V41Terminal,
4456) -> Result<ParsedSeekableVolume, FormatError> {
4457 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
4458 let volume_trailer = terminal.volume_trailer.clone();
4459 validate_trailer_identity(&volume_header, &volume_trailer)?;
4460
4461 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
4462 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
4463 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4464 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
4465 || volume_trailer
4466 .root_auth_footer_offset
4467 .checked_add(volume_trailer.root_auth_footer_length as u64)
4468 .ok_or(FormatError::InvalidArchive(
4469 "RootAuthFooter terminal boundary overflow",
4470 ))?
4471 != trailer_offset as u64
4472 {
4473 return Err(FormatError::InvalidArchive(
4474 "RootAuthFooter does not sit before selected trailer",
4475 ));
4476 }
4477 } else if manifest_end != trailer_offset {
4478 return Err(FormatError::InvalidArchive(
4479 "ManifestFooter does not end at selected trailer",
4480 ));
4481 }
4482 let manifest_bytes = &terminal.manifest_footer_bytes;
4483 let (manifest_footer, manifest_footer_error) =
4484 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4485 {
4486 Ok(footer) => (Some(footer), None),
4487 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4488 Err(err) => return Err(err),
4489 };
4490
4491 let block_region = parse_block_region(
4492 bytes,
4493 to_usize(block_records_start, "BlockRecord")?,
4494 manifest_offset,
4495 crypto_header.block_size as usize,
4496 &volume_header,
4497 &volume_trailer,
4498 )?;
4499
4500 Ok(ParsedSeekableVolume {
4501 volume_header,
4502 crypto_header,
4503 crypto_header_bytes,
4504 key_wrap_table_bytes,
4505 subkeys,
4506 manifest_footer,
4507 manifest_footer_error,
4508 root_auth_footer: terminal.root_auth_footer,
4509 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4510 volume_trailer,
4511 blocks: block_region.blocks,
4512 erased_block_indices: block_region.erased_block_indices,
4513 })
4514}
4515
4516fn parse_seekable_read_at_volume(
4517 reader: Arc<dyn ArchiveReadAt>,
4518 master_key: &MasterKey,
4519 options: ReaderOptions,
4520) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4521 let observed_len = reader.len()?;
4522 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4523 return Err(FormatError::InvalidLength {
4524 structure: "archive",
4525 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4526 actual: to_usize(observed_len, "archive")?,
4527 });
4528 }
4529
4530 let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
4531 Ok(prefix) => prefix,
4532 Err(prefix_err) => {
4533 if matches!(
4534 prefix_err,
4535 FormatError::UnsupportedVolumeFormatRevision { .. }
4536 ) {
4537 return Err(prefix_err);
4538 }
4539 return parse_seekable_read_at_volume_from_recovered_terminal(
4540 reader,
4541 observed_len,
4542 master_key,
4543 options,
4544 )
4545 .or(Err(prefix_err));
4546 }
4547 };
4548 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4549 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4550 Ok(parsed) => Ok(parsed),
4551 Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
4552 reader,
4553 observed_len,
4554 master_key,
4555 options,
4556 ) {
4557 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4558 Ok(recovered)
4559 }
4560 Ok(_) | Err(_) => Err(prefix_err),
4561 },
4562 }
4563}
4564
4565fn parse_seekable_read_at_volume_with_recipient_wrap_resolver<F>(
4566 reader: Arc<dyn ArchiveReadAt>,
4567 resolver: &mut F,
4568 options: ReaderOptions,
4569) -> Result<ParsedSeekableReadAtVolume, FormatError>
4570where
4571 F: FnMut(
4572 RecipientWrapRecordContext<'_>,
4573 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4574{
4575 let observed_len = reader.len()?;
4576 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
4577 return Err(FormatError::InvalidLength {
4578 structure: "archive",
4579 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
4580 actual: to_usize(observed_len, "archive")?,
4581 });
4582 }
4583
4584 let prefix = match parse_read_at_open_prefix_with_recipient_wrap_resolver(
4585 reader.as_ref(),
4586 resolver,
4587 ) {
4588 Ok(prefix) => prefix,
4589 Err(prefix_err) => {
4590 if recipient_wrap_prefix_error_precludes_recovery(&prefix_err) {
4591 return Err(prefix_err);
4592 }
4593 return parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4594 reader,
4595 observed_len,
4596 resolver,
4597 options,
4598 )
4599 .or(Err(prefix_err));
4600 }
4601 };
4602 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
4603 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
4604 Ok(parsed) => Ok(parsed),
4605 Err(prefix_err) => {
4606 match parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal(
4607 reader,
4608 observed_len,
4609 resolver,
4610 options,
4611 ) {
4612 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
4613 Ok(recovered)
4614 }
4615 Ok(_) | Err(_) => Err(prefix_err),
4616 }
4617 }
4618 }
4619}
4620
4621fn parse_seekable_read_at_volume_with_prefix(
4622 reader: Arc<dyn ArchiveReadAt>,
4623 observed_len: u64,
4624 prefix: ParsedReadAtOpenPrefix,
4625 options: ReaderOptions,
4626) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4627 let ParsedReadAtOpenPrefix {
4628 volume_header,
4629 crypto_header,
4630 crypto_header_bytes,
4631 key_wrap_table_bytes,
4632 block_records_start,
4633 subkeys,
4634 } = prefix;
4635
4636 let terminal = locate_v41_terminal_read_at(
4637 reader.as_ref(),
4638 observed_len,
4639 KeyHoldingTerminalContext {
4640 subkeys: &subkeys,
4641 volume_header: &volume_header,
4642 crypto_header: &crypto_header,
4643 crypto_header_bytes: &crypto_header_bytes,
4644 },
4645 options,
4646 )?;
4647 finish_parse_seekable_read_at_volume(
4648 reader,
4649 volume_header,
4650 crypto_header,
4651 crypto_header_bytes,
4652 key_wrap_table_bytes,
4653 block_records_start,
4654 subkeys,
4655 terminal,
4656 )
4657}
4658
4659fn parse_seekable_read_at_volume_from_recovered_terminal(
4660 reader: Arc<dyn ArchiveReadAt>,
4661 observed_len: u64,
4662 master_key: &MasterKey,
4663 options: ReaderOptions,
4664) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4665 let authority =
4666 locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
4667 parse_volume_format_dispatch(&authority.volume_header)?;
4668 if matches!(authority.kdf_params, KdfParams::RecipientWrap { .. }) {
4669 return Err(FormatError::KeyMaterialMismatch);
4670 }
4671 let block_records_start = startup_block_records_start(
4672 &authority.volume_header,
4673 &authority.kdf_params,
4674 |start, length| read_at_vec(reader.as_ref(), start, length, "KeyWrapTableV1"),
4675 )?;
4676 finish_parse_seekable_read_at_volume(
4677 reader,
4678 authority.volume_header,
4679 authority.crypto_header,
4680 authority.crypto_header_bytes,
4681 None,
4682 block_records_start,
4683 authority.subkeys,
4684 authority.terminal,
4685 )
4686}
4687
4688fn parse_seekable_read_at_volume_with_recipient_wrap_resolver_from_recovered_terminal<F>(
4689 reader: Arc<dyn ArchiveReadAt>,
4690 observed_len: u64,
4691 resolver: &mut F,
4692 options: ReaderOptions,
4693) -> Result<ParsedSeekableReadAtVolume, FormatError>
4694where
4695 F: FnMut(
4696 RecipientWrapRecordContext<'_>,
4697 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4698{
4699 let authority = locate_v41_recipient_wrap_terminal_authority_read_at(
4700 reader.as_ref(),
4701 observed_len,
4702 resolver,
4703 options,
4704 )?;
4705 finish_parse_seekable_read_at_volume(
4706 reader,
4707 authority.volume_header,
4708 authority.crypto_header,
4709 authority.crypto_header_bytes,
4710 Some(authority.key_wrap_table_bytes),
4711 authority.block_records_start,
4712 authority.subkeys,
4713 authority.terminal,
4714 )
4715}
4716
4717fn parse_read_at_open_prefix(
4718 reader: &dyn ArchiveReadAt,
4719 master_key: &MasterKey,
4720) -> Result<ParsedReadAtOpenPrefix, FormatError> {
4721 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4722 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4723 parse_volume_format_dispatch(&volume_header)?;
4724 let crypto_start = volume_header.crypto_header_offset as u64;
4725 let crypto_len = volume_header.crypto_header_length as u64;
4726 let crypto_bytes = read_at_vec(
4727 reader,
4728 crypto_start,
4729 to_usize(crypto_len, "CryptoHeader")?,
4730 "CryptoHeader",
4731 )?;
4732 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4733 if matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. }) {
4734 return Err(FormatError::KeyMaterialMismatch);
4735 }
4736 let subkeys = subkeys_for_open(
4737 Some(master_key),
4738 parsed_crypto.fixed.aead_algo,
4739 &volume_header.archive_uuid,
4740 &volume_header.session_id,
4741 )?;
4742 verify_integrity_tag(
4743 HmacDomain::CryptoHeader,
4744 parsed_crypto.fixed.aead_algo,
4745 volume_header.volume_format_rev,
4746 Some(&subkeys.mac_key),
4747 &volume_header.archive_uuid,
4748 &volume_header.session_id,
4749 parsed_crypto.hmac_covered_bytes,
4750 &parsed_crypto.header_hmac,
4751 )?;
4752 parsed_crypto.validate_extension_semantics()?;
4753 validate_seekable_supported_volume(
4754 &volume_header,
4755 &parsed_crypto.fixed,
4756 &parsed_crypto.extensions,
4757 )?;
4758 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4759 let block_records_start = startup_block_records_start(
4760 &volume_header,
4761 &parsed_crypto.kdf_params,
4762 |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4763 )?;
4764 let crypto_header = parsed_crypto.fixed.clone();
4765 drop(parsed_crypto);
4766 Ok(ParsedReadAtOpenPrefix {
4767 volume_header,
4768 crypto_header,
4769 crypto_header_bytes: crypto_bytes,
4770 key_wrap_table_bytes: None,
4771 block_records_start,
4772 subkeys,
4773 })
4774}
4775
4776fn parse_read_at_open_prefix_with_recipient_wrap_resolver<F>(
4777 reader: &dyn ArchiveReadAt,
4778 resolver: &mut F,
4779) -> Result<ParsedReadAtOpenPrefix, FormatError>
4780where
4781 F: FnMut(
4782 RecipientWrapRecordContext<'_>,
4783 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
4784{
4785 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
4786 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
4787 parse_volume_format_dispatch(&volume_header)?;
4788 let crypto_start = volume_header.crypto_header_offset as u64;
4789 let crypto_len = volume_header.crypto_header_length as u64;
4790 let crypto_bytes = read_at_vec(
4791 reader,
4792 crypto_start,
4793 to_usize(crypto_len, "CryptoHeader")?,
4794 "CryptoHeader",
4795 )?;
4796 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
4797 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
4798 || !parsed_crypto.fixed.aead_algo.is_encrypted()
4799 {
4800 return Err(FormatError::KeyMaterialMismatch);
4801 }
4802
4803 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
4804 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
4805
4806 let startup_key_wrap_table = startup_key_wrap_table(
4807 &volume_header,
4808 &parsed_crypto.kdf_params,
4809 |start, length| read_at_vec(reader, start, length, "KeyWrapTableV1"),
4810 )?
4811 .ok_or(FormatError::KeyMaterialMismatch)?;
4812 let key_wrap_table = startup_key_wrap_table.table;
4813 let key_wrap_table_bytes = Some(startup_key_wrap_table.bytes);
4814 let block_records_start = startup_key_wrap_table.block_records_start;
4815
4816 let subkeys = recipient_wrap_subkeys_from_table(
4817 &volume_header,
4818 &parsed_crypto,
4819 &key_wrap_table,
4820 resolver,
4821 )?;
4822 parsed_crypto.validate_extension_semantics()?;
4823 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
4824
4825 let crypto_header = parsed_crypto.fixed.clone();
4826 Ok(ParsedReadAtOpenPrefix {
4827 volume_header,
4828 crypto_header,
4829 crypto_header_bytes: crypto_bytes,
4830 key_wrap_table_bytes,
4831 block_records_start,
4832 subkeys,
4833 })
4834}
4835
4836#[allow(clippy::too_many_arguments)]
4837fn finish_parse_seekable_read_at_volume(
4838 reader: Arc<dyn ArchiveReadAt>,
4839 volume_header: VolumeHeader,
4840 crypto_header: CryptoHeaderFixed,
4841 crypto_header_bytes: Vec<u8>,
4842 key_wrap_table_bytes: Option<Vec<u8>>,
4843 block_records_start: u64,
4844 subkeys: Subkeys,
4845 terminal: V41Terminal,
4846) -> Result<ParsedSeekableReadAtVolume, FormatError> {
4847 let volume_trailer = terminal.volume_trailer.clone();
4848 validate_trailer_identity(&volume_header, &volume_trailer)?;
4849
4850 let manifest_offset = volume_trailer.manifest_footer_offset;
4851 let manifest_end = checked_u64_add(
4852 manifest_offset,
4853 MANIFEST_FOOTER_LEN as u64,
4854 "ManifestFooter",
4855 )?;
4856 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
4857 if volume_trailer.root_auth_footer_offset != manifest_end
4858 || volume_trailer
4859 .root_auth_footer_offset
4860 .checked_add(volume_trailer.root_auth_footer_length as u64)
4861 .ok_or(FormatError::InvalidArchive(
4862 "RootAuthFooter terminal boundary overflow",
4863 ))?
4864 != terminal.image.volume_trailer_offset
4865 {
4866 return Err(FormatError::InvalidArchive(
4867 "RootAuthFooter does not sit before selected trailer",
4868 ));
4869 }
4870 } else if manifest_end != terminal.image.volume_trailer_offset {
4871 return Err(FormatError::InvalidArchive(
4872 "ManifestFooter does not end at selected trailer",
4873 ));
4874 }
4875 validate_seekable_block_region_layout(
4876 block_records_start,
4877 manifest_offset,
4878 crypto_header.block_size as usize,
4879 &volume_trailer,
4880 )?;
4881
4882 let manifest_bytes = &terminal.manifest_footer_bytes;
4883 let (manifest_footer, manifest_footer_error) =
4884 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
4885 {
4886 Ok(footer) => (Some(footer), None),
4887 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
4888 Err(err) => return Err(err),
4889 };
4890
4891 Ok(ParsedSeekableReadAtVolume {
4892 reader,
4893 volume_header,
4894 crypto_header,
4895 crypto_header_bytes,
4896 key_wrap_table_bytes,
4897 subkeys,
4898 manifest_footer,
4899 manifest_footer_error,
4900 root_auth_footer: terminal.root_auth_footer,
4901 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
4902 volume_trailer,
4903 block_records_start,
4904 })
4905}
4906
4907#[derive(Debug)]
4908struct ParsedPublicNoKeyVolume {
4909 volume_header: VolumeHeader,
4910 crypto_header: CryptoHeaderFixed,
4911 kdf_params: KdfParams,
4912 root_auth_footer: RootAuthFooterV1,
4913 root_auth_footer_bytes: Vec<u8>,
4914 blocks: BTreeMap<u64, BlockRecord>,
4915}
4916
4917pub fn public_no_key_verify_volumes_with_options<F>(
4918 volumes: &[&[u8]],
4919 mut verifier: F,
4920 options: ReaderOptions,
4921) -> Result<PublicNoKeyVerification, FormatError>
4922where
4923 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
4924{
4925 validate_reader_options(options)?;
4926 if volumes.is_empty() {
4927 return Err(FormatError::InvalidArchive("no volumes supplied"));
4928 }
4929 let mut parsed = Vec::with_capacity(volumes.len());
4930 for volume in volumes {
4931 parsed.push(parse_public_no_key_volume(volume, options)?);
4932 }
4933 let first = parsed
4934 .first()
4935 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
4936 if parsed.len() != first.crypto_header.stripe_width as usize {
4937 return Err(FormatError::ReaderUnsupported(
4938 "public no-key verification requires a complete volume set",
4939 ));
4940 }
4941
4942 let mut seen_volume_indexes = BTreeSet::new();
4943 let mut blocks = BTreeMap::new();
4944 for volume in &parsed {
4945 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
4946 || volume.volume_header.session_id != first.volume_header.session_id
4947 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
4948 || !public_kdf_profiles_agree(&volume.kdf_params, &first.kdf_params)
4949 {
4950 return Err(FormatError::InvalidArchive(
4951 "public no-key volume global metadata differs",
4952 ));
4953 }
4954 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
4955 return Err(FormatError::InvalidArchive(
4956 "public no-key RootAuthFooter copies differ",
4957 ));
4958 }
4959 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
4960 return Err(FormatError::InvalidArchive(
4961 "duplicate public no-key volume index",
4962 ));
4963 }
4964 for (block_index, record) in &volume.blocks {
4965 if blocks.insert(*block_index, record.clone()).is_some() {
4966 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
4967 }
4968 }
4969 }
4970 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
4971
4972 let footer = &first.root_auth_footer;
4973 let mut data_leaves = blocks
4974 .values()
4975 .filter(|record| record.kind.is_data())
4976 .map(|record| DataBlockMerkleLeaf {
4977 block_index: record.block_index,
4978 kind: record.kind,
4979 flags: record.flags,
4980 payload: record.payload.clone(),
4981 })
4982 .collect::<Vec<_>>();
4983 data_leaves.sort_by_key(|leaf| leaf.block_index);
4984 let total_data_block_count = u64::try_from(data_leaves.len())
4985 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
4986 let observed_data_root = data_block_merkle_root_for_revision(
4987 footer.format_version,
4988 footer.volume_format_rev,
4989 &data_leaves,
4990 )?;
4991 if total_data_block_count != footer.total_data_block_count
4992 || observed_data_root != footer.data_block_merkle_root
4993 {
4994 return Err(FormatError::InvalidArchive(
4995 "public no-key data-block commitment mismatch",
4996 ));
4997 }
4998 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
4999 if archive_root != footer.archive_root {
5000 return Err(FormatError::InvalidArchive(
5001 "public no-key archive_root mismatch",
5002 ));
5003 }
5004 if !verifier(footer, &archive_root)? {
5005 return Err(FormatError::InvalidArchive(
5006 "public no-key authenticator verification failed",
5007 ));
5008 }
5009 Ok(PublicNoKeyVerification {
5010 format_version: footer.format_version,
5011 volume_format_rev: footer.volume_format_rev,
5012 archive_root,
5013 authenticator_id: footer.authenticator_id,
5014 signer_identity_type: footer.signer_identity_type,
5015 signer_identity_bytes: footer.signer_identity_bytes.clone(),
5016 total_data_block_count,
5017 diagnostics: vec![
5018 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
5019 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
5020 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
5021 ],
5022 })
5023}
5024
5025fn parse_public_no_key_volume(
5026 bytes: &[u8],
5027 options: ReaderOptions,
5028) -> Result<ParsedPublicNoKeyVolume, FormatError> {
5029 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
5030 return Err(FormatError::InvalidLength {
5031 structure: "archive",
5032 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
5033 actual: bytes.len(),
5034 });
5035 }
5036 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
5037 parse_volume_format_dispatch(&volume_header)?;
5038 let crypto_start = volume_header.crypto_header_offset as usize;
5039 let crypto_len = volume_header.crypto_header_length as usize;
5040 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
5041 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
5042 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
5043 parsed_crypto.validate_extension_semantics()?;
5044 validate_seekable_supported_volume(
5045 &volume_header,
5046 &parsed_crypto.fixed,
5047 &parsed_crypto.extensions,
5048 )?;
5049 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5050
5051 let terminal = locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto, options)?;
5052 let block_records_start = match &parsed_crypto.kdf_params {
5053 KdfParams::RecipientWrap {
5054 key_wrap_table_length,
5055 ..
5056 } => checked_add(
5057 crypto_end,
5058 *key_wrap_table_length as usize,
5059 "KeyWrapTableV1",
5060 )?,
5061 _ => crypto_end,
5062 };
5063 let block_region = parse_public_block_observation(
5064 bytes,
5065 block_records_start,
5066 &terminal.image,
5067 parsed_crypto.fixed.block_size as usize,
5068 &volume_header,
5069 )?;
5070 Ok(ParsedPublicNoKeyVolume {
5071 volume_header,
5072 crypto_header: parsed_crypto.fixed,
5073 kdf_params: parsed_crypto.kdf_params,
5074 root_auth_footer: terminal.root_auth_footer,
5075 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
5076 blocks: block_region,
5077 })
5078}
5079
5080fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
5081 left.length == right.length
5082 && left.stripe_width == right.stripe_width
5083 && left.block_size == right.block_size
5084 && left.compression_algo == right.compression_algo
5085 && left.aead_algo == right.aead_algo
5086 && left.fec_algo == right.fec_algo
5087 && left.kdf_algo == right.kdf_algo
5088}
5089
5090fn public_kdf_profiles_agree(left: &KdfParams, right: &KdfParams) -> bool {
5091 match (left, right) {
5092 (
5093 KdfParams::RecipientWrap {
5094 key_wrap_table_length: left_length,
5095 key_wrap_table_record_count: left_count,
5096 key_wrap_table_version: left_version,
5097 key_wrap_table_digest: left_digest,
5098 },
5099 KdfParams::RecipientWrap {
5100 key_wrap_table_length: right_length,
5101 key_wrap_table_record_count: right_count,
5102 key_wrap_table_version: right_version,
5103 key_wrap_table_digest: right_digest,
5104 },
5105 ) => {
5106 left_length == right_length
5107 && left_count == right_count
5108 && left_version == right_version
5109 && left_digest == right_digest
5110 }
5111 (KdfParams::RecipientWrap { .. }, _) | (_, KdfParams::RecipientWrap { .. }) => false,
5112 _ => true,
5113 }
5114}
5115
5116fn recompute_public_archive_root(
5117 footer: &RootAuthFooterV1,
5118 crypto_header: &CryptoHeaderFixed,
5119) -> Result<[u8; 32], FormatError> {
5120 let descriptor_digest = root_auth_descriptor_digest_for_revision(
5121 footer.format_version,
5122 footer.volume_format_rev,
5123 footer.authenticator_id,
5124 footer.signer_identity_type,
5125 &footer.signer_identity_bytes,
5126 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
5127 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
5128 })?,
5129 footer.footer_length()?,
5130 )?;
5131 let signer_digest =
5132 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
5133 if signer_digest != footer.signer_identity_digest {
5134 return Err(FormatError::InvalidArchive(
5135 "public no-key signer identity digest mismatch",
5136 ));
5137 }
5138 archive_root_for_revision(ArchiveRootInputs {
5139 archive_uuid: footer.archive_uuid,
5140 session_id: footer.session_id,
5141 format_version: footer.format_version,
5142 volume_format_rev: footer.volume_format_rev,
5143 compression_algo: crypto_header.compression_algo,
5144 aead_algo: crypto_header.aead_algo,
5145 fec_algo: crypto_header.fec_algo,
5146 kdf_algo: crypto_header.kdf_algo,
5147 critical_metadata_digest: footer.critical_metadata_digest,
5148 index_digest: footer.index_digest,
5149 fec_layout_digest: footer.fec_layout_digest,
5150 total_data_block_count: footer.total_data_block_count,
5151 data_block_merkle_root: footer.data_block_merkle_root,
5152 root_auth_descriptor_digest: descriptor_digest,
5153 signer_identity_digest: signer_digest,
5154 })
5155}
5156
5157fn parse_valid_manifest_footer(
5158 volume_header: &VolumeHeader,
5159 crypto_header: &CryptoHeaderFixed,
5160 subkeys: &Subkeys,
5161 manifest_bytes: &[u8],
5162) -> Result<ManifestFooter, FormatError> {
5163 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
5164 validate_manifest_footer(
5165 volume_header,
5166 crypto_header,
5167 &manifest_footer,
5168 subkeys,
5169 volume_header.volume_format_rev,
5170 manifest_bytes,
5171 )?;
5172 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5173 Ok(manifest_footer)
5174}
5175
5176fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
5177 matches!(
5178 error,
5179 FormatError::BadMagic {
5180 structure: "ManifestFooter",
5181 } | FormatError::NonZeroReserved {
5182 structure: "ManifestFooter",
5183 } | FormatError::InvalidAuthoritativeFlag(_)
5184 | FormatError::HmacMismatch {
5185 structure: "ManifestFooter",
5186 }
5187 | FormatError::IntegrityDigestMismatch {
5188 structure: "ManifestFooter",
5189 }
5190 )
5191}
5192
5193fn validate_seekable_supported_volume(
5194 volume_header: &VolumeHeader,
5195 crypto_header: &CryptoHeaderFixed,
5196 extensions: &[ExtensionTlv<'_>],
5197) -> Result<(), FormatError> {
5198 reject_unsupported_raw_stream_profile(extensions)?;
5199 if crypto_header.stripe_width != volume_header.stripe_width {
5200 return Err(FormatError::InvalidArchive(
5201 "VolumeHeader and CryptoHeader stripe_width differ",
5202 ));
5203 }
5204 Ok(())
5205}
5206
5207pub(crate) fn validate_crypto_class_parity_exactness(
5208 crypto_header: &CryptoHeaderFixed,
5209) -> Result<(), FormatError> {
5210 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
5211 if crypto_header.fec_parity_shards as u32 != fec {
5212 return Err(FormatError::InvalidArchive(
5213 "fec_parity_shards does not match v41 compute_parity",
5214 ));
5215 }
5216 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
5217 if crypto_header.index_fec_parity_shards as u32 != index {
5218 return Err(FormatError::InvalidArchive(
5219 "index_fec_parity_shards does not match v41 compute_parity",
5220 ));
5221 }
5222 let index_root = required_object_parity(
5223 crypto_header.index_root_fec_data_shards as u64,
5224 crypto_header,
5225 )?;
5226 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
5227 return Err(FormatError::InvalidArchive(
5228 "index_root_fec_parity_shards does not match v41 compute_parity",
5229 ));
5230 }
5231 Ok(())
5232}
5233
5234fn validate_volume_set_member(
5235 first: &ParsedSeekableVolume,
5236 candidate: &ParsedSeekableVolume,
5237) -> Result<(), FormatError> {
5238 validate_volume_set_member_metadata(
5239 &first.volume_header,
5240 &first.crypto_header,
5241 &first.crypto_header_bytes,
5242 &candidate.volume_header,
5243 &candidate.crypto_header,
5244 &candidate.crypto_header_bytes,
5245 )?;
5246 validate_key_wrap_table_bytes_match(
5247 &first.key_wrap_table_bytes,
5248 &candidate.key_wrap_table_bytes,
5249 )
5250}
5251
5252fn validate_key_wrap_table_bytes_match(
5253 first_key_wrap_table_bytes: &Option<Vec<u8>>,
5254 candidate_key_wrap_table_bytes: &Option<Vec<u8>>,
5255) -> Result<(), FormatError> {
5256 if candidate_key_wrap_table_bytes != first_key_wrap_table_bytes {
5257 return Err(FormatError::InvalidArchive("KeyWrapTableV1 copies differ"));
5258 }
5259 Ok(())
5260}
5261
5262fn validate_volume_set_member_metadata(
5263 first_volume_header: &VolumeHeader,
5264 first_crypto_header: &CryptoHeaderFixed,
5265 first_crypto_header_bytes: &[u8],
5266 candidate_volume_header: &VolumeHeader,
5267 candidate_crypto_header: &CryptoHeaderFixed,
5268 candidate_crypto_header_bytes: &[u8],
5269) -> Result<(), FormatError> {
5270 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
5271 || candidate_volume_header.session_id != first_volume_header.session_id
5272 {
5273 return Err(FormatError::InvalidArchive(
5274 "mixed archive or session IDs in volume set",
5275 ));
5276 }
5277 if candidate_crypto_header_bytes != first_crypto_header_bytes
5278 || candidate_crypto_header != first_crypto_header
5279 {
5280 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
5281 }
5282 Ok(())
5283}
5284
5285pub(crate) fn manifest_bootstrap_fields_match(
5286 left: &ManifestFooter,
5287 right: &ManifestFooter,
5288) -> bool {
5289 left.archive_uuid == right.archive_uuid
5290 && left.session_id == right.session_id
5291 && left.is_authoritative == right.is_authoritative
5292 && left.total_volumes == right.total_volumes
5293 && left.index_root_first_block == right.index_root_first_block
5294 && left.index_root_data_block_count == right.index_root_data_block_count
5295 && left.index_root_parity_block_count == right.index_root_parity_block_count
5296 && left.index_root_encrypted_size == right.index_root_encrypted_size
5297 && left.index_root_decompressed_size == right.index_root_decompressed_size
5298}
5299
5300fn validate_complete_global_block_coverage(
5301 blocks: &BTreeMap<u64, BlockRecord>,
5302 erased_block_indices: &BTreeSet<u64>,
5303) -> Result<(), FormatError> {
5304 let mut expected = 0u64;
5305 let mut block_iter = blocks.keys().copied().peekable();
5306 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
5307
5308 loop {
5309 let next_block = block_iter.peek().copied();
5310 let next_erasure = erasure_iter.peek().copied();
5311 let next = match (next_block, next_erasure) {
5312 (Some(block), Some(erasure)) if block == erasure => {
5313 return Err(FormatError::InvalidArchive(
5314 "BlockRecord index is both present and erased",
5315 ));
5316 }
5317 (Some(block), Some(erasure)) => block.min(erasure),
5318 (Some(block), None) => block,
5319 (None, Some(erasure)) => erasure,
5320 (None, None) => return Ok(()),
5321 };
5322
5323 if next != expected {
5324 return Err(FormatError::InvalidArchive(
5325 "complete volume set has missing global blocks",
5326 ));
5327 }
5328 if next_block == Some(next) {
5329 block_iter.next();
5330 }
5331 if next_erasure == Some(next) {
5332 erasure_iter.next();
5333 }
5334 expected = expected
5335 .checked_add(1)
5336 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
5337 }
5338}
5339
5340#[derive(Debug)]
5341struct V41Terminal {
5342 image: CriticalMetadataImage,
5343 manifest_footer_bytes: Vec<u8>,
5344 root_auth_footer_bytes: Option<Vec<u8>>,
5345 root_auth_footer: Option<RootAuthFooterV1>,
5346 volume_trailer: VolumeTrailer,
5347}
5348
5349pub(crate) struct SequentialTerminalMaterial {
5350 pub(crate) manifest_footer: ManifestFooter,
5351 pub(crate) volume_trailer: VolumeTrailer,
5352 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
5353}
5354
5355#[derive(Debug)]
5356struct V41PublicTerminal {
5357 image: CriticalMetadataImage,
5358 root_auth_footer_bytes: Vec<u8>,
5359 root_auth_footer: RootAuthFooterV1,
5360}
5361
5362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5363struct CmraDecoderTuple {
5364 shard_size: u32,
5365 data_shard_count: u16,
5366 parity_shard_count: u16,
5367 image_length: u32,
5368 image_sha256: [u8; 32],
5369}
5370
5371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5372struct CmraIdentityHints {
5373 archive_uuid: [u8; 16],
5374 session_id: [u8; 16],
5375 volume_index: u32,
5376}
5377
5378impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
5379 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5380 Self {
5381 shard_size: header.shard_size,
5382 data_shard_count: header.data_shard_count,
5383 parity_shard_count: header.parity_shard_count,
5384 image_length: header.image_length,
5385 image_sha256: header.image_sha256,
5386 }
5387 }
5388}
5389
5390impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
5391 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
5392 Self {
5393 archive_uuid: header.archive_uuid_hint,
5394 session_id: header.session_id_hint,
5395 volume_index: header.volume_index_hint,
5396 }
5397 }
5398}
5399
5400impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
5401 fn from(locator: CriticalRecoveryLocator) -> Self {
5402 Self {
5403 shard_size: locator.cmra_shard_size,
5404 data_shard_count: locator.cmra_data_shard_count,
5405 parity_shard_count: locator.cmra_parity_shard_count,
5406 image_length: locator.cmra_image_length,
5407 image_sha256: locator.cmra_image_sha256,
5408 }
5409 }
5410}
5411
5412impl From<CriticalRecoveryLocator> for CmraIdentityHints {
5413 fn from(locator: CriticalRecoveryLocator) -> Self {
5414 Self {
5415 archive_uuid: locator.archive_uuid_hint,
5416 session_id: locator.session_id_hint,
5417 volume_index: locator.volume_index_hint,
5418 }
5419 }
5420}
5421
5422#[derive(Debug)]
5423struct RecoveredCmra {
5424 image: CriticalMetadataImage,
5425 tuple: CmraDecoderTuple,
5426 header_hints: Option<CmraIdentityHints>,
5427 cmra_length: u64,
5428}
5429
5430#[derive(Debug)]
5431struct TerminalCandidate {
5432 terminal: V41Terminal,
5433 anchor: usize,
5434 locator_sequence: Option<u32>,
5435 cmra_offset: u64,
5436 cmra_length: u64,
5437}
5438
5439#[derive(Debug)]
5440struct PublicTerminalCandidate {
5441 terminal: V41PublicTerminal,
5442 anchor: usize,
5443 cmra_offset: u64,
5444 cmra_length: u64,
5445}
5446
5447#[derive(Debug)]
5448struct RecoveredTerminalAuthority {
5449 terminal: V41Terminal,
5450 volume_header: VolumeHeader,
5451 crypto_header: CryptoHeaderFixed,
5452 crypto_header_bytes: Vec<u8>,
5453 subkeys: Subkeys,
5454 kdf_params: KdfParams,
5455}
5456
5457#[derive(Debug)]
5458struct RecoveredRecipientWrapTerminalAuthority {
5459 terminal: V41Terminal,
5460 volume_header: VolumeHeader,
5461 crypto_header: CryptoHeaderFixed,
5462 crypto_header_bytes: Vec<u8>,
5463 key_wrap_table_bytes: Vec<u8>,
5464 block_records_start: u64,
5465 subkeys: Subkeys,
5466}
5467
5468#[derive(Debug)]
5469struct TerminalAuthorityCandidate {
5470 authority: RecoveredTerminalAuthority,
5471 anchor: usize,
5472 cmra_offset: u64,
5473 cmra_length: u64,
5474}
5475
5476#[derive(Debug)]
5477struct RecipientWrapTerminalAuthorityCandidate {
5478 authority: RecoveredRecipientWrapTerminalAuthority,
5479 anchor: usize,
5480 cmra_offset: u64,
5481 cmra_length: u64,
5482}
5483
5484#[derive(Debug, Clone, Copy)]
5485enum CmraRecoveryMode {
5486 KeyHolding,
5487 PublicNoKey,
5488}
5489
5490#[derive(Clone, Copy)]
5491pub(crate) struct KeyHoldingTerminalContext<'a> {
5492 pub(crate) subkeys: &'a Subkeys,
5493 pub(crate) volume_header: &'a VolumeHeader,
5494 pub(crate) crypto_header: &'a CryptoHeaderFixed,
5495 pub(crate) crypto_header_bytes: &'a [u8],
5496}
5497
5498fn locate_v41_terminal(
5499 bytes: &[u8],
5500 context: KeyHoldingTerminalContext<'_>,
5501 options: ReaderOptions,
5502) -> Result<V41Terminal, FormatError> {
5503 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
5504}
5505
5506fn locate_v41_terminal_read_at(
5507 reader: &dyn ArchiveReadAt,
5508 len: u64,
5509 context: KeyHoldingTerminalContext<'_>,
5510 options: ReaderOptions,
5511) -> Result<V41Terminal, FormatError> {
5512 let mut candidates = Vec::new();
5513 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5514 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5515 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
5516 }
5517 if len >= LOCATOR_PAIR_LEN as u64 {
5518 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5519 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
5520 }
5521
5522 if candidates.is_empty() {
5523 let scan = max_critical_recovery_scan(options)? as u64;
5524 let scan_start = len.saturating_sub(scan);
5525 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5526 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5527 let mut offset = tail.len().saturating_sub(4);
5528 while offset < tail.len() {
5529 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5530 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5531 collect_v41_locator_candidate_read_at(
5532 reader,
5533 absolute_offset,
5534 2,
5535 context,
5536 &mut candidates,
5537 );
5538 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5539 if let Ok(candidate) =
5540 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
5541 {
5542 candidates.push(candidate);
5543 }
5544 }
5545 if offset == 0 {
5546 break;
5547 }
5548 offset -= 1;
5549 }
5550 }
5551
5552 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5553}
5554
5555fn locate_v41_terminal_authority(
5556 bytes: &[u8],
5557 master_key: &MasterKey,
5558 options: ReaderOptions,
5559) -> Result<RecoveredTerminalAuthority, FormatError> {
5560 let mut candidates = Vec::new();
5561 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5562 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5563 collect_v41_locator_authority_candidate(
5564 bytes,
5565 final_offset,
5566 0,
5567 master_key,
5568 &mut candidates,
5569 );
5570 }
5571 if bytes.len() >= LOCATOR_PAIR_LEN {
5572 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5573 collect_v41_locator_authority_candidate(
5574 bytes,
5575 mirror_offset,
5576 1,
5577 master_key,
5578 &mut candidates,
5579 );
5580 }
5581
5582 if candidates.is_empty() {
5583 let scan = max_critical_recovery_scan(options)?;
5584 let scan_start = bytes.len().saturating_sub(scan);
5585 let mut offset = bytes.len().saturating_sub(4);
5586 while offset >= scan_start {
5587 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5588 collect_v41_locator_authority_candidate(
5589 bytes,
5590 offset,
5591 2,
5592 master_key,
5593 &mut candidates,
5594 );
5595 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5596 if let Ok(candidate) =
5597 parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
5598 {
5599 candidates.push(candidate);
5600 }
5601 }
5602 if offset == 0 {
5603 break;
5604 }
5605 offset -= 1;
5606 }
5607 }
5608
5609 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5610}
5611
5612fn locate_v41_recipient_wrap_terminal_authority<F>(
5613 bytes: &[u8],
5614 resolver: &mut F,
5615 options: ReaderOptions,
5616) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5617where
5618 F: FnMut(
5619 RecipientWrapRecordContext<'_>,
5620 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5621{
5622 let mut candidates = Vec::new();
5623 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5624 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5625 collect_v41_recipient_wrap_locator_authority_candidate(
5626 bytes,
5627 final_offset,
5628 0,
5629 resolver,
5630 &mut candidates,
5631 );
5632 }
5633 if bytes.len() >= LOCATOR_PAIR_LEN {
5634 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5635 collect_v41_recipient_wrap_locator_authority_candidate(
5636 bytes,
5637 mirror_offset,
5638 1,
5639 resolver,
5640 &mut candidates,
5641 );
5642 }
5643
5644 if candidates.is_empty() {
5645 let scan = max_critical_recovery_scan(options)?;
5646 let scan_start = bytes.len().saturating_sub(scan);
5647 let mut offset = bytes.len().saturating_sub(4);
5648 while offset >= scan_start {
5649 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5650 collect_v41_recipient_wrap_locator_authority_candidate(
5651 bytes,
5652 offset,
5653 2,
5654 resolver,
5655 &mut candidates,
5656 );
5657 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5658 if let Ok(candidate) = parse_locatorless_cmra_recipient_wrap_authority_candidate(
5659 bytes, offset, resolver,
5660 ) {
5661 candidates.push(candidate);
5662 }
5663 }
5664 if offset == 0 {
5665 break;
5666 }
5667 offset -= 1;
5668 }
5669 }
5670
5671 choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5672 .map(|candidate| candidate.authority)
5673}
5674
5675fn locate_v41_recipient_wrap_terminal_authority_read_at<F>(
5676 reader: &dyn ArchiveReadAt,
5677 len: u64,
5678 resolver: &mut F,
5679 options: ReaderOptions,
5680) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
5681where
5682 F: FnMut(
5683 RecipientWrapRecordContext<'_>,
5684 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5685{
5686 let mut candidates = Vec::new();
5687 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5688 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5689 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5690 reader,
5691 final_offset,
5692 0,
5693 resolver,
5694 &mut candidates,
5695 );
5696 }
5697 if len >= LOCATOR_PAIR_LEN as u64 {
5698 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5699 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5700 reader,
5701 mirror_offset,
5702 1,
5703 resolver,
5704 &mut candidates,
5705 );
5706 }
5707
5708 if candidates.is_empty() {
5709 let scan = max_critical_recovery_scan(options)? as u64;
5710 let scan_start = len.saturating_sub(scan);
5711 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5712 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5713 let mut offset = tail.len().saturating_sub(4);
5714 while offset < tail.len() {
5715 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5716 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5717 collect_v41_recipient_wrap_locator_authority_candidate_read_at(
5718 reader,
5719 absolute_offset,
5720 2,
5721 resolver,
5722 &mut candidates,
5723 );
5724 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5725 if let Ok(candidate) =
5726 parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at(
5727 reader,
5728 absolute_offset,
5729 resolver,
5730 )
5731 {
5732 candidates.push(candidate);
5733 }
5734 }
5735 if offset == 0 {
5736 break;
5737 }
5738 offset -= 1;
5739 }
5740 }
5741
5742 choose_v41_recipient_wrap_terminal_authority_candidate(candidates)
5743 .map(|candidate| candidate.authority)
5744}
5745
5746fn locate_v41_terminal_authority_read_at(
5747 reader: &dyn ArchiveReadAt,
5748 len: u64,
5749 master_key: &MasterKey,
5750 options: ReaderOptions,
5751) -> Result<RecoveredTerminalAuthority, FormatError> {
5752 let mut candidates = Vec::new();
5753 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
5754 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
5755 collect_v41_locator_authority_candidate_read_at(
5756 reader,
5757 final_offset,
5758 0,
5759 master_key,
5760 &mut candidates,
5761 );
5762 }
5763 if len >= LOCATOR_PAIR_LEN as u64 {
5764 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
5765 collect_v41_locator_authority_candidate_read_at(
5766 reader,
5767 mirror_offset,
5768 1,
5769 master_key,
5770 &mut candidates,
5771 );
5772 }
5773
5774 if candidates.is_empty() {
5775 let scan = max_critical_recovery_scan(options)? as u64;
5776 let scan_start = len.saturating_sub(scan);
5777 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
5778 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
5779 let mut offset = tail.len().saturating_sub(4);
5780 while offset < tail.len() {
5781 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
5782 if tail.get(offset..offset + 4) == Some(b"TZCL") {
5783 collect_v41_locator_authority_candidate_read_at(
5784 reader,
5785 absolute_offset,
5786 2,
5787 master_key,
5788 &mut candidates,
5789 );
5790 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
5791 if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
5792 reader,
5793 absolute_offset,
5794 master_key,
5795 ) {
5796 candidates.push(candidate);
5797 }
5798 }
5799 if offset == 0 {
5800 break;
5801 }
5802 offset -= 1;
5803 }
5804 }
5805
5806 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
5807}
5808
5809fn locate_v41_terminal_candidate(
5810 bytes: &[u8],
5811 context: KeyHoldingTerminalContext<'_>,
5812 options: ReaderOptions,
5813) -> Result<TerminalCandidate, FormatError> {
5814 let mut candidates = Vec::new();
5815 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5816 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5817 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
5818 }
5819 if bytes.len() >= LOCATOR_PAIR_LEN {
5820 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5821 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
5822 }
5823
5824 if candidates.is_empty() {
5825 let scan = max_critical_recovery_scan(options)?;
5826 let scan_start = bytes.len().saturating_sub(scan);
5827 let mut offset = bytes.len().saturating_sub(4);
5828 while offset >= scan_start {
5829 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5830 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
5831 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5832 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
5833 candidates.push(candidate);
5834 }
5835 }
5836 if offset == 0 {
5837 break;
5838 }
5839 offset -= 1;
5840 }
5841 }
5842
5843 choose_v41_terminal_candidate(candidates)
5844}
5845
5846fn locate_v41_public_terminal(
5847 bytes: &[u8],
5848 volume_header: &VolumeHeader,
5849 crypto_header: &CryptoHeader<'_>,
5850 options: ReaderOptions,
5851) -> Result<V41PublicTerminal, FormatError> {
5852 let mut candidates = Vec::new();
5853 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
5854 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
5855 collect_v41_public_locator_candidate(
5856 bytes,
5857 final_offset,
5858 0,
5859 volume_header,
5860 crypto_header,
5861 &mut candidates,
5862 );
5863 }
5864 if bytes.len() >= LOCATOR_PAIR_LEN {
5865 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
5866 collect_v41_public_locator_candidate(
5867 bytes,
5868 mirror_offset,
5869 1,
5870 volume_header,
5871 crypto_header,
5872 &mut candidates,
5873 );
5874 }
5875
5876 if candidates.is_empty() {
5877 let scan = max_critical_recovery_scan(options)?;
5878 let scan_start = bytes.len().saturating_sub(scan);
5879 let mut offset = bytes.len().saturating_sub(4);
5880 while offset >= scan_start {
5881 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
5882 collect_v41_public_locator_candidate(
5883 bytes,
5884 offset,
5885 2,
5886 volume_header,
5887 crypto_header,
5888 &mut candidates,
5889 );
5890 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
5891 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
5892 bytes,
5893 offset,
5894 volume_header,
5895 crypto_header,
5896 ) {
5897 candidates.push(candidate);
5898 }
5899 }
5900 if offset == 0 {
5901 break;
5902 }
5903 offset -= 1;
5904 }
5905 }
5906
5907 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
5908}
5909
5910fn collect_v41_locator_candidate(
5911 bytes: &[u8],
5912 offset: usize,
5913 expected_sequence: u32,
5914 context: KeyHoldingTerminalContext<'_>,
5915 candidates: &mut Vec<TerminalCandidate>,
5916) {
5917 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5918 return;
5919 };
5920 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5921 return;
5922 };
5923 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5924 return;
5925 }
5926 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
5927 candidates.push(candidate);
5928 }
5929}
5930
5931fn collect_v41_locator_candidate_read_at(
5932 reader: &dyn ArchiveReadAt,
5933 offset: u64,
5934 expected_sequence: u32,
5935 context: KeyHoldingTerminalContext<'_>,
5936 candidates: &mut Vec<TerminalCandidate>,
5937) {
5938 let Ok(raw) = read_at_vec(
5939 reader,
5940 offset,
5941 CRITICAL_RECOVERY_LOCATOR_LEN,
5942 "CriticalRecoveryLocator",
5943 ) else {
5944 return;
5945 };
5946 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
5947 return;
5948 };
5949 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5950 return;
5951 }
5952 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
5953 candidates.push(candidate);
5954 }
5955}
5956
5957fn collect_v41_locator_authority_candidate(
5958 bytes: &[u8],
5959 offset: usize,
5960 expected_sequence: u32,
5961 master_key: &MasterKey,
5962 candidates: &mut Vec<TerminalAuthorityCandidate>,
5963) {
5964 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5965 return;
5966 };
5967 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5968 return;
5969 };
5970 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5971 return;
5972 }
5973 if let Ok(candidate) =
5974 parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
5975 {
5976 candidates.push(candidate);
5977 }
5978}
5979
5980fn collect_v41_recipient_wrap_locator_authority_candidate<F>(
5981 bytes: &[u8],
5982 offset: usize,
5983 expected_sequence: u32,
5984 resolver: &mut F,
5985 candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
5986) where
5987 F: FnMut(
5988 RecipientWrapRecordContext<'_>,
5989 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
5990{
5991 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
5992 return;
5993 };
5994 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
5995 return;
5996 };
5997 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
5998 return;
5999 }
6000 if let Ok(candidate) =
6001 parse_locator_cmra_recipient_wrap_authority_candidate(bytes, offset, locator, resolver)
6002 {
6003 candidates.push(candidate);
6004 }
6005}
6006
6007fn collect_v41_recipient_wrap_locator_authority_candidate_read_at<F>(
6008 reader: &dyn ArchiveReadAt,
6009 offset: u64,
6010 expected_sequence: u32,
6011 resolver: &mut F,
6012 candidates: &mut Vec<RecipientWrapTerminalAuthorityCandidate>,
6013) where
6014 F: FnMut(
6015 RecipientWrapRecordContext<'_>,
6016 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6017{
6018 let Ok(raw) = read_at_vec(
6019 reader,
6020 offset,
6021 CRITICAL_RECOVERY_LOCATOR_LEN,
6022 "CriticalRecoveryLocator",
6023 ) else {
6024 return;
6025 };
6026 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6027 return;
6028 };
6029 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6030 return;
6031 }
6032 if let Ok(candidate) = parse_locator_cmra_recipient_wrap_authority_candidate_read_at(
6033 reader, offset, locator, resolver,
6034 ) {
6035 candidates.push(candidate);
6036 }
6037}
6038
6039fn collect_v41_locator_authority_candidate_read_at(
6040 reader: &dyn ArchiveReadAt,
6041 offset: u64,
6042 expected_sequence: u32,
6043 master_key: &MasterKey,
6044 candidates: &mut Vec<TerminalAuthorityCandidate>,
6045) {
6046 let Ok(raw) = read_at_vec(
6047 reader,
6048 offset,
6049 CRITICAL_RECOVERY_LOCATOR_LEN,
6050 "CriticalRecoveryLocator",
6051 ) else {
6052 return;
6053 };
6054 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
6055 return;
6056 };
6057 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6058 return;
6059 }
6060 if let Ok(candidate) =
6061 parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
6062 {
6063 candidates.push(candidate);
6064 }
6065}
6066
6067fn collect_v41_public_locator_candidate(
6068 bytes: &[u8],
6069 offset: usize,
6070 expected_sequence: u32,
6071 volume_header: &VolumeHeader,
6072 crypto_header: &CryptoHeader<'_>,
6073 candidates: &mut Vec<PublicTerminalCandidate>,
6074) {
6075 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
6076 return;
6077 };
6078 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
6079 return;
6080 };
6081 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
6082 return;
6083 }
6084 if let Ok(candidate) =
6085 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
6086 {
6087 candidates.push(candidate);
6088 }
6089}
6090
6091fn choose_v41_terminal_candidate(
6092 mut candidates: Vec<TerminalCandidate>,
6093) -> Result<TerminalCandidate, FormatError> {
6094 candidates.sort_by_key(|candidate| candidate.anchor);
6095 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6096 "no valid v41 CMRA candidate found",
6097 ))?;
6098 if let Some(previous) = candidates.last() {
6099 if previous.anchor == winner.anchor
6100 && (previous.cmra_offset != winner.cmra_offset
6101 || previous.cmra_length != winner.cmra_length)
6102 {
6103 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6104 }
6105 }
6106 Ok(winner)
6107}
6108
6109fn choose_v41_terminal_authority_candidate(
6110 mut candidates: Vec<TerminalAuthorityCandidate>,
6111) -> Result<TerminalAuthorityCandidate, FormatError> {
6112 candidates.sort_by_key(|candidate| candidate.anchor);
6113 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6114 "no valid v41 CMRA candidate found",
6115 ))?;
6116 if let Some(previous) = candidates.last() {
6117 if previous.anchor == winner.anchor
6118 && (previous.cmra_offset != winner.cmra_offset
6119 || previous.cmra_length != winner.cmra_length)
6120 {
6121 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6122 }
6123 }
6124 Ok(winner)
6125}
6126
6127fn choose_v41_recipient_wrap_terminal_authority_candidate(
6128 mut candidates: Vec<RecipientWrapTerminalAuthorityCandidate>,
6129) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError> {
6130 candidates.sort_by_key(|candidate| candidate.anchor);
6131 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6132 "no valid v41 CMRA candidate found",
6133 ))?;
6134 if let Some(previous) = candidates.last() {
6135 if previous.anchor == winner.anchor
6136 && (previous.cmra_offset != winner.cmra_offset
6137 || previous.cmra_length != winner.cmra_length)
6138 {
6139 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
6140 }
6141 }
6142 Ok(winner)
6143}
6144
6145fn choose_v41_public_terminal_candidate(
6146 mut candidates: Vec<PublicTerminalCandidate>,
6147) -> Result<PublicTerminalCandidate, FormatError> {
6148 candidates.sort_by_key(|candidate| candidate.anchor);
6149 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
6150 "no valid v41 public CMRA candidate found",
6151 ))?;
6152 if let Some(previous) = candidates.last() {
6153 if previous.anchor == winner.anchor
6154 && (previous.cmra_offset != winner.cmra_offset
6155 || previous.cmra_length != winner.cmra_length)
6156 {
6157 return Err(FormatError::InvalidArchive(
6158 "ambiguous v41 public CMRA candidates",
6159 ));
6160 }
6161 }
6162 Ok(winner)
6163}
6164
6165fn parse_locator_cmra_candidate(
6166 bytes: &[u8],
6167 locator_offset: usize,
6168 locator: CriticalRecoveryLocator,
6169 context: KeyHoldingTerminalContext<'_>,
6170) -> Result<TerminalCandidate, FormatError> {
6171 let tuple = CmraDecoderTuple::from(locator);
6172 validate_cmra_decoder_tuple(tuple)?;
6173 let expected_cmra_length = cmra_serialized_length(tuple)?;
6174 if locator.cmra_length as u64 != expected_cmra_length {
6175 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6176 }
6177 validate_locator_position(locator_offset, locator)?;
6178 let recovered = recover_cmra(
6179 bytes,
6180 locator.cmra_offset,
6181 Some(tuple),
6182 CmraRecoveryMode::KeyHolding,
6183 )?;
6184 if recovered.tuple != tuple {
6185 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6186 }
6187 if expected_cmra_length != recovered.cmra_length {
6188 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6189 }
6190 validate_locator_image_boundary(locator, &recovered.image)?;
6191 validate_cmra_identity_hints(
6192 recovered.header_hints,
6193 Some(CmraIdentityHints::from(locator)),
6194 &recovered.image,
6195 )?;
6196 let terminal =
6197 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
6198 Ok(TerminalCandidate {
6199 terminal,
6200 anchor: locator_offset
6201 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6202 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6203 locator_sequence: Some(locator.locator_sequence),
6204 cmra_offset: locator.cmra_offset,
6205 cmra_length: recovered.cmra_length,
6206 })
6207}
6208
6209fn parse_locator_cmra_candidate_read_at(
6210 reader: &dyn ArchiveReadAt,
6211 locator_offset: u64,
6212 locator: CriticalRecoveryLocator,
6213 context: KeyHoldingTerminalContext<'_>,
6214) -> Result<TerminalCandidate, FormatError> {
6215 let tuple = CmraDecoderTuple::from(locator);
6216 validate_cmra_decoder_tuple(tuple)?;
6217 let expected_cmra_length = cmra_serialized_length(tuple)?;
6218 if locator.cmra_length as u64 != expected_cmra_length {
6219 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6220 }
6221 validate_locator_position(
6222 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6223 locator,
6224 )?;
6225 let recovered = recover_cmra_read_at(
6226 reader,
6227 locator.cmra_offset,
6228 Some(tuple),
6229 CmraRecoveryMode::KeyHolding,
6230 )?;
6231 if recovered.tuple != tuple {
6232 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6233 }
6234 if expected_cmra_length != recovered.cmra_length {
6235 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6236 }
6237 validate_locator_image_boundary(locator, &recovered.image)?;
6238 validate_cmra_identity_hints(
6239 recovered.header_hints,
6240 Some(CmraIdentityHints::from(locator)),
6241 &recovered.image,
6242 )?;
6243 let terminal = validate_recovered_terminal_read_at(
6244 recovered.image,
6245 recovered.tuple,
6246 reader,
6247 context,
6248 false,
6249 )?;
6250 Ok(TerminalCandidate {
6251 terminal,
6252 anchor: to_usize(
6253 checked_u64_add(
6254 locator_offset,
6255 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6256 "locator anchor overflow",
6257 )?,
6258 "locator anchor overflow",
6259 )?,
6260 locator_sequence: Some(locator.locator_sequence),
6261 cmra_offset: locator.cmra_offset,
6262 cmra_length: recovered.cmra_length,
6263 })
6264}
6265
6266fn parse_locator_cmra_authority_candidate(
6267 bytes: &[u8],
6268 locator_offset: usize,
6269 locator: CriticalRecoveryLocator,
6270 master_key: &MasterKey,
6271) -> Result<TerminalAuthorityCandidate, FormatError> {
6272 let tuple = CmraDecoderTuple::from(locator);
6273 validate_cmra_decoder_tuple(tuple)?;
6274 let expected_cmra_length = cmra_serialized_length(tuple)?;
6275 if locator.cmra_length as u64 != expected_cmra_length {
6276 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6277 }
6278 validate_locator_position(locator_offset, locator)?;
6279 let recovered = recover_cmra(
6280 bytes,
6281 locator.cmra_offset,
6282 Some(tuple),
6283 CmraRecoveryMode::KeyHolding,
6284 )?;
6285 if recovered.tuple != tuple {
6286 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6287 }
6288 if expected_cmra_length != recovered.cmra_length {
6289 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6290 }
6291 validate_locator_image_boundary(locator, &recovered.image)?;
6292 validate_cmra_identity_hints(
6293 recovered.header_hints,
6294 Some(CmraIdentityHints::from(locator)),
6295 &recovered.image,
6296 )?;
6297 let cmra_length = recovered.cmra_length;
6298 let authority =
6299 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6300 Ok(TerminalAuthorityCandidate {
6301 authority,
6302 anchor: locator_offset
6303 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6304 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6305 cmra_offset: locator.cmra_offset,
6306 cmra_length,
6307 })
6308}
6309
6310fn parse_locator_cmra_recipient_wrap_authority_candidate<F>(
6311 bytes: &[u8],
6312 locator_offset: usize,
6313 locator: CriticalRecoveryLocator,
6314 resolver: &mut F,
6315) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6316where
6317 F: FnMut(
6318 RecipientWrapRecordContext<'_>,
6319 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6320{
6321 let tuple = CmraDecoderTuple::from(locator);
6322 validate_cmra_decoder_tuple(tuple)?;
6323 let expected_cmra_length = cmra_serialized_length(tuple)?;
6324 if locator.cmra_length as u64 != expected_cmra_length {
6325 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6326 }
6327 validate_locator_position(locator_offset, locator)?;
6328 let recovered = recover_cmra(
6329 bytes,
6330 locator.cmra_offset,
6331 Some(tuple),
6332 CmraRecoveryMode::KeyHolding,
6333 )?;
6334 if recovered.tuple != tuple {
6335 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6336 }
6337 if expected_cmra_length != recovered.cmra_length {
6338 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6339 }
6340 validate_locator_image_boundary(locator, &recovered.image)?;
6341 validate_cmra_identity_hints(
6342 recovered.header_hints,
6343 Some(CmraIdentityHints::from(locator)),
6344 &recovered.image,
6345 )?;
6346 let cmra_length = recovered.cmra_length;
6347 let authority = validate_recovered_recipient_wrap_terminal_authority(
6348 recovered.image,
6349 recovered.tuple,
6350 resolver,
6351 false,
6352 )?;
6353 Ok(RecipientWrapTerminalAuthorityCandidate {
6354 authority,
6355 anchor: locator_offset
6356 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6357 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6358 cmra_offset: locator.cmra_offset,
6359 cmra_length,
6360 })
6361}
6362
6363fn parse_locator_cmra_recipient_wrap_authority_candidate_read_at<F>(
6364 reader: &dyn ArchiveReadAt,
6365 locator_offset: u64,
6366 locator: CriticalRecoveryLocator,
6367 resolver: &mut F,
6368) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6369where
6370 F: FnMut(
6371 RecipientWrapRecordContext<'_>,
6372 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6373{
6374 let tuple = CmraDecoderTuple::from(locator);
6375 validate_cmra_decoder_tuple(tuple)?;
6376 let expected_cmra_length = cmra_serialized_length(tuple)?;
6377 if locator.cmra_length as u64 != expected_cmra_length {
6378 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6379 }
6380 validate_locator_position(
6381 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6382 locator,
6383 )?;
6384 let recovered = recover_cmra_read_at(
6385 reader,
6386 locator.cmra_offset,
6387 Some(tuple),
6388 CmraRecoveryMode::KeyHolding,
6389 )?;
6390 if recovered.tuple != tuple {
6391 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6392 }
6393 if expected_cmra_length != recovered.cmra_length {
6394 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6395 }
6396 validate_locator_image_boundary(locator, &recovered.image)?;
6397 validate_cmra_identity_hints(
6398 recovered.header_hints,
6399 Some(CmraIdentityHints::from(locator)),
6400 &recovered.image,
6401 )?;
6402 let cmra_length = recovered.cmra_length;
6403 let authority = validate_recovered_recipient_wrap_terminal_authority(
6404 recovered.image,
6405 recovered.tuple,
6406 resolver,
6407 false,
6408 )?;
6409 Ok(RecipientWrapTerminalAuthorityCandidate {
6410 authority,
6411 anchor: to_usize(
6412 checked_u64_add(
6413 locator_offset,
6414 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6415 "locator anchor overflow",
6416 )?,
6417 "locator anchor overflow",
6418 )?,
6419 cmra_offset: locator.cmra_offset,
6420 cmra_length,
6421 })
6422}
6423
6424fn parse_locator_cmra_authority_candidate_read_at(
6425 reader: &dyn ArchiveReadAt,
6426 locator_offset: u64,
6427 locator: CriticalRecoveryLocator,
6428 master_key: &MasterKey,
6429) -> Result<TerminalAuthorityCandidate, FormatError> {
6430 let tuple = CmraDecoderTuple::from(locator);
6431 validate_cmra_decoder_tuple(tuple)?;
6432 let expected_cmra_length = cmra_serialized_length(tuple)?;
6433 if locator.cmra_length as u64 != expected_cmra_length {
6434 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6435 }
6436 validate_locator_position(
6437 to_usize(locator_offset, "CriticalRecoveryLocator")?,
6438 locator,
6439 )?;
6440 let recovered = recover_cmra_read_at(
6441 reader,
6442 locator.cmra_offset,
6443 Some(tuple),
6444 CmraRecoveryMode::KeyHolding,
6445 )?;
6446 if recovered.tuple != tuple {
6447 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6448 }
6449 if expected_cmra_length != recovered.cmra_length {
6450 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6451 }
6452 validate_locator_image_boundary(locator, &recovered.image)?;
6453 validate_cmra_identity_hints(
6454 recovered.header_hints,
6455 Some(CmraIdentityHints::from(locator)),
6456 &recovered.image,
6457 )?;
6458 let cmra_length = recovered.cmra_length;
6459 let authority =
6460 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
6461 Ok(TerminalAuthorityCandidate {
6462 authority,
6463 anchor: to_usize(
6464 checked_u64_add(
6465 locator_offset,
6466 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6467 "locator anchor overflow",
6468 )?,
6469 "locator anchor overflow",
6470 )?,
6471 cmra_offset: locator.cmra_offset,
6472 cmra_length,
6473 })
6474}
6475
6476fn parse_public_locator_cmra_candidate(
6477 bytes: &[u8],
6478 locator_offset: usize,
6479 locator: CriticalRecoveryLocator,
6480 volume_header: &VolumeHeader,
6481 crypto_header: &CryptoHeader<'_>,
6482) -> Result<PublicTerminalCandidate, FormatError> {
6483 let tuple = CmraDecoderTuple::from(locator);
6484 validate_cmra_decoder_tuple(tuple)?;
6485 let expected_cmra_length = cmra_serialized_length(tuple)?;
6486 if locator.cmra_length as u64 != expected_cmra_length {
6487 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6488 }
6489 validate_locator_position(locator_offset, locator)?;
6490 let recovered = recover_cmra(
6491 bytes,
6492 locator.cmra_offset,
6493 Some(tuple),
6494 CmraRecoveryMode::PublicNoKey,
6495 )?;
6496 if recovered.tuple != tuple {
6497 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6498 }
6499 if expected_cmra_length != recovered.cmra_length {
6500 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
6501 }
6502 validate_locator_image_boundary(locator, &recovered.image)?;
6503 validate_cmra_identity_hints(
6504 recovered.header_hints,
6505 Some(CmraIdentityHints::from(locator)),
6506 &recovered.image,
6507 )?;
6508 let terminal = validate_recovered_public_terminal(
6509 recovered.image,
6510 bytes,
6511 volume_header,
6512 crypto_header,
6513 false,
6514 )?;
6515 Ok(PublicTerminalCandidate {
6516 terminal,
6517 anchor: locator_offset
6518 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6519 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
6520 cmra_offset: locator.cmra_offset,
6521 cmra_length: recovered.cmra_length,
6522 })
6523}
6524
6525fn parse_locatorless_cmra_candidate(
6526 bytes: &[u8],
6527 cmra_offset: usize,
6528 context: KeyHoldingTerminalContext<'_>,
6529) -> Result<TerminalCandidate, FormatError> {
6530 let recovered = recover_cmra(
6531 bytes,
6532 cmra_offset as u64,
6533 None,
6534 CmraRecoveryMode::KeyHolding,
6535 )?;
6536 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6537 return Err(FormatError::InvalidArchive(
6538 "locatorless CMRA boundary mismatch",
6539 ));
6540 }
6541 if recovered
6542 .image
6543 .volume_trailer_offset
6544 .checked_add(VOLUME_TRAILER_LEN as u64)
6545 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6546 != cmra_offset as u64
6547 {
6548 return Err(FormatError::InvalidArchive(
6549 "locatorless trailer boundary mismatch",
6550 ));
6551 }
6552 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6553 let terminal =
6554 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
6555 Ok(TerminalCandidate {
6556 terminal,
6557 anchor: cmra_offset
6558 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6559 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6560 locator_sequence: None,
6561 cmra_offset: cmra_offset as u64,
6562 cmra_length: recovered.cmra_length,
6563 })
6564}
6565
6566fn parse_locatorless_cmra_candidate_read_at(
6567 reader: &dyn ArchiveReadAt,
6568 cmra_offset: u64,
6569 context: KeyHoldingTerminalContext<'_>,
6570) -> Result<TerminalCandidate, FormatError> {
6571 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6572 if recovered.image.body_bytes_before_cmra != cmra_offset {
6573 return Err(FormatError::InvalidArchive(
6574 "locatorless CMRA boundary mismatch",
6575 ));
6576 }
6577 if recovered
6578 .image
6579 .volume_trailer_offset
6580 .checked_add(VOLUME_TRAILER_LEN as u64)
6581 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6582 != cmra_offset
6583 {
6584 return Err(FormatError::InvalidArchive(
6585 "locatorless trailer boundary mismatch",
6586 ));
6587 }
6588 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6589 let terminal = validate_recovered_terminal_read_at(
6590 recovered.image,
6591 recovered.tuple,
6592 reader,
6593 context,
6594 true,
6595 )?;
6596 Ok(TerminalCandidate {
6597 terminal,
6598 anchor: to_usize(
6599 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
6600 "CMRA anchor overflow",
6601 )?,
6602 locator_sequence: None,
6603 cmra_offset,
6604 cmra_length: recovered.cmra_length,
6605 })
6606}
6607
6608fn parse_locatorless_cmra_authority_candidate(
6609 bytes: &[u8],
6610 cmra_offset: usize,
6611 master_key: &MasterKey,
6612) -> Result<TerminalAuthorityCandidate, FormatError> {
6613 let recovered = recover_cmra(
6614 bytes,
6615 cmra_offset as u64,
6616 None,
6617 CmraRecoveryMode::KeyHolding,
6618 )?;
6619 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6620 return Err(FormatError::InvalidArchive(
6621 "locatorless CMRA boundary mismatch",
6622 ));
6623 }
6624 if recovered
6625 .image
6626 .volume_trailer_offset
6627 .checked_add(VOLUME_TRAILER_LEN as u64)
6628 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6629 != cmra_offset as u64
6630 {
6631 return Err(FormatError::InvalidArchive(
6632 "locatorless trailer boundary mismatch",
6633 ));
6634 }
6635 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6636 let cmra_length = recovered.cmra_length;
6637 let authority =
6638 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6639 Ok(TerminalAuthorityCandidate {
6640 authority,
6641 anchor: cmra_offset
6642 .checked_add(to_usize(cmra_length, "CMRA")?)
6643 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6644 cmra_offset: cmra_offset as u64,
6645 cmra_length,
6646 })
6647}
6648
6649fn parse_locatorless_cmra_recipient_wrap_authority_candidate<F>(
6650 bytes: &[u8],
6651 cmra_offset: usize,
6652 resolver: &mut F,
6653) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6654where
6655 F: FnMut(
6656 RecipientWrapRecordContext<'_>,
6657 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6658{
6659 let recovered = recover_cmra(
6660 bytes,
6661 cmra_offset as u64,
6662 None,
6663 CmraRecoveryMode::KeyHolding,
6664 )?;
6665 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6666 return Err(FormatError::InvalidArchive(
6667 "locatorless CMRA boundary mismatch",
6668 ));
6669 }
6670 if recovered
6671 .image
6672 .volume_trailer_offset
6673 .checked_add(VOLUME_TRAILER_LEN as u64)
6674 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6675 != cmra_offset as u64
6676 {
6677 return Err(FormatError::InvalidArchive(
6678 "locatorless trailer boundary mismatch",
6679 ));
6680 }
6681 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6682 let cmra_length = recovered.cmra_length;
6683 let authority = validate_recovered_recipient_wrap_terminal_authority(
6684 recovered.image,
6685 recovered.tuple,
6686 resolver,
6687 true,
6688 )?;
6689 Ok(RecipientWrapTerminalAuthorityCandidate {
6690 authority,
6691 anchor: cmra_offset
6692 .checked_add(to_usize(cmra_length, "CMRA")?)
6693 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6694 cmra_offset: cmra_offset as u64,
6695 cmra_length,
6696 })
6697}
6698
6699fn parse_locatorless_cmra_recipient_wrap_authority_candidate_read_at<F>(
6700 reader: &dyn ArchiveReadAt,
6701 cmra_offset: u64,
6702 resolver: &mut F,
6703) -> Result<RecipientWrapTerminalAuthorityCandidate, FormatError>
6704where
6705 F: FnMut(
6706 RecipientWrapRecordContext<'_>,
6707 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
6708{
6709 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6710 if recovered.image.body_bytes_before_cmra != cmra_offset {
6711 return Err(FormatError::InvalidArchive(
6712 "locatorless CMRA boundary mismatch",
6713 ));
6714 }
6715 if recovered
6716 .image
6717 .volume_trailer_offset
6718 .checked_add(VOLUME_TRAILER_LEN as u64)
6719 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6720 != cmra_offset
6721 {
6722 return Err(FormatError::InvalidArchive(
6723 "locatorless trailer boundary mismatch",
6724 ));
6725 }
6726 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6727 let cmra_length = recovered.cmra_length;
6728 let authority = validate_recovered_recipient_wrap_terminal_authority(
6729 recovered.image,
6730 recovered.tuple,
6731 resolver,
6732 true,
6733 )?;
6734 Ok(RecipientWrapTerminalAuthorityCandidate {
6735 authority,
6736 anchor: to_usize(
6737 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6738 "CMRA anchor overflow",
6739 )?,
6740 cmra_offset,
6741 cmra_length,
6742 })
6743}
6744
6745fn parse_locatorless_cmra_authority_candidate_read_at(
6746 reader: &dyn ArchiveReadAt,
6747 cmra_offset: u64,
6748 master_key: &MasterKey,
6749) -> Result<TerminalAuthorityCandidate, FormatError> {
6750 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
6751 if recovered.image.body_bytes_before_cmra != cmra_offset {
6752 return Err(FormatError::InvalidArchive(
6753 "locatorless CMRA boundary mismatch",
6754 ));
6755 }
6756 if recovered
6757 .image
6758 .volume_trailer_offset
6759 .checked_add(VOLUME_TRAILER_LEN as u64)
6760 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6761 != cmra_offset
6762 {
6763 return Err(FormatError::InvalidArchive(
6764 "locatorless trailer boundary mismatch",
6765 ));
6766 }
6767 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6768 let cmra_length = recovered.cmra_length;
6769 let authority =
6770 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
6771 Ok(TerminalAuthorityCandidate {
6772 authority,
6773 anchor: to_usize(
6774 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
6775 "CMRA anchor overflow",
6776 )?,
6777 cmra_offset,
6778 cmra_length,
6779 })
6780}
6781
6782fn parse_public_locatorless_cmra_candidate(
6783 bytes: &[u8],
6784 cmra_offset: usize,
6785 volume_header: &VolumeHeader,
6786 crypto_header: &CryptoHeader<'_>,
6787) -> Result<PublicTerminalCandidate, FormatError> {
6788 let recovered = recover_cmra(
6789 bytes,
6790 cmra_offset as u64,
6791 None,
6792 CmraRecoveryMode::PublicNoKey,
6793 )?;
6794 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
6795 return Err(FormatError::InvalidArchive(
6796 "locatorless CMRA boundary mismatch",
6797 ));
6798 }
6799 if recovered
6800 .image
6801 .volume_trailer_offset
6802 .checked_add(VOLUME_TRAILER_LEN as u64)
6803 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
6804 != cmra_offset as u64
6805 {
6806 return Err(FormatError::InvalidArchive(
6807 "locatorless trailer boundary mismatch",
6808 ));
6809 }
6810 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
6811 let terminal = validate_recovered_public_terminal(
6812 recovered.image,
6813 bytes,
6814 volume_header,
6815 crypto_header,
6816 true,
6817 )?;
6818 Ok(PublicTerminalCandidate {
6819 terminal,
6820 anchor: cmra_offset
6821 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
6822 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
6823 cmra_offset: cmra_offset as u64,
6824 cmra_length: recovered.cmra_length,
6825 })
6826}
6827
6828fn validate_locator_position(
6829 locator_offset: usize,
6830 locator: CriticalRecoveryLocator,
6831) -> Result<(), FormatError> {
6832 if locator.cmra_offset != locator.body_bytes_before_cmra {
6833 return Err(FormatError::InvalidArchive(
6834 "locator CMRA boundary mismatch",
6835 ));
6836 }
6837 if locator
6838 .volume_trailer_offset
6839 .checked_add(VOLUME_TRAILER_LEN as u64)
6840 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
6841 != locator.cmra_offset
6842 {
6843 return Err(FormatError::InvalidArchive(
6844 "locator trailer boundary mismatch",
6845 ));
6846 }
6847 let expected_offset = match locator.locator_sequence {
6848 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
6849 0 => locator
6850 .cmra_offset
6851 .checked_add(locator.cmra_length as u64)
6852 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
6853 _ => None,
6854 }
6855 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
6856 if expected_offset != locator_offset as u64 {
6857 return Err(FormatError::InvalidArchive(
6858 "locator position does not match sequence",
6859 ));
6860 }
6861 Ok(())
6862}
6863
6864fn validate_locator_image_boundary(
6865 locator: CriticalRecoveryLocator,
6866 image: &CriticalMetadataImage,
6867) -> Result<(), FormatError> {
6868 if locator.volume_format_rev != image.volume_format_rev
6869 || locator.volume_trailer_offset != image.volume_trailer_offset
6870 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
6871 || image
6872 .volume_trailer_offset
6873 .checked_add(VOLUME_TRAILER_LEN as u64)
6874 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
6875 != locator.cmra_offset
6876 {
6877 return Err(FormatError::InvalidArchive(
6878 "locator and CMRA image boundaries differ",
6879 ));
6880 }
6881 Ok(())
6882}
6883
6884fn validate_cmra_identity_hints(
6885 header_hints: Option<CmraIdentityHints>,
6886 locator_hints: Option<CmraIdentityHints>,
6887 image: &CriticalMetadataImage,
6888) -> Result<(), FormatError> {
6889 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
6890 if header != locator {
6891 return Err(FormatError::InvalidArchive(
6892 "CMRA header and locator identity hints differ",
6893 ));
6894 }
6895 }
6896 for hints in [header_hints, locator_hints].into_iter().flatten() {
6897 if hints.archive_uuid != image.archive_uuid
6898 || hints.session_id != image.session_id
6899 || hints.volume_index != image.volume_index
6900 {
6901 return Err(FormatError::InvalidArchive(
6902 "CMRA identity hints do not match recovered image",
6903 ));
6904 }
6905 }
6906 Ok(())
6907}
6908
6909fn recover_cmra(
6910 bytes: &[u8],
6911 cmra_offset: u64,
6912 locator_tuple: Option<CmraDecoderTuple>,
6913 mode: CmraRecoveryMode,
6914) -> Result<RecoveredCmra, FormatError> {
6915 let offset = to_usize(cmra_offset, "CMRA")?;
6916 let header_bytes = slice(
6917 bytes,
6918 offset,
6919 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6920 "CriticalMetadataRecoveryHeader",
6921 )?;
6922 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
6923 validate_cmra_decoder_tuple(tuple)?;
6924 let cmra_length = cmra_serialized_length(tuple)?;
6925 let cmra_len = to_usize(cmra_length, "CMRA")?;
6926 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
6927 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
6928}
6929
6930fn recover_cmra_read_at(
6931 reader: &dyn ArchiveReadAt,
6932 cmra_offset: u64,
6933 locator_tuple: Option<CmraDecoderTuple>,
6934 mode: CmraRecoveryMode,
6935) -> Result<RecoveredCmra, FormatError> {
6936 let header_bytes = read_at_vec(
6937 reader,
6938 cmra_offset,
6939 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
6940 "CriticalMetadataRecoveryHeader",
6941 )?;
6942 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
6943 validate_cmra_decoder_tuple(tuple)?;
6944 let cmra_length = cmra_serialized_length(tuple)?;
6945 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
6946 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
6947}
6948
6949fn recover_cmra_header_tuple(
6950 header_bytes: &[u8],
6951 locator_tuple: Option<CmraDecoderTuple>,
6952) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
6953 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
6954 Ok(match (parsed_header, locator_tuple) {
6955 (Ok(header), Some(locator_tuple)) => {
6956 let header_tuple = CmraDecoderTuple::from(header);
6957 if header_tuple != locator_tuple {
6958 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
6959 }
6960 (locator_tuple, Some(CmraIdentityHints::from(header)))
6961 }
6962 (Ok(header), None) => (
6963 CmraDecoderTuple::from(header),
6964 Some(CmraIdentityHints::from(header)),
6965 ),
6966 (Err(_), Some(tuple)) => (tuple, None),
6967 (Err(err), _) => return Err(err),
6968 })
6969}
6970
6971fn recover_cmra_from_bytes(
6972 cmra_bytes: &[u8],
6973 tuple: CmraDecoderTuple,
6974 header_hints: Option<CmraIdentityHints>,
6975 cmra_length: u64,
6976 mode: CmraRecoveryMode,
6977) -> Result<RecoveredCmra, FormatError> {
6978 let shard_size = tuple.shard_size as usize;
6979 let mut data_shards = vec![None; tuple.data_shard_count as usize];
6980 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
6981 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
6982 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
6983 let raw = slice(
6984 cmra_bytes,
6985 cursor,
6986 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
6987 "CriticalMetadataRecoveryShard",
6988 )?;
6989 let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
6990 if let Some(shard) = shard {
6991 validate_cmra_shard(&shard, idx, tuple)?;
6992 if shard.shard_role == 0 {
6993 let data_slot = data_shards
6994 .get_mut(idx)
6995 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
6996 *data_slot = Some(shard.payload);
6997 } else {
6998 let parity_idx = idx - tuple.data_shard_count as usize;
6999 let parity_slot =
7000 parity_shards
7001 .get_mut(parity_idx)
7002 .ok_or(FormatError::InvalidArchive(
7003 "CMRA parity shard out of range",
7004 ))?;
7005 *parity_slot = Some(shard.payload);
7006 }
7007 }
7008 cursor = checked_add(
7009 cursor,
7010 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
7011 "CriticalMetadataRecoveryShard",
7012 )?;
7013 }
7014 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
7015 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
7016 for shard in repaired {
7017 image_bytes.extend_from_slice(&shard);
7018 }
7019 image_bytes.truncate(tuple.image_length as usize);
7020 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
7021 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
7022 }
7023 let image = CriticalMetadataImage::parse(&image_bytes)?;
7024 validate_critical_metadata_image(&image, mode)?;
7025 Ok(RecoveredCmra {
7026 image,
7027 tuple,
7028 header_hints,
7029 cmra_length,
7030 })
7031}
7032
7033fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
7034 let shard_size = tuple.shard_size as u64;
7035 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
7036 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
7037 }
7038 let image_length = tuple.image_length as u64;
7039 let min = critical_image_min();
7040 let cap = critical_image_cap()?;
7041 if image_length < min || image_length > cap {
7042 return Err(FormatError::InvalidArchive(
7043 "CMRA image_length is outside bounds",
7044 ));
7045 }
7046 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
7047 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
7048 return Err(FormatError::InvalidArchive(
7049 "CMRA data_shard_count does not match image length",
7050 ));
7051 }
7052 let max_parity = 2u64.max(ceil_div_u64(
7053 checked_u64_mul(
7054 expected_data_shards,
7055 READER_MAX_CMRA_PARITY_PCT as u64,
7056 "CMRA parity overflow",
7057 )?,
7058 100,
7059 )?);
7060 if tuple.parity_shard_count as u64 > max_parity {
7061 return Err(FormatError::ReaderResourceLimitExceeded {
7062 field: "CMRA parity shard count",
7063 cap: max_parity,
7064 actual: tuple.parity_shard_count as u64,
7065 });
7066 }
7067 let total = expected_data_shards
7068 .checked_add(tuple.parity_shard_count as u64)
7069 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
7070 if total > 65_535 {
7071 return Err(FormatError::FecTooManyShards(total as usize));
7072 }
7073 Ok(())
7074}
7075
7076fn validate_cmra_writer_parity_lower_bound(
7077 tuple: CmraDecoderTuple,
7078 bit_rot_buffer_pct: u8,
7079) -> Result<(), FormatError> {
7080 let min_parity = 2u64.max(ceil_div_u64(
7081 checked_u64_mul(
7082 tuple.data_shard_count as u64,
7083 bit_rot_buffer_pct as u64,
7084 "CMRA parity lower-bound overflow",
7085 )?,
7086 100,
7087 )?);
7088 if (tuple.parity_shard_count as u64) < min_parity {
7089 return Err(FormatError::InvalidArchive(
7090 "CMRA parity shard count is below authenticated bit-rot lower bound",
7091 ));
7092 }
7093 Ok(())
7094}
7095
7096fn validate_cmra_shard(
7097 shard: &CriticalMetadataRecoveryShard,
7098 serialized_idx: usize,
7099 tuple: CmraDecoderTuple,
7100) -> Result<(), FormatError> {
7101 if shard.shard_index as usize != serialized_idx {
7102 return Err(FormatError::InvalidArchive(
7103 "CMRA shards are not in canonical order",
7104 ));
7105 }
7106 let data_count = tuple.data_shard_count as usize;
7107 let shard_size = tuple.shard_size as usize;
7108 if serialized_idx < data_count {
7109 if shard.shard_role != 0 {
7110 return Err(FormatError::InvalidArchive(
7111 "CMRA data shard has wrong role",
7112 ));
7113 }
7114 let expected_len = if serialized_idx + 1 == data_count {
7115 let used = tuple.image_length as usize - serialized_idx * shard_size;
7116 if used == 0 {
7117 shard_size
7118 } else {
7119 used
7120 }
7121 } else {
7122 shard_size
7123 };
7124 if shard.shard_payload_length as usize != expected_len {
7125 return Err(FormatError::InvalidArchive(
7126 "CMRA data shard payload length is non-canonical",
7127 ));
7128 }
7129 if serialized_idx + 1 == data_count
7130 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
7131 {
7132 return Err(FormatError::InvalidArchive(
7133 "CMRA final data shard padding is non-zero",
7134 ));
7135 }
7136 } else {
7137 if shard.shard_role != 1 {
7138 return Err(FormatError::InvalidArchive(
7139 "CMRA parity shard has wrong role",
7140 ));
7141 }
7142 if shard.shard_payload_length as usize != shard_size {
7143 return Err(FormatError::InvalidArchive(
7144 "CMRA parity shard payload length is non-canonical",
7145 ));
7146 }
7147 }
7148 Ok(())
7149}
7150
7151fn validate_critical_metadata_image(
7152 image: &CriticalMetadataImage,
7153 mode: CmraRecoveryMode,
7154) -> Result<(), FormatError> {
7155 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7156 let key_wrap_layout_present = image.layout_flags & 0x0000_0002 != 0;
7157 let key_wrap_region = image.region(6);
7158 if key_wrap_layout_present != key_wrap_region.is_some() {
7159 return Err(FormatError::InvalidArchive(
7160 "CriticalMetadataImage key-wrap layout flag mismatch",
7161 ));
7162 }
7163 let key_wrap_present = key_wrap_layout_present;
7164 if image.volume_header_offset != 0
7165 || image.volume_header_length != VOLUME_HEADER_LEN as u32
7166 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
7167 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7168 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
7169 || image.body_bytes_before_cmra
7170 != image
7171 .volume_trailer_offset
7172 .checked_add(VOLUME_TRAILER_LEN as u64)
7173 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
7174 {
7175 return Err(FormatError::InvalidArchive(
7176 "CriticalMetadataImage fixed layout is invalid",
7177 ));
7178 }
7179 if root_auth_present {
7180 if image.root_auth_footer_offset == 0
7181 || image.root_auth_footer_length == 0
7182 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7183 {
7184 return Err(FormatError::InvalidArchive(
7185 "CriticalMetadataImage root-auth range is invalid",
7186 ));
7187 }
7188 } else if image.root_auth_footer_offset != 0
7189 || image.root_auth_footer_length != 0
7190 || image.root_auth_footer_sha256 != [0u8; 32]
7191 {
7192 return Err(FormatError::InvalidArchive(
7193 "CriticalMetadataImage root-auth fields must be zero when absent",
7194 ));
7195 }
7196 let block_record_len = image_block_record_len_from_region(image)?;
7197 let block_record_len_u64 = u64::try_from(block_record_len)
7198 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
7199 match mode {
7200 CmraRecoveryMode::KeyHolding => {
7201 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
7202 FormatError::InvalidArchive("BlockRecord region length overflow"),
7203 )?;
7204 if image.block_records_length != expected_len {
7205 return Err(FormatError::InvalidArchive(
7206 "CriticalMetadataImage terminal equations are invalid",
7207 ));
7208 }
7209 }
7210 CmraRecoveryMode::PublicNoKey => {
7211 if image.block_records_length % block_record_len_u64 != 0 {
7212 return Err(FormatError::InvalidArchive(
7213 "CriticalMetadataImage BlockRecord region is not aligned",
7214 ));
7215 }
7216 }
7217 }
7218 let crypto_header_end = image
7219 .crypto_header_offset
7220 .checked_add(image.crypto_header_length as u64)
7221 .ok_or(FormatError::InvalidArchive(
7222 "CryptoHeader boundary overflow",
7223 ))?;
7224 let expected_block_records_offset = if key_wrap_present {
7225 let key_wrap_region = key_wrap_region.ok_or(FormatError::InvalidArchive(
7226 "missing CriticalMetadataImage key-wrap region",
7227 ))?;
7228 if image.key_wrap_table_offset != crypto_header_end
7229 || image.key_wrap_table_length == 0
7230 || key_wrap_region.offset != image.key_wrap_table_offset
7231 || key_wrap_region.bytes.len() != image.key_wrap_table_length as usize
7232 {
7233 return Err(FormatError::InvalidArchive(
7234 "CriticalMetadataImage key-wrap region is malformed",
7235 ));
7236 }
7237 image
7238 .key_wrap_table_offset
7239 .checked_add(image.key_wrap_table_length as u64)
7240 .ok_or(FormatError::InvalidArchive(
7241 "KeyWrapTableV1 boundary overflow",
7242 ))?
7243 } else {
7244 if image.key_wrap_table_offset != 0
7245 || image.key_wrap_table_length != 0
7246 || image.key_wrap_table_sha256 != [0u8; 32]
7247 {
7248 return Err(FormatError::InvalidArchive(
7249 "CriticalMetadataImage key-wrap fields must be zero when absent",
7250 ));
7251 }
7252 crypto_header_end
7253 };
7254 if image.block_records_offset != expected_block_records_offset
7255 || image.manifest_footer_offset
7256 != image
7257 .block_records_offset
7258 .checked_add(image.block_records_length)
7259 .ok_or(FormatError::InvalidArchive(
7260 "ManifestFooter boundary overflow",
7261 ))?
7262 {
7263 return Err(FormatError::InvalidArchive(
7264 "CriticalMetadataImage terminal equations are invalid",
7265 ));
7266 }
7267 let manifest_end = image
7268 .manifest_footer_offset
7269 .checked_add(MANIFEST_FOOTER_LEN as u64)
7270 .ok_or(FormatError::InvalidArchive(
7271 "RootAuthFooter boundary overflow",
7272 ))?;
7273 if root_auth_present {
7274 if image.root_auth_footer_offset != manifest_end
7275 || image
7276 .root_auth_footer_offset
7277 .checked_add(image.root_auth_footer_length as u64)
7278 .ok_or(FormatError::InvalidArchive(
7279 "VolumeTrailer boundary overflow",
7280 ))?
7281 != image.volume_trailer_offset
7282 {
7283 return Err(FormatError::InvalidArchive(
7284 "CriticalMetadataImage root-auth terminal equations are invalid",
7285 ));
7286 }
7287 } else if image.volume_trailer_offset != manifest_end {
7288 return Err(FormatError::InvalidArchive(
7289 "CriticalMetadataImage unsigned terminal equations are invalid",
7290 ));
7291 }
7292 let expected_types: &[u16] = match (key_wrap_present, root_auth_present) {
7293 (false, false) => &[1, 2, 3, 5],
7294 (false, true) => &[1, 2, 3, 4, 5],
7295 (true, false) => &[1, 2, 6, 3, 5],
7296 (true, true) => &[1, 2, 6, 3, 4, 5],
7297 };
7298 if image.regions.len() != expected_types.len()
7299 || image
7300 .regions
7301 .iter()
7302 .map(|region| region.region_type)
7303 .ne(expected_types.iter().copied())
7304 {
7305 return Err(FormatError::InvalidArchive(
7306 "CriticalMetadataImage regions are not canonical",
7307 ));
7308 }
7309 validate_image_region(
7310 image,
7311 1,
7312 image.volume_header_offset,
7313 image.volume_header_length,
7314 )?;
7315 validate_image_region(
7316 image,
7317 2,
7318 image.crypto_header_offset,
7319 image.crypto_header_length,
7320 )?;
7321 validate_image_region(
7322 image,
7323 3,
7324 image.manifest_footer_offset,
7325 image.manifest_footer_length,
7326 )?;
7327 if key_wrap_present {
7328 validate_image_region(
7329 image,
7330 6,
7331 image.key_wrap_table_offset,
7332 image.key_wrap_table_length,
7333 )?;
7334 }
7335 if root_auth_present {
7336 validate_image_region(
7337 image,
7338 4,
7339 image.root_auth_footer_offset,
7340 image.root_auth_footer_length,
7341 )?;
7342 }
7343 validate_image_region(
7344 image,
7345 5,
7346 image.volume_trailer_offset,
7347 image.volume_trailer_length,
7348 )?;
7349 if sha256_region(image, 1)? != image.volume_header_sha256
7350 || sha256_region(image, 2)? != image.crypto_header_sha256
7351 || (key_wrap_present && sha256_region(image, 6)? != image.key_wrap_table_sha256)
7352 || (!key_wrap_present && image.key_wrap_table_sha256 != [0u8; 32])
7353 || sha256_region(image, 3)? != image.manifest_footer_sha256
7354 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
7355 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
7356 || sha256_region(image, 5)? != image.volume_trailer_sha256
7357 {
7358 return Err(FormatError::InvalidArchive(
7359 "CriticalMetadataImage region digest mismatch",
7360 ));
7361 }
7362 Ok(())
7363}
7364
7365fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
7366 let crypto_region = image
7367 .region(2)
7368 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7369 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7370 crypto.fixed.validate_supported_profile()?;
7371 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
7372}
7373
7374fn validate_image_region(
7375 image: &CriticalMetadataImage,
7376 region_type: u16,
7377 offset: u64,
7378 length: u32,
7379) -> Result<(), FormatError> {
7380 let region = image
7381 .region(region_type)
7382 .ok_or(FormatError::InvalidArchive(
7383 "missing CriticalMetadataImage region",
7384 ))?;
7385 if region.offset != offset || region.bytes.len() != length as usize {
7386 return Err(FormatError::InvalidArchive(
7387 "CriticalMetadataImage region range mismatch",
7388 ));
7389 }
7390 Ok(())
7391}
7392
7393fn validate_image_identity(
7394 image: &CriticalMetadataImage,
7395 volume_header: &VolumeHeader,
7396 crypto_header: &CryptoHeaderFixed,
7397) -> Result<(), FormatError> {
7398 if image.volume_format_rev != volume_header.volume_format_rev
7399 || image.archive_uuid != volume_header.archive_uuid
7400 || image.session_id != volume_header.session_id
7401 || image.volume_index != volume_header.volume_index
7402 || image.stripe_width != volume_header.stripe_width
7403 || image.stripe_width != crypto_header.stripe_width
7404 {
7405 return Err(FormatError::InvalidArchive(
7406 "CriticalMetadataImage identity does not match selected volume",
7407 ));
7408 }
7409 Ok(())
7410}
7411
7412fn validate_image_key_wrap_table(
7413 image: &CriticalMetadataImage,
7414 volume_header: &VolumeHeader,
7415 kdf_params: &KdfParams,
7416) -> Result<(), FormatError> {
7417 match kdf_params {
7418 KdfParams::RecipientWrap {
7419 key_wrap_table_length,
7420 key_wrap_table_record_count,
7421 key_wrap_table_digest,
7422 ..
7423 } => {
7424 if image.layout_flags & 0x0000_0002 == 0
7425 || image.key_wrap_table_length != *key_wrap_table_length
7426 {
7427 return Err(FormatError::InvalidArchive(
7428 "CriticalMetadataImage key-wrap fields do not match KdfParams",
7429 ));
7430 }
7431 let region = image.region(6).ok_or(FormatError::InvalidArchive(
7432 "missing CriticalMetadataImage key-wrap region",
7433 ))?;
7434 if region.offset != image.key_wrap_table_offset
7435 || region.bytes.len() != *key_wrap_table_length as usize
7436 {
7437 return Err(FormatError::InvalidArchive(
7438 "CriticalMetadataImage key-wrap region is malformed",
7439 ));
7440 }
7441 if compute_key_wrap_table_digest(*key_wrap_table_length, ®ion.bytes)
7442 != *key_wrap_table_digest
7443 {
7444 return Err(FormatError::IntegrityDigestMismatch {
7445 structure: "KeyWrapTableV1",
7446 });
7447 }
7448 KeyWrapTableV1::parse(
7449 ®ion.bytes,
7450 &volume_header.archive_uuid,
7451 &volume_header.session_id,
7452 *key_wrap_table_length,
7453 *key_wrap_table_record_count,
7454 )?;
7455 Ok(())
7456 }
7457 _ => {
7458 if image.layout_flags & 0x0000_0002 != 0
7459 || image.region(6).is_some()
7460 || image.key_wrap_table_offset != 0
7461 || image.key_wrap_table_length != 0
7462 || image.key_wrap_table_sha256 != [0u8; 32]
7463 {
7464 return Err(FormatError::InvalidArchive(
7465 "CriticalMetadataImage key-wrap fields must be zero when absent",
7466 ));
7467 }
7468 Ok(())
7469 }
7470 }
7471}
7472
7473fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
7474 Ok(sha256_bytes(
7475 &image
7476 .region(region_type)
7477 .ok_or(FormatError::InvalidArchive(
7478 "missing CriticalMetadataImage region",
7479 ))?
7480 .bytes,
7481 ))
7482}
7483
7484fn validate_recovered_terminal_authority(
7485 image: CriticalMetadataImage,
7486 tuple: CmraDecoderTuple,
7487 master_key: &MasterKey,
7488 require_cmra_boundary_magic: bool,
7489) -> Result<RecoveredTerminalAuthority, FormatError> {
7490 let volume_header_region = image
7491 .region(1)
7492 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7493 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7494 let crypto_region = image
7495 .region(2)
7496 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7497 let crypto_header_bytes = crypto_region.bytes.clone();
7498 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7499 let kdf_params = parsed_crypto.kdf_params.clone();
7500 let subkeys = subkeys_for_open(
7501 Some(master_key),
7502 parsed_crypto.fixed.aead_algo,
7503 &volume_header.archive_uuid,
7504 &volume_header.session_id,
7505 )?;
7506 verify_integrity_tag(
7507 HmacDomain::CryptoHeader,
7508 parsed_crypto.fixed.aead_algo,
7509 volume_header.volume_format_rev,
7510 Some(&subkeys.mac_key),
7511 &volume_header.archive_uuid,
7512 &volume_header.session_id,
7513 parsed_crypto.hmac_covered_bytes,
7514 &parsed_crypto.header_hmac,
7515 )?;
7516 parsed_crypto.validate_extension_semantics()?;
7517 validate_seekable_supported_volume(
7518 &volume_header,
7519 &parsed_crypto.fixed,
7520 &parsed_crypto.extensions,
7521 )?;
7522 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7523 let crypto_header = parsed_crypto.fixed.clone();
7524 if crypto_header.bit_rot_buffer_pct == 0 {
7525 return Err(FormatError::InvalidArchive(
7526 "CMRA startup recovery requires a nonzero bit-rot budget",
7527 ));
7528 }
7529 drop(parsed_crypto);
7530
7531 let terminal = validate_recovered_terminal_inner(
7532 image,
7533 tuple,
7534 require_cmra_boundary_magic,
7535 true,
7536 KeyHoldingTerminalContext {
7537 subkeys: &subkeys,
7538 volume_header: &volume_header,
7539 crypto_header: &crypto_header,
7540 crypto_header_bytes: &crypto_header_bytes,
7541 },
7542 )?;
7543 Ok(RecoveredTerminalAuthority {
7544 terminal,
7545 volume_header,
7546 crypto_header,
7547 crypto_header_bytes,
7548 subkeys,
7549 kdf_params,
7550 })
7551}
7552
7553fn validate_recovered_recipient_wrap_terminal_authority<F>(
7554 image: CriticalMetadataImage,
7555 tuple: CmraDecoderTuple,
7556 resolver: &mut F,
7557 require_cmra_boundary_magic: bool,
7558) -> Result<RecoveredRecipientWrapTerminalAuthority, FormatError>
7559where
7560 F: FnMut(
7561 RecipientWrapRecordContext<'_>,
7562 ) -> Result<Vec<RecipientWrapCandidateMasterKey>, FormatError>,
7563{
7564 let volume_header_region = image
7565 .region(1)
7566 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7567 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7568 let crypto_region = image
7569 .region(2)
7570 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7571 let crypto_header_bytes = crypto_region.bytes.clone();
7572 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
7573 if !matches!(parsed_crypto.kdf_params, KdfParams::RecipientWrap { .. })
7574 || !parsed_crypto.fixed.aead_algo.is_encrypted()
7575 {
7576 return Err(FormatError::KeyMaterialMismatch);
7577 }
7578 validate_seekable_supported_volume(&volume_header, &parsed_crypto.fixed, &[])?;
7579 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7580 if parsed_crypto.fixed.bit_rot_buffer_pct == 0 {
7581 return Err(FormatError::InvalidArchive(
7582 "CMRA startup recovery requires a nonzero bit-rot budget",
7583 ));
7584 }
7585 validate_cmra_writer_parity_lower_bound(tuple, parsed_crypto.fixed.bit_rot_buffer_pct)?;
7586 validate_image_key_wrap_table(&image, &volume_header, &parsed_crypto.kdf_params)?;
7587 let key_wrap_region = image.region(6).ok_or(FormatError::InvalidArchive(
7588 "missing CriticalMetadataImage key-wrap region",
7589 ))?;
7590 let startup_key_wrap_table = parse_startup_key_wrap_table_bytes(
7591 &volume_header,
7592 &parsed_crypto.kdf_params,
7593 key_wrap_region.bytes.clone(),
7594 )?;
7595 let subkeys = recipient_wrap_subkeys_from_table(
7596 &volume_header,
7597 &parsed_crypto,
7598 &startup_key_wrap_table.table,
7599 resolver,
7600 )?;
7601 parsed_crypto.validate_extension_semantics()?;
7602 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
7603 let crypto_header = parsed_crypto.fixed.clone();
7604
7605 let terminal = validate_recovered_terminal_inner(
7606 image,
7607 tuple,
7608 require_cmra_boundary_magic,
7609 true,
7610 KeyHoldingTerminalContext {
7611 subkeys: &subkeys,
7612 volume_header: &volume_header,
7613 crypto_header: &crypto_header,
7614 crypto_header_bytes: &crypto_header_bytes,
7615 },
7616 )?;
7617 Ok(RecoveredRecipientWrapTerminalAuthority {
7618 terminal,
7619 volume_header,
7620 crypto_header,
7621 crypto_header_bytes,
7622 key_wrap_table_bytes: startup_key_wrap_table.bytes,
7623 block_records_start: startup_key_wrap_table.block_records_start,
7624 subkeys,
7625 })
7626}
7627
7628fn validate_recovered_terminal(
7629 image: CriticalMetadataImage,
7630 tuple: CmraDecoderTuple,
7631 bytes: &[u8],
7632 context: KeyHoldingTerminalContext<'_>,
7633 require_cmra_boundary_magic: bool,
7634) -> Result<V41Terminal, FormatError> {
7635 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7636 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
7637 validate_recovered_terminal_inner(
7638 image,
7639 tuple,
7640 require_cmra_boundary_magic,
7641 cmra_boundary_magic_ok,
7642 context,
7643 )
7644}
7645
7646fn validate_recovered_terminal_read_at(
7647 image: CriticalMetadataImage,
7648 tuple: CmraDecoderTuple,
7649 reader: &dyn ArchiveReadAt,
7650 context: KeyHoldingTerminalContext<'_>,
7651 require_cmra_boundary_magic: bool,
7652) -> Result<V41Terminal, FormatError> {
7653 let mut magic = [0u8; 4];
7654 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
7655 validate_recovered_terminal_inner(
7656 image,
7657 tuple,
7658 require_cmra_boundary_magic,
7659 magic == *b"TZCR",
7660 context,
7661 )
7662}
7663
7664fn validate_recovered_terminal_inner(
7665 image: CriticalMetadataImage,
7666 tuple: CmraDecoderTuple,
7667 require_cmra_boundary_magic: bool,
7668 cmra_boundary_magic_ok: bool,
7669 context: KeyHoldingTerminalContext<'_>,
7670) -> Result<V41Terminal, FormatError> {
7671 let subkeys = context.subkeys;
7672 let volume_header = context.volume_header;
7673 let crypto_header = context.crypto_header;
7674 let volume_header_region = image
7675 .region(1)
7676 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7677 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7678 if &recovered_volume_header != volume_header {
7679 return Err(FormatError::InvalidArchive(
7680 "CMRA VolumeHeader differs from parsed VolumeHeader",
7681 ));
7682 }
7683 validate_image_identity(&image, volume_header, crypto_header)?;
7684 let crypto_region = image
7685 .region(2)
7686 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7687 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7688 if recovered_crypto.fixed != *crypto_header {
7689 return Err(FormatError::InvalidArchive(
7690 "CMRA CryptoHeader differs from parsed CryptoHeader",
7691 ));
7692 }
7693 let recovered_pre_hmac_len = crypto_region
7694 .bytes
7695 .len()
7696 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7697 .ok_or(FormatError::InvalidArchive(
7698 "CMRA CryptoHeader is too short",
7699 ))?;
7700 let parsed_pre_hmac_len = context
7701 .crypto_header_bytes
7702 .len()
7703 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
7704 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
7705 if recovered_pre_hmac_len != parsed_pre_hmac_len
7706 || crypto_region.bytes[..recovered_pre_hmac_len]
7707 != context.crypto_header_bytes[..parsed_pre_hmac_len]
7708 {
7709 return Err(FormatError::InvalidArchive(
7710 "CMRA CryptoHeader differs from parsed CryptoHeader",
7711 ));
7712 }
7713 verify_integrity_tag(
7714 HmacDomain::CryptoHeader,
7715 recovered_crypto.fixed.aead_algo,
7716 volume_header.volume_format_rev,
7717 Some(&subkeys.mac_key),
7718 &volume_header.archive_uuid,
7719 &volume_header.session_id,
7720 recovered_crypto.hmac_covered_bytes,
7721 &recovered_crypto.header_hmac,
7722 )?;
7723 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
7724 recovered_crypto.validate_extension_semantics()?;
7725 validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7726
7727 let manifest_region = image
7728 .region(3)
7729 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7730 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
7731 validate_manifest_footer(
7732 volume_header,
7733 crypto_header,
7734 &manifest_footer,
7735 subkeys,
7736 volume_header.volume_format_rev,
7737 &manifest_region.bytes,
7738 )?;
7739 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
7740
7741 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
7742 let root_auth_region = image
7743 .region(4)
7744 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7745 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7746 if footer.format_version != volume_header.format_version
7747 || footer.volume_format_rev != volume_header.volume_format_rev
7748 {
7749 return Err(FormatError::InvalidArchive(
7750 "RootAuthFooter format/revision does not match VolumeHeader",
7751 ));
7752 }
7753 if footer.archive_uuid != volume_header.archive_uuid
7754 || footer.session_id != volume_header.session_id
7755 || footer.footer_length()? != image.root_auth_footer_length
7756 {
7757 return Err(FormatError::InvalidArchive(
7758 "RootAuthFooter identity or length does not match terminal image",
7759 ));
7760 }
7761 Some(footer)
7762 } else {
7763 None
7764 };
7765
7766 let trailer_region = image
7767 .region(5)
7768 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7769 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7770 verify_integrity_tag(
7771 HmacDomain::VolumeTrailer,
7772 crypto_header.aead_algo,
7773 volume_header.volume_format_rev,
7774 Some(&subkeys.mac_key),
7775 &volume_header.archive_uuid,
7776 &volume_header.session_id,
7777 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
7778 &trailer.trailer_hmac,
7779 )?;
7780 validate_trailer_identity(volume_header, &trailer)?;
7781 validate_v41_trailer_equations(&image, &trailer)?;
7782
7783 if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
7784 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7785 }
7786
7787 let manifest_footer_bytes = manifest_region.bytes.clone();
7788 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
7789 Ok(V41Terminal {
7790 image,
7791 manifest_footer_bytes,
7792 root_auth_footer_bytes,
7793 root_auth_footer,
7794 volume_trailer: trailer,
7795 })
7796}
7797
7798fn validate_recovered_public_terminal(
7799 image: CriticalMetadataImage,
7800 bytes: &[u8],
7801 volume_header: &VolumeHeader,
7802 public_crypto_header: &CryptoHeader<'_>,
7803 require_cmra_boundary_magic: bool,
7804) -> Result<V41PublicTerminal, FormatError> {
7805 if image.layout_flags & 0x0000_0001 == 0 {
7806 return Err(FormatError::ReaderUnsupported(
7807 "public no-key verification requires root-auth",
7808 ));
7809 }
7810 let volume_header_region = image
7811 .region(1)
7812 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
7813 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
7814 if &recovered_volume_header != volume_header {
7815 return Err(FormatError::InvalidArchive(
7816 "CMRA VolumeHeader differs from parsed VolumeHeader",
7817 ));
7818 }
7819 validate_image_identity(&image, volume_header, &public_crypto_header.fixed)?;
7820 let crypto_region = image
7821 .region(2)
7822 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
7823 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
7824 if !public_crypto_headers_agree(&recovered_crypto.fixed, &public_crypto_header.fixed)
7825 || !public_kdf_profiles_agree(
7826 &recovered_crypto.kdf_params,
7827 &public_crypto_header.kdf_params,
7828 )
7829 {
7830 return Err(FormatError::InvalidArchive(
7831 "CMRA CryptoHeader differs from parsed CryptoHeader",
7832 ));
7833 }
7834 recovered_crypto.validate_extension_semantics()?;
7835 validate_image_key_wrap_table(&image, volume_header, &recovered_crypto.kdf_params)?;
7836
7837 image
7838 .region(3)
7839 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
7840
7841 let root_auth_region = image
7842 .region(4)
7843 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
7844 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
7845 if root_auth_footer.format_version != volume_header.format_version
7846 || root_auth_footer.volume_format_rev != volume_header.volume_format_rev
7847 {
7848 return Err(FormatError::InvalidArchive(
7849 "public RootAuthFooter format/revision does not match VolumeHeader",
7850 ));
7851 }
7852 if root_auth_footer.archive_uuid != volume_header.archive_uuid
7853 || root_auth_footer.session_id != volume_header.session_id
7854 || root_auth_footer.footer_length()? != image.root_auth_footer_length
7855 {
7856 return Err(FormatError::InvalidArchive(
7857 "public RootAuthFooter identity or length does not match terminal image",
7858 ));
7859 }
7860
7861 let trailer_region = image
7862 .region(5)
7863 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
7864 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
7865 validate_trailer_identity(volume_header, &trailer)?;
7866 validate_v41_public_trailer_profile(&image, &trailer)?;
7867
7868 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
7869 if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
7870 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
7871 }
7872
7873 let root_auth_footer_bytes = root_auth_region.bytes.clone();
7874 Ok(V41PublicTerminal {
7875 image,
7876 root_auth_footer_bytes,
7877 root_auth_footer,
7878 })
7879}
7880
7881fn validate_v41_trailer_equations(
7882 image: &CriticalMetadataImage,
7883 trailer: &VolumeTrailer,
7884) -> Result<(), FormatError> {
7885 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
7886 if trailer.bytes_written != image.volume_trailer_offset
7887 || trailer.manifest_footer_offset != image.manifest_footer_offset
7888 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7889 || trailer.block_count != image.block_count
7890 {
7891 return Err(FormatError::InvalidArchive(
7892 "VolumeTrailer does not match v41 terminal layout",
7893 ));
7894 }
7895 if root_auth_present {
7896 if trailer.root_auth_flags != 0x0000_0001
7897 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7898 || trailer.root_auth_footer_length != image.root_auth_footer_length
7899 || image.root_auth_footer_offset
7900 != image
7901 .manifest_footer_offset
7902 .checked_add(MANIFEST_FOOTER_LEN as u64)
7903 .ok_or(FormatError::InvalidArchive(
7904 "RootAuthFooter trailer boundary overflow",
7905 ))?
7906 || image
7907 .root_auth_footer_offset
7908 .checked_add(image.root_auth_footer_length as u64)
7909 .ok_or(FormatError::InvalidArchive(
7910 "RootAuthFooter trailer boundary overflow",
7911 ))?
7912 != image.volume_trailer_offset
7913 {
7914 return Err(FormatError::InvalidArchive(
7915 "VolumeTrailer root-auth fields do not match v41 terminal layout",
7916 ));
7917 }
7918 } else if trailer.root_auth_footer_offset != 0
7919 || trailer.root_auth_footer_length != 0
7920 || trailer.root_auth_flags != 0
7921 {
7922 return Err(FormatError::InvalidArchive(
7923 "VolumeTrailer root-auth fields must be zero when absent",
7924 ));
7925 }
7926 Ok(())
7927}
7928
7929fn validate_v41_public_trailer_profile(
7930 image: &CriticalMetadataImage,
7931 trailer: &VolumeTrailer,
7932) -> Result<(), FormatError> {
7933 if trailer.bytes_written != image.volume_trailer_offset
7934 || trailer.manifest_footer_offset != image.manifest_footer_offset
7935 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
7936 {
7937 return Err(FormatError::InvalidArchive(
7938 "VolumeTrailer does not match v41 public terminal layout",
7939 ));
7940 }
7941 if trailer.root_auth_flags != 0x0000_0001
7942 || trailer.root_auth_footer_offset == 0
7943 || trailer.root_auth_footer_length == 0
7944 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
7945 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
7946 || trailer.root_auth_footer_length != image.root_auth_footer_length
7947 || image.root_auth_footer_offset
7948 != image
7949 .manifest_footer_offset
7950 .checked_add(MANIFEST_FOOTER_LEN as u64)
7951 .ok_or(FormatError::InvalidArchive(
7952 "RootAuthFooter trailer boundary overflow",
7953 ))?
7954 || image
7955 .root_auth_footer_offset
7956 .checked_add(image.root_auth_footer_length as u64)
7957 .ok_or(FormatError::InvalidArchive(
7958 "RootAuthFooter trailer boundary overflow",
7959 ))?
7960 != image.volume_trailer_offset
7961 {
7962 return Err(FormatError::InvalidArchive(
7963 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
7964 ));
7965 }
7966 Ok(())
7967}
7968
7969fn critical_image_min() -> u64 {
7970 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
7971 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
7972 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
7973 + VOLUME_HEADER_LEN as u64
7974 + MIN_CRYPTO_HEADER_LEN
7975 + MANIFEST_FOOTER_LEN as u64
7976 + VOLUME_TRAILER_LEN as u64
7977 + IMAGE_CRC_LEN as u64
7978}
7979
7980fn critical_image_cap() -> Result<u64, FormatError> {
7981 [
7982 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
7983 6 * SERIALIZED_REGION_HEADER_LEN as u64,
7984 VOLUME_HEADER_LEN as u64,
7985 READER_MAX_CRYPTO_HEADER_LEN as u64,
7986 READER_MAX_KEY_WRAP_TABLE_LEN as u64,
7987 MANIFEST_FOOTER_LEN as u64,
7988 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
7989 VOLUME_TRAILER_LEN as u64,
7990 IMAGE_CRC_LEN as u64,
7991 ]
7992 .into_iter()
7993 .try_fold(0u64, |total, value| {
7994 total
7995 .checked_add(value)
7996 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
7997 })
7998}
7999
8000fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
8001 let shard_total = (tuple.data_shard_count as u64)
8002 .checked_add(tuple.parity_shard_count as u64)
8003 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
8004 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
8005 .checked_add(tuple.shard_size as u64)
8006 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
8007 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
8008 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
8009 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
8010}
8011
8012fn cmra_worst_case_cap() -> Result<u64, FormatError> {
8013 let cap = critical_image_cap()?;
8014 let mut worst = 0u64;
8015 let mut shard_size = 512u64;
8016 while shard_size <= 4096 {
8017 let data = ceil_div_u64(cap, shard_size)?;
8018 let parity = 2u64.max(ceil_div_u64(
8019 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
8020 100,
8021 )?);
8022 let tuple = CmraDecoderTuple {
8023 shard_size: shard_size as u32,
8024 data_shard_count: u16::try_from(data)
8025 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
8026 parity_shard_count: u16::try_from(parity)
8027 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
8028 image_length: u32::try_from(cap)
8029 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
8030 image_sha256: [0u8; 32],
8031 };
8032 worst = worst.max(cmra_serialized_length(tuple)?);
8033 shard_size += 2;
8034 }
8035 Ok(worst)
8036}
8037
8038pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
8039 let total = [
8040 MANIFEST_FOOTER_LEN as u64,
8041 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
8042 VOLUME_TRAILER_LEN as u64,
8043 cmra_worst_case_cap()?,
8044 LOCATOR_PAIR_LEN as u64,
8045 ]
8046 .into_iter()
8047 .try_fold(0u64, |sum, value| {
8048 sum.checked_add(value)
8049 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
8050 })?;
8051 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
8052}
8053
8054fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
8055 let worst = cmra_worst_case_cap()?;
8056 let total = options
8057 .max_trailing_garbage_scan
8058 .try_into()
8059 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8060 .and_then(|scan: u64| {
8061 scan.checked_add(worst)
8062 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
8063 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
8064 })?;
8065 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
8066}
8067
8068fn validate_bootstrap_single_volume_input(
8069 volume_header: &VolumeHeader,
8070 crypto_header: &CryptoHeaderFixed,
8071) -> Result<(), FormatError> {
8072 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
8073 return Err(FormatError::ReaderUnsupported(
8074 "bootstrap sidecar reader supports only single-volume archive input",
8075 ));
8076 }
8077 if crypto_header.stripe_width != volume_header.stripe_width {
8078 return Err(FormatError::InvalidArchive(
8079 "VolumeHeader and CryptoHeader stripe_width differ",
8080 ));
8081 }
8082 Ok(())
8083}
8084
8085#[derive(Debug)]
8086struct ParsedBootstrapSidecar {
8087 manifest_footer: Option<ManifestFooter>,
8088 index_root_records_section: Option<(u64, u64)>,
8089 dictionary_records_section: Option<(u64, u64)>,
8090}
8091
8092pub(crate) struct NonSeekableBootstrapMaterial {
8093 pub(crate) manifest_footer: ManifestFooter,
8094 pub(crate) payload_dictionary: Option<Vec<u8>>,
8095}
8096
8097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8098enum BootstrapSidecarUse {
8099 SeekableAssist,
8100 NonSeekableRandomAccess,
8101}
8102
8103impl ParsedBootstrapSidecar {
8104 fn require_sections_for(
8105 &self,
8106 sidecar_use: BootstrapSidecarUse,
8107 crypto_header: &CryptoHeaderFixed,
8108 ) -> Result<(), FormatError> {
8109 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
8110 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
8111 return Err(FormatError::ReaderUnsupported(
8112 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8113 ));
8114 }
8115 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
8116 return Err(FormatError::ReaderUnsupported(
8117 "dictionary bootstrap required",
8118 ));
8119 }
8120 }
8121 Ok(())
8122 }
8123}
8124
8125pub(crate) fn parse_non_seekable_bootstrap_material(
8126 bootstrap_sidecar: &[u8],
8127 volume_header: &VolumeHeader,
8128 crypto_header: &CryptoHeaderFixed,
8129 subkeys: &Subkeys,
8130) -> Result<NonSeekableBootstrapMaterial, FormatError> {
8131 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
8132 let sidecar =
8133 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
8134 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
8135 let manifest_footer = sidecar
8136 .manifest_footer
8137 .clone()
8138 .ok_or(FormatError::ReaderUnsupported(
8139 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8140 ))?;
8141
8142 let mut blocks = BTreeMap::new();
8143 let (offset, length) =
8144 sidecar
8145 .index_root_records_section
8146 .ok_or(FormatError::ReaderUnsupported(
8147 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
8148 ))?;
8149 let index_root_records = parse_sidecar_block_records(
8150 bootstrap_sidecar,
8151 crypto_header.block_size as usize,
8152 SidecarBlockRecordsSection {
8153 offset,
8154 length,
8155 extent: index_root_extent_from_manifest(&manifest_footer),
8156 data_kind: BlockKind::IndexRootData,
8157 parity_kind: BlockKind::IndexRootParity,
8158 structure: "IndexRoot",
8159 },
8160 )?;
8161 insert_sidecar_records(&mut blocks, index_root_records)?;
8162
8163 let limits = metadata_limits(crypto_header);
8164 let index_root_plaintext = load_metadata_object_from_parts(
8165 &blocks,
8166 ObjectLoadContext::index_root(
8167 volume_header,
8168 crypto_header,
8169 subkeys,
8170 index_root_extent_from_manifest(&manifest_footer),
8171 ),
8172 manifest_footer.index_root_decompressed_size,
8173 )?;
8174 let index_root = IndexRoot::parse(
8175 &index_root_plaintext,
8176 crypto_header.has_dictionary != 0,
8177 limits,
8178 )?;
8179
8180 if crypto_header.has_dictionary != 0 {
8181 let (offset, length) =
8182 sidecar
8183 .dictionary_records_section
8184 .ok_or(FormatError::ReaderUnsupported(
8185 "dictionary bootstrap required",
8186 ))?;
8187 let dictionary_records = parse_sidecar_block_records(
8188 bootstrap_sidecar,
8189 crypto_header.block_size as usize,
8190 SidecarBlockRecordsSection {
8191 offset,
8192 length,
8193 extent: dictionary_extent_from_index_root(&index_root)?,
8194 data_kind: BlockKind::DictionaryData,
8195 parity_kind: BlockKind::DictionaryParity,
8196 structure: "dictionary",
8197 },
8198 )?;
8199 insert_sidecar_records(&mut blocks, dictionary_records)?;
8200 }
8201 let payload_dictionary =
8202 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
8203
8204 Ok(NonSeekableBootstrapMaterial {
8205 manifest_footer,
8206 payload_dictionary,
8207 })
8208}
8209
8210fn parse_bootstrap_sidecar(
8211 bytes: &[u8],
8212 volume_header: &VolumeHeader,
8213 crypto_header: &CryptoHeaderFixed,
8214 subkeys: &Subkeys,
8215) -> Result<ParsedBootstrapSidecar, FormatError> {
8216 let header_bytes = slice(
8217 bytes,
8218 0,
8219 BOOTSTRAP_SIDECAR_HEADER_LEN,
8220 "BootstrapSidecarHeader",
8221 )?;
8222 let header = BootstrapSidecarHeader::parse(header_bytes)?;
8223 if header.archive_uuid != volume_header.archive_uuid
8224 || header.session_id != volume_header.session_id
8225 {
8226 return Err(FormatError::InvalidArchive(
8227 "bootstrap sidecar identity does not match VolumeHeader",
8228 ));
8229 }
8230 verify_integrity_tag(
8231 HmacDomain::BootstrapSidecar,
8232 crypto_header.aead_algo,
8233 volume_header.volume_format_rev,
8234 Some(&subkeys.mac_key),
8235 &volume_header.archive_uuid,
8236 &volume_header.session_id,
8237 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
8238 &header.sidecar_hmac,
8239 )?;
8240 header.validate_packed_layout(bytes.len() as u64)?;
8241 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
8242
8243 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
8244 return Err(FormatError::InvalidArchive(
8245 "bootstrap sidecar has dictionary records while has_dictionary is false",
8246 ));
8247 }
8248
8249 let manifest_footer = if header.has_manifest_footer() {
8250 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
8251 let manifest_bytes = slice(
8252 bytes,
8253 manifest_offset,
8254 MANIFEST_FOOTER_LEN,
8255 "ManifestFooter",
8256 )?;
8257 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
8258 validate_sidecar_manifest_footer(
8259 volume_header,
8260 crypto_header,
8261 &manifest_footer,
8262 subkeys,
8263 volume_header.volume_format_rev,
8264 manifest_bytes,
8265 )?;
8266 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
8267 Some(manifest_footer)
8268 } else {
8269 None
8270 };
8271
8272 Ok(ParsedBootstrapSidecar {
8273 manifest_footer,
8274 index_root_records_section: header.has_index_root_records().then_some((
8275 header.index_root_records_offset,
8276 header.index_root_records_length,
8277 )),
8278 dictionary_records_section: header.has_dictionary_records().then_some((
8279 header.dictionary_records_offset,
8280 header.dictionary_records_length,
8281 )),
8282 })
8283}
8284
8285fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
8286 ObjectExtent {
8287 first_block_index: manifest_footer.index_root_first_block,
8288 data_block_count: manifest_footer.index_root_data_block_count,
8289 parity_block_count: manifest_footer.index_root_parity_block_count,
8290 encrypted_size: manifest_footer.index_root_encrypted_size,
8291 }
8292}
8293
8294fn insert_sidecar_records(
8295 blocks: &mut BTreeMap<u64, BlockRecord>,
8296 records: Vec<BlockRecord>,
8297) -> Result<(), FormatError> {
8298 for record in records {
8299 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
8300 if existing != record {
8301 return Err(FormatError::InvalidArchive(
8302 "bootstrap sidecar conflicts with volume BlockRecord",
8303 ));
8304 }
8305 }
8306 }
8307 Ok(())
8308}
8309
8310fn validate_sidecar_manifest_footer(
8311 volume_header: &VolumeHeader,
8312 crypto_header: &CryptoHeaderFixed,
8313 footer: &ManifestFooter,
8314 subkeys: &Subkeys,
8315 volume_format_rev: u16,
8316 raw: &[u8],
8317) -> Result<(), FormatError> {
8318 if footer.archive_uuid != volume_header.archive_uuid
8319 || footer.session_id != volume_header.session_id
8320 {
8321 return Err(FormatError::InvalidArchive(
8322 "sidecar ManifestFooter identity does not match VolumeHeader",
8323 ));
8324 }
8325 if footer.volume_index != 0 {
8326 return Err(FormatError::InvalidArchive(
8327 "sidecar ManifestFooter volume_index must be zero",
8328 ));
8329 }
8330 if footer.total_volumes != crypto_header.stripe_width {
8331 return Err(FormatError::InvalidArchive(
8332 "sidecar ManifestFooter total_volumes does not match stripe_width",
8333 ));
8334 }
8335 if footer.is_authoritative != 1 {
8336 return Err(FormatError::InvalidArchive(
8337 "sidecar ManifestFooter is not authoritative",
8338 ));
8339 }
8340 verify_integrity_tag(
8341 HmacDomain::ManifestFooter,
8342 crypto_header.aead_algo,
8343 volume_format_rev,
8344 Some(&subkeys.mac_key),
8345 &volume_header.archive_uuid,
8346 &volume_header.session_id,
8347 &raw[..MANIFEST_HMAC_COVERED_LEN],
8348 &footer.manifest_hmac,
8349 )
8350}
8351
8352fn validate_sidecar_size_cap(
8353 header: &BootstrapSidecarHeader,
8354 crypto_header: &CryptoHeaderFixed,
8355 file_size: u64,
8356) -> Result<(), FormatError> {
8357 let record_len = checked_u64_add(
8358 crypto_header.block_size as u64,
8359 BLOCK_RECORD_FRAMING_LEN as u64,
8360 "bootstrap sidecar cap overflow",
8361 )?;
8362 let max_index_records = crypto_header.index_root_fec_data_shards as u64
8363 + crypto_header.index_root_fec_parity_shards as u64;
8364 let max_record_section_bytes = checked_u64_mul(
8365 max_index_records,
8366 record_len,
8367 "bootstrap sidecar cap overflow",
8368 )?;
8369 if header.index_root_records_length % record_len != 0 {
8370 return Err(FormatError::InvalidArchive(
8371 "bootstrap sidecar IndexRoot records length is not aligned",
8372 ));
8373 }
8374 if header.index_root_records_length / record_len > max_index_records {
8375 return Err(FormatError::InvalidArchive(
8376 "bootstrap sidecar IndexRoot records exceed resource cap",
8377 ));
8378 }
8379 if header.dictionary_records_length % record_len != 0 {
8380 return Err(FormatError::InvalidArchive(
8381 "bootstrap sidecar dictionary records length is not aligned",
8382 ));
8383 }
8384 if header.dictionary_records_length / record_len > max_index_records {
8385 return Err(FormatError::InvalidArchive(
8386 "bootstrap sidecar dictionary records exceed resource cap",
8387 ));
8388 }
8389
8390 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
8391 if header.has_manifest_footer() {
8392 cap = cap
8393 .checked_add(MANIFEST_FOOTER_LEN as u64)
8394 .ok_or(FormatError::InvalidArchive(
8395 "bootstrap sidecar cap overflow",
8396 ))?;
8397 }
8398 if header.has_index_root_records() {
8399 cap = checked_u64_add(
8400 cap,
8401 max_record_section_bytes,
8402 "bootstrap sidecar cap overflow",
8403 )?;
8404 }
8405 if header.has_dictionary_records() {
8406 cap = checked_u64_add(
8407 cap,
8408 max_record_section_bytes,
8409 "bootstrap sidecar cap overflow",
8410 )?;
8411 }
8412 if file_size > cap {
8413 return Err(FormatError::InvalidArchive(
8414 "bootstrap sidecar exceeds resource cap",
8415 ));
8416 }
8417 Ok(())
8418}
8419
8420#[derive(Debug, Clone, Copy)]
8421struct SidecarBlockRecordsSection {
8422 offset: u64,
8423 length: u64,
8424 extent: ObjectExtent,
8425 data_kind: BlockKind,
8426 parity_kind: BlockKind,
8427 structure: &'static str,
8428}
8429
8430fn parse_sidecar_block_records(
8431 sidecar_bytes: &[u8],
8432 block_size: usize,
8433 section: SidecarBlockRecordsSection,
8434) -> Result<Vec<BlockRecord>, FormatError> {
8435 let record_len = block_size
8436 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8437 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8438 if section.length % record_len as u64 != 0 {
8439 return Err(FormatError::InvalidArchive(
8440 "sidecar BlockRecord section is not aligned",
8441 ));
8442 }
8443 let expected_count =
8444 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
8445 let actual_count = usize::try_from(section.length / record_len as u64)
8446 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
8447 if actual_count != expected_count {
8448 return Err(FormatError::InvalidArchive(
8449 "sidecar BlockRecord section does not match declared extent",
8450 ));
8451 }
8452 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
8453 let raw = slice(
8454 sidecar_bytes,
8455 start,
8456 to_usize(section.length, "BootstrapSidecarHeader")?,
8457 "BootstrapSidecarHeader",
8458 )?;
8459 let mut records = Vec::with_capacity(expected_count);
8460
8461 for idx in 0..expected_count {
8462 let record = BlockRecord::parse(
8463 slice(raw, idx * record_len, record_len, "BlockRecord")?,
8464 block_size,
8465 )?;
8466 let expected_block_index = checked_u64_add(
8467 section.extent.first_block_index,
8468 idx as u64,
8469 section.structure,
8470 )?;
8471 if record.block_index != expected_block_index {
8472 return Err(FormatError::InvalidArchive(
8473 "sidecar BlockRecord section has missing or duplicate blocks",
8474 ));
8475 }
8476 let expected_kind = if idx < section.extent.data_block_count as usize {
8477 section.data_kind
8478 } else {
8479 section.parity_kind
8480 };
8481 if record.kind != expected_kind {
8482 return Err(FormatError::InvalidArchive(
8483 "sidecar BlockRecord section has wrong kind",
8484 ));
8485 }
8486 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
8487 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
8488 {
8489 return Err(FormatError::InvalidArchive(
8490 "sidecar BlockRecord section has wrong last-data flag",
8491 ));
8492 }
8493 records.push(record);
8494 }
8495
8496 Ok(records)
8497}
8498
8499fn validate_trailer_identity(
8500 volume_header: &VolumeHeader,
8501 trailer: &VolumeTrailer,
8502) -> Result<(), FormatError> {
8503 if trailer.archive_uuid != volume_header.archive_uuid
8504 || trailer.session_id != volume_header.session_id
8505 || trailer.volume_index != volume_header.volume_index
8506 {
8507 return Err(FormatError::InvalidArchive(
8508 "VolumeTrailer identity does not match VolumeHeader",
8509 ));
8510 }
8511 Ok(())
8512}
8513
8514fn validate_manifest_footer(
8515 volume_header: &VolumeHeader,
8516 crypto_header: &CryptoHeaderFixed,
8517 footer: &ManifestFooter,
8518 subkeys: &Subkeys,
8519 volume_format_rev: u16,
8520 raw: &[u8],
8521) -> Result<(), FormatError> {
8522 if footer.archive_uuid != volume_header.archive_uuid
8523 || footer.session_id != volume_header.session_id
8524 || footer.volume_index != volume_header.volume_index
8525 {
8526 return Err(FormatError::InvalidArchive(
8527 "ManifestFooter identity does not match VolumeHeader",
8528 ));
8529 }
8530 if footer.total_volumes != volume_header.stripe_width {
8531 return Err(FormatError::InvalidArchive(
8532 "ManifestFooter total_volumes does not match stripe_width",
8533 ));
8534 }
8535 if footer.is_authoritative != 1 {
8536 return Err(FormatError::InvalidArchive(
8537 "ManifestFooter is not authoritative",
8538 ));
8539 }
8540 verify_integrity_tag(
8541 HmacDomain::ManifestFooter,
8542 crypto_header.aead_algo,
8543 volume_format_rev,
8544 Some(&subkeys.mac_key),
8545 &volume_header.archive_uuid,
8546 &volume_header.session_id,
8547 &raw[..MANIFEST_HMAC_COVERED_LEN],
8548 &footer.manifest_hmac,
8549 )
8550}
8551
8552#[derive(Debug)]
8553struct ParsedBlockRegion {
8554 blocks: BTreeMap<u64, BlockRecord>,
8555 erased_block_indices: BTreeSet<u64>,
8556}
8557
8558fn parse_block_region(
8559 bytes: &[u8],
8560 start: usize,
8561 end: usize,
8562 block_size: usize,
8563 volume_header: &VolumeHeader,
8564 trailer: &VolumeTrailer,
8565) -> Result<ParsedBlockRegion, FormatError> {
8566 if end < start {
8567 return Err(FormatError::InvalidArchive(
8568 "ManifestFooter starts before BlockRecord region",
8569 ));
8570 }
8571 let record_len = block_size
8572 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8573 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8574 let region_len = end - start;
8575 if region_len % record_len != 0 {
8576 return Err(FormatError::InvalidArchive(
8577 "BlockRecord region length is not aligned",
8578 ));
8579 }
8580 let observed_count = region_len / record_len;
8581 if observed_count as u64 != trailer.block_count {
8582 return Err(FormatError::InvalidArchive(
8583 "VolumeTrailer block_count does not match BlockRecord region",
8584 ));
8585 }
8586
8587 let mut blocks = BTreeMap::new();
8588 let mut erased_block_indices = BTreeSet::new();
8589 for idx in 0..observed_count {
8590 let offset = start + idx * record_len;
8591 let expected_block_index = checked_u64_add(
8592 volume_header.volume_index as u64,
8593 checked_u64_mul(
8594 idx as u64,
8595 volume_header.stripe_width as u64,
8596 "BlockRecord index overflow",
8597 )?,
8598 "BlockRecord index overflow",
8599 )?;
8600 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8601 match BlockRecord::parse(raw, block_size) {
8602 Ok(record) => {
8603 if record.block_index != expected_block_index {
8604 return Err(FormatError::InvalidArchive(
8605 "BlockRecord index does not match volume position",
8606 ));
8607 }
8608 if blocks.insert(record.block_index, record).is_some() {
8609 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8610 }
8611 }
8612 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
8613 if !erased_block_indices.insert(expected_block_index) {
8614 return Err(FormatError::InvalidArchive(
8615 "duplicate erased BlockRecord index",
8616 ));
8617 }
8618 }
8619 Err(err) => return Err(err),
8620 }
8621 }
8622
8623 Ok(ParsedBlockRegion {
8624 blocks,
8625 erased_block_indices,
8626 })
8627}
8628
8629fn validate_seekable_block_region_layout(
8630 start: u64,
8631 end: u64,
8632 block_size: usize,
8633 trailer: &VolumeTrailer,
8634) -> Result<(), FormatError> {
8635 if end < start {
8636 return Err(FormatError::InvalidArchive(
8637 "ManifestFooter starts before BlockRecord region",
8638 ));
8639 }
8640 let record_len = block_record_len(block_size)?;
8641 let region_len = end - start;
8642 if region_len % record_len != 0 {
8643 return Err(FormatError::InvalidArchive(
8644 "BlockRecord region length is not aligned",
8645 ));
8646 }
8647 let observed_count = region_len / record_len;
8648 if observed_count != trailer.block_count {
8649 return Err(FormatError::InvalidArchive(
8650 "VolumeTrailer block_count does not match BlockRecord region",
8651 ));
8652 }
8653 Ok(())
8654}
8655
8656fn parse_public_block_observation(
8657 bytes: &[u8],
8658 start: usize,
8659 image: &CriticalMetadataImage,
8660 block_size: usize,
8661 volume_header: &VolumeHeader,
8662) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
8663 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
8664 if start != image_start {
8665 return Err(FormatError::InvalidArchive(
8666 "public BlockRecord observation start mismatch",
8667 ));
8668 }
8669 let scan_limit_u64 = image
8670 .block_records_offset
8671 .checked_add(image.block_records_length)
8672 .ok_or(FormatError::InvalidArchive(
8673 "public BlockRecord observation limit overflow",
8674 ))?;
8675 if scan_limit_u64 != image.manifest_footer_offset {
8676 return Err(FormatError::InvalidArchive(
8677 "public BlockRecord observation limit mismatch",
8678 ));
8679 }
8680 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
8681 if scan_limit < start {
8682 return Err(FormatError::InvalidArchive(
8683 "public BlockRecord observation limit before start",
8684 ));
8685 }
8686 let record_len = block_size
8687 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8688 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8689 let region_len = scan_limit - start;
8690 if region_len % record_len != 0 {
8691 return Err(FormatError::InvalidArchive(
8692 "public BlockRecord observation window is not aligned",
8693 ));
8694 }
8695
8696 let mut blocks = BTreeMap::new();
8697 let mut offset = start;
8698 let mut observed_slot = 0u64;
8699 while offset < scan_limit {
8700 let magic_end = checked_add(offset, 4, "BlockRecord")?;
8701 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
8702 break;
8703 }
8704 let record_end = checked_add(offset, record_len, "BlockRecord")?;
8705 if record_end > scan_limit {
8706 return Err(FormatError::InvalidArchive(
8707 "public BlockRecord observation slot is incomplete",
8708 ));
8709 }
8710 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8711 let record = BlockRecord::parse(raw, block_size)?;
8712 let expected_block_index = checked_u64_add(
8713 volume_header.volume_index as u64,
8714 checked_u64_mul(
8715 observed_slot,
8716 volume_header.stripe_width as u64,
8717 "BlockRecord index overflow",
8718 )?,
8719 "BlockRecord index overflow",
8720 )?;
8721 if record.block_index != expected_block_index {
8722 return Err(FormatError::InvalidArchive(
8723 "public BlockRecord index does not match volume position",
8724 ));
8725 }
8726 if blocks.insert(record.block_index, record).is_some() {
8727 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8728 }
8729 offset = record_end;
8730 observed_slot = observed_slot
8731 .checked_add(1)
8732 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8733 }
8734
8735 let mut scan = if offset < scan_limit {
8736 checked_add(offset, record_len, "BlockRecord")?
8737 } else {
8738 scan_limit
8739 };
8740 while scan < scan_limit {
8741 let magic_end = checked_add(scan, 4, "BlockRecord")?;
8742 let record_end = checked_add(scan, record_len, "BlockRecord")?;
8743 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
8744 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
8745 if BlockRecord::parse(raw, block_size).is_ok() {
8746 return Err(FormatError::InvalidArchive(
8747 "public observation has ambiguous extra BlockRecord",
8748 ));
8749 }
8750 }
8751 scan = record_end;
8752 }
8753
8754 Ok(blocks)
8755}
8756
8757pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
8758 matches!(
8759 error,
8760 FormatError::BadCrc {
8761 structure: "BlockRecord",
8762 } | FormatError::BadMagic {
8763 structure: "BlockRecord",
8764 } | FormatError::NonZeroReserved {
8765 structure: "BlockRecord",
8766 }
8767 )
8768}
8769
8770fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
8771 let len = block_size
8772 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8773 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8774 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
8775}
8776
8777fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
8778 lhs.checked_mul(rhs)
8779 .ok_or(FormatError::InvalidArchive(reason))
8780}
8781
8782fn parse_stream_block_prefix(
8783 bytes: &[u8],
8784 start: usize,
8785 block_size: usize,
8786 volume_header: &VolumeHeader,
8787) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
8788 let record_len = block_size
8789 .checked_add(BLOCK_RECORD_FRAMING_LEN)
8790 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
8791 let mut blocks = BTreeMap::new();
8792 let mut offset = start;
8793 let mut observed_block_count = 0u64;
8794
8795 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
8796 let expected_block_index =
8797 expected_stream_block_index(volume_header, observed_block_count)?;
8798 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8799 match BlockRecord::parse(raw, block_size) {
8800 Ok(record) => {
8801 if record.block_index != expected_block_index {
8802 return Err(FormatError::InvalidArchive(
8803 "BlockRecord index does not match stream position",
8804 ));
8805 }
8806 if blocks.insert(record.block_index, record).is_some() {
8807 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
8808 }
8809 }
8810 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
8811 Err(err) => return Err(err),
8812 }
8813 offset = checked_add(offset, record_len, "BlockRecord")?;
8814 observed_block_count = observed_block_count
8815 .checked_add(1)
8816 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
8817 }
8818
8819 Ok((blocks, offset, observed_block_count))
8820}
8821
8822pub(crate) fn expected_stream_block_index(
8823 volume_header: &VolumeHeader,
8824 observed_block_count: u64,
8825) -> Result<u64, FormatError> {
8826 checked_u64_add(
8827 volume_header.volume_index as u64,
8828 checked_u64_mul(
8829 observed_block_count,
8830 volume_header.stripe_width as u64,
8831 "BlockRecord index overflow",
8832 )?,
8833 "BlockRecord index overflow",
8834 )
8835}
8836
8837fn parse_sequential_block_or_erasure(
8838 bytes: &[u8],
8839 offset: usize,
8840 record_len: usize,
8841 block_size: usize,
8842 volume_header: &VolumeHeader,
8843 observed_block_count: u64,
8844) -> Result<Option<BlockRecord>, FormatError> {
8845 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
8846 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
8847 match BlockRecord::parse(raw, block_size) {
8848 Ok(record) => {
8849 if record.block_index != expected_block_index {
8850 return Err(FormatError::InvalidArchive(
8851 "BlockRecord index does not match stream position",
8852 ));
8853 }
8854 Ok(Some(record))
8855 }
8856 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
8857 Err(err) => Err(err),
8858 }
8859}
8860
8861fn parse_terminal_material(
8862 bytes: &[u8],
8863 manifest_offset: usize,
8864 observed_block_count: u64,
8865 context: KeyHoldingTerminalContext<'_>,
8866 options: ReaderOptions,
8867) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
8868 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
8869 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
8870 return Err(FormatError::InvalidArchive(
8871 "sequential terminal does not end at EOF",
8872 ));
8873 }
8874 let terminal = candidate.terminal;
8875 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
8876 return Err(FormatError::InvalidArchive(
8877 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8878 ));
8879 }
8880 if terminal.volume_trailer.block_count != observed_block_count {
8881 return Err(FormatError::InvalidArchive(
8882 "VolumeTrailer block_count does not match observed stream",
8883 ));
8884 }
8885 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8886 Ok((
8887 manifest_footer,
8888 terminal.volume_trailer,
8889 terminal.root_auth_footer,
8890 ))
8891}
8892
8893pub(crate) fn parse_terminal_material_read_at(
8894 reader: &dyn ArchiveReadAt,
8895 input_len: u64,
8896 manifest_offset: u64,
8897 observed_block_count: u64,
8898 context: KeyHoldingTerminalContext<'_>,
8899) -> Result<SequentialTerminalMaterial, FormatError> {
8900 let mut candidates = Vec::new();
8901 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
8902 collect_v41_locator_candidate_read_at(
8903 reader,
8904 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
8905 0,
8906 context,
8907 &mut candidates,
8908 );
8909 }
8910 if input_len >= LOCATOR_PAIR_LEN as u64 {
8911 collect_v41_locator_candidate_read_at(
8912 reader,
8913 input_len - LOCATOR_PAIR_LEN as u64,
8914 1,
8915 context,
8916 &mut candidates,
8917 );
8918 }
8919
8920 let candidate = choose_v41_terminal_candidate(candidates)?;
8921 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
8922 return Err(FormatError::InvalidArchive(
8923 "sequential terminal does not end at EOF",
8924 ));
8925 }
8926 let terminal = candidate.terminal;
8927 if terminal.image.manifest_footer_offset != manifest_offset {
8928 return Err(FormatError::InvalidArchive(
8929 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
8930 ));
8931 }
8932 if terminal.volume_trailer.block_count != observed_block_count {
8933 return Err(FormatError::InvalidArchive(
8934 "VolumeTrailer block_count does not match observed stream",
8935 ));
8936 }
8937 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
8938 Ok(SequentialTerminalMaterial {
8939 manifest_footer,
8940 volume_trailer: terminal.volume_trailer,
8941 root_auth_footer: terminal.root_auth_footer,
8942 })
8943}
8944
8945fn terminal_candidate_reaches_eof(
8946 candidate: &TerminalCandidate,
8947 input_len: usize,
8948) -> Result<bool, FormatError> {
8949 let expected_end =
8950 match candidate.locator_sequence {
8951 Some(0) => candidate.anchor,
8952 Some(1) => candidate
8953 .anchor
8954 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
8955 .ok_or(FormatError::InvalidArchive(
8956 "terminal EOF boundary overflow",
8957 ))?,
8958 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
8959 FormatError::InvalidArchive("terminal EOF boundary overflow"),
8960 )?,
8961 Some(_) => {
8962 return Err(FormatError::InvalidArchive(
8963 "invalid terminal locator sequence",
8964 ))
8965 }
8966 };
8967 Ok(expected_end == input_len)
8968}
8969
8970#[derive(Debug, Default)]
8971struct PendingSequentialEnvelope {
8972 data_shards: Vec<Option<Vec<u8>>>,
8973 parity_shards: Vec<Option<Vec<u8>>>,
8974 saw_last_data: bool,
8975 awaiting_tentative_parity: bool,
8976}
8977
8978impl PendingSequentialEnvelope {
8979 fn is_empty(&self) -> bool {
8980 self.data_shards.is_empty() && self.parity_shards.is_empty()
8981 }
8982}
8983
8984fn handle_sequential_payload_erasure(
8985 pending: &mut PendingSequentialEnvelope,
8986 crypto_header: &CryptoHeaderFixed,
8987 metadata_seen: bool,
8988) -> Result<(), FormatError> {
8989 if metadata_seen || pending.saw_last_data {
8990 return Err(FormatError::BadCrc {
8991 structure: "BlockRecord",
8992 });
8993 }
8994 if !sequential_payload_parity_is_guaranteed(crypto_header) {
8995 return Err(FormatError::BadCrc {
8996 structure: "BlockRecord",
8997 });
8998 }
8999 pending.data_shards.push(None);
9000 pending.awaiting_tentative_parity = true;
9001 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
9002 return Err(FormatError::InvalidArchive(
9003 "sequential payload envelope exceeds data-shard cap",
9004 ));
9005 }
9006 Ok(())
9007}
9008
9009fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
9010 crypto_header.fec_parity_shards > 0
9011 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
9012}
9013
9014fn sequential_extract_tar_stream_with_options(
9015 bytes: &[u8],
9016 master_key: &MasterKey,
9017 options: ReaderOptions,
9018) -> Result<Vec<u8>, FormatError> {
9019 validate_reader_options(options)?;
9020 if bytes.len() < VOLUME_HEADER_LEN {
9021 return Err(FormatError::InvalidLength {
9022 structure: "archive",
9023 expected: VOLUME_HEADER_LEN,
9024 actual: bytes.len(),
9025 });
9026 }
9027
9028 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
9029 let crypto_start = volume_header.crypto_header_offset as usize;
9030 let crypto_len = volume_header.crypto_header_length as usize;
9031 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
9032 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
9033 let subkeys = subkeys_for_open(
9034 Some(master_key),
9035 parsed_crypto.fixed.aead_algo,
9036 &volume_header.archive_uuid,
9037 &volume_header.session_id,
9038 )?;
9039 verify_integrity_tag(
9040 HmacDomain::CryptoHeader,
9041 parsed_crypto.fixed.aead_algo,
9042 volume_header.volume_format_rev,
9043 Some(&subkeys.mac_key),
9044 &volume_header.archive_uuid,
9045 &volume_header.session_id,
9046 parsed_crypto.hmac_covered_bytes,
9047 &parsed_crypto.header_hmac,
9048 )?;
9049 parsed_crypto.validate_extension_semantics()?;
9050 validate_sequential_supported_volume(
9051 &volume_header,
9052 &parsed_crypto.fixed,
9053 &parsed_crypto.extensions,
9054 )?;
9055 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
9056 let block_records_start = startup_block_records_start(
9057 &volume_header,
9058 &parsed_crypto.kdf_params,
9059 |start, length| {
9060 let start = to_usize(start, "KeyWrapTableV1")?;
9061 Ok(slice(bytes, start, length, "KeyWrapTableV1")?.to_vec())
9062 },
9063 )?;
9064
9065 let block_size = parsed_crypto.fixed.block_size as usize;
9066 let record_len = block_size
9067 .checked_add(BLOCK_RECORD_FRAMING_LEN)
9068 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
9069 let mut offset = to_usize(block_records_start, "BlockRecord")?;
9070 let mut observed_block_count = 0u64;
9071 let mut metadata_seen = false;
9072 let mut pending = PendingSequentialEnvelope::default();
9073 let mut next_envelope_index = 0u64;
9074 let mut tar_stream = Vec::new();
9075 let max_tar_stream_size = options.max_verify_tar_size;
9076 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
9077 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
9078 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
9079 parsed_crypto.fixed.max_path_length,
9080 total_extraction_cap,
9081 );
9082
9083 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
9084 let record = parse_sequential_block_or_erasure(
9085 bytes,
9086 offset,
9087 record_len,
9088 block_size,
9089 &volume_header,
9090 observed_block_count,
9091 )?;
9092 observed_block_count = observed_block_count
9093 .checked_add(1)
9094 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
9095 let Some(record) = record else {
9096 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
9097 offset = checked_add(offset, record_len, "BlockRecord")?;
9098 continue;
9099 };
9100
9101 match record.kind {
9102 BlockKind::PayloadData => {
9103 if metadata_seen {
9104 return Err(FormatError::InvalidArchive(
9105 "payload BlockRecord appears after metadata",
9106 ));
9107 }
9108 if pending.awaiting_tentative_parity {
9109 return Err(FormatError::InvalidArchive(
9110 "sequential payload envelope boundary is ambiguous after CRC erasure",
9111 ));
9112 }
9113 if pending.saw_last_data {
9114 finalize_sequential_envelope(
9115 &mut pending,
9116 SequentialEnvelopeDecodeContext {
9117 crypto_header: &parsed_crypto.fixed,
9118 subkeys: &subkeys,
9119 volume_header: &volume_header,
9120 next_envelope_index: &mut next_envelope_index,
9121 tar_stream: &mut tar_stream,
9122 max_tar_stream_size,
9123 tar_stream_total_validator: &mut tar_stream_total_validator,
9124 },
9125 )?;
9126 }
9127 let is_last_data = record.is_last_data();
9128 pending.data_shards.push(Some(record.payload));
9129 if is_last_data {
9130 pending.saw_last_data = true;
9131 }
9132 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
9133 return Err(FormatError::InvalidArchive(
9134 "sequential payload envelope exceeds data-shard cap",
9135 ));
9136 }
9137 }
9138 BlockKind::PayloadParity => {
9139 if metadata_seen {
9140 return Err(FormatError::InvalidArchive(
9141 "payload parity BlockRecord appears after metadata",
9142 ));
9143 }
9144 if pending.awaiting_tentative_parity {
9145 pending.awaiting_tentative_parity = false;
9146 pending.saw_last_data = true;
9147 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
9148 return Err(FormatError::InvalidArchive(
9149 "payload parity appears before envelope data is complete",
9150 ));
9151 }
9152 pending.parity_shards.push(Some(record.payload));
9153 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
9154 return Err(FormatError::InvalidArchive(
9155 "sequential payload envelope exceeds parity-shard cap",
9156 ));
9157 }
9158 }
9159 _ => {
9160 if !pending.is_empty() {
9161 finalize_sequential_envelope(
9162 &mut pending,
9163 SequentialEnvelopeDecodeContext {
9164 crypto_header: &parsed_crypto.fixed,
9165 subkeys: &subkeys,
9166 volume_header: &volume_header,
9167 next_envelope_index: &mut next_envelope_index,
9168 tar_stream: &mut tar_stream,
9169 max_tar_stream_size,
9170 tar_stream_total_validator: &mut tar_stream_total_validator,
9171 },
9172 )?;
9173 }
9174 metadata_seen = true;
9175 }
9176 }
9177
9178 offset = checked_add(offset, record_len, "BlockRecord")?;
9179 }
9180
9181 if !pending.is_empty() {
9182 finalize_sequential_envelope(
9183 &mut pending,
9184 SequentialEnvelopeDecodeContext {
9185 crypto_header: &parsed_crypto.fixed,
9186 subkeys: &subkeys,
9187 volume_header: &volume_header,
9188 next_envelope_index: &mut next_envelope_index,
9189 tar_stream: &mut tar_stream,
9190 max_tar_stream_size,
9191 tar_stream_total_validator: &mut tar_stream_total_validator,
9192 },
9193 )?;
9194 }
9195
9196 parse_terminal_material(
9197 bytes,
9198 offset,
9199 observed_block_count,
9200 KeyHoldingTerminalContext {
9201 subkeys: &subkeys,
9202 volume_header: &volume_header,
9203 crypto_header: &parsed_crypto.fixed,
9204 crypto_header_bytes: crypto_bytes,
9205 },
9206 options,
9207 )?;
9208 validate_tar_stream_total_extraction_size(
9211 &tar_stream,
9212 parsed_crypto.fixed.max_path_length,
9213 total_extraction_cap,
9214 )?;
9215 Ok(tar_stream)
9216}
9217
9218fn validate_sequential_supported_volume(
9219 volume_header: &VolumeHeader,
9220 crypto_header: &CryptoHeaderFixed,
9221 extensions: &[ExtensionTlv<'_>],
9222) -> Result<(), FormatError> {
9223 reject_unsupported_raw_stream_profile(extensions)?;
9224 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
9225 return Err(FormatError::ReaderUnsupported(
9226 "sequential reader supports only single-volume archive input",
9227 ));
9228 }
9229 if crypto_header.stripe_width != volume_header.stripe_width {
9230 return Err(FormatError::InvalidArchive(
9231 "VolumeHeader and CryptoHeader stripe_width differ",
9232 ));
9233 }
9234 if crypto_header.has_dictionary != 0 {
9235 return Err(FormatError::ReaderUnsupported(
9236 "dictionary bootstrap required for non-seekable sequential extraction",
9237 ));
9238 }
9239 Ok(())
9240}
9241
9242struct SequentialEnvelopeDecodeContext<'a> {
9243 crypto_header: &'a CryptoHeaderFixed,
9244 subkeys: &'a Subkeys,
9245 volume_header: &'a VolumeHeader,
9246 next_envelope_index: &'a mut u64,
9247 tar_stream: &'a mut Vec<u8>,
9248 max_tar_stream_size: usize,
9249 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
9250}
9251
9252fn finalize_sequential_envelope(
9253 pending: &mut PendingSequentialEnvelope,
9254 context: SequentialEnvelopeDecodeContext<'_>,
9255) -> Result<(), FormatError> {
9256 if !pending.saw_last_data {
9257 return Err(FormatError::InvalidArchive(
9258 "sequential payload envelope is missing last-data flag",
9259 ));
9260 }
9261 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
9262 return Err(FormatError::InvalidArchive(
9263 "sequential payload envelope exceeds data-shard cap",
9264 ));
9265 }
9266 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
9267 return Err(FormatError::InvalidArchive(
9268 "sequential payload envelope exceeds parity-shard cap",
9269 ));
9270 }
9271 let required_parity =
9272 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
9273 if pending.parity_shards.len() < required_parity as usize {
9274 return Err(FormatError::InvalidArchive(
9275 "sequential payload envelope has insufficient parity for recovery settings",
9276 ));
9277 }
9278
9279 let repaired = repair_data_gf16(
9280 &pending.data_shards,
9281 &pending.parity_shards,
9282 context.crypto_header.block_size as usize,
9283 )?;
9284 let mut encrypted =
9285 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
9286 for shard in repaired {
9287 encrypted.extend_from_slice(&shard);
9288 }
9289 let plaintext = decrypt_padded_aead_object(
9290 AeadObjectContext {
9291 algo: context.crypto_header.aead_algo,
9292 key: &context.subkeys.enc_key,
9293 nonce_seed: &context.subkeys.nonce_seed,
9294 domain: b"envelope",
9295 archive_uuid: &context.volume_header.archive_uuid,
9296 session_id: &context.volume_header.session_id,
9297 counter: *context.next_envelope_index,
9298 },
9299 &encrypted,
9300 )?;
9301 decode_concatenated_zstd_frames_with_cap(
9302 &plaintext,
9303 None,
9304 context.tar_stream,
9305 context.max_tar_stream_size,
9306 Some(context.tar_stream_total_validator),
9307 )?;
9308 *context.next_envelope_index = (*context.next_envelope_index)
9309 .checked_add(1)
9310 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
9311 *pending = PendingSequentialEnvelope::default();
9312 Ok(())
9313}
9314
9315fn decode_concatenated_zstd_frames_with_cap(
9316 plaintext: &[u8],
9317 dictionary: Option<&[u8]>,
9318 output: &mut Vec<u8>,
9319 max_output_len: usize,
9320 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9321) -> Result<(), FormatError> {
9322 let mut cursor = 0usize;
9323 while cursor < plaintext.len() {
9324 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
9325 .map_err(|_| FormatError::InvalidZstdFrame)?;
9326 if frame_len == 0 {
9327 return Err(FormatError::InvalidZstdFrame);
9328 }
9329 let end = checked_add(cursor, frame_len, "zstd frame")?;
9330 validate_exact_zstd_frame(&plaintext[cursor..end])?;
9331 if let Some(dictionary) = dictionary {
9332 let mut decoder =
9333 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
9334 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9335 read_zstd_frame_to_capped_output(
9336 &mut decoder,
9337 output,
9338 max_output_len,
9339 tar_stream_total_validator.as_deref_mut(),
9340 )?;
9341 } else {
9342 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
9343 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9344 read_zstd_frame_to_capped_output(
9345 &mut decoder,
9346 output,
9347 max_output_len,
9348 tar_stream_total_validator.as_deref_mut(),
9349 )?;
9350 }
9351 cursor = end;
9352 }
9353 Ok(())
9354}
9355
9356fn read_zstd_frame_to_capped_output<R: Read>(
9357 decoder: &mut R,
9358 output: &mut Vec<u8>,
9359 max_output_len: usize,
9360 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
9361) -> Result<(), FormatError> {
9362 let mut buf = [0u8; 64 * 1024];
9363 loop {
9364 let read = decoder
9365 .read(&mut buf)
9366 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
9367 if read == 0 {
9368 return Ok(());
9369 }
9370 let next_len = output
9371 .len()
9372 .checked_add(read)
9373 .ok_or(FormatError::ReaderUnsupported(
9374 "sequential tar stream exceeds configured verification cap",
9375 ))?;
9376 if next_len > max_output_len {
9377 return Err(FormatError::ReaderUnsupported(
9378 "sequential tar stream exceeds configured verification cap",
9379 ));
9380 }
9381 output.extend_from_slice(&buf[..read]);
9382 if let Some(validator) = tar_stream_total_validator.as_mut() {
9383 validator.observe(output)?;
9384 }
9385 }
9386}
9387
9388fn load_archive_dictionary(
9389 blocks: &impl BlockProvider,
9390 subkeys: &Subkeys,
9391 volume_header: &VolumeHeader,
9392 crypto_header: &CryptoHeaderFixed,
9393 index_root: &IndexRoot,
9394) -> Result<Option<Vec<u8>>, FormatError> {
9395 if crypto_header.has_dictionary == 0 {
9396 return Ok(None);
9397 }
9398 let plaintext = load_metadata_object_from_parts(
9399 blocks,
9400 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
9401 index_root.header.dictionary_decompressed_size,
9402 )?;
9403 Ok(Some(plaintext))
9404}
9405
9406#[derive(Clone, Copy)]
9407struct ObjectLoadContext<'a> {
9408 volume_header: &'a VolumeHeader,
9409 crypto_header: &'a CryptoHeaderFixed,
9410 extent: ObjectExtent,
9411 data_kind: BlockKind,
9412 parity_kind: BlockKind,
9413 key: &'a [u8; 32],
9414 nonce_seed: &'a [u8; 32],
9415 domain: &'a [u8],
9416 counter: u64,
9417 class_data_shard_max: u16,
9418 class_parity_shard_max: u16,
9419}
9420
9421impl<'a> ObjectLoadContext<'a> {
9422 fn index_root(
9423 volume_header: &'a VolumeHeader,
9424 crypto_header: &'a CryptoHeaderFixed,
9425 subkeys: &'a Subkeys,
9426 extent: ObjectExtent,
9427 ) -> Self {
9428 Self {
9429 volume_header,
9430 crypto_header,
9431 extent,
9432 data_kind: BlockKind::IndexRootData,
9433 parity_kind: BlockKind::IndexRootParity,
9434 key: &subkeys.index_root_key,
9435 nonce_seed: &subkeys.index_nonce_seed,
9436 domain: b"idxroot",
9437 counter: 0,
9438 class_data_shard_max: crypto_header.index_root_fec_data_shards,
9439 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9440 }
9441 }
9442
9443 fn index_shard(
9444 volume_header: &'a VolumeHeader,
9445 crypto_header: &'a CryptoHeaderFixed,
9446 subkeys: &'a Subkeys,
9447 entry: &ShardEntry,
9448 ) -> Self {
9449 Self {
9450 volume_header,
9451 crypto_header,
9452 extent: ObjectExtent {
9453 first_block_index: entry.first_block_index,
9454 data_block_count: entry.data_block_count,
9455 parity_block_count: entry.parity_block_count,
9456 encrypted_size: entry.encrypted_size,
9457 },
9458 data_kind: BlockKind::IndexShardData,
9459 parity_kind: BlockKind::IndexShardParity,
9460 key: &subkeys.index_shard_key,
9461 nonce_seed: &subkeys.index_nonce_seed,
9462 domain: b"idxshard",
9463 counter: entry.shard_index,
9464 class_data_shard_max: crypto_header.index_fec_data_shards,
9465 class_parity_shard_max: crypto_header.index_fec_parity_shards,
9466 }
9467 }
9468
9469 fn directory_hint(
9470 volume_header: &'a VolumeHeader,
9471 crypto_header: &'a CryptoHeaderFixed,
9472 subkeys: &'a Subkeys,
9473 entry: &DirectoryHintShardEntry,
9474 ) -> Self {
9475 Self {
9476 volume_header,
9477 crypto_header,
9478 extent: ObjectExtent {
9479 first_block_index: entry.first_block_index,
9480 data_block_count: entry.data_block_count,
9481 parity_block_count: entry.parity_block_count,
9482 encrypted_size: entry.encrypted_size,
9483 },
9484 data_kind: BlockKind::DirectoryHintData,
9485 parity_kind: BlockKind::DirectoryHintParity,
9486 key: &subkeys.dir_hint_key,
9487 nonce_seed: &subkeys.index_nonce_seed,
9488 domain: b"dirhint",
9489 counter: entry.hint_shard_index,
9490 class_data_shard_max: crypto_header.index_fec_data_shards,
9491 class_parity_shard_max: crypto_header.index_fec_parity_shards,
9492 }
9493 }
9494
9495 fn dictionary(
9496 volume_header: &'a VolumeHeader,
9497 crypto_header: &'a CryptoHeaderFixed,
9498 subkeys: &'a Subkeys,
9499 index_root: &IndexRoot,
9500 ) -> Result<Self, FormatError> {
9501 Ok(Self {
9502 volume_header,
9503 crypto_header,
9504 extent: dictionary_extent_from_index_root(index_root)?,
9505 data_kind: BlockKind::DictionaryData,
9506 parity_kind: BlockKind::DictionaryParity,
9507 key: &subkeys.dictionary_key,
9508 nonce_seed: &subkeys.index_nonce_seed,
9509 domain: b"dict",
9510 counter: 0,
9511 class_data_shard_max: crypto_header.index_root_fec_data_shards,
9512 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
9513 })
9514 }
9515
9516 fn payload(
9517 volume_header: &'a VolumeHeader,
9518 crypto_header: &'a CryptoHeaderFixed,
9519 subkeys: &'a Subkeys,
9520 envelope: &EnvelopeEntry,
9521 ) -> Self {
9522 Self {
9523 volume_header,
9524 crypto_header,
9525 extent: ObjectExtent {
9526 first_block_index: envelope.first_block_index,
9527 data_block_count: envelope.data_block_count,
9528 parity_block_count: envelope.parity_block_count,
9529 encrypted_size: envelope.encrypted_size,
9530 },
9531 data_kind: BlockKind::PayloadData,
9532 parity_kind: BlockKind::PayloadParity,
9533 key: &subkeys.enc_key,
9534 nonce_seed: &subkeys.nonce_seed,
9535 domain: b"envelope",
9536 counter: envelope.envelope_index,
9537 class_data_shard_max: crypto_header.fec_data_shards,
9538 class_parity_shard_max: crypto_header.fec_parity_shards,
9539 }
9540 }
9541}
9542
9543fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
9544 if index_root.header.dictionary_data_block_count == 0
9545 || index_root.header.dictionary_encrypted_size == 0
9546 || index_root.header.dictionary_decompressed_size == 0
9547 {
9548 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
9549 }
9550 Ok(ObjectExtent {
9551 first_block_index: index_root.header.dictionary_first_block,
9552 data_block_count: index_root.header.dictionary_data_block_count,
9553 parity_block_count: index_root.header.dictionary_parity_block_count,
9554 encrypted_size: index_root.header.dictionary_encrypted_size,
9555 })
9556}
9557
9558fn load_metadata_object_from_parts(
9559 blocks: &impl BlockProvider,
9560 context: ObjectLoadContext<'_>,
9561 decompressed_size: u32,
9562) -> Result<Vec<u8>, FormatError> {
9563 let compressed = load_decrypted_object_from_parts(blocks, context)?;
9564 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
9565}
9566
9567fn load_decrypted_object_from_parts(
9568 blocks: &impl BlockProvider,
9569 context: ObjectLoadContext<'_>,
9570) -> Result<Vec<u8>, FormatError> {
9571 load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
9572}
9573
9574fn load_decrypted_object_from_parts_with_parity_policy(
9575 blocks: &impl BlockProvider,
9576 context: ObjectLoadContext<'_>,
9577 parity_policy: ParityReadPolicy,
9578) -> Result<Vec<u8>, FormatError> {
9579 let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
9580 blocks,
9581 context.crypto_header,
9582 context.extent,
9583 context.data_kind,
9584 context.parity_kind,
9585 context.class_data_shard_max,
9586 context.class_parity_shard_max,
9587 parity_policy,
9588 )?;
9589 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
9590 for shard in repaired {
9591 encrypted.extend_from_slice(&shard);
9592 }
9593 if encrypted.len() != context.extent.encrypted_size as usize {
9594 return Err(FormatError::InvalidArchive(
9595 "object encrypted size does not match repaired shards",
9596 ));
9597 }
9598
9599 decrypt_padded_aead_object(
9600 AeadObjectContext {
9601 algo: context.crypto_header.aead_algo,
9602 key: context.key,
9603 nonce_seed: context.nonce_seed,
9604 domain: context.domain,
9605 archive_uuid: &context.volume_header.archive_uuid,
9606 session_id: &context.volume_header.session_id,
9607 counter: context.counter,
9608 },
9609 &encrypted,
9610 )
9611}
9612
9613fn load_repaired_object_data_shards_from_parts(
9614 blocks: &impl BlockProvider,
9615 crypto_header: &CryptoHeaderFixed,
9616 extent: ObjectExtent,
9617 data_kind: BlockKind,
9618 parity_kind: BlockKind,
9619 class_data_shard_max: u16,
9620 class_parity_shard_max: u16,
9621) -> Result<Vec<Vec<u8>>, FormatError> {
9622 load_repaired_object_data_shards_from_parts_with_parity_policy(
9623 blocks,
9624 crypto_header,
9625 extent,
9626 data_kind,
9627 parity_kind,
9628 class_data_shard_max,
9629 class_parity_shard_max,
9630 ParityReadPolicy::Always,
9631 )
9632}
9633
9634#[allow(clippy::too_many_arguments)]
9635fn load_repaired_object_data_shards_from_parts_with_parity_policy(
9636 blocks: &impl BlockProvider,
9637 crypto_header: &CryptoHeaderFixed,
9638 extent: ObjectExtent,
9639 data_kind: BlockKind,
9640 parity_kind: BlockKind,
9641 class_data_shard_max: u16,
9642 class_parity_shard_max: u16,
9643 parity_policy: ParityReadPolicy,
9644) -> Result<Vec<Vec<u8>>, FormatError> {
9645 validate_object_extent(
9646 extent,
9647 crypto_header,
9648 class_data_shard_max,
9649 class_parity_shard_max,
9650 )?;
9651 let block_size = crypto_header.block_size as usize;
9652 let data_count = extent.data_block_count as usize;
9653 let parity_count = extent.parity_block_count as usize;
9654 let mut data_shards = Vec::with_capacity(data_count);
9655 let mut parity_shards = Vec::with_capacity(parity_count);
9656
9657 for offset in 0..data_count {
9658 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
9659 if let Some(record) = blocks.block(block_index)? {
9660 if record.kind != data_kind {
9661 return Err(FormatError::InvalidArchive(
9662 "object data block has unexpected kind",
9663 ));
9664 }
9665 let should_be_last = offset + 1 == data_count;
9666 if record.is_last_data() != should_be_last {
9667 return Err(FormatError::InvalidArchive(
9668 "object last-data flag is not on the final data block",
9669 ));
9670 }
9671 data_shards.push(Some(record.payload.clone()));
9672 } else {
9673 data_shards.push(None);
9674 }
9675 }
9676
9677 if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
9678 return repair_data_gf16(&data_shards, &[], block_size);
9679 }
9680
9681 for offset in 0..parity_count {
9682 let block_index = checked_u64_add(
9683 extent.first_block_index,
9684 data_count as u64 + offset as u64,
9685 "object",
9686 )?;
9687 if let Some(record) = blocks.block(block_index)? {
9688 if record.kind != parity_kind {
9689 return Err(FormatError::InvalidArchive(
9690 "object parity block has unexpected kind",
9691 ));
9692 }
9693 if record.is_last_data() {
9694 return Err(FormatError::InvalidArchive(
9695 "object parity block has last-data flag",
9696 ));
9697 }
9698 parity_shards.push(Some(record.payload.clone()));
9699 } else {
9700 parity_shards.push(None);
9701 }
9702 }
9703
9704 repair_data_gf16(&data_shards, &parity_shards, block_size)
9705}
9706
9707fn validate_object_extent(
9708 extent: ObjectExtent,
9709 crypto_header: &CryptoHeaderFixed,
9710 class_data_shard_max: u16,
9711 class_parity_shard_max: u16,
9712) -> Result<(), FormatError> {
9713 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
9714 return Err(FormatError::InvalidArchive(
9715 "encrypted object has zero data blocks or size",
9716 ));
9717 }
9718 if extent.data_block_count > class_data_shard_max as u32 {
9719 return Err(FormatError::InvalidArchive(
9720 "encrypted object exceeds its class data-shard maximum",
9721 ));
9722 }
9723 if extent.parity_block_count > class_parity_shard_max as u32 {
9724 return Err(FormatError::InvalidArchive(
9725 "encrypted object exceeds its class parity-shard maximum",
9726 ));
9727 }
9728 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
9729 if extent.parity_block_count != required_parity {
9730 return Err(FormatError::InvalidArchive(
9731 "encrypted object parity does not match v41 compute_parity",
9732 ));
9733 }
9734 let total = checked_u64_add(
9735 extent.data_block_count as u64,
9736 extent.parity_block_count as u64,
9737 "encrypted object shard count overflow",
9738 )?;
9739 if total > 65_535 {
9740 return Err(FormatError::FecTooManyShards(total as usize));
9741 }
9742 let expected = checked_u64_mul(
9743 extent.data_block_count as u64,
9744 crypto_header.block_size as u64,
9745 "encrypted object size overflow",
9746 )?;
9747 if expected != extent.encrypted_size as u64 {
9748 return Err(FormatError::InvalidArchive(
9749 "encrypted object size is not data_block_count * block_size",
9750 ));
9751 }
9752 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
9753 return Err(FormatError::InvalidArchive(
9754 "encrypted object is too small for AEAD tag",
9755 ));
9756 }
9757 Ok(())
9758}
9759
9760pub(crate) fn required_object_parity(
9761 data_block_count: u64,
9762 crypto_header: &CryptoHeaderFixed,
9763) -> Result<u32, FormatError> {
9764 let min_parity =
9765 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
9766 1
9767 } else {
9768 0
9769 };
9770 let mut parity = 0u64;
9771 for _ in 0..100 {
9772 let total = data_block_count
9773 .checked_add(parity)
9774 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
9775 let by_volume = checked_u64_mul(
9776 crypto_header.volume_loss_tolerance as u64,
9777 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
9778 "volume-loss parity overflow",
9779 )?;
9780 let by_bitrot = ceil_div_u64(
9781 checked_u64_mul(
9782 total,
9783 crypto_header.bit_rot_buffer_pct as u64,
9784 "bit-rot parity overflow",
9785 )?,
9786 100,
9787 )?;
9788 let next = by_volume
9789 .checked_add(by_bitrot)
9790 .ok_or(FormatError::InvalidArchive("parity overflow"))?
9791 .max(min_parity);
9792 if next == parity {
9793 return u32::try_from(next)
9794 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
9795 }
9796 parity = next;
9797 }
9798 Err(FormatError::InvalidArchive(
9799 "parity calculation did not converge",
9800 ))
9801}
9802
9803fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
9804 if denominator == 0 {
9805 return Err(FormatError::InvalidArchive("division by zero"));
9806 }
9807 numerator
9808 .checked_add(denominator - 1)
9809 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
9810 .map(|value| value / denominator)
9811}
9812
9813fn frame_range_for_file<'b>(
9814 shard: &'b IndexShard,
9815 file: &FileEntry,
9816) -> Result<&'b [FrameEntry], FormatError> {
9817 let start = shard
9818 .frames
9819 .binary_search_by_key(&file.first_frame_index, |entry| entry.frame_index)
9820 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))?;
9821 let count = usize::try_from(file.frame_count)
9822 .map_err(|_| FormatError::InvalidArchive("FileEntry frame count overflow"))?;
9823 let end = start.checked_add(count).ok_or(FormatError::InvalidArchive(
9824 "FileEntry frame range overflow",
9825 ))?;
9826 let frames = shard
9827 .frames
9828 .get(start..end)
9829 .ok_or(FormatError::InvalidArchive(
9830 "FileEntry references missing FrameEntry",
9831 ))?;
9832 for (offset, frame) in frames.iter().enumerate() {
9833 let expected = file.first_frame_index.checked_add(offset as u64).ok_or(
9834 FormatError::InvalidArchive("FileEntry frame range overflow"),
9835 )?;
9836 if frame.frame_index != expected {
9837 return Err(FormatError::InvalidArchive(
9838 "FileEntry references missing FrameEntry",
9839 ));
9840 }
9841 }
9842 Ok(frames)
9843}
9844
9845fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
9846 MetadataLimits {
9847 block_size: crypto_header.block_size,
9848 max_path_length: crypto_header.max_path_length,
9849 max_payload_data_shards: crypto_header.fec_data_shards,
9850 max_payload_parity_shards: crypto_header.fec_parity_shards,
9851 max_index_data_shards: crypto_header.index_fec_data_shards,
9852 max_index_parity_shards: crypto_header.index_fec_parity_shards,
9853 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
9854 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
9855 ..MetadataLimits::default()
9856 }
9857}
9858
9859fn verify_dense_keys<T>(
9860 entries: &BTreeMap<u64, T>,
9861 expected_count: u64,
9862 structure: &'static str,
9863) -> Result<(), FormatError> {
9864 if entries.len() as u64 != expected_count {
9865 return Err(FormatError::InvalidArchive(
9866 "decoded table count does not match IndexRoot",
9867 ));
9868 }
9869 for expected in 0..expected_count {
9870 if !entries.contains_key(&expected) {
9871 return Err(FormatError::InvalidMetadata {
9872 structure,
9873 reason: "global index coverage has a gap",
9874 });
9875 }
9876 }
9877 Ok(())
9878}
9879
9880fn validate_envelope_frame_coverage(
9881 frames: &BTreeMap<u64, FrameEntry>,
9882 envelopes: &BTreeMap<u64, EnvelopeEntry>,
9883) -> Result<(), FormatError> {
9884 let mut accounted_frames = BTreeSet::new();
9885 for envelope in envelopes.values() {
9886 let first = envelope.first_frame_index;
9887 let end =
9888 first
9889 .checked_add(envelope.frame_count as u64)
9890 .ok_or(FormatError::InvalidArchive(
9891 "EnvelopeEntry frame range overflow",
9892 ))?;
9893 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
9894 for frame_index in first..end {
9895 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
9896 "EnvelopeEntry references missing FrameEntry",
9897 ))?;
9898 if frame.envelope_index != envelope.envelope_index {
9899 return Err(FormatError::InvalidArchive(
9900 "FrameEntry envelope_index does not match containing EnvelopeEntry",
9901 ));
9902 }
9903 if !accounted_frames.insert(frame_index) {
9904 return Err(FormatError::InvalidArchive(
9905 "FrameEntry is covered by multiple EnvelopeEntries",
9906 ));
9907 }
9908 let start = frame.offset_in_envelope as usize;
9909 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
9910 if end > envelope.plaintext_size as usize {
9911 return Err(FormatError::InvalidArchive(
9912 "FrameEntry exceeds EnvelopeEntry plaintext_size",
9913 ));
9914 }
9915 ranges.push((start, end));
9916 }
9917 validate_exact_coverage_ranges(
9918 &mut ranges,
9919 envelope.plaintext_size as usize,
9920 "EnvelopeEntry frame coverage has a gap or overlap",
9921 )?;
9922 }
9923
9924 for frame_index in frames.keys() {
9925 if !accounted_frames.contains(frame_index) {
9926 return Err(FormatError::InvalidArchive(
9927 "FrameEntry is not covered by any EnvelopeEntry",
9928 ));
9929 }
9930 }
9931 Ok(())
9932}
9933
9934fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
9935 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
9936 for shard in shards {
9937 for (idx, file) in shard.files.iter().enumerate() {
9938 let path = shard
9939 .file_path(idx)
9940 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
9941 .to_vec();
9942 let start = shard
9943 .tar_member_group_start(idx)
9944 .ok_or(FormatError::InvalidArchive(
9945 "FileEntry tar member start is missing",
9946 ))?;
9947 let key = (file.path_hash, path, start);
9948 validate_global_file_table_key_step(previous.as_ref(), &key)?;
9949 previous = Some(key);
9950 }
9951 }
9952 Ok(())
9953}
9954
9955fn validate_global_file_table_key_step(
9956 previous: Option<&([u8; 8], Vec<u8>, u64)>,
9957 current: &([u8; 8], Vec<u8>, u64),
9958) -> Result<(), FormatError> {
9959 if let Some(previous) = previous {
9960 if previous >= current {
9961 return Err(FormatError::InvalidArchive(
9962 "global FileEntry rows are not sorted and unique",
9963 ));
9964 }
9965 }
9966 Ok(())
9967}
9968
9969fn validate_file_extent_coverage_ranges(
9970 extents: &[(u64, u64)],
9971 tar_len: u64,
9972) -> Result<(), FormatError> {
9973 let mut ranges = Vec::with_capacity(extents.len());
9974 for (start, len) in extents {
9975 let end = checked_u64_add(*start, *len, "FileEntry")?;
9976 if end > tar_len {
9977 return Err(FormatError::InvalidArchive(
9978 "FileEntry extent exceeds IndexRoot tar_total_size",
9979 ));
9980 }
9981 ranges.push((*start, end));
9982 }
9983 validate_exact_coverage_ranges_u64(
9984 &mut ranges,
9985 tar_len,
9986 "FileEntry extents do not cover tar stream exactly",
9987 )
9988}
9989
9990fn add_expected_directory_hint_rows(
9991 map: &mut DirectoryHintMap,
9992 shard_row_index: u32,
9993 path: &[u8],
9994 kind: TarEntryKind,
9995) {
9996 map.entry(Vec::new()).or_default().insert(shard_row_index);
9997 for (idx, byte) in path.iter().enumerate() {
9998 if *byte == b'/' {
9999 map.entry(path[..idx].to_vec())
10000 .or_default()
10001 .insert(shard_row_index);
10002 }
10003 }
10004 if kind == TarEntryKind::Directory {
10005 map.entry(path.to_vec())
10006 .or_default()
10007 .insert(shard_row_index);
10008 }
10009}
10010
10011fn validate_directory_hint_tables_against_expected(
10012 tables: &[DirectoryHintTable],
10013 expected: &DirectoryHintMap,
10014) -> Result<(), FormatError> {
10015 let mut actual = Vec::new();
10016 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
10017
10018 for table in tables {
10019 for entry_index in 0..table.entries.len() {
10020 let path = table
10021 .entry_path(entry_index)
10022 .ok_or(FormatError::InvalidArchive(
10023 "DirectoryHintEntry path is missing",
10024 ))?;
10025 let key = (hash_prefix(path), path.to_vec());
10026 if let Some(previous) = &previous_key {
10027 if previous >= &key {
10028 return Err(FormatError::InvalidArchive(
10029 "DirectoryHintEntry rows are not globally sorted",
10030 ));
10031 }
10032 }
10033 previous_key = Some(key);
10034
10035 let rows =
10036 table
10037 .shard_rows_for_entry(entry_index)
10038 .ok_or(FormatError::InvalidArchive(
10039 "DirectoryHintEntry shard rows are missing",
10040 ))?;
10041 actual.push((path.to_vec(), rows.to_vec()));
10042 }
10043 }
10044
10045 if actual != sorted_directory_hint_rows(expected) {
10046 return Err(FormatError::InvalidArchive(
10047 "directory hint map does not match decoded files",
10048 ));
10049 }
10050 Ok(())
10051}
10052
10053fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
10054 let mut rows = map
10055 .iter()
10056 .map(|(path, shard_rows)| {
10057 (
10058 path.clone(),
10059 shard_rows.iter().copied().collect::<Vec<u32>>(),
10060 )
10061 })
10062 .collect::<Vec<_>>();
10063 rows.sort_by(|(left_path, _), (right_path, _)| {
10064 hash_prefix(left_path)
10065 .cmp(&hash_prefix(right_path))
10066 .then_with(|| left_path.cmp(right_path))
10067 });
10068 rows
10069}
10070
10071fn validate_exact_coverage_ranges(
10072 ranges: &mut [(usize, usize)],
10073 expected_end: usize,
10074 reason: &'static str,
10075) -> Result<(), FormatError> {
10076 ranges.sort_unstable();
10077 let mut cursor = 0usize;
10078 for (start, end) in ranges.iter().copied() {
10079 if start != cursor || end < start {
10080 return Err(FormatError::InvalidArchive(reason));
10081 }
10082 cursor = end;
10083 }
10084 if cursor != expected_end {
10085 return Err(FormatError::InvalidArchive(reason));
10086 }
10087 Ok(())
10088}
10089
10090fn validate_exact_coverage_ranges_u64(
10091 ranges: &mut [(u64, u64)],
10092 expected_end: u64,
10093 reason: &'static str,
10094) -> Result<(), FormatError> {
10095 ranges.sort_unstable();
10096 let mut cursor = 0u64;
10097 for (start, end) in ranges.iter().copied() {
10098 if start != cursor || end < start {
10099 return Err(FormatError::InvalidArchive(reason));
10100 }
10101 cursor = end;
10102 }
10103 if cursor != expected_end {
10104 return Err(FormatError::InvalidArchive(reason));
10105 }
10106 Ok(())
10107}
10108
10109fn object_block_range(
10110 first_block_index: u64,
10111 data_block_count: u32,
10112 parity_block_count: u32,
10113 structure: &'static str,
10114) -> Result<(u64, u64), FormatError> {
10115 let total = data_block_count as u64 + parity_block_count as u64;
10116 if total == 0 {
10117 return Err(FormatError::InvalidArchive(structure));
10118 }
10119 let end = checked_u64_add(first_block_index, total, structure)?;
10120 Ok((first_block_index, end))
10121}
10122
10123fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
10124 ranges.sort_unstable();
10125 for pair in ranges.windows(2) {
10126 if pair[0].1 > pair[1].0 {
10127 return Err(FormatError::InvalidArchive(
10128 "encrypted object block ranges overlap",
10129 ));
10130 }
10131 }
10132 Ok(())
10133}
10134
10135pub(crate) fn observed_archive_size(
10136 sizes: impl IntoIterator<Item = u64>,
10137) -> Result<u64, FormatError> {
10138 sizes.into_iter().try_fold(0u64, |sum, size| {
10139 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
10140 "observed archive size overflow",
10141 ))
10142 })
10143}
10144
10145pub(crate) fn total_extraction_size_cap(
10146 options: ReaderOptions,
10147 observed_archive_bytes: u64,
10148) -> u64 {
10149 options
10150 .max_total_extraction_size
10151 .min(observed_archive_bytes.saturating_mul(10))
10152}
10153
10154fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
10155 std::str::from_utf8(bytes)
10156 .map(|path| path.to_owned())
10157 .map_err(|_| FormatError::UnsafeArchivePath)
10158}
10159
10160fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
10161 let mut bytes = [0u8; 104];
10162 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
10163 bytes[36..40].fill(0);
10164 bytes
10165}
10166
10167fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
10168 let digest = Sha256::digest(bytes);
10169 let mut out = [0u8; 32];
10170 out.copy_from_slice(&digest);
10171 out
10172}
10173
10174fn slice<'b>(
10175 bytes: &'b [u8],
10176 offset: usize,
10177 len: usize,
10178 structure: &'static str,
10179) -> Result<&'b [u8], FormatError> {
10180 let end = checked_add(offset, len, structure)?;
10181 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
10182 structure,
10183 expected: end,
10184 actual: bytes.len(),
10185 })
10186}
10187
10188fn read_at_vec(
10189 reader: &dyn ArchiveReadAt,
10190 offset: u64,
10191 len: usize,
10192 structure: &'static str,
10193) -> Result<Vec<u8>, FormatError> {
10194 let expected_end = offset
10195 .checked_add(len as u64)
10196 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
10197 let observed_len = reader.len()?;
10198 if expected_end > observed_len {
10199 return Err(FormatError::InvalidLength {
10200 structure,
10201 expected: to_usize(expected_end, structure)?,
10202 actual: to_usize(observed_len, structure)?,
10203 });
10204 }
10205 let mut out = vec![0u8; len];
10206 reader.read_exact_at(offset, &mut out)?;
10207 Ok(out)
10208}
10209
10210fn read_at_vec_unchecked(
10211 reader: &dyn ArchiveReadAt,
10212 offset: u64,
10213 len: usize,
10214) -> Result<Vec<u8>, FormatError> {
10215 let mut out = vec![0u8; len];
10216 reader.read_exact_at(offset, &mut out)?;
10217 Ok(out)
10218}
10219
10220fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
10221where
10222 T: Sync,
10223 U: Send,
10224 F: Fn(&T) -> Result<U, FormatError> + Sync,
10225{
10226 if jobs <= 1 || items.len() <= 1 {
10227 return items.iter().map(f).collect();
10228 }
10229 let worker_count = jobs.min(items.len());
10230 let chunk_size = items.len().div_ceil(worker_count);
10231 let mut out = Vec::with_capacity(items.len());
10232 thread::scope(|scope| {
10233 let handles = items
10234 .chunks(chunk_size)
10235 .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
10236 .collect::<Vec<_>>();
10237 for handle in handles {
10238 let mut chunk = handle
10239 .join()
10240 .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
10241 out.append(&mut chunk);
10242 }
10243 Ok(out)
10244 })
10245}
10246
10247fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
10248 lhs.checked_add(rhs)
10249 .ok_or(FormatError::InvalidArchive(structure))
10250}
10251
10252fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
10253 lhs.checked_add(rhs)
10254 .ok_or(FormatError::InvalidArchive(structure))
10255}
10256
10257fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
10258 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
10259}
10260
10261#[cfg(test)]
10262mod tests {
10263 use std::fs;
10264
10265 use super::*;
10266 use crate::compression::compress_zstd_frame;
10267 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
10268 use crate::fec::encode_parity_gf16;
10269 use crate::format::{
10270 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_EXTENSION_HEADER_LEN,
10271 CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION, READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10272 VOLUME_FORMAT_REV, VOLUME_FORMAT_REV_44,
10273 };
10274 use crate::metadata::{
10275 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
10276 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
10277 };
10278 use crate::non_seekable_reader::{
10279 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
10280 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
10281 verify_non_seekable_stream_with_recipient_wrap_resolver_options, NonSeekableReaderOptions,
10282 SequentialRootAuthStatus,
10283 };
10284 use crate::raw_stream_profile::{
10285 serialize_raw_stream_content_model_extension, RAW_STREAM_UNSUPPORTED_MESSAGE,
10286 };
10287 use crate::wire::RecipientRecordV1;
10288 use crate::writer::{
10289 write_archive, write_archive_unencrypted, write_archive_with_dictionary,
10290 write_archive_with_kdf, write_archive_with_recipient_wrap_records,
10291 write_archive_with_root_auth, write_archive_with_root_auth_and_recipient_wrap_records,
10292 RegularFile, RootAuthSigningRequest, RootAuthWriterConfig, WriterOptions,
10293 };
10294
10295 fn master_key() -> MasterKey {
10296 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
10297 }
10298
10299 fn recipient_wrap_test_record() -> RecipientRecordV1 {
10300 RecipientRecordV1 {
10301 record_length: 0,
10302 profile_id: 1,
10303 recipient_identity_type: 2,
10304 flags: 0,
10305 recipient_identity_length: 0,
10306 profile_payload_length: 0,
10307 recipient_identity_digest: [0u8; 32],
10308 recipient_identity_bytes: b"recipient-a".to_vec(),
10309 profile_payload_bytes: b"profile-payload".to_vec(),
10310 }
10311 }
10312
10313 fn recipient_wrap_layout(volume: &[u8]) -> (usize, usize, usize, usize, u32) {
10314 let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10315 let crypto_start = header.crypto_header_offset as usize;
10316 let crypto_len = header.crypto_header_length as usize;
10317 let crypto_end = crypto_start + crypto_len;
10318 let crypto = CryptoHeader::parse(
10319 &volume[crypto_start..crypto_end],
10320 header.crypto_header_length,
10321 )
10322 .unwrap();
10323 let KdfParams::RecipientWrap {
10324 key_wrap_table_length,
10325 ..
10326 } = crypto.kdf_params
10327 else {
10328 panic!("expected RecipientWrap KdfParams");
10329 };
10330 (
10331 crypto_start,
10332 crypto_len,
10333 crypto_end,
10334 key_wrap_table_length as usize,
10335 key_wrap_table_length,
10336 )
10337 }
10338
10339 fn rewrite_recipient_wrap_kdf_digest(crypto_bytes: &mut [u8], digest: [u8; 32]) {
10340 let digest_start = CRYPTO_HEADER_FIXED_LEN + 14;
10341 crypto_bytes[digest_start..digest_start + 32].copy_from_slice(&digest);
10342 }
10343
10344 fn mutate_top_level_recipient_wrap_public_profile(volume: &mut [u8]) {
10345 let (crypto_start, crypto_len, table_start, table_len, table_len_u32) =
10346 recipient_wrap_layout(volume);
10347 let table_end = table_start + table_len;
10348 volume[table_end - 1] ^= 0x5a;
10349 let digest = compute_key_wrap_table_digest(table_len_u32, &volume[table_start..table_end]);
10350 rewrite_recipient_wrap_kdf_digest(
10351 &mut volume[crypto_start..crypto_start + crypto_len],
10352 digest,
10353 );
10354 }
10355
10356 fn mutate_cmra_recipient_wrap_public_profile(volume: &mut [u8]) {
10357 rewrite_public_cmra_image(volume, |image| {
10358 let table_region = image
10359 .regions
10360 .iter_mut()
10361 .find(|region| region.region_type == 6)
10362 .unwrap();
10363 *table_region.bytes.last_mut().unwrap() ^= 0x5a;
10364 let digest =
10365 compute_key_wrap_table_digest(table_region.bytes.len() as u32, &table_region.bytes);
10366 let crypto_region = image
10367 .regions
10368 .iter_mut()
10369 .find(|region| region.region_type == 2)
10370 .unwrap();
10371 rewrite_recipient_wrap_kdf_digest(&mut crypto_region.bytes, digest);
10372 });
10373 }
10374
10375 fn add_raw_stream_profile_to_physical_crypto_header(volume: &mut Vec<u8>) {
10376 let mut header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10377 let crypto_start = header.crypto_header_offset as usize;
10378 let crypto_len = header.crypto_header_length as usize;
10379 let hmac_start = crypto_start + crypto_len - CRYPTO_HEADER_HMAC_LEN;
10380 let terminator_start = hmac_start - CRYPTO_EXTENSION_HEADER_LEN;
10381 let extension = serialize_raw_stream_content_model_extension();
10382 let new_crypto_len = header.crypto_header_length + extension.len() as u32;
10383
10384 volume.splice(terminator_start..terminator_start, extension);
10385 header.crypto_header_length = new_crypto_len;
10386 volume[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10387 volume[crypto_start + 4..crypto_start + 8].copy_from_slice(&new_crypto_len.to_le_bytes());
10388 }
10389
10390 fn recompute_physical_crypto_header_hmac(volume: &mut [u8], master_key: &MasterKey) {
10391 let header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
10392 let crypto_start = header.crypto_header_offset as usize;
10393 let crypto_end = crypto_start + header.crypto_header_length as usize;
10394 let hmac_start = crypto_end - CRYPTO_HEADER_HMAC_LEN;
10395 let subkeys =
10396 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
10397 let hmac = compute_hmac(
10398 HmacDomain::CryptoHeader,
10399 &subkeys.mac_key,
10400 &header.archive_uuid,
10401 &header.session_id,
10402 &volume[crypto_start..hmac_start],
10403 );
10404 volume[hmac_start..crypto_end].copy_from_slice(&hmac);
10405 }
10406
10407 #[test]
10408 fn reader_defaults_use_available_parallelism_jobs() {
10409 let options = ReaderOptions::default();
10410
10411 assert_eq!(options.jobs, default_jobs());
10412 assert!(options.jobs >= 1);
10413 }
10414
10415 #[test]
10416 fn reader_options_reject_zero_jobs() {
10417 let err = OpenedArchive::open_with_options(
10418 &[],
10419 &master_key(),
10420 ReaderOptions {
10421 jobs: 0,
10422 ..ReaderOptions::default()
10423 },
10424 )
10425 .unwrap_err();
10426
10427 assert_eq!(
10428 err,
10429 FormatError::ReaderUnsupported("jobs must be at least 1")
10430 );
10431 }
10432
10433 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
10434 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
10435
10436 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
10437 RootAuthWriterConfig {
10438 authenticator_id: TEST_ROOT_AUTH_ID,
10439 signer_identity_type: 0,
10440 signer_identity: &[],
10441 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
10442 }
10443 }
10444
10445 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
10446 request.archive_root.to_vec()
10447 }
10448
10449 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
10450 footer.authenticator_id == TEST_ROOT_AUTH_ID
10451 && footer.signer_identity_type == 0
10452 && footer.signer_identity_bytes.is_empty()
10453 && footer.authenticator_value.as_slice() == archive_root
10454 }
10455
10456 fn dictionary() -> &'static [u8] {
10457 b"dir/dict.txt common words common words common words dictionary payload"
10458 }
10459
10460 #[derive(Clone)]
10461 struct CountingReadAt {
10462 bytes: std::sync::Arc<Vec<u8>>,
10463 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
10464 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
10465 }
10466
10467 impl CountingReadAt {
10468 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
10469 Self {
10470 bytes: std::sync::Arc::new(bytes),
10471 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
10472 denied_ranges: std::sync::Arc::new(denied_ranges),
10473 }
10474 }
10475
10476 fn reads(&self) -> Vec<(u64, u64)> {
10477 self.reads.lock().unwrap().clone()
10478 }
10479 }
10480
10481 impl ArchiveReadAt for CountingReadAt {
10482 fn len(&self) -> Result<u64, FormatError> {
10483 u64::try_from(self.bytes.as_ref().len())
10484 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
10485 }
10486
10487 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
10488 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
10489 self.reads.lock().unwrap().push((offset, end));
10490 if self
10491 .denied_ranges
10492 .iter()
10493 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
10494 {
10495 return Err(FormatError::InvalidArchive("denied test read"));
10496 }
10497 let start = to_usize(offset, "archive")?;
10498 let end_usize = checked_add(start, buf.len(), "archive")?;
10499 let source = self
10500 .bytes
10501 .get(start..end_usize)
10502 .ok_or(FormatError::InvalidLength {
10503 structure: "archive",
10504 expected: end_usize,
10505 actual: self.bytes.as_ref().len(),
10506 })?;
10507 buf.copy_from_slice(source);
10508 Ok(())
10509 }
10510 }
10511
10512 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
10513 left_start < right_end && right_start < left_end
10514 }
10515
10516 fn single_stream_options() -> WriterOptions {
10517 WriterOptions {
10518 stripe_width: 1,
10519 volume_loss_tolerance: 0,
10520 ..WriterOptions::default()
10521 }
10522 }
10523
10524 struct ChunkedReader {
10525 bytes: Vec<u8>,
10526 cursor: usize,
10527 max_chunk: usize,
10528 }
10529
10530 impl ChunkedReader {
10531 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
10532 Self {
10533 bytes,
10534 cursor: 0,
10535 max_chunk,
10536 }
10537 }
10538 }
10539
10540 impl std::io::Read for ChunkedReader {
10541 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
10542 if self.cursor >= self.bytes.len() {
10543 return Ok(0);
10544 }
10545 let available = self.bytes.len() - self.cursor;
10546 let len = available.min(buf.len()).min(self.max_chunk);
10547 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
10548 self.cursor += len;
10549 Ok(len)
10550 }
10551 }
10552
10553 #[test]
10554 fn global_file_table_key_step_rejects_distinct_path_regression() {
10555 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
10556 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
10557
10558 assert_eq!(
10559 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
10560 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10561 );
10562 }
10563
10564 #[test]
10565 fn global_file_table_key_step_rejects_duplicate_full_key() {
10566 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
10567 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
10568
10569 assert_eq!(
10570 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
10571 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
10572 );
10573 }
10574
10575 fn small_block_recovery_options() -> WriterOptions {
10576 WriterOptions {
10577 block_size: 4096,
10578 chunk_size: 32 * 1024,
10579 envelope_target_size: 32 * 1024,
10580 stripe_width: 1,
10581 volume_loss_tolerance: 0,
10582 bit_rot_buffer_pct: 1,
10583 fec_data_shards: 16,
10584 fec_parity_shards: 1,
10585 index_fec_data_shards: 4,
10586 index_fec_parity_shards: 1,
10587 index_root_fec_data_shards: 16,
10588 index_root_fec_parity_shards: 1,
10589 ..WriterOptions::default()
10590 }
10591 }
10592
10593 fn parity_rich_recovery_options() -> WriterOptions {
10594 WriterOptions {
10595 block_size: 4096,
10596 chunk_size: 32 * 1024,
10597 envelope_target_size: 32 * 1024,
10598 stripe_width: 1,
10599 volume_loss_tolerance: 0,
10600 bit_rot_buffer_pct: 40,
10601 fec_data_shards: 16,
10602 fec_parity_shards: 16,
10603 index_fec_data_shards: 4,
10604 index_fec_parity_shards: 4,
10605 index_root_fec_data_shards: 16,
10606 index_root_fec_parity_shards: 16,
10607 ..WriterOptions::default()
10608 }
10609 }
10610
10611 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
10612 let mut state = 0x1234_5678u32;
10613 (0..len)
10614 .map(|_| {
10615 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
10616 (state >> 24) as u8
10617 })
10618 .collect()
10619 }
10620
10621 #[test]
10622 fn opens_lists_verifies_and_extracts_one_file_archive() {
10623 let archive = write_archive(
10624 &[RegularFile::new("dir/hello.txt", b"hello m7")],
10625 &master_key(),
10626 single_stream_options(),
10627 )
10628 .unwrap();
10629 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10630
10631 assert_eq!(
10632 opened.list_files().unwrap(),
10633 vec![ArchiveEntry {
10634 path: "dir/hello.txt".to_string(),
10635 file_data_size: 8,
10636 kind: TarEntryKind::Regular,
10637 mode: 0o644,
10638 mtime: 0,
10639 diagnostics: Vec::new(),
10640 }]
10641 );
10642 opened.verify().unwrap();
10643 assert_eq!(
10644 opened.extract_file("dir/hello.txt").unwrap(),
10645 Some(b"hello m7".to_vec())
10646 );
10647 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
10648 }
10649
10650 #[test]
10651 fn root_auth_archive_round_trips_and_verifies_with_callback() {
10652 let archive = write_archive_with_root_auth(
10653 &[RegularFile::new("signed.txt", b"root-auth payload")],
10654 &master_key(),
10655 single_stream_options(),
10656 RootAuthWriterConfig {
10657 authenticator_id: 0x7777,
10658 signer_identity_type: 1,
10659 signer_identity: b"test signer",
10660 authenticator_value_length: 32,
10661 },
10662 |request| Ok(request.archive_root.to_vec()),
10663 )
10664 .unwrap();
10665
10666 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10667 opened.verify().unwrap();
10668 let verified = opened
10669 .verify_root_auth_with(|footer, archive_root| {
10670 Ok(footer.authenticator_value == archive_root.as_slice())
10671 })
10672 .unwrap();
10673
10674 assert_eq!(verified.authenticator_id, 0x7777);
10675 assert_eq!(verified.signer_identity_type, 1);
10676 assert_eq!(verified.signer_identity_bytes, b"test signer");
10677 assert_eq!(
10678 verified.archive_root,
10679 opened.root_auth_footer.as_ref().unwrap().archive_root
10680 );
10681 assert_eq!(
10682 verified.diagnostics,
10683 vec![
10684 RootAuthDiagnostic::RootAuthContentVerified,
10685 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
10686 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
10687 RootAuthDiagnostic::RecoveryMarginUnchecked,
10688 ]
10689 );
10690 }
10691
10692 #[test]
10693 fn root_auth_rejects_fast_content_verification_token() {
10694 let archive = write_archive_with_root_auth(
10695 &[RegularFile::new("signed.txt", b"root-auth payload")],
10696 &master_key(),
10697 single_stream_options(),
10698 RootAuthWriterConfig {
10699 authenticator_id: 0x7777,
10700 signer_identity_type: 1,
10701 signer_identity: b"test signer",
10702 authenticator_value_length: 32,
10703 },
10704 |request| Ok(request.archive_root.to_vec()),
10705 )
10706 .unwrap();
10707
10708 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10709 let content_verification = opened.verify_content_fast().unwrap();
10710 assert_eq!(
10711 opened
10712 .verify_root_auth_with_verified_content(&content_verification, |_, _| Ok(true))
10713 .unwrap_err(),
10714 FormatError::ReaderUnsupported(
10715 "RootAuth verification requires full archive content verification"
10716 )
10717 );
10718 }
10719
10720 #[test]
10721 fn root_auth_verification_requires_authenticator_success() {
10722 let archive = write_archive_with_root_auth(
10723 &[RegularFile::new("signed.txt", b"root-auth payload")],
10724 &master_key(),
10725 single_stream_options(),
10726 RootAuthWriterConfig {
10727 authenticator_id: 9,
10728 signer_identity_type: 1,
10729 signer_identity: b"test signer",
10730 authenticator_value_length: 32,
10731 },
10732 |request| Ok(request.archive_root.to_vec()),
10733 )
10734 .unwrap();
10735 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10736
10737 assert_eq!(
10738 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
10739 FormatError::InvalidArchive("root-auth authenticator verification failed")
10740 );
10741 }
10742
10743 #[test]
10744 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
10745 let archive = write_archive_with_root_auth(
10746 &[RegularFile::new("public.txt", b"public commitment")],
10747 &master_key(),
10748 single_stream_options(),
10749 RootAuthWriterConfig {
10750 authenticator_id: 0x2222,
10751 signer_identity_type: 1,
10752 signer_identity: b"public verifier",
10753 authenticator_value_length: 32,
10754 },
10755 |request| Ok(request.archive_root.to_vec()),
10756 )
10757 .unwrap();
10758
10759 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
10760 Ok(footer.authenticator_value == archive_root.as_slice())
10761 })
10762 .unwrap();
10763
10764 assert_eq!(verified.authenticator_id, 0x2222);
10765 assert_eq!(verified.signer_identity_bytes, b"public verifier");
10766 assert!(verified.total_data_block_count > 0);
10767 }
10768
10769 #[test]
10770 fn public_no_key_verifier_not_invoked_for_future_revision() {
10771 let archive = write_archive_with_root_auth(
10772 &[RegularFile::new("public.txt", b"public callback")],
10773 &master_key(),
10774 single_stream_options(),
10775 RootAuthWriterConfig {
10776 authenticator_id: 0x2222,
10777 signer_identity_type: 1,
10778 signer_identity: b"public verifier",
10779 authenticator_value_length: 32,
10780 },
10781 |request| Ok(request.archive_root.to_vec()),
10782 )
10783 .unwrap();
10784 let mut bytes = archive.bytes;
10785 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10786 header.volume_format_rev = 45;
10787 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10788
10789 let mut called = false;
10790 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10791 called = true;
10792 Ok(true)
10793 })
10794 .unwrap_err();
10795
10796 assert!(!called);
10797 assert_eq!(
10798 err,
10799 FormatError::UnsupportedVolumeFormatRevision {
10800 format_version: FORMAT_VERSION,
10801 volume_format_rev: 45,
10802 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10803 }
10804 );
10805 }
10806
10807 #[test]
10808 fn public_no_key_rejects_public_header_revision_mismatch() {
10809 let archive = write_archive_with_root_auth(
10810 &[RegularFile::new("public.txt", b"public v44 only")],
10811 &master_key(),
10812 single_stream_options(),
10813 RootAuthWriterConfig {
10814 authenticator_id: 0x2222,
10815 signer_identity_type: 1,
10816 signer_identity: b"public verifier",
10817 authenticator_value_length: 32,
10818 },
10819 |request| Ok(request.archive_root.to_vec()),
10820 )
10821 .unwrap();
10822 let mut bytes = archive.bytes;
10823 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
10824 header.volume_format_rev = 43;
10825 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
10826
10827 let mut called = false;
10828 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10829 called = true;
10830 Ok(true)
10831 })
10832 .unwrap_err();
10833
10834 assert!(!called);
10835 assert_eq!(
10836 err,
10837 FormatError::UnsupportedVolumeFormatRevision {
10838 format_version: FORMAT_VERSION,
10839 volume_format_rev: 43,
10840 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
10841 }
10842 );
10843 }
10844
10845 #[test]
10846 fn public_no_key_rejects_recovered_footer_revision_mismatch() {
10847 let archive = write_archive_with_root_auth(
10848 &[RegularFile::new("public.txt", b"public footer mismatch")],
10849 &master_key(),
10850 single_stream_options(),
10851 RootAuthWriterConfig {
10852 authenticator_id: 0x2222,
10853 signer_identity_type: 1,
10854 signer_identity: b"public verifier",
10855 authenticator_value_length: 32,
10856 },
10857 |request| Ok(request.archive_root.to_vec()),
10858 )
10859 .unwrap();
10860 let mut bytes = archive.bytes;
10861 rewrite_public_cmra_image(&mut bytes, |image| {
10862 let root_auth_region = image
10863 .regions
10864 .iter_mut()
10865 .find(|region| region.region_type == 4)
10866 .unwrap();
10867 rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
10868 });
10869
10870 let mut called = false;
10871 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10872 called = true;
10873 Ok(true)
10874 })
10875 .unwrap_err();
10876
10877 assert!(!called);
10878 assert_eq!(
10879 err,
10880 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10881 );
10882 }
10883
10884 #[test]
10885 fn public_no_key_rejects_recovered_image_with_unknown_layout_flags() {
10886 let archive = write_archive_with_root_auth(
10887 &[RegularFile::new("public.txt", b"public image mismatch")],
10888 &master_key(),
10889 single_stream_options(),
10890 RootAuthWriterConfig {
10891 authenticator_id: 0x2222,
10892 signer_identity_type: 1,
10893 signer_identity: b"public verifier",
10894 authenticator_value_length: 32,
10895 },
10896 |request| Ok(request.archive_root.to_vec()),
10897 )
10898 .unwrap();
10899 let bytes = rewrite_cmra_image_variable_len(
10900 &archive.bytes,
10901 CmraRecoveryMode::PublicNoKey,
10902 |image| {
10903 image.layout_flags |= 0x8000_0000;
10904 },
10905 );
10906
10907 let mut called = false;
10908 let err = public_no_key_verify_archive_with(&bytes, |_, _| {
10909 called = true;
10910 Ok(true)
10911 })
10912 .unwrap_err();
10913
10914 assert!(!called);
10915 assert_eq!(
10916 err,
10917 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
10918 );
10919 }
10920
10921 #[test]
10922 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
10923 let archive = write_archive_with_root_auth(
10924 &[RegularFile::new(
10925 "public-fields.txt",
10926 b"public source authority",
10927 )],
10928 &master_key(),
10929 single_stream_options(),
10930 RootAuthWriterConfig {
10931 authenticator_id: 0x2222,
10932 signer_identity_type: 1,
10933 signer_identity: b"public verifier",
10934 authenticator_value_length: 32,
10935 },
10936 |request| Ok(request.archive_root.to_vec()),
10937 )
10938 .unwrap();
10939 let mut bytes = archive.bytes.clone();
10940
10941 rewrite_public_cmra_image(&mut bytes, |image| {
10942 let manifest_region = image
10943 .regions
10944 .iter_mut()
10945 .find(|region| region.region_type == 3)
10946 .unwrap();
10947 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
10948
10949 let trailer_region = image
10950 .regions
10951 .iter_mut()
10952 .find(|region| region.region_type == 5)
10953 .unwrap();
10954 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
10955 trailer.block_count += 7;
10956 trailer_region.bytes = trailer.to_bytes().to_vec();
10957 });
10958
10959 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
10960 Ok(footer.authenticator_value == archive_root.as_slice())
10961 })
10962 .unwrap();
10963 }
10964
10965 #[test]
10966 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
10967 let archive = write_archive_with_root_auth(
10968 &[RegularFile::new(
10969 "public-crypto.txt",
10970 b"cross-volume public profile",
10971 )],
10972 &master_key(),
10973 WriterOptions {
10974 stripe_width: 2,
10975 volume_loss_tolerance: 0,
10976 ..WriterOptions::default()
10977 },
10978 RootAuthWriterConfig {
10979 authenticator_id: 0x3333,
10980 signer_identity_type: 1,
10981 signer_identity: b"public verifier",
10982 authenticator_value_length: 32,
10983 },
10984 |request| Ok(request.archive_root.to_vec()),
10985 )
10986 .unwrap();
10987 let mut volumes = archive.volumes.clone();
10988 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
10989 let crypto_offset = volume_header.crypto_header_offset as usize;
10990 let expected_volume_size = 123_456_789u64;
10991 volumes[1][crypto_offset + 52..crypto_offset + 60]
10992 .copy_from_slice(&expected_volume_size.to_le_bytes());
10993 rewrite_public_cmra_image(&mut volumes[1], |image| {
10994 let crypto_region = image
10995 .regions
10996 .iter_mut()
10997 .find(|region| region.region_type == 2)
10998 .unwrap();
10999 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
11000 });
11001
11002 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
11003 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
11004 Ok(footer.authenticator_value == archive_root.as_slice())
11005 })
11006 .unwrap();
11007 }
11008
11009 #[test]
11010 fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
11011 let archive = write_archive_with_root_auth(
11012 &[RegularFile::new("cmra-header.txt", b"header fallback")],
11013 &master_key(),
11014 single_stream_options(),
11015 RootAuthWriterConfig {
11016 authenticator_id: 0x4444,
11017 signer_identity_type: 1,
11018 signer_identity: b"public verifier",
11019 authenticator_value_length: 32,
11020 },
11021 |request| Ok(request.archive_root.to_vec()),
11022 )
11023 .unwrap();
11024 let final_locator = final_recovery_locator(&archive.bytes);
11025
11026 let mut bad_crc = archive.bytes.clone();
11027 let crc_offset =
11028 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
11029 bad_crc[crc_offset] ^= 0x55;
11030 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
11031 Ok(footer.authenticator_value == archive_root.as_slice())
11032 })
11033 .unwrap();
11034
11035 let mut bad_magic = archive.bytes.clone();
11036 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
11037 public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
11038 Ok(footer.authenticator_value == archive_root.as_slice())
11039 })
11040 .unwrap();
11041
11042 let mut bad_hint = archive.bytes.clone();
11043 bad_hint[crc_offset] ^= 0xAA;
11044 for offset in [
11045 bad_hint.len() - LOCATOR_PAIR_LEN,
11046 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
11047 ] {
11048 let mut locator = CriticalRecoveryLocator::parse(
11049 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11050 )
11051 .unwrap();
11052 locator.volume_index_hint += 1;
11053 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
11054 .copy_from_slice(&locator.to_bytes());
11055 }
11056 assert_eq!(
11057 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
11058 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11059 );
11060 }
11061
11062 #[test]
11063 fn recovers_physical_volume_header_magic_from_cmra_authority() {
11064 let payload = b"front header authority".to_vec();
11065 let archive = write_archive(
11066 &[RegularFile::new("volume-header.txt", &payload)],
11067 &master_key(),
11068 small_block_recovery_options(),
11069 )
11070 .unwrap();
11071
11072 let mut corrupted = archive.bytes;
11073 corrupted[0] ^= 0x55;
11074
11075 let opened = open_archive(&corrupted, &master_key()).unwrap();
11076 assert_eq!(
11077 opened.extract_file("volume-header.txt").unwrap(),
11078 Some(payload)
11079 );
11080 opened.verify().unwrap();
11081 }
11082
11083 #[test]
11084 fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11085 let payload = b"crc-valid wrong volume index".to_vec();
11086 let mut options = small_block_recovery_options();
11087 options.stripe_width = 2;
11088 options.volume_loss_tolerance = 0;
11089 let archive = write_archive(
11090 &[RegularFile::new("volume-index.txt", &payload)],
11091 &master_key(),
11092 options,
11093 )
11094 .unwrap();
11095
11096 let mut corrupted = archive.volumes[0].clone();
11097 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11098 assert_eq!(header.volume_index, 0);
11099 header.volume_index = 1;
11100 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11101 assert_eq!(
11102 VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
11103 .unwrap()
11104 .volume_index,
11105 1
11106 );
11107
11108 let opened = open_archive_volumes(
11109 &[corrupted.as_slice(), archive.volumes[1].as_slice()],
11110 &master_key(),
11111 )
11112 .unwrap();
11113 assert_eq!(opened.volume_header.volume_index, 0);
11114 assert_eq!(
11115 opened.extract_file("volume-index.txt").unwrap(),
11116 Some(payload)
11117 );
11118 opened.verify().unwrap();
11119 }
11120
11121 #[test]
11122 fn recovers_physical_crypto_header_magic_from_cmra_authority() {
11123 let payload = b"crypto header authority".to_vec();
11124 let archive = write_archive(
11125 &[RegularFile::new("crypto-header.txt", &payload)],
11126 &master_key(),
11127 small_block_recovery_options(),
11128 )
11129 .unwrap();
11130 let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
11131 .unwrap()
11132 .crypto_header_offset;
11133
11134 let mut corrupted = archive.bytes;
11135 corrupted[crypto_offset as usize] ^= 0x55;
11136
11137 let opened = open_archive(&corrupted, &master_key()).unwrap();
11138 assert_eq!(
11139 opened.extract_file("crypto-header.txt").unwrap(),
11140 Some(payload)
11141 );
11142 opened.verify().unwrap();
11143 }
11144
11145 #[test]
11146 fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
11147 let payload = b"read-at header authority".to_vec();
11148 let archive = write_archive(
11149 &[RegularFile::new("read-at-header.txt", &payload)],
11150 &master_key(),
11151 small_block_recovery_options(),
11152 )
11153 .unwrap();
11154
11155 let mut corrupted = archive.bytes;
11156 corrupted[0] ^= 0x55;
11157
11158 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
11159 assert_eq!(
11160 opened.extract_file("read-at-header.txt").unwrap(),
11161 Some(payload)
11162 );
11163 opened.verify().unwrap();
11164 }
11165
11166 #[test]
11167 fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
11168 let payload = b"read-at crc-valid wrong volume index".to_vec();
11169 let mut options = small_block_recovery_options();
11170 options.stripe_width = 2;
11171 options.volume_loss_tolerance = 0;
11172 let archive = write_archive(
11173 &[RegularFile::new("read-at-volume-index.txt", &payload)],
11174 &master_key(),
11175 options,
11176 )
11177 .unwrap();
11178
11179 let mut corrupted = archive.volumes[0].clone();
11180 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
11181 assert_eq!(header.volume_index, 0);
11182 header.volume_index = 1;
11183 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11184
11185 let opened = open_seekable_archive_volumes(
11186 vec![corrupted, archive.volumes[1].clone()],
11187 &master_key(),
11188 )
11189 .unwrap();
11190 assert_eq!(opened.volume_header.volume_index, 0);
11191 assert_eq!(
11192 opened.extract_file("read-at-volume-index.txt").unwrap(),
11193 Some(payload)
11194 );
11195 opened.verify().unwrap();
11196 }
11197
11198 #[test]
11199 fn recovers_cmra_header_magic_from_locator_tuple() {
11200 let payload = b"cmra header authority".to_vec();
11201 let archive = write_archive(
11202 &[RegularFile::new("cmra-header-magic.txt", &payload)],
11203 &master_key(),
11204 small_block_recovery_options(),
11205 )
11206 .unwrap();
11207 let locator = final_recovery_locator(&archive.bytes);
11208
11209 let mut corrupted = archive.bytes;
11210 corrupted[locator.cmra_offset as usize] ^= 0x55;
11211
11212 let opened = open_archive(&corrupted, &master_key()).unwrap();
11213 assert_eq!(
11214 opened.extract_file("cmra-header-magic.txt").unwrap(),
11215 Some(payload)
11216 );
11217 opened.verify().unwrap();
11218 }
11219
11220 #[test]
11221 fn recovers_cmra_shard_magic_as_erasure() {
11222 let payload = b"cmra shard authority".to_vec();
11223 let archive = write_archive(
11224 &[RegularFile::new("cmra-shard-magic.txt", &payload)],
11225 &master_key(),
11226 small_block_recovery_options(),
11227 )
11228 .unwrap();
11229 let locator = final_recovery_locator(&archive.bytes);
11230 let first_shard_offset =
11231 locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
11232
11233 let mut corrupted = archive.bytes;
11234 corrupted[first_shard_offset] ^= 0x55;
11235
11236 let opened = open_archive(&corrupted, &master_key()).unwrap();
11237 assert_eq!(
11238 opened.extract_file("cmra-shard-magic.txt").unwrap(),
11239 Some(payload)
11240 );
11241 opened.verify().unwrap();
11242 }
11243
11244 #[test]
11245 fn key_holding_rejects_recovered_image_with_unknown_layout_flags() {
11246 let archive = write_archive(
11247 &[RegularFile::new("cmra-image-revision.txt", b"payload")],
11248 &master_key(),
11249 small_block_recovery_options(),
11250 )
11251 .unwrap();
11252 let mut mutated = rewrite_cmra_image_variable_len(
11253 &archive.bytes,
11254 CmraRecoveryMode::KeyHolding,
11255 |image| {
11256 image.layout_flags |= 0x8000_0000;
11257 },
11258 );
11259 mutated[0] ^= 0x55;
11260
11261 assert!(open_archive(&mutated, &master_key()).is_err());
11262 }
11263
11264 #[test]
11265 fn key_holding_rejects_recovered_footer_revision_mismatch() {
11266 let archive = write_archive_with_root_auth(
11267 &[RegularFile::new("cmra-footer-revision.txt", b"payload")],
11268 &master_key(),
11269 single_stream_options(),
11270 test_root_auth_config(),
11271 |request| Ok(test_root_auth_value(request)),
11272 )
11273 .unwrap();
11274 let mut mutated = archive.bytes;
11275 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
11276 let root_auth_region = image
11277 .regions
11278 .iter_mut()
11279 .find(|region| region.region_type == 4)
11280 .unwrap();
11281 rewrite_root_auth_footer_revision_bytes(&mut root_auth_region.bytes, 43);
11282 });
11283 mutated[0] ^= 0x55;
11284
11285 assert!(open_archive(&mutated, &master_key()).is_err());
11286 }
11287
11288 #[test]
11289 fn key_holding_rejects_locator_image_revision_mismatch() {
11290 let archive = write_archive(
11291 &[RegularFile::new("cmra-locator-revision.txt", b"payload")],
11292 &master_key(),
11293 small_block_recovery_options(),
11294 )
11295 .unwrap();
11296 let mut mutated = archive.bytes;
11297 let locator = final_recovery_locator(&mutated);
11298 let mirror_offset = mutated.len() - LOCATOR_PAIR_LEN;
11299 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11300 for offset in [mirror_offset, final_offset] {
11301 rewrite_recovery_locator(&mut mutated, offset, |locator| {
11302 locator.volume_format_rev = 43;
11303 });
11304 }
11305 mutated[0] ^= 0x55;
11306 mutated[locator.cmra_offset as usize] ^= 0x55;
11307
11308 assert!(open_archive(&mutated, &master_key()).is_err());
11309 }
11310
11311 #[test]
11312 fn image_identity_allows_matching_current_revision() {
11313 let archive = write_archive(
11314 &[RegularFile::new("matching-v44.txt", b"payload")],
11315 &master_key(),
11316 single_stream_options(),
11317 )
11318 .unwrap();
11319 let locator = final_recovery_locator(&archive.bytes);
11320 let recovered = recover_cmra(
11321 &archive.bytes,
11322 locator.cmra_offset,
11323 Some(CmraDecoderTuple::from(locator)),
11324 CmraRecoveryMode::KeyHolding,
11325 )
11326 .unwrap();
11327 let image = recovered.image;
11328 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11329 let crypto_start = header.crypto_header_offset as usize;
11330 let crypto_end = crypto_start + header.crypto_header_length as usize;
11331 let crypto = CryptoHeader::parse(
11332 &archive.bytes[crypto_start..crypto_end],
11333 header.crypto_header_length,
11334 )
11335 .unwrap();
11336
11337 validate_image_identity(&image, &header, &crypto.fixed).unwrap();
11338 }
11339
11340 #[test]
11341 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
11342 let archive = write_archive(
11343 &[RegularFile::new(
11344 "cmra-floor.txt",
11345 b"authenticated CMRA floor",
11346 )],
11347 &master_key(),
11348 single_stream_options(),
11349 )
11350 .unwrap();
11351 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
11352 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11353 let locator = CriticalRecoveryLocator::parse(
11354 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
11355 )
11356 .unwrap();
11357 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
11358 let crypto_start = volume_header.crypto_header_offset as usize;
11359 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11360 let crypto_header = CryptoHeader::parse(
11361 &malformed[crypto_start..crypto_end],
11362 volume_header.crypto_header_length,
11363 )
11364 .unwrap();
11365 let subkeys = Subkeys::derive(
11366 &master_key(),
11367 &volume_header.archive_uuid,
11368 &volume_header.session_id,
11369 )
11370 .unwrap();
11371
11372 assert_eq!(
11373 parse_locator_cmra_candidate(
11374 &malformed,
11375 final_offset,
11376 locator,
11377 KeyHoldingTerminalContext {
11378 subkeys: &subkeys,
11379 volume_header: &volume_header,
11380 crypto_header: &crypto_header.fixed,
11381 crypto_header_bytes: &malformed[crypto_start..crypto_end],
11382 },
11383 )
11384 .unwrap_err(),
11385 FormatError::InvalidArchive(
11386 "CMRA parity shard count is below authenticated bit-rot lower bound"
11387 )
11388 );
11389 assert!(open_archive(&malformed, &master_key()).is_err());
11390 }
11391
11392 #[test]
11393 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
11394 let archive = write_archive(
11395 &[RegularFile::new(
11396 "locator-order.txt",
11397 b"locator tuple first",
11398 )],
11399 &master_key(),
11400 single_stream_options(),
11401 )
11402 .unwrap();
11403 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
11404 let mut locator = final_recovery_locator(&archive.bytes);
11405 locator.cmra_shard_size = 513;
11406 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
11407 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11408 let crypto_start = volume_header.crypto_header_offset as usize;
11409 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
11410 let crypto_header = CryptoHeader::parse(
11411 &archive.bytes[crypto_start..crypto_end],
11412 volume_header.crypto_header_length,
11413 )
11414 .unwrap();
11415 let subkeys = Subkeys::derive(
11416 &master_key(),
11417 &volume_header.archive_uuid,
11418 &volume_header.session_id,
11419 )
11420 .unwrap();
11421
11422 assert_eq!(
11423 parse_locator_cmra_candidate(
11424 &archive.bytes,
11425 final_offset,
11426 locator,
11427 KeyHoldingTerminalContext {
11428 subkeys: &subkeys,
11429 volume_header: &volume_header,
11430 crypto_header: &crypto_header.fixed,
11431 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
11432 },
11433 )
11434 .unwrap_err(),
11435 FormatError::InvalidArchive("CMRA shard_size is invalid")
11436 );
11437 }
11438
11439 #[test]
11440 fn sequential_extract_rejects_bytes_after_terminal_locator() {
11441 let archive = write_archive(
11442 &[RegularFile::new("seq.txt", b"sequential EOF")],
11443 &master_key(),
11444 single_stream_options(),
11445 )
11446 .unwrap();
11447 let mut appended = archive.bytes.clone();
11448 appended.extend_from_slice(&[0xAA; 32]);
11449
11450 assert_eq!(
11451 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
11452 FormatError::InvalidArchive("sequential terminal does not end at EOF")
11453 );
11454 }
11455
11456 #[test]
11457 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
11458 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
11459 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
11460
11461 assert_eq!(
11462 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
11463 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
11464 );
11465 }
11466
11467 #[test]
11468 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
11469 let archive = write_archive_with_root_auth(
11470 &[RegularFile::new("signed.txt", b"ed25519 payload")],
11471 &master_key(),
11472 single_stream_options(),
11473 test_root_auth_config(),
11474 |request| Ok(test_root_auth_value(request)),
11475 )
11476 .unwrap();
11477
11478 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11479 let root_auth = opened
11480 .verify_root_auth_with(|footer, archive_root| {
11481 Ok(test_root_auth_verifies(footer, archive_root))
11482 })
11483 .unwrap();
11484 assert_eq!(
11485 root_auth.archive_root,
11486 opened.root_auth_footer.as_ref().unwrap().archive_root
11487 );
11488
11489 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
11490 Ok(test_root_auth_verifies(footer, archive_root))
11491 })
11492 .unwrap();
11493 assert_eq!(public.archive_root, root_auth.archive_root);
11494 assert_eq!(
11495 public.diagnostics,
11496 vec![
11497 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
11498 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
11499 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
11500 ]
11501 );
11502 }
11503
11504 #[test]
11505 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
11506 let options = WriterOptions {
11507 block_size: 4096,
11508 chunk_size: 16 * 1024,
11509 envelope_target_size: 16 * 1024,
11510 stripe_width: 2,
11511 volume_loss_tolerance: 1,
11512 bit_rot_buffer_pct: 0,
11513 fec_data_shards: 16,
11514 fec_parity_shards: 1,
11515 index_fec_data_shards: 4,
11516 index_fec_parity_shards: 1,
11517 index_root_fec_data_shards: 16,
11518 index_root_fec_parity_shards: 1,
11519 ..WriterOptions::default()
11520 };
11521 let archive = write_archive_with_root_auth(
11522 &[RegularFile::new("missing-volume.txt", b"recover me")],
11523 &master_key(),
11524 options,
11525 test_root_auth_config(),
11526 |request| Ok(test_root_auth_value(request)),
11527 )
11528 .unwrap();
11529
11530 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
11531 let root_auth = opened
11532 .verify_root_auth_with(|footer, archive_root| {
11533 Ok(test_root_auth_verifies(footer, archive_root))
11534 })
11535 .unwrap();
11536 assert!(root_auth
11537 .diagnostics
11538 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
11539 }
11540
11541 #[test]
11542 fn public_no_key_rejects_unsigned_archives() {
11543 let archive = write_archive(
11544 &[RegularFile::new("plain.txt", b"unsigned")],
11545 &master_key(),
11546 single_stream_options(),
11547 )
11548 .unwrap();
11549
11550 assert_eq!(
11551 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
11552 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
11553 );
11554 }
11555
11556 #[test]
11557 fn unsigned_archive_reports_root_auth_absent() {
11558 let archive = write_archive(
11559 &[RegularFile::new("plain.txt", b"unsigned")],
11560 &master_key(),
11561 single_stream_options(),
11562 )
11563 .unwrap();
11564 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11565
11566 assert_eq!(
11567 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
11568 FormatError::ReaderUnsupported("root-auth footer is absent")
11569 );
11570 }
11571
11572 #[test]
11573 fn safe_extract_writes_regular_file_under_root() {
11574 let archive = write_archive(
11575 &[RegularFile::new("dir/hello.txt", b"safe m8")],
11576 &master_key(),
11577 single_stream_options(),
11578 )
11579 .unwrap();
11580 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11581 let tmp = tempfile::tempdir().unwrap();
11582
11583 opened
11584 .extract_file_to(
11585 "dir/hello.txt",
11586 tmp.path(),
11587 SafeExtractionOptions::default(),
11588 )
11589 .unwrap()
11590 .unwrap();
11591
11592 assert_eq!(
11593 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
11594 b"safe m8"
11595 );
11596 }
11597
11598 #[test]
11599 fn seekable_extract_all_to_streams_unique_archive() {
11600 let archive = write_archive(
11601 &[
11602 RegularFile::new("alpha.txt", b"alpha"),
11603 RegularFile::new("dir/beta.txt", b"beta"),
11604 ],
11605 &master_key(),
11606 single_stream_options(),
11607 )
11608 .unwrap();
11609 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11610 let tmp = tempfile::tempdir().unwrap();
11611
11612 let diagnostics = opened
11613 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11614 .unwrap();
11615
11616 assert_eq!(diagnostics.len(), 2);
11617 assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
11618 assert_eq!(
11619 fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
11620 b"beta"
11621 );
11622 }
11623
11624 #[test]
11625 fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
11626 let archive = write_archive(
11627 &[
11628 RegularFile::new("same.txt", b"old"),
11629 RegularFile::new("same.txt", b"new"),
11630 ],
11631 &master_key(),
11632 single_stream_options(),
11633 )
11634 .unwrap();
11635 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11636 let tmp = tempfile::tempdir().unwrap();
11637
11638 assert_eq!(
11639 opened
11640 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
11641 .unwrap_err(),
11642 FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
11643 );
11644 }
11645
11646 #[test]
11647 fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
11648 let archive = write_archive(
11649 &[
11650 RegularFile::new("same.txt", b"old"),
11651 RegularFile::new("same.txt", b"new"),
11652 ],
11653 &master_key(),
11654 single_stream_options(),
11655 )
11656 .unwrap();
11657 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11658 let tmp = tempfile::tempdir().unwrap();
11659
11660 let diagnostics = opened
11661 .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
11662 .unwrap();
11663
11664 assert_eq!(diagnostics.len(), 1);
11665 assert_eq!(diagnostics[0].0, "same.txt");
11666 assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
11667 }
11668
11669 #[test]
11670 fn safe_extract_rejects_overwriting_existing_file_by_default() {
11671 let archive = write_archive(
11672 &[RegularFile::new("hello.txt", b"new")],
11673 &master_key(),
11674 single_stream_options(),
11675 )
11676 .unwrap();
11677 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11678 let tmp = tempfile::tempdir().unwrap();
11679 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
11680
11681 assert_eq!(
11682 opened
11683 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
11684 .unwrap_err(),
11685 FormatError::UnsafeOverwrite
11686 );
11687 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
11688 }
11689
11690 #[test]
11691 fn opens_and_verifies_empty_archive() {
11692 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11693 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11694
11695 assert!(opened.list_files().unwrap().is_empty());
11696 opened.verify().unwrap();
11697 }
11698
11699 #[test]
11700 fn default_reader_options_allow_v36_trailing_garbage_scan() {
11701 let archive = write_archive(
11702 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
11703 &master_key(),
11704 single_stream_options(),
11705 )
11706 .unwrap();
11707 let mut with_trailing_garbage = archive.bytes.clone();
11708 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
11709
11710 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
11711 assert_eq!(
11712 opened.extract_file("garbage-tolerant.txt").unwrap(),
11713 Some(b"still intact".to_vec())
11714 );
11715 }
11716
11717 #[test]
11718 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
11719 assert_eq!(
11720 open_archive(
11721 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
11722 &master_key()
11723 )
11724 .unwrap_err(),
11725 FormatError::InvalidLength {
11726 structure: "archive",
11727 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11728 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
11729 }
11730 );
11731
11732 let mut header = test_volume_header();
11733 header.crypto_header_length = 512;
11734 let mut unavailable_crypto = header.to_bytes().to_vec();
11735 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
11736
11737 assert_eq!(
11738 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
11739 FormatError::InvalidLength {
11740 structure: "CryptoHeader",
11741 expected: VOLUME_HEADER_LEN + 512,
11742 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
11743 }
11744 );
11745 }
11746
11747 #[test]
11748 fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
11749 let archive = write_archive(
11750 &[RegularFile::new("offset.txt", b"offset")],
11751 &master_key(),
11752 small_block_recovery_options(),
11753 )
11754 .unwrap();
11755 let mut mutated = archive.bytes;
11756 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
11757 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
11758 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11759
11760 let opened = open_archive(&mutated, &master_key()).unwrap();
11761 assert_eq!(
11762 opened.volume_header.crypto_header_offset,
11763 VOLUME_HEADER_LEN as u32
11764 );
11765 assert_eq!(
11766 opened.extract_file("offset.txt").unwrap(),
11767 Some(b"offset".to_vec())
11768 );
11769 opened.verify().unwrap();
11770 }
11771
11772 #[test]
11773 fn rejects_wrong_key_before_metadata_release() {
11774 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11775 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11776
11777 assert_eq!(
11778 open_archive(&archive.bytes, &wrong).unwrap_err(),
11779 FormatError::HmacMismatch {
11780 structure: "CryptoHeader"
11781 }
11782 );
11783 }
11784
11785 #[test]
11786 fn ordinary_encrypted_writers_emit_v44_archives() {
11787 let raw_key_archive = write_archive(
11788 &[RegularFile::new("raw.txt", b"raw key payload")],
11789 &master_key(),
11790 single_stream_options(),
11791 )
11792 .unwrap();
11793 let raw_header = VolumeHeader::parse(&raw_key_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11794 assert_eq!(raw_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11795 let raw_opened = open_archive(&raw_key_archive.bytes, &master_key()).unwrap();
11796 assert_eq!(
11797 raw_opened.volume_header.volume_format_rev,
11798 VOLUME_FORMAT_REV_44
11799 );
11800 assert_eq!(
11801 raw_opened.extract_file("raw.txt").unwrap(),
11802 Some(b"raw key payload".to_vec())
11803 );
11804
11805 let passphrase_kdf = KdfParams::Argon2id {
11806 t_cost: 1,
11807 m_cost_kib: 8,
11808 parallelism: 1,
11809 salt: b"0123456789abcdef".to_vec(),
11810 };
11811 let passphrase_archive = write_archive_with_kdf(
11812 &[RegularFile::new("pass.txt", b"passphrase payload")],
11813 &master_key(),
11814 single_stream_options(),
11815 &passphrase_kdf,
11816 )
11817 .unwrap();
11818 let passphrase_header =
11819 VolumeHeader::parse(&passphrase_archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11820 assert_eq!(passphrase_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11821 let passphrase_opened = open_archive(&passphrase_archive.bytes, &master_key()).unwrap();
11822 assert_eq!(
11823 passphrase_opened.volume_header.volume_format_rev,
11824 VOLUME_FORMAT_REV_44
11825 );
11826 assert_eq!(
11827 passphrase_opened.extract_file("pass.txt").unwrap(),
11828 Some(b"passphrase payload".to_vec())
11829 );
11830 }
11831
11832 #[test]
11833 fn rejects_future_volume_format_revision_before_key_mismatch() {
11834 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11835 let mut bytes = archive.bytes;
11836 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
11837 header.volume_format_rev = 45;
11838 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
11839 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
11840
11841 assert_eq!(
11842 open_archive(&bytes, &wrong).unwrap_err(),
11843 FormatError::UnsupportedVolumeFormatRevision {
11844 format_version: FORMAT_VERSION,
11845 volume_format_rev: 45,
11846 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
11847 }
11848 );
11849 }
11850
11851 #[test]
11852 fn open_archive_unencrypted_accepts_v44_profile() {
11853 let archive = write_archive_unencrypted(
11854 &[RegularFile::new("payload.txt", b"smoke-v44-unencrypted")],
11855 WriterOptions {
11856 aead_algo: AeadAlgo::None,
11857 ..single_stream_options()
11858 },
11859 )
11860 .unwrap();
11861
11862 let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11863 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11864
11865 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11866 assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11867 assert_eq!(
11868 opened.extract_file("payload.txt").unwrap(),
11869 Some(b"smoke-v44-unencrypted".to_vec())
11870 );
11871 opened.verify().unwrap();
11872 }
11873
11874 #[test]
11875 fn root_auth_unencrypted_v44_round_trips_with_recomputed_archive_root() {
11876 let archive = write_archive_with_root_auth(
11877 &[RegularFile::new(
11878 "signed-v44.txt",
11879 b"root-auth v44 plaintext",
11880 )],
11881 &master_key(),
11882 WriterOptions {
11883 aead_algo: AeadAlgo::None,
11884 ..single_stream_options()
11885 },
11886 test_root_auth_config(),
11887 |request| Ok(test_root_auth_value(request)),
11888 )
11889 .unwrap();
11890 let opened = open_archive_unencrypted(&archive.bytes).unwrap();
11891 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11892
11893 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV_44);
11894 assert_eq!(opened.volume_header.volume_format_rev, VOLUME_FORMAT_REV_44);
11895 assert_eq!(
11896 opened.extract_file("signed-v44.txt").unwrap(),
11897 Some(b"root-auth v44 plaintext".to_vec())
11898 );
11899
11900 let verified = opened
11901 .verify_root_auth_with(|footer, archive_root| {
11902 Ok(test_root_auth_verifies(footer, archive_root))
11903 })
11904 .unwrap();
11905
11906 assert_eq!(verified.format_version, FORMAT_VERSION);
11907 assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
11908 assert_eq!(
11909 verified.archive_root,
11910 opened.root_auth_footer.as_ref().unwrap().archive_root
11911 );
11912 }
11913
11914 #[test]
11915 fn recipientwrap_open_accepts_candidate_after_header_hmac() {
11916 let master = master_key();
11917 let archive = write_archive_with_recipient_wrap_records(
11918 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11919 &master,
11920 single_stream_options(),
11921 vec![recipient_wrap_test_record()],
11922 )
11923 .unwrap();
11924
11925 let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
11926 assert_eq!(
11927 context.archive_identity.volume_format_rev,
11928 VOLUME_FORMAT_REV_44
11929 );
11930 assert_eq!(context.record.profile_id, 1);
11931 Ok(vec![master.0])
11932 })
11933 .unwrap();
11934
11935 assert_eq!(
11936 opened.extract_file("wrapped.txt").unwrap(),
11937 Some(b"recipient payload".to_vec())
11938 );
11939 opened.verify().unwrap();
11940 }
11941
11942 #[test]
11943 fn recipientwrap_seekable_open_uses_lazy_block_source() {
11944 let master = master_key();
11945 let archive = write_archive_with_recipient_wrap_records(
11946 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11947 &master,
11948 single_stream_options(),
11949 vec![recipient_wrap_test_record()],
11950 )
11951 .unwrap();
11952
11953 let opened = open_seekable_archive_with_recipient_wrap_resolver_options(
11954 CountingReadAt::new(archive.bytes, vec![]),
11955 |context| {
11956 assert_eq!(
11957 context.archive_identity.volume_format_rev,
11958 VOLUME_FORMAT_REV_44
11959 );
11960 assert_eq!(context.record.profile_id, 1);
11961 Ok(vec![master.0])
11962 },
11963 ReaderOptions::default(),
11964 )
11965 .unwrap();
11966
11967 assert!(opened.blocks.is_empty());
11968 assert!(opened.lazy_blocks.is_some());
11969 assert_eq!(
11970 opened.extract_file("wrapped.txt").unwrap(),
11971 Some(b"recipient payload".to_vec())
11972 );
11973 opened.verify().unwrap();
11974 }
11975
11976 #[test]
11977 fn recipientwrap_seekable_volume_set_opens_with_resolver() {
11978 let master = master_key();
11979 let archive = write_archive_with_recipient_wrap_records(
11980 &[RegularFile::new("wrapped.txt", b"recipient payload")],
11981 &master,
11982 WriterOptions {
11983 stripe_width: 2,
11984 volume_loss_tolerance: 0,
11985 ..WriterOptions::default()
11986 },
11987 vec![recipient_wrap_test_record()],
11988 )
11989 .unwrap();
11990 assert_eq!(archive.volumes.len(), 2);
11991
11992 let opened = open_seekable_archive_volumes_with_recipient_wrap_resolver_options(
11993 archive.volumes,
11994 |context| {
11995 assert_eq!(
11996 context.archive_identity.volume_format_rev,
11997 VOLUME_FORMAT_REV_44
11998 );
11999 assert_eq!(context.record.profile_id, 1);
12000 Ok(vec![master.0])
12001 },
12002 ReaderOptions::default(),
12003 )
12004 .unwrap();
12005
12006 assert_eq!(opened.observed_volume_count, 2);
12007 assert!(opened.blocks.is_empty());
12008 assert!(opened.lazy_blocks.is_some());
12009 assert_eq!(
12010 opened.extract_file("wrapped.txt").unwrap(),
12011 Some(b"recipient payload".to_vec())
12012 );
12013 opened.verify().unwrap();
12014 }
12015
12016 #[test]
12017 fn recipientwrap_open_tries_subsequent_records_after_failed_candidate() {
12018 let master = master_key();
12019 let mut first_record = recipient_wrap_test_record();
12020 first_record.recipient_identity_bytes = b"first-candidate".to_vec();
12021 let mut second_record = recipient_wrap_test_record();
12022 second_record.recipient_identity_bytes = b"second-candidate".to_vec();
12023
12024 let archive = write_archive_with_recipient_wrap_records(
12025 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12026 &master,
12027 single_stream_options(),
12028 vec![first_record, second_record],
12029 )
12030 .unwrap();
12031
12032 let mut attempts = Vec::new();
12033 let opened = open_archive_with_recipient_wrap_resolver(&archive.bytes, |context| {
12034 attempts.push(context.record.recipient_identity_bytes.clone());
12035 if context.record.recipient_identity_bytes.as_slice() == b"second-candidate" {
12036 Ok(vec![master.0])
12037 } else {
12038 Ok(vec![[0x99u8; 32]])
12039 }
12040 })
12041 .unwrap();
12042
12043 assert_eq!(
12044 attempts,
12045 vec![b"first-candidate".to_vec(), b"second-candidate".to_vec(),]
12046 );
12047 assert_eq!(
12048 opened.extract_file("wrapped.txt").unwrap(),
12049 Some(b"recipient payload".to_vec())
12050 );
12051 opened.verify().unwrap();
12052 }
12053
12054 #[test]
12055 fn recipientwrap_startup_rejects_malformed_record_length() {
12056 let master = master_key();
12057 let options = WriterOptions {
12058 bit_rot_buffer_pct: 0,
12059 ..single_stream_options()
12060 };
12061 let archive = write_archive_with_recipient_wrap_records(
12062 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12063 &master,
12064 options,
12065 vec![recipient_wrap_test_record()],
12066 )
12067 .unwrap();
12068 let mut bytes = archive.bytes;
12069 let (_, _, table_start, _table_len, _) = recipient_wrap_layout(&bytes);
12070 bytes[table_start + 96..table_start + 100].copy_from_slice(&1u32.to_le_bytes());
12071
12072 assert_eq!(
12073 open_archive_with_recipient_wrap_resolver(&bytes, |_| { Ok(vec![master.0]) })
12074 .unwrap_err(),
12075 FormatError::InvalidArchive("RecipientRecordV1 record_length is too small")
12076 );
12077 }
12078
12079 #[test]
12080 fn recipientwrap_future_revision_rejects_before_resolver_callback() {
12081 let archive = write_archive_with_recipient_wrap_records(
12082 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12083 &master_key(),
12084 single_stream_options(),
12085 vec![recipient_wrap_test_record()],
12086 )
12087 .unwrap();
12088 let mut bytes = archive.bytes;
12089 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12090 header.volume_format_rev = VOLUME_FORMAT_REV_44 + 1;
12091 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12092
12093 let mut called = false;
12094 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12095 called = true;
12096 Ok(vec![master_key().0])
12097 })
12098 .unwrap_err();
12099
12100 assert!(!called);
12101 assert_eq!(
12102 err,
12103 FormatError::UnsupportedVolumeFormatRevision {
12104 format_version: FORMAT_VERSION,
12105 volume_format_rev: VOLUME_FORMAT_REV_44 + 1,
12106 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12107 }
12108 );
12109 }
12110
12111 #[test]
12112 fn recipientwrap_stripe_width_mismatch_rejects_before_resolver_callback() {
12113 let archive = write_archive_with_recipient_wrap_records(
12114 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12115 &master_key(),
12116 single_stream_options(),
12117 vec![recipient_wrap_test_record()],
12118 )
12119 .unwrap();
12120 let mut bytes = archive.bytes;
12121 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12122 header.stripe_width += 1;
12123 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12124
12125 let mut called = false;
12126 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12127 called = true;
12128 Ok(vec![master_key().0])
12129 })
12130 .unwrap_err();
12131
12132 assert!(!called);
12133 assert_eq!(
12134 err,
12135 FormatError::InvalidArchive("VolumeHeader and CryptoHeader stripe_width differ")
12136 );
12137 }
12138
12139 #[test]
12140 fn recipientwrap_defers_raw_stream_profile_rejection_until_after_resolver_callback() {
12141 let master = master_key();
12142 let archive = write_archive_with_recipient_wrap_records(
12143 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12144 &master,
12145 single_stream_options(),
12146 vec![recipient_wrap_test_record()],
12147 )
12148 .unwrap();
12149 let mut bytes = archive.bytes;
12150 add_raw_stream_profile_to_physical_crypto_header(&mut bytes);
12151 recompute_physical_crypto_header_hmac(&mut bytes, &master);
12152
12153 let mut called = false;
12154 let err = open_archive_with_recipient_wrap_resolver(&bytes, |_| {
12155 called = true;
12156 Ok(vec![master.0])
12157 })
12158 .unwrap_err();
12159
12160 assert!(called);
12161 assert_eq!(
12162 err,
12163 FormatError::ReaderUnsupported(RAW_STREAM_UNSUPPORTED_MESSAGE)
12164 );
12165 }
12166
12167 #[test]
12168 fn recipientwrap_recovers_physical_key_wrap_table_from_cmra_authority() {
12169 let master = master_key();
12170 let archive = write_archive_with_recipient_wrap_records(
12171 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12172 &master,
12173 single_stream_options(),
12174 vec![recipient_wrap_test_record()],
12175 )
12176 .unwrap();
12177 let mut bytes = archive.bytes;
12178 let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12179 let crypto_start = header.crypto_header_offset as usize;
12180 let crypto_end = crypto_start + header.crypto_header_length as usize;
12181 let crypto_header = CryptoHeader::parse(
12182 &bytes[crypto_start..crypto_end],
12183 header.crypto_header_length,
12184 )
12185 .unwrap();
12186 let KdfParams::RecipientWrap {
12187 key_wrap_table_length,
12188 ..
12189 } = crypto_header.kdf_params
12190 else {
12191 panic!("expected RecipientWrap KdfParams");
12192 };
12193 let table_start = crypto_end;
12194 let table_end = table_start + key_wrap_table_length as usize;
12195 bytes[table_end - 1] ^= 0x01;
12196
12197 let mut called = false;
12198 let opened = open_archive_with_recipient_wrap_resolver(&bytes, |context| {
12199 called = true;
12200 assert_eq!(
12201 context.archive_identity.volume_format_rev,
12202 VOLUME_FORMAT_REV_44
12203 );
12204 assert_eq!(context.record.profile_id, 1);
12205 Ok(vec![master.0])
12206 })
12207 .unwrap();
12208
12209 assert!(called);
12210 assert_eq!(
12211 opened.extract_file("wrapped.txt").unwrap(),
12212 Some(b"recipient payload".to_vec())
12213 );
12214 opened.verify().unwrap();
12215 }
12216
12217 #[test]
12218 fn recipientwrap_open_rejects_wrong_candidate_header_hmac() {
12219 let archive = write_archive_with_recipient_wrap_records(
12220 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12221 &master_key(),
12222 single_stream_options(),
12223 vec![recipient_wrap_test_record()],
12224 )
12225 .unwrap();
12226 let wrong_candidate = [0x99u8; 32];
12227
12228 assert_eq!(
12229 open_archive_with_recipient_wrap_resolver(&archive.bytes, |_| {
12230 Ok(vec![wrong_candidate])
12231 })
12232 .unwrap_err(),
12233 FormatError::KeyMaterialMismatch
12234 );
12235 }
12236
12237 #[test]
12238 fn recipientwrap_open_recovers_tampered_physical_crypto_header_hmac_from_cmra() {
12239 let master = master_key();
12240 let archive = write_archive_with_recipient_wrap_records(
12241 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12242 &master,
12243 single_stream_options(),
12244 vec![recipient_wrap_test_record()],
12245 )
12246 .unwrap();
12247 let mut bytes = archive.bytes.clone();
12248 let header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12249 let hmac_end = header.crypto_header_offset as usize + header.crypto_header_length as usize;
12250 bytes[hmac_end - 1] ^= 0x55;
12251
12252 let opened =
12253 open_archive_with_recipient_wrap_resolver(&bytes, |_| Ok(vec![master.0])).unwrap();
12254
12255 assert_eq!(
12256 opened.extract_file("wrapped.txt").unwrap(),
12257 Some(b"recipient payload".to_vec())
12258 );
12259 assert_eq!(
12260 opened.crypto_header_bytes,
12261 archive.bytes[header.crypto_header_offset as usize..hmac_end]
12262 );
12263 opened.verify().unwrap();
12264 }
12265
12266 #[test]
12267 fn recipientwrap_archive_does_not_fall_back_to_raw_master_key_open() {
12268 let master = master_key();
12269 let archive = write_archive_with_recipient_wrap_records(
12270 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12271 &master,
12272 single_stream_options(),
12273 vec![recipient_wrap_test_record()],
12274 )
12275 .unwrap();
12276
12277 assert_eq!(
12278 open_archive(&archive.bytes, &master).unwrap_err(),
12279 FormatError::KeyMaterialMismatch
12280 );
12281 }
12282
12283 #[test]
12284 fn recipientwrap_seekable_archive_does_not_fall_back_to_raw_master_key_open() {
12285 let master = master_key();
12286 let archive = write_archive_with_recipient_wrap_records(
12287 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12288 &master,
12289 single_stream_options(),
12290 vec![recipient_wrap_test_record()],
12291 )
12292 .unwrap();
12293
12294 assert_eq!(
12295 open_seekable_archive(archive.bytes, &master).unwrap_err(),
12296 FormatError::KeyMaterialMismatch
12297 );
12298 }
12299
12300 #[test]
12301 fn public_no_key_verifies_signed_recipientwrap_block_commitment() {
12302 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12303 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12304 &master_key(),
12305 single_stream_options(),
12306 vec![recipient_wrap_test_record()],
12307 test_root_auth_config(),
12308 |request| Ok(test_root_auth_value(request)),
12309 )
12310 .unwrap();
12311
12312 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
12313 Ok(test_root_auth_verifies(footer, archive_root))
12314 })
12315 .unwrap();
12316
12317 assert_eq!(verified.volume_format_rev, VOLUME_FORMAT_REV_44);
12318 assert_eq!(verified.total_data_block_count, 3);
12319 }
12320
12321 #[test]
12322 fn public_no_key_rejects_recipientwrap_startup_and_cmra_kdf_mismatch() {
12323 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12324 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12325 &master_key(),
12326 single_stream_options(),
12327 vec![recipient_wrap_test_record()],
12328 test_root_auth_config(),
12329 |request| Ok(test_root_auth_value(request)),
12330 )
12331 .unwrap();
12332 let mut bytes = archive.bytes;
12333 mutate_top_level_recipient_wrap_public_profile(&mut bytes);
12334
12335 let err = public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
12336 Ok(test_root_auth_verifies(footer, archive_root))
12337 })
12338 .unwrap_err();
12339
12340 assert_eq!(
12341 err,
12342 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
12343 );
12344 }
12345
12346 #[test]
12347 fn public_no_key_rejects_recipientwrap_kdf_profile_mismatch_across_volumes() {
12348 let archive = write_archive_with_root_auth_and_recipient_wrap_records(
12349 &[RegularFile::new("wrapped.txt", b"recipient payload")],
12350 &master_key(),
12351 WriterOptions {
12352 stripe_width: 2,
12353 volume_loss_tolerance: 0,
12354 ..WriterOptions::default()
12355 },
12356 vec![recipient_wrap_test_record()],
12357 test_root_auth_config(),
12358 |request| Ok(test_root_auth_value(request)),
12359 )
12360 .unwrap();
12361 let mut volumes = archive.volumes;
12362 mutate_top_level_recipient_wrap_public_profile(&mut volumes[1]);
12363 mutate_cmra_recipient_wrap_public_profile(&mut volumes[1]);
12364
12365 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12366 let err = public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
12367 Ok(test_root_auth_verifies(footer, archive_root))
12368 })
12369 .unwrap_err();
12370
12371 assert_eq!(
12372 err,
12373 FormatError::InvalidArchive("public no-key volume global metadata differs")
12374 );
12375 }
12376
12377 #[test]
12378 fn write_archive_defaults_to_current_revision() {
12379 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12380 let header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
12381
12382 assert_eq!(header.format_version, FORMAT_VERSION);
12383 assert_eq!(header.volume_format_rev, VOLUME_FORMAT_REV);
12384 }
12385
12386 #[test]
12387 fn non_seekable_stream_rejects_future_volume_format_revision() {
12388 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12389 let mut bytes = archive.bytes;
12390 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12391 header.volume_format_rev = 45;
12392 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12393
12394 assert_eq!(
12395 verify_non_seekable_stream(std::io::Cursor::new(bytes), &master_key()).unwrap_err(),
12396 FormatError::UnsupportedVolumeFormatRevision {
12397 format_version: FORMAT_VERSION,
12398 volume_format_rev: 45,
12399 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12400 }
12401 );
12402 }
12403
12404 #[test]
12405 fn open_seekable_archive_rejects_future_volume_format_revision() {
12406 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
12407 let mut bytes = archive.bytes;
12408 let mut header = VolumeHeader::parse(&bytes[..VOLUME_HEADER_LEN]).unwrap();
12409 header.volume_format_rev = 45;
12410 bytes[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
12411
12412 assert_eq!(
12413 open_seekable_archive(CountingReadAt::new(bytes, vec![]), &master_key()).unwrap_err(),
12414 FormatError::UnsupportedVolumeFormatRevision {
12415 format_version: FORMAT_VERSION,
12416 volume_format_rev: 45,
12417 reader_max_supported_revision: READER_MAX_SUPPORTED_VOLUME_FORMAT_REV,
12418 }
12419 );
12420 }
12421
12422 #[test]
12423 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
12424 let mut archive = write_archive(
12425 &[RegularFile::new("file.txt", b"authenticated")],
12426 &master_key(),
12427 single_stream_options(),
12428 )
12429 .unwrap()
12430 .bytes;
12431 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
12432 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
12433 let crypto = CryptoHeader::parse(
12434 &archive[VOLUME_HEADER_LEN..crypto_end],
12435 volume.crypto_header_length,
12436 )
12437 .unwrap();
12438 let block_size = crypto.fixed.block_size as usize;
12439 archive[crypto_end + 16] ^= 1;
12440 let crc_offset = crypto_end + 16 + block_size;
12441 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
12442 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
12443
12444 let opened = open_archive(&archive, &master_key()).unwrap();
12445 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12446 }
12447
12448 #[test]
12449 fn list_and_extract_use_final_view_for_duplicate_paths() {
12450 let archive = write_archive(
12451 &[
12452 RegularFile {
12453 mtime: 1_700_000_000,
12454 ..RegularFile::new("same.txt", b"old")
12455 },
12456 RegularFile {
12457 mtime: 1_700_000_100,
12458 ..RegularFile::new("same.txt", b"newer")
12459 },
12460 ],
12461 &master_key(),
12462 single_stream_options(),
12463 )
12464 .unwrap();
12465 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12466
12467 let listed = opened.list_index_entries().unwrap();
12468 assert_eq!(listed.len(), 1);
12469 assert_eq!(listed[0].path, "same.txt");
12470 assert_eq!(listed[0].name, "same.txt");
12471 assert_eq!(listed[0].file_data_size, 5);
12472 assert_eq!(listed[0].kind, TarEntryKind::Regular);
12473 assert_eq!(listed[0].mode, 0o644);
12474 assert_eq!(listed[0].mtime, 1_700_000_100);
12475 assert_eq!(listed[0].frame_count, 1);
12476 assert!(listed[0].layout.compressed_size > 0);
12477 let looked_up = opened.lookup_index_entry("same.txt").unwrap().unwrap();
12478 assert_eq!(looked_up.path, "same.txt");
12479 assert_eq!(looked_up.file_data_size, 5);
12480 assert_eq!(looked_up.kind, TarEntryKind::Regular);
12481 assert_eq!(looked_up.mode, 0o644);
12482 assert_eq!(looked_up.mtime, 1_700_000_100);
12483 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
12484 assert_eq!(
12485 opened.list_files().unwrap(),
12486 vec![ArchiveEntry {
12487 path: "same.txt".to_string(),
12488 file_data_size: 5,
12489 kind: TarEntryKind::Regular,
12490 mode: 0o644,
12491 mtime: 1_700_000_100,
12492 diagnostics: Vec::new(),
12493 }]
12494 );
12495 assert_eq!(
12496 opened.extract_file("same.txt").unwrap(),
12497 Some(b"newer".to_vec())
12498 );
12499 opened.verify().unwrap();
12500 }
12501
12502 #[test]
12503 fn index_entries_do_not_decrypt_payload_envelopes() {
12504 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12505 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12506
12507 let listed = opened.list_index_entries().unwrap();
12508 assert_eq!(listed.len(), 2);
12509 assert_eq!(listed[0].path, "broken.txt");
12510 assert_eq!(listed[0].file_data_size, b"broken payload\n".len() as u64);
12511 assert_eq!(listed[0].kind, TarEntryKind::Regular);
12512 assert_eq!(listed[0].mode, 0o644);
12513 assert_eq!(listed[0].mtime, 0);
12514 assert_eq!(listed[1].path, "healthy.txt");
12515 assert_eq!(listed[1].file_data_size, b"healthy payload\n".len() as u64);
12516 assert_eq!(listed[1].kind, TarEntryKind::Regular);
12517 assert_eq!(listed[1].mode, 0o644);
12518 assert_eq!(listed[1].mtime, 0);
12519 let looked_up = opened.lookup_index_entry("broken.txt").unwrap().unwrap();
12520 assert_eq!(looked_up.path, "broken.txt");
12521 assert_eq!(looked_up.file_data_size, b"broken payload\n".len() as u64);
12522 assert_eq!(looked_up.kind, TarEntryKind::Regular);
12523 assert_eq!(looked_up.mode, 0o644);
12524 assert_eq!(looked_up.mtime, 0);
12525 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
12526 }
12527
12528 #[test]
12529 fn index_entry_layout_stats_match_frame_and_envelope_tables() {
12530 let payload = pseudo_random_bytes(12 * 1024);
12531 let archive = write_archive(
12532 &[RegularFile::new("chunked.bin", &payload)],
12533 &master_key(),
12534 WriterOptions {
12535 block_size: 4096,
12536 chunk_size: 1024,
12537 envelope_target_size: 2048,
12538 stripe_width: 1,
12539 volume_loss_tolerance: 0,
12540 bit_rot_buffer_pct: 0,
12541 fec_data_shards: 16,
12542 fec_parity_shards: 0,
12543 ..WriterOptions::default()
12544 },
12545 )
12546 .unwrap();
12547 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12548 let entry = opened.list_index_entries().unwrap().remove(0);
12549 let located = opened
12550 .locate_index_file(b"chunked.bin")
12551 .unwrap()
12552 .expect("indexed file exists");
12553 let file = &located.shard.files[located.file_index];
12554 let frames = frame_range_for_file(&located.shard, file).unwrap();
12555 assert!(frames.len() > 1);
12556
12557 let expected_compressed_size = frames
12558 .iter()
12559 .map(|frame| frame.compressed_size as u64)
12560 .sum::<u64>();
12561 let expected_decompressed_frame_size = frames
12562 .iter()
12563 .map(|frame| frame.decompressed_size as u64)
12564 .sum::<u64>();
12565 let envelope_indexes = frames
12566 .iter()
12567 .map(|frame| frame.envelope_index)
12568 .collect::<BTreeSet<_>>();
12569 assert!(envelope_indexes.len() > 1);
12570
12571 let envelopes = envelope_indexes
12572 .iter()
12573 .map(|index| envelope_by_index(&located.shard, *index).unwrap())
12574 .collect::<Vec<_>>();
12575 let expected_first_payload_block_index = envelopes
12576 .iter()
12577 .map(|envelope| envelope.first_block_index)
12578 .min();
12579 let expected_payload_data_block_count = envelopes
12580 .iter()
12581 .map(|envelope| envelope.data_block_count as u64)
12582 .sum::<u64>();
12583 let expected_payload_parity_block_count = envelopes
12584 .iter()
12585 .map(|envelope| envelope.parity_block_count as u64)
12586 .sum::<u64>();
12587 let expected_payload_encrypted_size = envelopes
12588 .iter()
12589 .map(|envelope| envelope.encrypted_size as u64)
12590 .sum::<u64>();
12591
12592 assert_eq!(entry.path, "chunked.bin");
12593 assert_eq!(entry.name, "chunked.bin");
12594 assert_eq!(entry.file_data_size, payload.len() as u64);
12595 assert_eq!(entry.kind, TarEntryKind::Regular);
12596 assert_eq!(entry.mode, 0o644);
12597 assert_eq!(entry.tar_member_group_size, file.tar_member_group_size);
12598 assert_eq!(entry.first_frame_index, file.first_frame_index);
12599 assert_eq!(entry.frame_count, file.frame_count);
12600 assert_eq!(
12601 entry.offset_in_first_frame_plaintext,
12602 file.offset_in_first_frame_plaintext
12603 );
12604 assert_eq!(entry.layout.compressed_size, expected_compressed_size);
12605 assert_eq!(
12606 entry.layout.decompressed_frame_size,
12607 expected_decompressed_frame_size
12608 );
12609 assert_eq!(entry.layout.envelope_count as usize, envelope_indexes.len());
12610 assert_eq!(
12611 entry.layout.first_envelope_index,
12612 envelope_indexes.iter().next().copied()
12613 );
12614 assert_eq!(
12615 entry.layout.last_envelope_index,
12616 envelope_indexes.iter().next_back().copied()
12617 );
12618 assert_eq!(
12619 entry.layout.first_payload_block_index,
12620 expected_first_payload_block_index
12621 );
12622 assert_eq!(
12623 entry.layout.payload_data_block_count,
12624 expected_payload_data_block_count
12625 );
12626 assert_eq!(
12627 entry.layout.payload_parity_block_count,
12628 expected_payload_parity_block_count
12629 );
12630 assert_eq!(
12631 entry.layout.payload_encrypted_size,
12632 expected_payload_encrypted_size
12633 );
12634 }
12635
12636 #[test]
12637 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
12638 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
12641 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
12642
12643 assert_eq!(
12644 opened.extract_file("healthy.txt").unwrap(),
12645 Some(b"healthy payload\n".to_vec())
12646 );
12647 assert_eq!(
12648 opened.extract_file("broken.txt").unwrap_err(),
12649 FormatError::AeadFailure
12650 );
12651 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
12652 }
12653
12654 #[test]
12655 fn seekable_extract_does_not_read_unselected_payload_envelope() {
12656 let healthy = pseudo_random_bytes(64 * 1024);
12657 let broken = pseudo_random_bytes(64 * 1024);
12658 let options = WriterOptions {
12659 block_size: 4096,
12660 chunk_size: 4096,
12661 envelope_target_size: 8192,
12662 stripe_width: 1,
12663 volume_loss_tolerance: 0,
12664 bit_rot_buffer_pct: 0,
12665 fec_data_shards: 4,
12666 fec_parity_shards: 0,
12667 index_fec_data_shards: 4,
12668 index_fec_parity_shards: 0,
12669 index_root_fec_data_shards: 4,
12670 index_root_fec_parity_shards: 0,
12671 ..WriterOptions::default()
12672 };
12673 let archive = write_archive(
12674 &[
12675 RegularFile::new("healthy.bin", &healthy),
12676 RegularFile::new("broken.bin", &broken),
12677 ],
12678 &master_key(),
12679 options,
12680 )
12681 .unwrap();
12682 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12683 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
12684 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
12685 let denied_block_indices = broken_envelopes
12686 .iter()
12687 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
12688 .flat_map(|envelope| {
12689 let block_count =
12690 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
12691 envelope.first_block_index..envelope.first_block_index + block_count
12692 })
12693 .collect::<BTreeSet<_>>();
12694 assert!(
12695 !denied_block_indices.is_empty(),
12696 "fixture must place broken.bin in at least one unshared envelope"
12697 );
12698 let denied_ranges = block_record_slots(&archive.bytes)
12699 .into_iter()
12700 .filter_map(|(offset, len, record)| {
12701 denied_block_indices
12702 .contains(&record.block_index)
12703 .then_some((offset as u64, (offset + len) as u64))
12704 })
12705 .collect::<Vec<_>>();
12706 assert!(!denied_ranges.is_empty());
12707
12708 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12709 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12710
12711 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
12712 for (read_start, read_end) in reader.reads() {
12713 assert!(
12714 denied_ranges
12715 .iter()
12716 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12717 "targeted extract read an unrelated payload BlockRecord range"
12718 );
12719 }
12720 assert_eq!(
12721 opened.extract_file("broken.bin").unwrap_err(),
12722 FormatError::InvalidArchive("denied test read")
12723 );
12724 }
12725
12726 #[test]
12727 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
12728 struct FailOnFirstWrite;
12729
12730 impl std::io::Write for FailOnFirstWrite {
12731 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
12732 Err(std::io::Error::other("sink stopped"))
12733 }
12734
12735 fn flush(&mut self) -> std::io::Result<()> {
12736 Ok(())
12737 }
12738 }
12739
12740 let payload = pseudo_random_bytes(128 * 1024);
12741 let options = WriterOptions {
12742 block_size: 4096,
12743 chunk_size: 4096,
12744 envelope_target_size: 8192,
12745 stripe_width: 1,
12746 volume_loss_tolerance: 0,
12747 bit_rot_buffer_pct: 0,
12748 fec_data_shards: 4,
12749 fec_parity_shards: 0,
12750 index_fec_data_shards: 4,
12751 index_fec_parity_shards: 0,
12752 index_root_fec_data_shards: 4,
12753 index_root_fec_parity_shards: 0,
12754 ..WriterOptions::default()
12755 };
12756 let archive = write_archive(
12757 &[RegularFile::new("large.bin", &payload)],
12758 &master_key(),
12759 options,
12760 )
12761 .unwrap();
12762 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12763 let envelopes = envelope_entries_for_path(&eager, "large.bin");
12764 let first_envelope = envelopes
12765 .first()
12766 .expect("large fixture should have at least one envelope")
12767 .envelope_index;
12768 let later_envelope_blocks = envelopes
12769 .iter()
12770 .filter(|entry| entry.envelope_index != first_envelope)
12771 .flat_map(|entry| {
12772 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
12773 entry.first_block_index..entry.first_block_index + block_count
12774 })
12775 .collect::<BTreeSet<_>>();
12776 assert!(
12777 !later_envelope_blocks.is_empty(),
12778 "fixture must span more than one payload envelope"
12779 );
12780 let denied_ranges = block_record_slots(&archive.bytes)
12781 .into_iter()
12782 .filter_map(|(offset, len, record)| {
12783 later_envelope_blocks
12784 .contains(&record.block_index)
12785 .then_some((offset as u64, (offset + len) as u64))
12786 })
12787 .collect::<Vec<_>>();
12788 assert!(!denied_ranges.is_empty());
12789
12790 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
12791 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
12792 let mut writer = FailOnFirstWrite;
12793
12794 let err = opened
12795 .extract_file_to_writer("large.bin", &mut writer)
12796 .unwrap_err();
12797 assert_eq!(err.to_string(), "extraction output write failed");
12798 for (read_start, read_end) in reader.reads() {
12799 assert!(
12800 denied_ranges
12801 .iter()
12802 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
12803 "streaming writer read a later payload envelope before surfacing writer failure"
12804 );
12805 }
12806 }
12807
12808 #[test]
12809 fn extract_file_to_writer_writes_bounded_chunks() {
12810 struct ChunkRecorder {
12811 total: usize,
12812 max_write: usize,
12813 writes: usize,
12814 }
12815
12816 impl std::io::Write for ChunkRecorder {
12817 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12818 self.total += buf.len();
12819 self.max_write = self.max_write.max(buf.len());
12820 self.writes += 1;
12821 Ok(buf.len())
12822 }
12823
12824 fn flush(&mut self) -> std::io::Result<()> {
12825 Ok(())
12826 }
12827 }
12828
12829 let payload = pseudo_random_bytes(128 * 1024);
12830 let options = WriterOptions {
12831 block_size: 4096,
12832 chunk_size: 4096,
12833 envelope_target_size: 8192,
12834 stripe_width: 1,
12835 volume_loss_tolerance: 0,
12836 bit_rot_buffer_pct: 0,
12837 fec_data_shards: 4,
12838 fec_parity_shards: 0,
12839 index_fec_data_shards: 4,
12840 index_fec_parity_shards: 0,
12841 index_root_fec_data_shards: 4,
12842 index_root_fec_parity_shards: 0,
12843 ..WriterOptions::default()
12844 };
12845 let archive = write_archive(
12846 &[RegularFile::new("large.bin", &payload)],
12847 &master_key(),
12848 options,
12849 )
12850 .unwrap();
12851 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12852 let mut writer = ChunkRecorder {
12853 total: 0,
12854 max_write: 0,
12855 writes: 0,
12856 };
12857
12858 opened
12859 .extract_file_to_writer("large.bin", &mut writer)
12860 .unwrap()
12861 .unwrap();
12862
12863 assert_eq!(writer.total, payload.len());
12864 assert!(writer.writes > 1);
12865 assert!(
12866 writer.max_write <= options.chunk_size as usize,
12867 "writer saw a {} byte chunk, larger than the {} byte frame target",
12868 writer.max_write,
12869 options.chunk_size
12870 );
12871 }
12872
12873 #[test]
12874 fn extract_file_to_writer_with_progress_reports_payload_bytes() {
12875 struct ChunkRecorder {
12876 total: usize,
12877 }
12878
12879 impl std::io::Write for ChunkRecorder {
12880 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
12881 self.total += buf.len();
12882 Ok(buf.len())
12883 }
12884
12885 fn flush(&mut self) -> std::io::Result<()> {
12886 Ok(())
12887 }
12888 }
12889
12890 let payload = pseudo_random_bytes(128 * 1024);
12891 let options = WriterOptions {
12892 block_size: 4096,
12893 chunk_size: 4096,
12894 envelope_target_size: 8192,
12895 stripe_width: 1,
12896 volume_loss_tolerance: 0,
12897 bit_rot_buffer_pct: 0,
12898 fec_data_shards: 4,
12899 fec_parity_shards: 0,
12900 index_fec_data_shards: 4,
12901 index_fec_parity_shards: 0,
12902 index_root_fec_data_shards: 4,
12903 index_root_fec_parity_shards: 0,
12904 ..WriterOptions::default()
12905 };
12906 let archive = write_archive(
12907 &[RegularFile::new("large.bin", &payload)],
12908 &master_key(),
12909 options,
12910 )
12911 .unwrap();
12912 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
12913 let mut writer = ChunkRecorder { total: 0 };
12914 let mut progress_events = Vec::new();
12915 let mut progress = |archive_path: &str, bytes: u64| {
12916 progress_events.push((archive_path.to_owned(), bytes));
12917 };
12918
12919 opened
12920 .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
12921 .unwrap()
12922 .unwrap();
12923
12924 let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
12925 assert_eq!(writer.total, payload.len());
12926 assert_eq!(reported_bytes, payload.len() as u64);
12927 assert!(progress_events.len() > 1);
12928 assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
12929 }
12930
12931 #[test]
12932 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
12933 let payload = pseudo_random_bytes(128 * 1024);
12934 let options = WriterOptions {
12935 block_size: 4096,
12936 chunk_size: 4096,
12937 envelope_target_size: 8192,
12938 stripe_width: 1,
12939 volume_loss_tolerance: 0,
12940 bit_rot_buffer_pct: 0,
12941 fec_data_shards: 4,
12942 fec_parity_shards: 0,
12943 index_fec_data_shards: 4,
12944 index_fec_parity_shards: 0,
12945 index_root_fec_data_shards: 4,
12946 index_root_fec_parity_shards: 0,
12947 ..WriterOptions::default()
12948 };
12949 let archive = write_archive(
12950 &[RegularFile::new("large.bin", &payload)],
12951 &master_key(),
12952 options,
12953 )
12954 .unwrap();
12955 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
12956 let envelopes = envelope_entries_for_path(&eager, "large.bin");
12957 let last_envelope = envelopes
12958 .last()
12959 .expect("large fixture should have at least one envelope");
12960 assert_ne!(
12961 envelopes.first().unwrap().envelope_index,
12962 last_envelope.envelope_index,
12963 "fixture must span more than one payload envelope"
12964 );
12965 let corrupt_slot = block_record_slots(&archive.bytes)
12966 .into_iter()
12967 .enumerate()
12968 .find_map(|(slot, (_, _, record))| {
12969 (record.block_index == last_envelope.first_block_index).then_some(slot)
12970 })
12971 .unwrap();
12972 let mut corrupted = archive.bytes;
12973 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
12974 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
12975 let tmp = tempfile::tempdir().unwrap();
12976
12977 assert!(matches!(
12978 opened
12979 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
12980 .unwrap_err(),
12981 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
12982 ));
12983 assert!(!tmp.path().join("large.bin").exists());
12984 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
12985 }
12986
12987 #[test]
12988 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
12989 let archive = write_archive(
12990 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
12991 &master_key(),
12992 single_stream_options(),
12993 )
12994 .unwrap();
12995 let opened = open_archive_with_bootstrap_sidecar(
12996 &archive.bytes,
12997 &archive.bootstrap_sidecar,
12998 &master_key(),
12999 )
13000 .unwrap();
13001
13002 assert_eq!(
13003 opened.list_files().unwrap(),
13004 vec![ArchiveEntry {
13005 path: "dir/sidecar.txt".to_string(),
13006 file_data_size: 13,
13007 kind: TarEntryKind::Regular,
13008 mode: 0o644,
13009 mtime: 0,
13010 diagnostics: Vec::new(),
13011 }]
13012 );
13013 assert_eq!(
13014 opened.extract_file("dir/sidecar.txt").unwrap(),
13015 Some(b"hello sidecar".to_vec())
13016 );
13017 opened.verify().unwrap();
13018 }
13019
13020 #[test]
13021 fn fast_verify_plaintext_zero_recovery_defers_payload_semantics() {
13022 let options = WriterOptions {
13023 aead_algo: AeadAlgo::None,
13024 volume_loss_tolerance: 0,
13025 bit_rot_buffer_pct: 0,
13026 ..single_stream_options()
13027 };
13028 let archive = write_archive_unencrypted(
13029 &[RegularFile::new(
13030 "payload.txt",
13031 b"payload bytes large enough to produce a zstd frame",
13032 )],
13033 options,
13034 )
13035 .unwrap();
13036 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13037 let tables = opened.load_payload_index_tables().unwrap();
13038 let first_envelope = tables.envelopes.values().next().unwrap();
13039 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
13040 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
13041 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13042 let payload_offset = crypto_end + first_envelope.first_block_index as usize * record_len;
13043
13044 let mut tampered = archive.bytes.clone();
13045 tampered[payload_offset + 16] ^= 0x01;
13046 let crc_offset = payload_offset + 16 + opened.crypto_header.block_size as usize;
13047 let crc = crc32c::crc32c(&tampered[payload_offset..crc_offset]);
13048 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
13049
13050 let tampered_opened = open_archive(&tampered, &master_key()).unwrap();
13051 assert!(tampered_opened.fast_verify_defers_payload_semantics());
13052 tampered_opened.verify_content_fast().unwrap();
13053 assert!(tampered_opened.verify_content().is_err());
13054 }
13055
13056 #[test]
13057 fn fast_verify_root_auth_archive_requires_full_root_auth_scan() {
13058 let archive = write_archive_with_root_auth(
13059 &[RegularFile::new("signed.txt", b"root-auth payload")],
13060 &master_key(),
13061 single_stream_options(),
13062 RootAuthWriterConfig {
13063 authenticator_id: 0x7777,
13064 signer_identity_type: 1,
13065 signer_identity: b"test signer",
13066 authenticator_value_length: 32,
13067 },
13068 |request| Ok(request.archive_root.to_vec()),
13069 )
13070 .unwrap();
13071
13072 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13073 assert!(!opened.fast_verify_defers_payload_semantics());
13074 assert!(matches!(
13075 opened.verify_content_fast().unwrap().mode,
13076 ContentVerificationMode::Fast
13077 ));
13078 }
13079
13080 #[test]
13081 fn fast_verify_dictionary_archive_does_not_defer_payload_semantics() {
13082 let archive = write_archive_with_dictionary(
13083 &[RegularFile::new("dict.txt", b"dictionary payload")],
13084 &master_key(),
13085 single_stream_options(),
13086 dictionary(),
13087 )
13088 .unwrap();
13089
13090 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13091 assert!(!opened.fast_verify_defers_payload_semantics());
13092 }
13093
13094 #[test]
13095 fn fast_verify_encrypted_archive_does_not_defer_payload_semantics() {
13096 let options = WriterOptions {
13097 aead_algo: AeadAlgo::AesGcmSiv256,
13098 volume_loss_tolerance: 0,
13099 bit_rot_buffer_pct: 0,
13100 ..single_stream_options()
13101 };
13102 let archive = write_archive(
13103 &[RegularFile::new("payload.txt", b"encrypted payload")],
13104 &master_key(),
13105 options,
13106 )
13107 .unwrap();
13108
13109 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13110 assert!(!opened.fast_verify_defers_payload_semantics());
13111 }
13112
13113 #[test]
13114 fn fast_verify_repair_archive_does_not_defer_payload_semantics() {
13115 let options = WriterOptions {
13116 fec_parity_shards: 2,
13117 volume_loss_tolerance: 0,
13118 bit_rot_buffer_pct: 0,
13119 ..single_stream_options()
13120 };
13121 let archive = write_archive(
13122 &[RegularFile::new("payload.txt", b"payload for repair")],
13123 &master_key(),
13124 options,
13125 )
13126 .unwrap();
13127
13128 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13129 assert!(!opened.fast_verify_defers_payload_semantics());
13130 }
13131
13132 #[test]
13133 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
13134 let archive = write_archive_with_dictionary(
13135 &[RegularFile::new(
13136 "dir/dict.txt",
13137 b"common words common words dictionary payload",
13138 )],
13139 &master_key(),
13140 single_stream_options(),
13141 dictionary(),
13142 )
13143 .unwrap();
13144 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13145
13146 assert_eq!(opened.crypto_header.has_dictionary, 1);
13147 assert!(opened.index_root.header.dictionary_data_block_count > 0);
13148 assert_eq!(
13149 opened.list_files().unwrap(),
13150 vec![ArchiveEntry {
13151 path: "dir/dict.txt".to_string(),
13152 file_data_size: 44,
13153 kind: TarEntryKind::Regular,
13154 mode: 0o644,
13155 mtime: 0,
13156 diagnostics: Vec::new(),
13157 }]
13158 );
13159 assert_eq!(
13160 opened.extract_file("dir/dict.txt").unwrap(),
13161 Some(b"common words common words dictionary payload".to_vec())
13162 );
13163 opened.verify().unwrap();
13164 }
13165
13166 #[test]
13167 fn dictionary_object_tamper_fails_before_payload_decompression() {
13168 let archive = write_archive_with_dictionary(
13169 &[RegularFile::new(
13170 "dir/dict.txt",
13171 b"common words common words dictionary payload",
13172 )],
13173 &master_key(),
13174 single_stream_options(),
13175 dictionary(),
13176 )
13177 .unwrap();
13178 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13179 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
13180 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
13181 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13182 let dictionary_offset =
13183 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
13184
13185 let mut tampered = archive.bytes.clone();
13186 tampered[dictionary_offset + 16] ^= 0x01;
13187 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
13188 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
13189 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
13190
13191 assert_eq!(
13192 open_archive(&tampered, &master_key()).unwrap_err(),
13193 FormatError::AeadFailure
13194 );
13195 }
13196
13197 #[test]
13198 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
13199 let archive = write_archive_with_dictionary(
13200 &[RegularFile::new(
13201 "dict-sidecar.txt",
13202 b"common words common words sidecar payload",
13203 )],
13204 &master_key(),
13205 single_stream_options(),
13206 dictionary(),
13207 )
13208 .unwrap();
13209 let opened = open_non_seekable_archive(
13210 &archive.bytes,
13211 &master_key(),
13212 Some(&archive.bootstrap_sidecar),
13213 )
13214 .unwrap();
13215
13216 assert_eq!(
13217 opened.extract_file("dict-sidecar.txt").unwrap(),
13218 Some(b"common words common words sidecar payload".to_vec())
13219 );
13220 opened.verify().unwrap();
13221 }
13222
13223 #[test]
13224 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
13225 let archive = write_archive(
13226 &[RegularFile::new(
13227 "sidecar-terminal.txt",
13228 b"sidecar authority",
13229 )],
13230 &master_key(),
13231 single_stream_options(),
13232 )
13233 .unwrap();
13234 let mut corrupted = archive.bytes.clone();
13235 corrupt_v41_terminal_recovery(&mut corrupted);
13236 assert!(open_archive(&corrupted, &master_key()).is_err());
13237
13238 let opened =
13239 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
13240 .unwrap();
13241
13242 assert!(opened.volume_trailer.is_none());
13243 assert_eq!(
13244 opened.extract_file("sidecar-terminal.txt").unwrap(),
13245 Some(b"sidecar authority".to_vec())
13246 );
13247 opened.verify().unwrap();
13248 }
13249
13250 #[test]
13251 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
13252 let archive = write_archive_with_dictionary(
13253 &[RegularFile::new(
13254 "dict-no-terminal.txt",
13255 b"common words common words without terminal",
13256 )],
13257 &master_key(),
13258 single_stream_options(),
13259 dictionary(),
13260 )
13261 .unwrap();
13262 let terminal_offset = terminal_material_offset(&archive.bytes);
13263 let truncated = archive.bytes[..terminal_offset].to_vec();
13264 assert!(open_archive(&truncated, &master_key()).is_err());
13265
13266 let opened =
13267 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
13268 .unwrap();
13269
13270 assert!(opened.volume_trailer.is_none());
13271 assert_eq!(
13272 opened.extract_file("dict-no-terminal.txt").unwrap(),
13273 Some(b"common words common words without terminal".to_vec())
13274 );
13275 opened.verify().unwrap();
13276 }
13277
13278 #[test]
13279 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
13280 let archive = write_archive(
13281 &[RegularFile::new(
13282 "sidecar-erasure.txt",
13283 b"repair through sidecar",
13284 )],
13285 &master_key(),
13286 single_stream_options(),
13287 )
13288 .unwrap();
13289 let mut corrupted = archive.bytes.clone();
13290 corrupt_first_block_record_payload(&mut corrupted);
13291
13292 let opened = open_archive_with_bootstrap_sidecar(
13293 &corrupted,
13294 &archive.bootstrap_sidecar,
13295 &master_key(),
13296 )
13297 .unwrap();
13298 assert_eq!(
13299 opened.extract_file("sidecar-erasure.txt").unwrap(),
13300 Some(b"repair through sidecar".to_vec())
13301 );
13302 }
13303
13304 #[test]
13305 fn extraction_rejects_logical_payload_above_total_size_cap() {
13306 let archive = write_archive(
13307 &[RegularFile::new("cap.txt", b"payload")],
13308 &master_key(),
13309 single_stream_options(),
13310 )
13311 .unwrap();
13312 let options = ReaderOptions {
13313 max_total_extraction_size: 3,
13314 ..ReaderOptions::default()
13315 };
13316 let opened =
13317 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13318
13319 assert_eq!(
13320 opened.extract_file("cap.txt").unwrap_err(),
13321 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13322 );
13323 }
13324
13325 #[test]
13326 fn verify_does_not_apply_extraction_payload_cap() {
13327 let archive = write_archive(
13328 &[RegularFile::new("verify-cap.txt", b"payload")],
13329 &master_key(),
13330 single_stream_options(),
13331 )
13332 .unwrap();
13333 let options = ReaderOptions {
13334 max_total_extraction_size: 3,
13335 ..ReaderOptions::default()
13336 };
13337 let opened =
13338 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13339
13340 opened.verify().unwrap();
13341 assert_eq!(
13342 opened.extract_file("verify-cap.txt").unwrap_err(),
13343 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13344 );
13345 }
13346
13347 #[test]
13348 fn verify_streams_past_legacy_in_memory_tar_cap() {
13349 let data = vec![0x5a; 4096];
13350 let archive = write_archive(
13351 &[RegularFile::new("verify-large.txt", &data)],
13352 &master_key(),
13353 single_stream_options(),
13354 )
13355 .unwrap();
13356 let options = ReaderOptions {
13357 max_verify_tar_size: 1,
13358 ..ReaderOptions::default()
13359 };
13360 let opened =
13361 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
13362
13363 opened.verify().unwrap();
13364 }
13365
13366 #[test]
13367 fn dictionary_sidecar_requires_dictionary_record_section() {
13368 let archive = write_archive_with_dictionary(
13369 &[RegularFile::new("dict-missing.txt", b"common words")],
13370 &master_key(),
13371 single_stream_options(),
13372 dictionary(),
13373 )
13374 .unwrap();
13375 let header = BootstrapSidecarHeader::parse(
13376 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
13377 )
13378 .unwrap();
13379 let mut missing_dictionary =
13380 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
13381 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
13382 header.flags &= !0x04;
13383 header.dictionary_records_offset = 0;
13384 header.dictionary_records_length = 0;
13385 });
13386
13387 assert_eq!(
13388 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
13389 .unwrap_err(),
13390 FormatError::ReaderUnsupported("dictionary bootstrap required")
13391 );
13392 }
13393
13394 #[test]
13395 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
13396 let archive = write_archive_with_dictionary(
13397 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
13398 &master_key(),
13399 single_stream_options(),
13400 dictionary(),
13401 )
13402 .unwrap();
13403
13404 let mut wrong_kind = archive.bootstrap_sidecar.clone();
13405 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
13406 record.kind = BlockKind::IndexRootData;
13407 });
13408 assert_eq!(
13409 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
13410 .unwrap_err(),
13411 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
13412 );
13413
13414 let mut wrong_last = archive.bootstrap_sidecar.clone();
13415 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
13416 record.flags = 0;
13417 });
13418 assert_eq!(
13419 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
13420 .unwrap_err(),
13421 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
13422 );
13423 }
13424
13425 #[test]
13426 fn non_seekable_random_access_requires_sidecar() {
13427 let archive = write_archive(
13428 &[RegularFile::new("file.txt", b"payload")],
13429 &master_key(),
13430 single_stream_options(),
13431 )
13432 .unwrap();
13433
13434 assert_eq!(
13435 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13436 FormatError::ReaderUnsupported(
13437 "non-seekable random access requires a bootstrap sidecar"
13438 )
13439 );
13440 assert!(open_non_seekable_archive(
13441 &archive.bytes,
13442 &master_key(),
13443 Some(&archive.bootstrap_sidecar)
13444 )
13445 .is_ok());
13446 }
13447
13448 #[test]
13449 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
13450 let archive = write_archive(
13451 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
13452 &master_key(),
13453 single_stream_options(),
13454 )
13455 .unwrap();
13456 let index_root_only = sparse_bootstrap_sidecar(
13457 &archive.bootstrap_sidecar,
13458 &master_key(),
13459 false,
13460 true,
13461 false,
13462 );
13463
13464 assert_eq!(
13465 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
13466 .unwrap_err(),
13467 FormatError::ReaderUnsupported(
13468 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13469 )
13470 );
13471 }
13472
13473 #[test]
13474 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
13475 let archive = write_archive(
13476 &[RegularFile::new("sparse-index.txt", b"recover index root")],
13477 &master_key(),
13478 single_stream_options(),
13479 )
13480 .unwrap();
13481 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13482 let mut corrupted = archive.bytes.clone();
13483 corrupt_object_extent_records(
13484 &mut corrupted,
13485 index_root_extent_from_manifest(&opened.manifest_footer),
13486 );
13487 assert!(open_archive(&corrupted, &master_key()).is_err());
13488
13489 let index_root_only = sparse_bootstrap_sidecar(
13490 &archive.bootstrap_sidecar,
13491 &master_key(),
13492 false,
13493 true,
13494 false,
13495 );
13496 let recovered =
13497 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
13498 .unwrap();
13499
13500 assert_eq!(
13501 recovered.extract_file("sparse-index.txt").unwrap(),
13502 Some(b"recover index root".to_vec())
13503 );
13504 recovered.verify().unwrap();
13505 }
13506
13507 #[test]
13508 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
13509 let archive = write_archive_with_dictionary(
13510 &[RegularFile::new(
13511 "sparse-dict.txt",
13512 b"common words common words sparse dictionary",
13513 )],
13514 &master_key(),
13515 single_stream_options(),
13516 dictionary(),
13517 )
13518 .unwrap();
13519 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
13520 let mut corrupted = archive.bytes.clone();
13521 corrupt_object_extent_records(
13522 &mut corrupted,
13523 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
13524 );
13525 assert!(open_archive(&corrupted, &master_key()).is_err());
13526
13527 let dictionary_only = sparse_bootstrap_sidecar(
13528 &archive.bootstrap_sidecar,
13529 &master_key(),
13530 false,
13531 false,
13532 true,
13533 );
13534 assert_eq!(
13535 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
13536 .unwrap_err(),
13537 FormatError::ReaderUnsupported(
13538 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
13539 )
13540 );
13541
13542 let recovered =
13543 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
13544 .unwrap();
13545 assert_eq!(
13546 recovered.extract_file("sparse-dict.txt").unwrap(),
13547 Some(b"common words common words sparse dictionary".to_vec())
13548 );
13549 recovered.verify().unwrap();
13550 }
13551
13552 #[test]
13553 fn sequential_extracts_dictionary_free_tar_stream() {
13554 let archive = write_archive(
13555 &[RegularFile::new("seq.txt", b"streaming")],
13556 &master_key(),
13557 single_stream_options(),
13558 )
13559 .unwrap();
13560
13561 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
13562 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13563 assert_eq!(member.path, b"seq.txt");
13564 assert_eq!(member.data, b"streaming");
13565 }
13566
13567 #[test]
13568 fn sequential_rejects_logical_payload_above_total_size_cap() {
13569 let archive = write_archive(
13570 &[RegularFile::new("seq-cap.txt", b"payload")],
13571 &master_key(),
13572 single_stream_options(),
13573 )
13574 .unwrap();
13575 let options = ReaderOptions {
13576 max_total_extraction_size: 3,
13577 ..ReaderOptions::default()
13578 };
13579
13580 assert_eq!(
13581 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13582 .unwrap_err(),
13583 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
13584 );
13585 }
13586
13587 #[test]
13588 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
13589 let archive = write_archive(
13590 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
13591 &master_key(),
13592 single_stream_options(),
13593 )
13594 .unwrap();
13595 let options = ReaderOptions {
13596 max_verify_tar_size: 512,
13597 ..ReaderOptions::default()
13598 };
13599
13600 assert_eq!(
13601 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
13602 .unwrap_err(),
13603 FormatError::ReaderUnsupported(
13604 "sequential tar stream exceeds configured verification cap"
13605 )
13606 );
13607 }
13608
13609 #[test]
13610 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
13611 let archive = write_archive(
13612 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
13613 &master_key(),
13614 single_stream_options(),
13615 )
13616 .unwrap();
13617 let mut corrupted = archive.bytes;
13618 corrupt_first_block_record_payload(&mut corrupted);
13619
13620 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
13621 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
13622 assert_eq!(member.path, b"seq-erasure.txt");
13623 assert_eq!(member.data, b"stream repair");
13624 }
13625
13626 #[test]
13627 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
13628 let archive = write_archive(
13629 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
13630 &master_key(),
13631 WriterOptions {
13632 bit_rot_buffer_pct: 0,
13633 fec_parity_shards: 0,
13634 index_fec_parity_shards: 0,
13635 index_root_fec_parity_shards: 0,
13636 ..single_stream_options()
13637 },
13638 )
13639 .unwrap();
13640 let mut corrupted = archive.bytes;
13641 corrupt_first_block_record_payload(&mut corrupted);
13642
13643 assert_eq!(
13644 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
13645 FormatError::BadCrc {
13646 structure: "BlockRecord"
13647 }
13648 );
13649 }
13650
13651 #[test]
13652 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
13653 let archive = write_archive(
13654 &[RegularFile::new(
13655 "seq.txt",
13656 b"payload must not be returned after terminal auth failure",
13657 )],
13658 &master_key(),
13659 single_stream_options(),
13660 )
13661 .unwrap();
13662 let mut corrupted = archive.bytes;
13663 corrupt_v41_terminal_recovery(&mut corrupted);
13664
13665 match sequential_extract_tar_stream(&corrupted, &master_key()) {
13666 Ok(bytes) => panic!(
13667 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
13668 bytes.len()
13669 ),
13670 Err(err) => assert_eq!(
13671 err,
13672 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13673 ),
13674 }
13675 }
13676
13677 #[test]
13678 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
13679 let archive = write_archive_with_dictionary(
13680 &[RegularFile::new(
13681 "seq-dict.txt",
13682 b"common words common words dictionary payload",
13683 )],
13684 &master_key(),
13685 single_stream_options(),
13686 b"common words dictionary",
13687 )
13688 .unwrap();
13689
13690 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
13691 Ok(bytes) => panic!(
13692 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
13693 bytes.len()
13694 ),
13695 Err(err) => assert_eq!(
13696 err,
13697 FormatError::ReaderUnsupported(
13698 "dictionary bootstrap required for non-seekable sequential extraction"
13699 )
13700 ),
13701 }
13702 }
13703
13704 #[test]
13705 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
13706 let archive = write_archive_with_dictionary(
13707 &[RegularFile::new(
13708 "seq-dict-open.txt",
13709 b"common words common words bootstrap required",
13710 )],
13711 &master_key(),
13712 single_stream_options(),
13713 b"common words bootstrap",
13714 )
13715 .unwrap();
13716
13717 assert_eq!(
13718 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
13719 FormatError::ReaderUnsupported(
13720 "non-seekable random access requires a bootstrap sidecar"
13721 )
13722 );
13723 }
13724
13725 #[test]
13726 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
13727 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
13728 let mut output = Vec::new();
13729
13730 assert_eq!(
13731 decode_concatenated_zstd_frames_with_cap(
13732 &skippable,
13733 None,
13734 &mut output,
13735 usize::MAX,
13736 None,
13737 )
13738 .unwrap_err(),
13739 FormatError::NotStandardZstdFrame
13740 );
13741 assert!(output.is_empty());
13742 }
13743
13744 #[test]
13745 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
13746 let archive = write_archive(
13747 &[RegularFile::new("live.txt", b"stream verify")],
13748 &master_key(),
13749 single_stream_options(),
13750 )
13751 .unwrap();
13752
13753 let report =
13754 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13755
13756 assert_eq!(report.file_count, 1);
13757 assert_eq!(report.total_volumes, 1);
13758 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13759 assert!(report.payload_block_count > 0);
13760 }
13761
13762 #[test]
13763 fn live_non_seekable_verify_stream_accepts_recipientwrap_archive() {
13764 let master = master_key();
13765 let archive = write_archive_with_recipient_wrap_records(
13766 &[RegularFile::new(
13767 "wrapped-live.txt",
13768 b"stream recipient verify",
13769 )],
13770 &master,
13771 single_stream_options(),
13772 vec![recipient_wrap_test_record()],
13773 )
13774 .unwrap();
13775
13776 let mut called = false;
13777 let report = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13778 std::io::Cursor::new(archive.bytes),
13779 |context| {
13780 called = true;
13781 assert_eq!(
13782 context.archive_identity.volume_format_rev,
13783 VOLUME_FORMAT_REV_44
13784 );
13785 assert_eq!(context.record.profile_id, 1);
13786 Ok(vec![master.0])
13787 },
13788 NonSeekableReaderOptions::default(),
13789 )
13790 .unwrap();
13791
13792 assert!(called);
13793 assert_eq!(report.file_count, 1);
13794 assert_eq!(report.total_volumes, 1);
13795 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
13796 }
13797
13798 #[test]
13799 fn live_non_seekable_recipientwrap_resolver_rejects_unencrypted_archive() {
13800 let archive = write_archive_unencrypted(
13801 &[RegularFile::new("plain-live.txt", b"plaintext payload")],
13802 single_stream_options(),
13803 )
13804 .unwrap();
13805
13806 let mut called = false;
13807 let err = verify_non_seekable_stream_with_recipient_wrap_resolver_options(
13808 std::io::Cursor::new(archive.bytes),
13809 |_| {
13810 called = true;
13811 Ok(vec![master_key().0])
13812 },
13813 NonSeekableReaderOptions::default(),
13814 )
13815 .unwrap_err();
13816
13817 assert!(!called);
13818 assert_eq!(err, FormatError::KeyMaterialMismatch);
13819 }
13820
13821 #[test]
13822 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
13823 let archive = write_archive(
13824 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
13825 &master_key(),
13826 single_stream_options(),
13827 )
13828 .unwrap();
13829
13830 let report =
13831 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
13832 .unwrap();
13833
13834 assert_eq!(report.file_count, 1);
13835 assert_eq!(report.tar_total_size % 512, 0);
13836 }
13837
13838 #[test]
13839 fn live_non_seekable_verify_stream_accepts_empty_archive() {
13840 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
13841
13842 let report =
13843 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
13844
13845 assert_eq!(report.file_count, 0);
13846 assert_eq!(report.payload_block_count, 0);
13847 assert_eq!(report.tar_total_size, 0);
13848 }
13849
13850 #[test]
13851 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
13852 let archive = write_archive_with_dictionary(
13853 &[RegularFile::new(
13854 "live-dict.txt",
13855 b"common words common words dictionary payload",
13856 )],
13857 &master_key(),
13858 single_stream_options(),
13859 b"common words dictionary",
13860 )
13861 .unwrap();
13862
13863 assert_eq!(
13864 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
13865 .unwrap_err(),
13866 FormatError::ReaderUnsupported(
13867 "dictionary bootstrap required for non-seekable sequential verification"
13868 )
13869 );
13870 }
13871
13872 #[test]
13873 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
13874 let archive = write_archive_with_dictionary(
13875 &[RegularFile::new(
13876 "live-dict-sidecar.txt",
13877 b"common words common words dictionary payload",
13878 )],
13879 &master_key(),
13880 single_stream_options(),
13881 b"common words dictionary",
13882 )
13883 .unwrap();
13884
13885 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
13886 std::io::Cursor::new(archive.bytes),
13887 &archive.bootstrap_sidecar,
13888 &master_key(),
13889 NonSeekableReaderOptions::default(),
13890 )
13891 .unwrap();
13892
13893 assert_eq!(report.file_count, 1);
13894 assert_eq!(report.total_volumes, 1);
13895 }
13896
13897 #[test]
13898 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
13899 let archive = write_archive(
13900 &[RegularFile::new("tail-cap.txt", b"payload")],
13901 &master_key(),
13902 single_stream_options(),
13903 )
13904 .unwrap();
13905 let options = NonSeekableReaderOptions {
13906 max_terminal_tail_size: 8,
13907 ..NonSeekableReaderOptions::default()
13908 };
13909
13910 assert_eq!(
13911 verify_non_seekable_stream_with_options(
13912 std::io::Cursor::new(archive.bytes),
13913 &master_key(),
13914 options
13915 )
13916 .unwrap_err(),
13917 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
13918 );
13919 }
13920
13921 #[test]
13922 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
13923 let archive = write_archive(
13924 &[RegularFile::new("metadata-cap.txt", b"payload")],
13925 &master_key(),
13926 single_stream_options(),
13927 )
13928 .unwrap();
13929 let options = NonSeekableReaderOptions {
13930 max_retained_metadata_bytes: 1,
13931 ..NonSeekableReaderOptions::default()
13932 };
13933
13934 assert_eq!(
13935 verify_non_seekable_stream_with_options(
13936 std::io::Cursor::new(archive.bytes),
13937 &master_key(),
13938 options
13939 )
13940 .unwrap_err(),
13941 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
13942 );
13943 }
13944
13945 #[test]
13946 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
13947 let archive = write_archive(
13948 &[RegularFile::new("metadata-erasure.txt", b"payload")],
13949 &master_key(),
13950 single_stream_options(),
13951 )
13952 .unwrap();
13953 let mut corrupted = archive.bytes;
13954 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
13955 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
13956
13957 let report =
13958 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
13959
13960 assert_eq!(report.file_count, 1);
13961 }
13962
13963 #[test]
13964 fn live_non_seekable_verify_rejects_member_count_above_cap() {
13965 let archive = write_archive(
13966 &[RegularFile::new("member-cap.txt", b"payload")],
13967 &master_key(),
13968 single_stream_options(),
13969 )
13970 .unwrap();
13971 let options = NonSeekableReaderOptions {
13972 max_streamed_member_count: 0,
13973 ..NonSeekableReaderOptions::default()
13974 };
13975
13976 assert_eq!(
13977 verify_non_seekable_stream_with_options(
13978 std::io::Cursor::new(archive.bytes),
13979 &master_key(),
13980 options
13981 )
13982 .unwrap_err(),
13983 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
13984 );
13985 }
13986
13987 #[test]
13988 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
13989 let archive = write_archive(
13990 &[RegularFile::new("live-total-cap.txt", b"payload")],
13991 &master_key(),
13992 single_stream_options(),
13993 )
13994 .unwrap();
13995 let mut options = NonSeekableReaderOptions::default();
13996 options.reader.max_total_extraction_size = 3;
13997
13998 assert_eq!(
13999 verify_non_seekable_stream_with_options(
14000 std::io::Cursor::new(archive.bytes),
14001 &master_key(),
14002 options
14003 )
14004 .unwrap_err(),
14005 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
14006 );
14007 }
14008
14009 #[test]
14010 fn live_non_seekable_verify_reports_root_auth_wire_only() {
14011 let archive = write_archive_with_root_auth(
14012 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
14013 &master_key(),
14014 single_stream_options(),
14015 test_root_auth_config(),
14016 |request| Ok(test_root_auth_value(request)),
14017 )
14018 .unwrap();
14019
14020 let report =
14021 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
14022
14023 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
14024 }
14025
14026 #[test]
14027 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
14028 let archive = write_archive(
14029 &[
14030 RegularFile::new("alpha.txt", b"alpha"),
14031 RegularFile::new("nested/beta.txt", b"beta"),
14032 ],
14033 &master_key(),
14034 single_stream_options(),
14035 )
14036 .unwrap();
14037 let tmp = tempfile::tempdir().unwrap();
14038 let out = tmp.path().join("out");
14039
14040 let report = extract_non_seekable_stream_to_dir(
14041 std::io::Cursor::new(archive.bytes),
14042 &master_key(),
14043 &out,
14044 NonSeekableReaderOptions::default(),
14045 SafeExtractionOptions::default(),
14046 )
14047 .unwrap();
14048
14049 assert_eq!(report.verification.file_count, 2);
14050 assert_eq!(report.extracted_member_count, 2);
14051 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
14052 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
14053 }
14054
14055 #[test]
14056 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
14057 let archive = write_archive(
14058 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
14059 &master_key(),
14060 single_stream_options(),
14061 )
14062 .unwrap();
14063 let tmp = tempfile::tempdir().unwrap();
14064 let out = tmp.path().join("out");
14065
14066 extract_non_seekable_stream_to_dir(
14067 ChunkedReader::new(archive.bytes, 1),
14068 &master_key(),
14069 &out,
14070 NonSeekableReaderOptions::default(),
14071 SafeExtractionOptions::default(),
14072 )
14073 .unwrap();
14074
14075 assert_eq!(
14076 fs::read(out.join("tiny-extract.txt")).unwrap(),
14077 b"chunked extraction"
14078 );
14079 }
14080
14081 #[test]
14082 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
14083 let archive = write_archive(
14084 &[RegularFile::new("late-fail.txt", b"must remain staged")],
14085 &master_key(),
14086 single_stream_options(),
14087 )
14088 .unwrap();
14089 let mut corrupted = archive.bytes;
14090 corrupt_v41_terminal_recovery(&mut corrupted);
14091 let tmp = tempfile::tempdir().unwrap();
14092 let out = tmp.path().join("out");
14093
14094 match extract_non_seekable_stream_to_dir(
14095 std::io::Cursor::new(corrupted),
14096 &master_key(),
14097 &out,
14098 NonSeekableReaderOptions::default(),
14099 SafeExtractionOptions::default(),
14100 )
14101 .unwrap_err()
14102 {
14103 ExtractError::Format(err) => assert_eq!(
14104 err,
14105 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
14106 ),
14107 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
14108 }
14109 assert!(!out.exists());
14110 }
14111
14112 #[test]
14113 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
14114 let archive = write_archive(
14115 &[RegularFile::new("same.txt", b"new")],
14116 &master_key(),
14117 single_stream_options(),
14118 )
14119 .unwrap();
14120 let tmp = tempfile::tempdir().unwrap();
14121 let out = tmp.path().join("out");
14122 fs::create_dir(&out).unwrap();
14123 fs::write(out.join("same.txt"), b"old").unwrap();
14124
14125 match extract_non_seekable_stream_to_dir(
14126 std::io::Cursor::new(archive.bytes.clone()),
14127 &master_key(),
14128 &out,
14129 NonSeekableReaderOptions::default(),
14130 SafeExtractionOptions::default(),
14131 )
14132 .unwrap_err()
14133 {
14134 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
14135 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
14136 }
14137 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
14138
14139 extract_non_seekable_stream_to_dir(
14140 std::io::Cursor::new(archive.bytes),
14141 &master_key(),
14142 &out,
14143 NonSeekableReaderOptions::default(),
14144 SafeExtractionOptions {
14145 overwrite_existing: true,
14146 },
14147 )
14148 .unwrap();
14149 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
14150 }
14151
14152 #[test]
14153 fn live_non_seekable_list_stream_matches_seekable_final_view() {
14154 let archive = write_archive(
14155 &[
14156 RegularFile::new("a.txt", b"a"),
14157 RegularFile::new("b.txt", b"bb"),
14158 ],
14159 &master_key(),
14160 single_stream_options(),
14161 )
14162 .unwrap();
14163 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
14164 let expected = seekable.list_files().unwrap();
14165
14166 let report = list_non_seekable_stream(
14167 std::io::Cursor::new(archive.bytes),
14168 &master_key(),
14169 NonSeekableReaderOptions::default(),
14170 )
14171 .unwrap();
14172
14173 assert_eq!(report.verification.file_count, 2);
14174 assert_eq!(report.entries, expected);
14175 }
14176
14177 #[test]
14178 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
14179 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14180 let mut bad_flags = archive.bootstrap_sidecar.clone();
14181 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
14182 header.flags |= 0x08;
14183 });
14184 assert_eq!(
14185 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
14186 .unwrap_err(),
14187 FormatError::UnknownBootstrapSidecarFlags(0x0b)
14188 );
14189
14190 let mut trailing = archive.bootstrap_sidecar.clone();
14191 trailing.push(0);
14192 assert_eq!(
14193 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
14194 .unwrap_err(),
14195 FormatError::NonCanonicalBootstrapSidecarLayout
14196 );
14197 }
14198
14199 #[test]
14200 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
14201 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14202 let mut wrong_volume = archive.bootstrap_sidecar.clone();
14203 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
14204 footer.volume_index = 1;
14205 });
14206 assert_eq!(
14207 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
14208 .unwrap_err(),
14209 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
14210 );
14211
14212 let mut non_authoritative = archive.bootstrap_sidecar.clone();
14213 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
14214 footer.is_authoritative = 0;
14215 });
14216 assert_eq!(
14217 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
14218 .unwrap_err(),
14219 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
14220 );
14221 }
14222
14223 #[test]
14224 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
14225 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14226 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14227 let crypto_start = volume_header.crypto_header_offset as usize;
14228 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14229 let crypto_header = CryptoHeader::parse(
14230 &archive.bytes[crypto_start..crypto_end],
14231 volume_header.crypto_header_length,
14232 )
14233 .unwrap();
14234 let subkeys = Subkeys::derive(
14235 &master_key(),
14236 &volume_header.archive_uuid,
14237 &volume_header.session_id,
14238 )
14239 .unwrap();
14240 let mut opened_header = volume_header;
14241 opened_header.volume_index = 1;
14242
14243 let parsed = parse_bootstrap_sidecar(
14244 &archive.bootstrap_sidecar,
14245 &opened_header,
14246 &crypto_header.fixed,
14247 &subkeys,
14248 )
14249 .unwrap();
14250
14251 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
14252 }
14253
14254 #[test]
14255 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
14256 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14257 let mut conflicting = archive.bootstrap_sidecar.clone();
14258 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
14259 footer.index_root_first_block += 1;
14260 });
14261
14262 assert_eq!(
14263 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
14264 .unwrap_err(),
14265 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
14266 );
14267 }
14268
14269 #[test]
14270 fn sidecar_size_cap_counts_only_present_sparse_sections() {
14271 let mut crypto_header = test_crypto_header();
14272 crypto_header.has_dictionary = 1;
14273 crypto_header.index_root_fec_data_shards = 1;
14274 crypto_header.index_root_fec_parity_shards = 0;
14275 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14276 let header = BootstrapSidecarHeader {
14277 archive_uuid: [0x31; 16],
14278 session_id: [0x42; 16],
14279 flags: 0x04,
14280 manifest_footer_offset: 0,
14281 manifest_footer_length: 0,
14282 index_root_records_offset: 0,
14283 index_root_records_length: 0,
14284 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14285 dictionary_records_length: record_len,
14286 sidecar_hmac: [0u8; 32],
14287 header_crc32c: 0,
14288 };
14289
14290 validate_sidecar_size_cap(
14291 &header,
14292 &crypto_header,
14293 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
14294 )
14295 .unwrap();
14296 assert_eq!(
14297 validate_sidecar_size_cap(
14298 &header,
14299 &crypto_header,
14300 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
14301 )
14302 .unwrap_err(),
14303 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14304 );
14305 }
14306
14307 #[test]
14308 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
14309 let mut crypto_header = test_crypto_header();
14310 crypto_header.index_root_fec_data_shards = 1;
14311 crypto_header.index_root_fec_parity_shards = 0;
14312 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14313 let header = BootstrapSidecarHeader {
14314 archive_uuid: [0x31; 16],
14315 session_id: [0x42; 16],
14316 flags: 0x02,
14317 manifest_footer_offset: 0,
14318 manifest_footer_length: 0,
14319 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14320 index_root_records_length: record_len * 2,
14321 dictionary_records_offset: 0,
14322 dictionary_records_length: 0,
14323 sidecar_hmac: [0u8; 32],
14324 header_crc32c: 0,
14325 };
14326
14327 assert_eq!(
14328 validate_sidecar_size_cap(
14329 &header,
14330 &crypto_header,
14331 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
14332 )
14333 .unwrap_err(),
14334 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
14335 );
14336 }
14337
14338 #[test]
14339 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
14340 let mut crypto_header = test_crypto_header();
14341 crypto_header.block_size = u32::MAX;
14342 crypto_header.index_root_fec_data_shards = u16::MAX;
14343 crypto_header.index_root_fec_parity_shards = u16::MAX;
14344 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
14345 let max_records = crypto_header.index_root_fec_data_shards as u64
14346 + crypto_header.index_root_fec_parity_shards as u64;
14347 let max_section_len = max_records * record_len;
14348 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
14349 + MANIFEST_FOOTER_LEN as u64
14350 + max_section_len
14351 + max_section_len;
14352 let header = BootstrapSidecarHeader {
14353 archive_uuid: [0x31; 16],
14354 session_id: [0x42; 16],
14355 flags: 0x01 | 0x02 | 0x04,
14356 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
14357 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14358 index_root_records_offset: 0,
14359 index_root_records_length: max_section_len,
14360 dictionary_records_offset: 0,
14361 dictionary_records_length: max_section_len,
14362 sidecar_hmac: [0u8; 32],
14363 header_crc32c: 0,
14364 };
14365
14366 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
14367 assert_eq!(
14368 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
14369 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
14370 );
14371 }
14372
14373 #[test]
14374 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
14375 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14376 let mut with_dictionary = archive.bootstrap_sidecar.clone();
14377 let header =
14378 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
14379 .unwrap();
14380 let record_len = sidecar_record_len(&with_dictionary);
14381 let first_record = header.index_root_records_offset as usize;
14382 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
14383 let dictionary_offset = with_dictionary.len() as u64;
14384 with_dictionary.extend_from_slice(&copied_record);
14385 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
14386 header.flags |= 0x04;
14387 header.dictionary_records_offset = dictionary_offset;
14388 header.dictionary_records_length = record_len as u64;
14389 });
14390
14391 assert_eq!(
14392 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
14393 .unwrap_err(),
14394 FormatError::InvalidArchive(
14395 "bootstrap sidecar has dictionary records while has_dictionary is false"
14396 )
14397 );
14398 }
14399
14400 #[test]
14401 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
14402 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
14403 let mut missing = archive.bootstrap_sidecar.clone();
14404 let record_len = sidecar_record_len(&missing);
14405 let new_len = missing.len() - record_len;
14406 missing.truncate(new_len);
14407 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
14408 header.index_root_records_length -= record_len as u64;
14409 });
14410 assert_eq!(
14411 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
14412 .unwrap_err(),
14413 FormatError::InvalidArchive(
14414 "sidecar BlockRecord section does not match declared extent"
14415 )
14416 );
14417
14418 let mut duplicate = archive.bootstrap_sidecar.clone();
14419 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
14420 record.block_index -= 1;
14421 });
14422 assert_eq!(
14423 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
14424 .unwrap_err(),
14425 FormatError::InvalidArchive(
14426 "sidecar BlockRecord section has missing or duplicate blocks"
14427 )
14428 );
14429
14430 let mut misordered = archive.bootstrap_sidecar.clone();
14431 swap_sidecar_index_records(&mut misordered, 0, 1);
14432 assert_eq!(
14433 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
14434 .unwrap_err(),
14435 FormatError::InvalidArchive(
14436 "sidecar BlockRecord section has missing or duplicate blocks"
14437 )
14438 );
14439
14440 let mut wrong_kind = archive.bootstrap_sidecar.clone();
14441 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
14442 record.kind = BlockKind::PayloadData;
14443 });
14444 assert_eq!(
14445 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
14446 .unwrap_err(),
14447 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
14448 );
14449
14450 let mut wrong_last = archive.bootstrap_sidecar.clone();
14451 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
14452 record.flags = 0;
14453 });
14454 assert_eq!(
14455 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
14456 .unwrap_err(),
14457 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
14458 );
14459 }
14460
14461 #[test]
14462 fn verify_helper_rejects_envelope_frame_coverage_gap() {
14463 let frames = BTreeMap::from([(
14464 0,
14465 FrameEntry {
14466 frame_index: 0,
14467 envelope_index: 0,
14468 offset_in_envelope: 0,
14469 compressed_size: 10,
14470 decompressed_size: 512,
14471 flags: 0,
14472 tar_stream_offset: 0,
14473 },
14474 )]);
14475 let envelopes = BTreeMap::from([(
14476 0,
14477 EnvelopeEntry {
14478 envelope_index: 0,
14479 first_block_index: 0,
14480 data_block_count: 1,
14481 parity_block_count: 1,
14482 encrypted_size: 4096,
14483 plaintext_size: 11,
14484 first_frame_index: 0,
14485 frame_count: 1,
14486 },
14487 )]);
14488
14489 assert_eq!(
14490 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
14491 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
14492 );
14493 }
14494
14495 #[test]
14496 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
14497 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
14498 assert_eq!(
14499 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
14500 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14501 );
14502 assert_eq!(
14503 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
14504 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
14505 );
14506 }
14507
14508 #[test]
14509 fn verify_rejects_authenticated_content_hash_mismatch() {
14510 let options = WriterOptions {
14511 index_root_fec_parity_shards: 0,
14512 ..single_stream_options()
14513 };
14514 let archive = write_archive(
14515 &[RegularFile::new("content-hash.txt", b"hash covered")],
14516 &master_key(),
14517 options,
14518 )
14519 .unwrap();
14520 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14521
14522 let mut root = opened.index_root.clone();
14523 root.header.content_sha256 = [0xa5; 32];
14524 let root_plaintext = root.to_bytes();
14525 IndexRoot::parse(
14526 &root_plaintext,
14527 false,
14528 metadata_limits(&opened.crypto_header),
14529 )
14530 .unwrap();
14531 assert_eq!(
14532 root_plaintext.len() as u32,
14533 opened.manifest_footer.index_root_decompressed_size
14534 );
14535
14536 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14537 let mut next_block_index = opened.manifest_footer.index_root_first_block;
14538 let replacement = encrypt_test_object(
14539 &compressed_root,
14540 &opened.subkeys.index_root_key,
14541 &opened.subkeys.index_nonce_seed,
14542 b"idxroot",
14543 0,
14544 BlockKind::IndexRootData,
14545 &mut next_block_index,
14546 &opened.crypto_header,
14547 &opened.volume_header,
14548 );
14549 assert_eq!(
14550 replacement.extent.data_block_count,
14551 opened.manifest_footer.index_root_data_block_count
14552 );
14553 assert_eq!(
14554 replacement.extent.encrypted_size,
14555 opened.manifest_footer.index_root_encrypted_size
14556 );
14557
14558 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14559 let crypto_end = volume_header.crypto_header_offset as usize
14560 + volume_header.crypto_header_length as usize;
14561 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14562 let mut malformed = archive.bytes.clone();
14563 for record in replacement.records {
14564 let offset = crypto_end + record.block_index as usize * record_len;
14565 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14566 }
14567
14568 let reopened = open_archive(&malformed, &master_key()).unwrap();
14569 assert_eq!(
14570 reopened.verify().unwrap_err(),
14571 FormatError::InvalidArchive(
14572 "IndexRoot content_sha256 does not match decoded tar stream"
14573 )
14574 );
14575 }
14576
14577 #[test]
14578 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
14579 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
14580 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
14581 path[0] = b'x';
14582 });
14583 assert_eq!(
14584 path_mismatch.verify().unwrap_err(),
14585 FormatError::InvalidArchive("tar member path does not match FileEntry path")
14586 );
14587
14588 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
14589 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
14590 file.file_data_size += 1;
14591 });
14592 assert_eq!(
14593 size_mismatch.verify().unwrap_err(),
14594 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
14595 );
14596 }
14597
14598 #[test]
14599 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
14600 let (mut opened, _) = multi_envelope_reader_fixture();
14601 let locating = opened.index_root.shards[0].clone();
14602 let mut duplicate = opened.load_index_shard(&locating).unwrap();
14603 duplicate.header.shard_index = 1;
14604 duplicate.frames[0].flags ^= 0x0000_0001;
14605 let duplicate_plaintext = duplicate.to_bytes();
14606 let mut next_block_index = opened
14607 .blocks
14608 .keys()
14609 .last()
14610 .copied()
14611 .map(|index| index + 1)
14612 .unwrap_or(0);
14613 let duplicate_object = encrypt_test_object(
14614 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14615 &opened.subkeys.index_shard_key,
14616 &opened.subkeys.index_nonce_seed,
14617 b"idxshard",
14618 1,
14619 BlockKind::IndexShardData,
14620 &mut next_block_index,
14621 &opened.crypto_header,
14622 &opened.volume_header,
14623 );
14624 insert_records(&mut opened.blocks, &duplicate_object.records);
14625 opened.index_root.shards.push(ShardEntry {
14626 shard_index: 1,
14627 first_block_index: duplicate_object.extent.first_block_index,
14628 data_block_count: duplicate_object.extent.data_block_count,
14629 parity_block_count: 0,
14630 encrypted_size: duplicate_object.extent.encrypted_size,
14631 decompressed_size: duplicate_plaintext.len() as u32,
14632 file_count: locating.file_count,
14633 first_path_hash: locating.first_path_hash,
14634 last_path_hash: locating.last_path_hash,
14635 });
14636 opened.index_root.header.file_count += locating.file_count as u64;
14637
14638 assert_eq!(
14639 opened.verify().unwrap_err(),
14640 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
14641 );
14642 }
14643
14644 #[test]
14645 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
14646 let (mut opened, _) = multi_envelope_reader_fixture();
14647 let locating = opened.index_root.shards[0].clone();
14648 let mut duplicate = opened.load_index_shard(&locating).unwrap();
14649 duplicate.header.shard_index = 1;
14650 duplicate.envelopes[0].first_block_index += 1;
14651 let duplicate_plaintext = duplicate.to_bytes();
14652 let mut next_block_index = opened
14653 .blocks
14654 .keys()
14655 .last()
14656 .copied()
14657 .map(|index| index + 1)
14658 .unwrap_or(0);
14659 let duplicate_object = encrypt_test_object(
14660 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
14661 &opened.subkeys.index_shard_key,
14662 &opened.subkeys.index_nonce_seed,
14663 b"idxshard",
14664 1,
14665 BlockKind::IndexShardData,
14666 &mut next_block_index,
14667 &opened.crypto_header,
14668 &opened.volume_header,
14669 );
14670 insert_records(&mut opened.blocks, &duplicate_object.records);
14671 opened.index_root.shards.push(ShardEntry {
14672 shard_index: 1,
14673 first_block_index: duplicate_object.extent.first_block_index,
14674 data_block_count: duplicate_object.extent.data_block_count,
14675 parity_block_count: 0,
14676 encrypted_size: duplicate_object.extent.encrypted_size,
14677 decompressed_size: duplicate_plaintext.len() as u32,
14678 file_count: locating.file_count,
14679 first_path_hash: locating.first_path_hash,
14680 last_path_hash: locating.last_path_hash,
14681 });
14682 opened.index_root.header.file_count += locating.file_count as u64;
14683
14684 assert_eq!(
14685 opened.verify().unwrap_err(),
14686 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
14687 );
14688 }
14689
14690 #[test]
14691 fn verify_rejects_non_contiguous_global_envelope_indexes() {
14692 let (mut opened, _) = multi_envelope_reader_fixture();
14693 replace_first_index_shard(&mut opened, |shard| {
14694 let frame = shard
14695 .frames
14696 .iter_mut()
14697 .find(|entry| entry.frame_index == 1)
14698 .unwrap();
14699 frame.envelope_index = 2;
14700
14701 let envelope = shard
14702 .envelopes
14703 .iter_mut()
14704 .find(|entry| entry.envelope_index == 1)
14705 .unwrap();
14706 envelope.envelope_index = 2;
14707 });
14708
14709 assert_eq!(
14710 opened.verify().unwrap_err(),
14711 FormatError::InvalidMetadata {
14712 structure: "EnvelopeEntry",
14713 reason: "global index coverage has a gap",
14714 }
14715 );
14716 }
14717
14718 #[test]
14719 fn verify_rejects_payload_object_extent_overlap() {
14720 let (mut opened, _) = multi_envelope_reader_fixture();
14721 replace_first_index_shard(&mut opened, |shard| {
14722 let first_block_index = shard.envelopes[0].first_block_index;
14723 shard.envelopes[1].first_block_index = first_block_index;
14724 });
14725
14726 assert_eq!(
14727 opened.verify().unwrap_err(),
14728 FormatError::InvalidArchive("encrypted object block ranges overlap")
14729 );
14730 }
14731
14732 #[test]
14733 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
14734 let volume_header = test_volume_header();
14735 let crypto_header = test_crypto_header();
14736 let subkeys = Subkeys::derive(
14737 &master_key(),
14738 &volume_header.archive_uuid,
14739 &volume_header.session_id,
14740 )
14741 .unwrap();
14742 let mut next_block_index = 0u64;
14743 let mut blocks = BTreeMap::new();
14744
14745 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
14746 let beta = test_member(b"beta.txt", b"beta cross shard\n");
14747 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
14748 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
14749 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
14750 let envelope_plaintext =
14751 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
14752 let payload = encrypt_test_object(
14753 &envelope_plaintext,
14754 &subkeys.enc_key,
14755 &subkeys.nonce_seed,
14756 b"envelope",
14757 0,
14758 BlockKind::PayloadData,
14759 &mut next_block_index,
14760 &crypto_header,
14761 &volume_header,
14762 );
14763 insert_records(&mut blocks, &payload.records);
14764
14765 let envelope = EnvelopeEntry {
14766 envelope_index: 0,
14767 first_block_index: payload.extent.first_block_index,
14768 data_block_count: payload.extent.data_block_count,
14769 parity_block_count: 0,
14770 encrypted_size: payload.extent.encrypted_size,
14771 plaintext_size: envelope_plaintext.len() as u32,
14772 first_frame_index: 0,
14773 frame_count: 2,
14774 };
14775 let frame0 = FrameEntry {
14776 frame_index: 0,
14777 envelope_index: 0,
14778 offset_in_envelope: 0,
14779 compressed_size: frame0_plaintext.len() as u32,
14780 decompressed_size: alpha.len() as u32,
14781 flags: 0x0000_0003,
14782 tar_stream_offset: 0,
14783 };
14784 let frame1 = FrameEntry {
14785 frame_index: 1,
14786 envelope_index: 0,
14787 offset_in_envelope: frame0_plaintext.len() as u32,
14788 compressed_size: frame1_plaintext.len() as u32,
14789 decompressed_size: beta.len() as u32,
14790 flags: 0x0000_0003,
14791 tar_stream_offset: alpha.len() as u64,
14792 };
14793
14794 let (shard0_plaintext, first0, last0) = build_test_index_shard(
14795 &[TestFileMeta {
14796 path: b"alpha.txt".to_vec(),
14797 frame_index: 0,
14798 tar_stream_offset: 0,
14799 member_group_size: alpha.len() as u64,
14800 file_data_size: b"alpha cross shard\n".len() as u64,
14801 }],
14802 &[frame0],
14803 std::slice::from_ref(&envelope),
14804 );
14805 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
14806 &[TestFileMeta {
14807 path: b"beta.txt".to_vec(),
14808 frame_index: 1,
14809 tar_stream_offset: alpha.len() as u64,
14810 member_group_size: beta.len() as u64,
14811 file_data_size: b"beta cross shard\n".len() as u64,
14812 }],
14813 &[frame1],
14814 std::slice::from_ref(&envelope),
14815 );
14816 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
14817
14818 let shard0 = encrypt_test_object(
14819 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
14820 &subkeys.index_shard_key,
14821 &subkeys.index_nonce_seed,
14822 b"idxshard",
14823 0,
14824 BlockKind::IndexShardData,
14825 &mut next_block_index,
14826 &crypto_header,
14827 &volume_header,
14828 );
14829 let shard1 = encrypt_test_object(
14830 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
14831 &subkeys.index_shard_key,
14832 &subkeys.index_nonce_seed,
14833 b"idxshard",
14834 1,
14835 BlockKind::IndexShardData,
14836 &mut next_block_index,
14837 &crypto_header,
14838 &volume_header,
14839 );
14840 insert_records(&mut blocks, &shard0.records);
14841 insert_records(&mut blocks, &shard1.records);
14842
14843 let index_root = IndexRoot {
14844 header: IndexRootHeader {
14845 frame_count: 2,
14846 envelope_count: 1,
14847 file_count: 2,
14848 payload_block_count: payload.extent.data_block_count as u64,
14849 tar_total_size: tar_stream.len() as u64,
14850 content_sha256: sha256_bytes(&tar_stream),
14851 ..IndexRootHeader::empty()
14852 },
14853 shards: vec![
14854 ShardEntry {
14855 shard_index: 0,
14856 first_block_index: shard0.extent.first_block_index,
14857 data_block_count: shard0.extent.data_block_count,
14858 parity_block_count: 0,
14859 encrypted_size: shard0.extent.encrypted_size,
14860 decompressed_size: shard0_plaintext.len() as u32,
14861 file_count: 1,
14862 first_path_hash: first0,
14863 last_path_hash: last0,
14864 },
14865 ShardEntry {
14866 shard_index: 1,
14867 first_block_index: shard1.extent.first_block_index,
14868 data_block_count: shard1.extent.data_block_count,
14869 parity_block_count: 0,
14870 encrypted_size: shard1.extent.encrypted_size,
14871 decompressed_size: shard1_plaintext.len() as u32,
14872 file_count: 1,
14873 first_path_hash: first1,
14874 last_path_hash: last1,
14875 },
14876 ],
14877 directory_hint_shards: Vec::new(),
14878 };
14879
14880 let index_root_plaintext = index_root.to_bytes();
14881 let index_root_object = encrypt_test_object(
14882 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14883 &subkeys.index_root_key,
14884 &subkeys.index_nonce_seed,
14885 b"idxroot",
14886 0,
14887 BlockKind::IndexRootData,
14888 &mut next_block_index,
14889 &crypto_header,
14890 &volume_header,
14891 );
14892 insert_records(&mut blocks, &index_root_object.records);
14893
14894 let archive_uuid = volume_header.archive_uuid;
14895 let session_id = volume_header.session_id;
14896 let opened = OpenedArchive {
14897 options: ReaderOptions::default(),
14898 observed_archive_bytes: 1_000_000,
14899 observed_volume_count: 1,
14900 subkeys,
14901 blocks,
14902 lazy_blocks: None,
14903 crypto_header_bytes: Vec::new(),
14904 volume_header,
14905 crypto_header,
14906 manifest_footer: ManifestFooter {
14907 archive_uuid,
14908 session_id,
14909 volume_index: 0,
14910 is_authoritative: 1,
14911 total_volumes: 1,
14912 index_root_first_block: index_root_object.extent.first_block_index,
14913 index_root_data_block_count: index_root_object.extent.data_block_count,
14914 index_root_parity_block_count: 0,
14915 index_root_encrypted_size: index_root_object.extent.encrypted_size,
14916 index_root_decompressed_size: index_root_plaintext.len() as u32,
14917 manifest_hmac: [0u8; 32],
14918 },
14919 volume_trailer: Some(VolumeTrailer {
14920 archive_uuid,
14921 session_id,
14922 volume_index: 0,
14923 block_count: next_block_index,
14924 bytes_written: 0,
14925 manifest_footer_offset: 0,
14926 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14927 closed_at_ns: 0,
14928 root_auth_footer_offset: 0,
14929 root_auth_footer_length: 0,
14930 root_auth_flags: 0,
14931 trailer_hmac: [0u8; 32],
14932 }),
14933 root_auth_footer: None,
14934 index_root,
14935 payload_dictionary: None,
14936 };
14937
14938 opened.verify().unwrap();
14939 }
14940
14941 #[test]
14942 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
14943 let options = WriterOptions {
14944 index_root_fec_parity_shards: 0,
14945 ..single_stream_options()
14946 };
14947 let archive = write_archive(
14948 &[RegularFile::new("only.txt", b"only payload")],
14949 &master_key(),
14950 options,
14951 )
14952 .unwrap();
14953 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
14954 assert!(opened.index_root.directory_hint_shards.is_empty());
14955
14956 let mut root = opened.index_root.clone();
14957 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
14958 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
14959 let root_plaintext = root.to_bytes();
14960 IndexRoot::parse(
14961 &root_plaintext,
14962 false,
14963 metadata_limits(&opened.crypto_header),
14964 )
14965 .unwrap();
14966 assert_eq!(
14967 root_plaintext.len() as u32,
14968 opened.manifest_footer.index_root_decompressed_size
14969 );
14970
14971 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
14972 let mut next_block_index = opened.manifest_footer.index_root_first_block;
14973 let replacement = encrypt_test_object(
14974 &compressed_root,
14975 &opened.subkeys.index_root_key,
14976 &opened.subkeys.index_nonce_seed,
14977 b"idxroot",
14978 0,
14979 BlockKind::IndexRootData,
14980 &mut next_block_index,
14981 &opened.crypto_header,
14982 &opened.volume_header,
14983 );
14984 assert_eq!(
14985 replacement.extent.first_block_index,
14986 opened.manifest_footer.index_root_first_block
14987 );
14988 assert_eq!(
14989 replacement.extent.data_block_count,
14990 opened.manifest_footer.index_root_data_block_count
14991 );
14992 assert_eq!(
14993 replacement.extent.encrypted_size,
14994 opened.manifest_footer.index_root_encrypted_size
14995 );
14996
14997 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
14998 let crypto_end = volume_header.crypto_header_offset as usize
14999 + volume_header.crypto_header_length as usize;
15000 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
15001 let mut malformed = archive.bytes.clone();
15002 for record in replacement.records {
15003 let offset = crypto_end + record.block_index as usize * record_len;
15004 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
15005 }
15006
15007 let reopened = open_archive(&malformed, &master_key()).unwrap();
15008 assert_eq!(
15009 reopened.index_root.header.file_count,
15010 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
15011 );
15012 assert!(reopened.index_root.directory_hint_shards.is_empty());
15013
15014 assert_eq!(
15015 reopened.verify().unwrap_err(),
15016 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
15017 );
15018 }
15019
15020 #[test]
15021 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
15022 let mut map = DirectoryHintMap::new();
15023 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
15024 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
15025
15026 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
15027 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
15028 assert_eq!(
15029 map.get(b"foo/bar".as_slice()),
15030 Some(&BTreeSet::from([2, 4]))
15031 );
15032 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
15033 assert!(!map.contains_key(b"foobar".as_slice()));
15034 }
15035
15036 #[test]
15037 fn directory_hint_validation_requires_exact_global_map() {
15038 let mut expected = DirectoryHintMap::new();
15039 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
15040 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
15041 let rows = sorted_directory_hint_rows(&expected);
15042 let table = directory_hint_table_from_rows(7, &rows, 2);
15043
15044 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
15045 .unwrap();
15046
15047 let mut missing_root = expected.clone();
15048 missing_root.remove(&Vec::new());
15049 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
15050 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
15051 assert_eq!(
15052 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
15053 .unwrap_err(),
15054 FormatError::InvalidArchive("directory hint map does not match decoded files")
15055 );
15056
15057 let mut expected_missing_directory_entry = expected.clone();
15058 expected_missing_directory_entry
15059 .get_mut(b"foo".as_slice())
15060 .unwrap()
15061 .remove(&1);
15062 assert_eq!(
15063 validate_directory_hint_tables_against_expected(
15064 std::slice::from_ref(&table),
15065 &expected_missing_directory_entry,
15066 )
15067 .unwrap_err(),
15068 FormatError::InvalidArchive("directory hint map does not match decoded files")
15069 );
15070
15071 let mut extra = expected.clone();
15072 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
15073 let extra_rows = sorted_directory_hint_rows(&extra);
15074 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
15075 assert_eq!(
15076 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
15077 FormatError::InvalidArchive("directory hint map does not match decoded files")
15078 );
15079 }
15080
15081 #[test]
15082 fn directory_hint_validation_rejects_global_order_mismatch() {
15083 let mut expected = DirectoryHintMap::new();
15084 expected.insert(Vec::new(), BTreeSet::from([0]));
15085 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
15086 let rows = sorted_directory_hint_rows(&expected);
15087 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
15088 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
15089
15090 assert_eq!(
15091 validate_directory_hint_tables_against_expected(&[second, first], &expected)
15092 .unwrap_err(),
15093 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
15094 );
15095 }
15096
15097 #[test]
15098 fn object_extent_rejects_parity_above_class_cap() {
15099 let crypto_header = CryptoHeaderFixed {
15100 length: 0,
15101 compression_algo: CompressionAlgo::ZstdFramed,
15102 aead_algo: AeadAlgo::AesGcmSiv256,
15103 fec_algo: FecAlgo::ReedSolomonGF16,
15104 kdf_algo: KdfAlgo::Raw,
15105 chunk_size: 1024,
15106 envelope_target_size: 4096,
15107 block_size: 4096,
15108 fec_data_shards: 1,
15109 fec_parity_shards: 1,
15110 index_fec_data_shards: 1,
15111 index_fec_parity_shards: 1,
15112 index_root_fec_data_shards: 1,
15113 index_root_fec_parity_shards: 1,
15114 stripe_width: 1,
15115 volume_loss_tolerance: 0,
15116 bit_rot_buffer_pct: 0,
15117 has_dictionary: 0,
15118 max_path_length: 4096,
15119 expected_volume_size: 0,
15120 };
15121 let extent = ObjectExtent {
15122 first_block_index: 0,
15123 data_block_count: 1,
15124 parity_block_count: 2,
15125 encrypted_size: 4096,
15126 };
15127
15128 assert_eq!(
15129 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
15130 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
15131 );
15132 }
15133
15134 #[test]
15135 fn object_extent_rejects_parity_below_recoverability_requirement() {
15136 let crypto_header = CryptoHeaderFixed {
15137 length: 0,
15138 compression_algo: CompressionAlgo::ZstdFramed,
15139 aead_algo: AeadAlgo::AesGcmSiv256,
15140 fec_algo: FecAlgo::ReedSolomonGF16,
15141 kdf_algo: KdfAlgo::Raw,
15142 chunk_size: 1024,
15143 envelope_target_size: 4096,
15144 block_size: 4096,
15145 fec_data_shards: 1,
15146 fec_parity_shards: 1,
15147 index_fec_data_shards: 1,
15148 index_fec_parity_shards: 1,
15149 index_root_fec_data_shards: 1,
15150 index_root_fec_parity_shards: 1,
15151 stripe_width: 2,
15152 volume_loss_tolerance: 1,
15153 bit_rot_buffer_pct: 0,
15154 has_dictionary: 0,
15155 max_path_length: 4096,
15156 expected_volume_size: 0,
15157 };
15158 let extent = ObjectExtent {
15159 first_block_index: 0,
15160 data_block_count: 1,
15161 parity_block_count: 0,
15162 encrypted_size: 4096,
15163 };
15164
15165 assert_eq!(
15166 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
15167 FormatError::InvalidArchive(
15168 "encrypted object parity does not match v41 compute_parity"
15169 )
15170 );
15171 }
15172
15173 #[test]
15174 fn encrypted_object_extent_matrix_rejects_overlaps() {
15175 let (opened, _) = multi_envelope_reader_fixture();
15176 let loaded_shard = opened
15177 .load_index_shard(&opened.index_root.shards[0])
15178 .unwrap();
15179 let base_envelopes = loaded_shard
15180 .envelopes
15181 .iter()
15182 .map(|entry| (entry.envelope_index, entry.clone()))
15183 .collect::<BTreeMap<_, _>>();
15184 let payload_start = loaded_shard.envelopes[0].first_block_index;
15185 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
15186
15187 let mut payload_overlap = base_envelopes.clone();
15188 payload_overlap
15189 .get_mut(&loaded_shard.envelopes[1].envelope_index)
15190 .unwrap()
15191 .first_block_index = payload_start;
15192 assert_eq!(
15193 opened
15194 .validate_encrypted_object_block_ranges(&payload_overlap)
15195 .unwrap_err(),
15196 overlap
15197 );
15198
15199 let mut shard_overlap = opened.clone();
15200 let shard = shard_overlap.index_root.shards[0].clone();
15201 shard_overlap.index_root.shards.push(ShardEntry {
15202 shard_index: 1,
15203 ..shard
15204 });
15205 assert_eq!(
15206 shard_overlap
15207 .validate_encrypted_object_block_ranges(&base_envelopes)
15208 .unwrap_err(),
15209 overlap
15210 );
15211
15212 let mut dictionary_overlap = opened.clone();
15213 dictionary_overlap.crypto_header.has_dictionary = 1;
15214 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
15215 dictionary_overlap
15216 .index_root
15217 .header
15218 .dictionary_data_block_count = 1;
15219 dictionary_overlap
15220 .index_root
15221 .header
15222 .dictionary_parity_block_count = 0;
15223 dictionary_overlap
15224 .index_root
15225 .header
15226 .dictionary_encrypted_size = 4096;
15227 dictionary_overlap
15228 .index_root
15229 .header
15230 .dictionary_decompressed_size = 128;
15231 assert_eq!(
15232 dictionary_overlap
15233 .validate_encrypted_object_block_ranges(&base_envelopes)
15234 .unwrap_err(),
15235 overlap
15236 );
15237
15238 let mut hint_overlap = opened.clone();
15239 hint_overlap
15240 .index_root
15241 .directory_hint_shards
15242 .push(DirectoryHintShardEntry {
15243 hint_shard_index: 0,
15244 first_dir_hash: [0; 8],
15245 last_dir_hash: [0; 8],
15246 first_block_index: payload_start,
15247 data_block_count: 1,
15248 parity_block_count: 0,
15249 encrypted_size: 4096,
15250 decompressed_size: 128,
15251 entry_count: 1,
15252 });
15253 assert_eq!(
15254 hint_overlap
15255 .validate_encrypted_object_block_ranges(&base_envelopes)
15256 .unwrap_err(),
15257 overlap
15258 );
15259 }
15260
15261 #[test]
15262 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
15263 let volume_header = test_volume_header();
15264 let crypto_header = test_crypto_header();
15265 let subkeys = Subkeys::derive(
15266 &master_key(),
15267 &volume_header.archive_uuid,
15268 &volume_header.session_id,
15269 )
15270 .unwrap();
15271 let mut next_block_index = 0u64;
15272
15273 let index_root_payload = b"index root metadata object";
15274 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
15275 assert_metadata_object_from_compressed(
15276 &{
15277 let mut bytes = index_root_compressed.clone();
15278 bytes.push(0);
15279 bytes
15280 },
15281 index_root_payload.len(),
15282 &subkeys,
15283 &volume_header,
15284 &crypto_header,
15285 &subkeys.index_root_key,
15286 &subkeys.index_nonce_seed,
15287 b"idxroot",
15288 0,
15289 BlockKind::IndexRootData,
15290 BlockKind::IndexRootParity,
15291 crypto_header.index_root_fec_data_shards,
15292 crypto_header.index_root_fec_parity_shards,
15293 &mut next_block_index,
15294 FormatError::TrailingBytesAfterZstdFrame,
15295 );
15296 assert_metadata_object_from_compressed(
15297 &index_root_compressed,
15298 index_root_payload.len() + 1,
15299 &subkeys,
15300 &volume_header,
15301 &crypto_header,
15302 &subkeys.index_root_key,
15303 &subkeys.index_nonce_seed,
15304 b"idxroot",
15305 0,
15306 BlockKind::IndexRootData,
15307 BlockKind::IndexRootParity,
15308 crypto_header.index_root_fec_data_shards,
15309 crypto_header.index_root_fec_parity_shards,
15310 &mut next_block_index,
15311 FormatError::ZstdDecompressedSizeMismatch {
15312 expected: index_root_payload.len() + 1,
15313 actual: index_root_payload.len(),
15314 },
15315 );
15316
15317 let index_shard_payload = b"index shard metadata object";
15318 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
15319 assert_metadata_object_from_compressed(
15320 &{
15321 let mut bytes = index_shard_compressed.clone();
15322 bytes.push(0);
15323 bytes
15324 },
15325 index_shard_payload.len(),
15326 &subkeys,
15327 &volume_header,
15328 &crypto_header,
15329 &subkeys.index_shard_key,
15330 &subkeys.index_nonce_seed,
15331 b"idxshard",
15332 1,
15333 BlockKind::IndexShardData,
15334 BlockKind::IndexShardParity,
15335 crypto_header.index_fec_data_shards,
15336 crypto_header.index_fec_parity_shards,
15337 &mut next_block_index,
15338 FormatError::TrailingBytesAfterZstdFrame,
15339 );
15340 assert_metadata_object_from_compressed(
15341 &index_shard_compressed,
15342 index_shard_payload.len() + 1,
15343 &subkeys,
15344 &volume_header,
15345 &crypto_header,
15346 &subkeys.index_shard_key,
15347 &subkeys.index_nonce_seed,
15348 b"idxshard",
15349 1,
15350 BlockKind::IndexShardData,
15351 BlockKind::IndexShardParity,
15352 crypto_header.index_fec_data_shards,
15353 crypto_header.index_fec_parity_shards,
15354 &mut next_block_index,
15355 FormatError::ZstdDecompressedSizeMismatch {
15356 expected: index_shard_payload.len() + 1,
15357 actual: index_shard_payload.len(),
15358 },
15359 );
15360
15361 let directory_hint_payload = b"directory hint metadata object";
15362 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
15363 assert_metadata_object_from_compressed(
15364 &{
15365 let mut bytes = directory_hint_compressed.clone();
15366 bytes.push(0);
15367 bytes
15368 },
15369 directory_hint_payload.len(),
15370 &subkeys,
15371 &volume_header,
15372 &crypto_header,
15373 &subkeys.dir_hint_key,
15374 &subkeys.index_nonce_seed,
15375 b"dirhint",
15376 0,
15377 BlockKind::DirectoryHintData,
15378 BlockKind::DirectoryHintParity,
15379 crypto_header.index_fec_data_shards,
15380 crypto_header.index_fec_parity_shards,
15381 &mut next_block_index,
15382 FormatError::TrailingBytesAfterZstdFrame,
15383 );
15384 assert_metadata_object_from_compressed(
15385 &directory_hint_compressed,
15386 directory_hint_payload.len() + 1,
15387 &subkeys,
15388 &volume_header,
15389 &crypto_header,
15390 &subkeys.dir_hint_key,
15391 &subkeys.index_nonce_seed,
15392 b"dirhint",
15393 0,
15394 BlockKind::DirectoryHintData,
15395 BlockKind::DirectoryHintParity,
15396 crypto_header.index_fec_data_shards,
15397 crypto_header.index_fec_parity_shards,
15398 &mut next_block_index,
15399 FormatError::ZstdDecompressedSizeMismatch {
15400 expected: directory_hint_payload.len() + 1,
15401 actual: directory_hint_payload.len(),
15402 },
15403 );
15404
15405 let dictionary_payload = b"dictionary metadata object";
15406 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
15407 assert_metadata_object_from_compressed(
15408 &{
15409 let mut bytes = dictionary_compressed.clone();
15410 bytes.push(0);
15411 bytes
15412 },
15413 dictionary_payload.len(),
15414 &subkeys,
15415 &volume_header,
15416 &crypto_header,
15417 &subkeys.dictionary_key,
15418 &subkeys.index_nonce_seed,
15419 b"dict",
15420 0,
15421 BlockKind::DictionaryData,
15422 BlockKind::DictionaryParity,
15423 crypto_header.index_root_fec_data_shards,
15424 crypto_header.index_root_fec_parity_shards,
15425 &mut next_block_index,
15426 FormatError::TrailingBytesAfterZstdFrame,
15427 );
15428 assert_metadata_object_from_compressed(
15429 &dictionary_compressed,
15430 dictionary_payload.len() + 1,
15431 &subkeys,
15432 &volume_header,
15433 &crypto_header,
15434 &subkeys.dictionary_key,
15435 &subkeys.index_nonce_seed,
15436 b"dict",
15437 0,
15438 BlockKind::DictionaryData,
15439 BlockKind::DictionaryParity,
15440 crypto_header.index_root_fec_data_shards,
15441 crypto_header.index_root_fec_parity_shards,
15442 &mut next_block_index,
15443 FormatError::ZstdDecompressedSizeMismatch {
15444 expected: dictionary_payload.len() + 1,
15445 actual: dictionary_payload.len(),
15446 },
15447 );
15448 }
15449
15450 #[test]
15451 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
15452 let volume_header = test_volume_header();
15453 let crypto_header = test_crypto_header();
15454 let subkeys = Subkeys::derive(
15455 &master_key(),
15456 &volume_header.archive_uuid,
15457 &volume_header.session_id,
15458 )
15459 .unwrap();
15460 let mut next_block_index = 0u64;
15461
15462 let index_root_payload = b"index root metadata object";
15463 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
15464 index_root_payload,
15465 &subkeys,
15466 &volume_header,
15467 &crypto_header,
15468 &subkeys.index_root_key,
15469 &subkeys.index_nonce_seed,
15470 b"idxroot",
15471 0,
15472 BlockKind::IndexRootData,
15473 &mut next_block_index,
15474 );
15475 let mut index_root_extent = index_root_extent;
15476 index_root_extent.encrypted_size = index_root_extent
15477 .encrypted_size
15478 .saturating_add(crypto_header.block_size);
15479 assert_eq!(
15480 load_metadata_object_from_parts(
15481 &index_root_records,
15482 ObjectLoadContext::index_root(
15483 &volume_header,
15484 &crypto_header,
15485 &subkeys,
15486 index_root_extent,
15487 ),
15488 index_root_payload.len() as u32,
15489 )
15490 .unwrap_err(),
15491 FormatError::InvalidArchive(
15492 "encrypted object size is not data_block_count * block_size"
15493 )
15494 );
15495
15496 let index_shard_payload = b"index shard metadata object";
15497 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
15498 index_shard_payload,
15499 &subkeys,
15500 &volume_header,
15501 &crypto_header,
15502 &subkeys.index_shard_key,
15503 &subkeys.index_nonce_seed,
15504 b"idxshard",
15505 1,
15506 BlockKind::IndexShardData,
15507 &mut next_block_index,
15508 );
15509 let mut index_shard_extent = index_shard_extent;
15510 index_shard_extent.encrypted_size = index_shard_extent
15511 .encrypted_size
15512 .saturating_add(crypto_header.block_size);
15513 assert_eq!(
15514 load_metadata_object_from_parts(
15515 &index_shard_records,
15516 ObjectLoadContext {
15517 volume_header: &volume_header,
15518 crypto_header: &crypto_header,
15519 extent: index_shard_extent,
15520 data_kind: BlockKind::IndexShardData,
15521 parity_kind: BlockKind::IndexShardParity,
15522 key: &subkeys.index_shard_key,
15523 nonce_seed: &subkeys.index_nonce_seed,
15524 domain: b"idxshard",
15525 counter: 1,
15526 class_data_shard_max: crypto_header.index_fec_data_shards,
15527 class_parity_shard_max: crypto_header.index_fec_parity_shards,
15528 },
15529 index_shard_payload.len() as u32,
15530 )
15531 .unwrap_err(),
15532 FormatError::InvalidArchive(
15533 "encrypted object size is not data_block_count * block_size"
15534 )
15535 );
15536
15537 let directory_hint_payload = b"directory hint metadata object";
15538 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
15539 directory_hint_payload,
15540 &subkeys,
15541 &volume_header,
15542 &crypto_header,
15543 &subkeys.dir_hint_key,
15544 &subkeys.index_nonce_seed,
15545 b"dirhint",
15546 0,
15547 BlockKind::DirectoryHintData,
15548 &mut next_block_index,
15549 );
15550 let mut directory_hint_extent = directory_hint_extent;
15551 directory_hint_extent.encrypted_size = directory_hint_extent
15552 .encrypted_size
15553 .saturating_add(crypto_header.block_size);
15554 assert_eq!(
15555 load_metadata_object_from_parts(
15556 &directory_hint_records,
15557 ObjectLoadContext {
15558 volume_header: &volume_header,
15559 crypto_header: &crypto_header,
15560 extent: directory_hint_extent,
15561 data_kind: BlockKind::DirectoryHintData,
15562 parity_kind: BlockKind::DirectoryHintParity,
15563 key: &subkeys.dir_hint_key,
15564 nonce_seed: &subkeys.index_nonce_seed,
15565 domain: b"dirhint",
15566 counter: 0,
15567 class_data_shard_max: crypto_header.index_fec_data_shards,
15568 class_parity_shard_max: crypto_header.index_fec_parity_shards,
15569 },
15570 directory_hint_payload.len() as u32,
15571 )
15572 .unwrap_err(),
15573 FormatError::InvalidArchive(
15574 "encrypted object size is not data_block_count * block_size"
15575 )
15576 );
15577
15578 let dictionary_payload = b"dictionary metadata object";
15579 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
15580 dictionary_payload,
15581 &subkeys,
15582 &volume_header,
15583 &crypto_header,
15584 &subkeys.dictionary_key,
15585 &subkeys.index_nonce_seed,
15586 b"dict",
15587 0,
15588 BlockKind::DictionaryData,
15589 &mut next_block_index,
15590 );
15591 let mut dictionary_extent = dictionary_extent;
15592 dictionary_extent.encrypted_size = dictionary_extent
15593 .encrypted_size
15594 .saturating_add(crypto_header.block_size);
15595 assert_eq!(
15596 load_metadata_object_from_parts(
15597 &dictionary_records,
15598 ObjectLoadContext {
15599 volume_header: &volume_header,
15600 crypto_header: &crypto_header,
15601 extent: dictionary_extent,
15602 data_kind: BlockKind::DictionaryData,
15603 parity_kind: BlockKind::DictionaryParity,
15604 key: &subkeys.dictionary_key,
15605 nonce_seed: &subkeys.index_nonce_seed,
15606 domain: b"dict",
15607 counter: 0,
15608 class_data_shard_max: crypto_header.index_root_fec_data_shards,
15609 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
15610 },
15611 dictionary_payload.len() as u32,
15612 )
15613 .unwrap_err(),
15614 FormatError::InvalidArchive(
15615 "encrypted object size is not data_block_count * block_size"
15616 )
15617 );
15618 }
15619
15620 #[test]
15621 fn opens_complete_multi_volume_archive() {
15622 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
15623 let archive = write_archive(
15624 &files,
15625 &master_key(),
15626 WriterOptions {
15627 stripe_width: 2,
15628 volume_loss_tolerance: 1,
15629 ..single_stream_options()
15630 },
15631 )
15632 .unwrap();
15633 assert_eq!(archive.volumes.len(), 2);
15634
15635 let volume_refs = archive
15636 .volumes
15637 .iter()
15638 .map(Vec::as_slice)
15639 .collect::<Vec<_>>();
15640 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15641
15642 assert_eq!(opened.volume_header.stripe_width, 2);
15643 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
15644 assert_eq!(
15645 opened.extract_file("alpha.txt").unwrap(),
15646 Some(b"hello from volume stripes".to_vec())
15647 );
15648 opened.verify().unwrap();
15649 }
15650
15651 #[test]
15652 fn recovers_from_one_missing_volume_when_parity_allows() {
15653 let files = [RegularFile::new("alpha.txt", b"recover me")];
15654 let archive = write_archive(
15655 &files,
15656 &master_key(),
15657 WriterOptions {
15658 stripe_width: 2,
15659 volume_loss_tolerance: 1,
15660 ..single_stream_options()
15661 },
15662 )
15663 .unwrap();
15664
15665 let recovered =
15666 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
15667 assert_eq!(
15668 recovered.extract_file("alpha.txt").unwrap(),
15669 Some(b"recover me".to_vec())
15670 );
15671 recovered.verify().unwrap();
15672 }
15673
15674 #[test]
15675 fn recovers_from_crc_corrupted_block_when_parity_allows() {
15676 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
15677 let archive = write_archive(
15678 &files,
15679 &master_key(),
15680 WriterOptions {
15681 stripe_width: 2,
15682 volume_loss_tolerance: 1,
15683 ..single_stream_options()
15684 },
15685 )
15686 .unwrap();
15687 let mut volumes = archive.volumes.clone();
15688 corrupt_first_block_record_payload(&mut volumes[0]);
15689
15690 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15691 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15692
15693 assert_eq!(
15694 recovered.extract_file("alpha.txt").unwrap(),
15695 Some(b"repair corrupt block".to_vec())
15696 );
15697 recovered.verify().unwrap();
15698 }
15699
15700 #[test]
15701 fn rejects_multi_volume_count_mismatch_without_tolerance() {
15702 let files = [RegularFile::new("alpha.txt", b"count check")];
15703 let archive = write_archive(
15704 &files,
15705 &master_key(),
15706 WriterOptions {
15707 stripe_width: 3,
15708 volume_loss_tolerance: 0,
15709 ..single_stream_options()
15710 },
15711 )
15712 .unwrap();
15713
15714 assert_eq!(
15715 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
15716 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15717 );
15718 }
15719
15720 #[test]
15721 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
15722 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
15723 let archive = write_archive(
15724 &files,
15725 &master_key(),
15726 WriterOptions {
15727 stripe_width: 2,
15728 volume_loss_tolerance: 1,
15729 ..single_stream_options()
15730 },
15731 )
15732 .unwrap();
15733
15734 let mut bad_first = archive.volumes[0].clone();
15735 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
15736 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
15737 });
15738
15739 open_archive_volumes(
15740 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
15741 &master_key(),
15742 )
15743 .unwrap();
15744 }
15745
15746 #[test]
15747 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
15748 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
15749 let archive = write_archive(
15750 &files,
15751 &master_key(),
15752 WriterOptions {
15753 stripe_width: 2,
15754 volume_loss_tolerance: 1,
15755 ..single_stream_options()
15756 },
15757 )
15758 .unwrap();
15759 let mut volumes = archive.volumes.clone();
15760
15761 let mut corrupted = false;
15762 for volume in &mut volumes {
15763 if let Some(slot) =
15764 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
15765 {
15766 corrupt_block_record_payload_at_slot(volume, *slot);
15767 corrupted = true;
15768 break;
15769 }
15770 }
15771 assert!(corrupted, "expected an IndexRootData record");
15772
15773 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15774 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15775 assert_eq!(
15776 opened.extract_file("alpha.txt").unwrap(),
15777 Some(b"repair meta root".to_vec())
15778 );
15779 opened.verify().unwrap();
15780 }
15781
15782 #[test]
15783 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
15784 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
15785 let archive = write_archive(
15786 &files,
15787 &master_key(),
15788 WriterOptions {
15789 stripe_width: 2,
15790 volume_loss_tolerance: 1,
15791 ..single_stream_options()
15792 },
15793 )
15794 .unwrap();
15795 let mut volumes = archive.volumes.clone();
15796
15797 let mut corrupted = false;
15798 for volume in &mut volumes {
15799 if let Some(slot) =
15800 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
15801 {
15802 corrupt_block_record_payload_at_slot(volume, *slot);
15803 corrupted = true;
15804 break;
15805 }
15806 }
15807 assert!(corrupted, "expected an IndexShardData record");
15808
15809 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
15810 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
15811 assert_eq!(
15812 opened.extract_file("alpha.txt").unwrap(),
15813 Some(b"repair meta shard".to_vec())
15814 );
15815 opened.verify().unwrap();
15816 }
15817
15818 #[test]
15819 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
15820 let files = [RegularFile::new(
15821 "alpha.txt",
15822 b"bitrot parity is not volume loss",
15823 )];
15824 let archive = write_archive(
15825 &files,
15826 &master_key(),
15827 WriterOptions {
15828 stripe_width: 2,
15829 volume_loss_tolerance: 0,
15830 bit_rot_buffer_pct: 1,
15831 ..single_stream_options()
15832 },
15833 )
15834 .unwrap();
15835
15836 assert_eq!(
15837 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
15838 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
15839 );
15840 }
15841
15842 #[test]
15843 fn repairs_crc_erasure_only_within_parity_budget() {
15844 let payload = pseudo_random_bytes(12_000);
15845 let archive = write_archive(
15846 &[RegularFile::new("rot.bin", &payload)],
15847 &master_key(),
15848 small_block_recovery_options(),
15849 )
15850 .unwrap();
15851 let payload_slots = first_payload_data_run_slots(&archive.bytes);
15852 assert!(
15853 payload_slots.len() >= 2,
15854 "fixture must contain a multi-block payload object"
15855 );
15856
15857 let mut one_erasure = archive.bytes.clone();
15858 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
15859 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
15860 assert_eq!(
15861 repaired.extract_file("rot.bin").unwrap(),
15862 Some(payload.clone())
15863 );
15864
15865 let mut two_erasures = archive.bytes.clone();
15866 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
15867 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
15868 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
15869 assert_eq!(
15870 unrepaired.extract_file("rot.bin").unwrap_err(),
15871 FormatError::FecTooFewAvailableShards
15872 );
15873 }
15874
15875 #[test]
15876 fn verify_rejects_missing_required_object_block_extent() {
15877 let (mut opened, missing_block) = multi_envelope_reader_fixture();
15878 assert!(opened.blocks.remove(&missing_block).is_some());
15879
15880 assert_eq!(
15881 opened.verify().unwrap_err(),
15882 FormatError::FecTooFewAvailableShards
15883 );
15884 }
15885
15886 #[test]
15887 fn parity_crc_erasure_does_not_hide_authenticated_data() {
15888 let payload = pseudo_random_bytes(12_000);
15889 let archive = write_archive(
15890 &[RegularFile::new("parity-erasure.bin", &payload)],
15891 &master_key(),
15892 parity_rich_recovery_options(),
15893 )
15894 .unwrap();
15895 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15896 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
15897 assert!(
15898 parity_slots.len() >= 2,
15899 "fixture must contain redundant parity shards"
15900 );
15901 let mut corrupted = archive.bytes;
15902 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15903 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
15904
15905 let opened = open_archive(&corrupted, &master_key()).unwrap();
15906 assert_eq!(
15907 opened.extract_file("parity-erasure.bin").unwrap(),
15908 Some(payload)
15909 );
15910 opened.verify().unwrap();
15911 }
15912
15913 #[test]
15914 fn repair_patches_restore_crc_erased_payload_block() {
15915 let payload = pseudo_random_bytes(12_000);
15916 let archive = write_archive(
15917 &[RegularFile::new("rot.bin", &payload)],
15918 &master_key(),
15919 small_block_recovery_options(),
15920 )
15921 .unwrap();
15922 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15923 let mut corrupted = archive.bytes.clone();
15924 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
15925
15926 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15927 opened.verify().unwrap();
15928 let patches = opened.repair_patches().unwrap();
15929 assert_eq!(patches.len(), 1);
15930 apply_repair_patches(&mut corrupted, &patches);
15931
15932 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15933 repaired.verify().unwrap();
15934 assert!(repaired.repair_patches().unwrap().is_empty());
15935 }
15936
15937 #[test]
15938 fn repair_patches_restore_crc_erased_payload_parity_block() {
15939 let payload = pseudo_random_bytes(12_000);
15940 let archive = write_archive(
15941 &[RegularFile::new("parity-erasure.bin", &payload)],
15942 &master_key(),
15943 parity_rich_recovery_options(),
15944 )
15945 .unwrap();
15946 let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
15947 let mut corrupted = archive.bytes.clone();
15948 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
15949
15950 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
15951 opened.verify().unwrap();
15952 let patches = opened.repair_patches().unwrap();
15953 assert_eq!(patches.len(), 1);
15954 apply_repair_patches(&mut corrupted, &patches);
15955
15956 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
15957 repaired.verify().unwrap();
15958 assert!(repaired.repair_patches().unwrap().is_empty());
15959 }
15960
15961 #[test]
15962 fn recovers_physical_odd_block_size_from_cmra_authority() {
15963 let archive = write_archive(
15964 &[RegularFile::new("odd-block.txt", b"payload")],
15965 &master_key(),
15966 small_block_recovery_options(),
15967 )
15968 .unwrap();
15969 let mut malformed = archive.bytes;
15970 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
15971 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
15972 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
15973
15974 let opened = open_archive(&malformed, &master_key()).unwrap();
15975 assert_ne!(opened.crypto_header.block_size, 4097);
15976 assert_eq!(
15977 opened.extract_file("odd-block.txt").unwrap(),
15978 Some(b"payload".to_vec())
15979 );
15980 opened.verify().unwrap();
15981 }
15982
15983 #[test]
15984 fn repairs_structurally_malformed_payload_block_slots() {
15985 let payload = pseudo_random_bytes(12_000);
15986 let archive = write_archive(
15987 &[RegularFile::new("structural-block.bin", &payload)],
15988 &master_key(),
15989 small_block_recovery_options(),
15990 )
15991 .unwrap();
15992 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
15993
15994 let mut bad_magic = archive.bytes.clone();
15995 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
15996 assert_eq!(
15997 open_archive(&bad_magic, &master_key())
15998 .unwrap()
15999 .extract_file("structural-block.bin")
16000 .unwrap(),
16001 Some(payload.clone())
16002 );
16003
16004 let mut bad_reserved = archive.bytes;
16005 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
16006 assert_eq!(
16007 open_archive(&bad_reserved, &master_key())
16008 .unwrap()
16009 .extract_file("structural-block.bin")
16010 .unwrap(),
16011 Some(payload)
16012 );
16013 }
16014
16015 #[test]
16016 fn repair_patches_restore_structurally_malformed_payload_block_slot() {
16017 let payload = pseudo_random_bytes(12_000);
16018 let archive = write_archive(
16019 &[RegularFile::new("structural-patch.bin", &payload)],
16020 &master_key(),
16021 small_block_recovery_options(),
16022 )
16023 .unwrap();
16024 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
16025 let mut corrupted = archive.bytes.clone();
16026 corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
16027
16028 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
16029 opened.verify().unwrap();
16030 assert_eq!(
16031 opened.extract_file("structural-patch.bin").unwrap(),
16032 Some(payload)
16033 );
16034 let patches = opened.repair_patches().unwrap();
16035 assert_eq!(patches.len(), 1);
16036 apply_repair_patches(&mut corrupted, &patches);
16037
16038 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
16039 repaired.verify().unwrap();
16040 assert!(repaired.repair_patches().unwrap().is_empty());
16041 }
16042
16043 #[test]
16044 fn repairs_structurally_malformed_index_root_block_slot() {
16045 let archive = write_archive(
16046 &[RegularFile::new(
16047 "structural-index-root.txt",
16048 b"metadata repair",
16049 )],
16050 &master_key(),
16051 small_block_recovery_options(),
16052 )
16053 .unwrap();
16054 let index_root_slot =
16055 first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
16056 let mut corrupted = archive.bytes;
16057 corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
16058
16059 let opened = open_archive(&corrupted, &master_key()).unwrap();
16060 assert_eq!(
16061 opened.extract_file("structural-index-root.txt").unwrap(),
16062 Some(b"metadata repair".to_vec())
16063 );
16064 opened.verify().unwrap();
16065 }
16066
16067 #[test]
16068 fn rejects_parity_block_with_last_data_flag() {
16069 let archive = write_archive(
16070 &[RegularFile::new("parity-flag.txt", b"payload")],
16071 &master_key(),
16072 small_block_recovery_options(),
16073 )
16074 .unwrap();
16075 let parity_slot =
16076 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
16077 let mut malformed = archive.bytes;
16078 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
16079 record.flags = 0x01;
16080 });
16081
16082 assert_eq!(
16083 open_archive(&malformed, &master_key()).unwrap_err(),
16084 FormatError::ParityBlockHasLastDataFlag
16085 );
16086 }
16087
16088 #[test]
16089 fn rejects_missing_and_duplicate_payload_last_data_flags() {
16090 let payload = pseudo_random_bytes(12_000);
16091 let archive = write_archive(
16092 &[RegularFile::new("flags.bin", &payload)],
16093 &master_key(),
16094 small_block_recovery_options(),
16095 )
16096 .unwrap();
16097 let payload_slots = first_payload_data_run_slots(&archive.bytes);
16098 assert!(
16099 payload_slots.len() >= 2,
16100 "fixture must contain a multi-block payload object"
16101 );
16102
16103 let mut duplicate_last = archive.bytes.clone();
16104 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
16105 record.flags = 0x01;
16106 });
16107 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
16108 assert_eq!(
16109 opened.extract_file("flags.bin").unwrap_err(),
16110 FormatError::InvalidArchive("object last-data flag is not on the final data block")
16111 );
16112
16113 let mut missing_last = archive.bytes;
16114 mutate_block_record_at_slot(
16115 &mut missing_last,
16116 *payload_slots.last().unwrap(),
16117 |record| {
16118 record.flags = 0;
16119 },
16120 );
16121 let opened = open_archive(&missing_last, &master_key()).unwrap();
16122 assert_eq!(
16123 opened.extract_file("flags.bin").unwrap_err(),
16124 FormatError::InvalidArchive("object last-data flag is not on the final data block")
16125 );
16126 }
16127
16128 #[test]
16129 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
16130 let files = [RegularFile::new(
16131 "footer-copy.txt",
16132 b"survives one bad footer",
16133 )];
16134 let archive = write_archive(
16135 &files,
16136 &master_key(),
16137 WriterOptions {
16138 stripe_width: 2,
16139 volume_loss_tolerance: 1,
16140 ..single_stream_options()
16141 },
16142 )
16143 .unwrap();
16144 let mut volumes = archive.volumes.clone();
16145 corrupt_manifest_footer_hmac(&mut volumes[0]);
16146
16147 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16148 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
16149 assert_eq!(opened.manifest_footer.volume_index, 0);
16150 assert_eq!(opened.volume_header.volume_index, 0);
16151 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
16152 assert_eq!(
16153 opened.extract_file("footer-copy.txt").unwrap(),
16154 Some(b"survives one bad footer".to_vec())
16155 );
16156 opened.verify().unwrap();
16157 }
16158
16159 #[test]
16160 fn manifest_footer_corruption_requires_trusted_sidecar() {
16161 let archive = write_archive(
16162 &[RegularFile::new("footer.txt", b"sidecar authority")],
16163 &master_key(),
16164 single_stream_options(),
16165 )
16166 .unwrap();
16167 let manifest_offset = terminal_material_offset(&archive.bytes);
16168 let mut corrupted = archive.bytes.clone();
16169 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
16170 corrupt_v41_terminal_recovery(&mut corrupted);
16171
16172 assert!(open_archive(&corrupted, &master_key()).is_err());
16173
16174 let opened =
16175 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
16176 .unwrap();
16177 assert!(opened.volume_trailer.is_none());
16178 assert_eq!(
16179 opened.extract_file("footer.txt").unwrap(),
16180 Some(b"sidecar authority".to_vec())
16181 );
16182 opened.verify().unwrap();
16183 }
16184
16185 #[test]
16186 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
16187 let archive = write_archive(
16188 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
16189 &master_key(),
16190 single_stream_options(),
16191 )
16192 .unwrap();
16193 let strict_options = ReaderOptions {
16194 max_trailing_garbage_scan: 0,
16195 ..ReaderOptions::default()
16196 };
16197
16198 let manifest_offset = terminal_material_offset(&archive.bytes);
16199 for offset in [
16200 manifest_offset + 71,
16201 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
16202 ] {
16203 let mut corrupted = archive.bytes.clone();
16204 corrupted[offset] ^= 0x01;
16205 open_archive(&corrupted, &master_key()).unwrap();
16206 }
16207
16208 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16209 for offset in [
16210 trailer_offset + 75,
16211 trailer_offset + TRAILER_HMAC_COVERED_LEN,
16212 ] {
16213 let mut corrupted = archive.bytes.clone();
16214 corrupted[offset] ^= 0x01;
16215 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
16216 }
16217
16218 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
16219 let mut header =
16220 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16221 .unwrap();
16222 header.manifest_footer_offset += 1;
16223 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16224 assert_eq!(
16225 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
16226 .unwrap_err(),
16227 FormatError::HmacMismatch {
16228 structure: "BootstrapSidecarHeader"
16229 }
16230 );
16231
16232 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
16233 let mut header =
16234 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
16235 header.sidecar_hmac[0] ^= 1;
16236 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
16237 assert_eq!(
16238 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
16239 .unwrap_err(),
16240 FormatError::HmacMismatch {
16241 structure: "BootstrapSidecarHeader"
16242 }
16243 );
16244
16245 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
16246 let header =
16247 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
16248 .unwrap();
16249 let mut header_bytes = header.to_bytes();
16250 header_bytes[124] ^= 0x01;
16251 let crc = crc32c::crc32c(&header_bytes[..124]);
16252 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
16253 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
16254 let opened = open_archive_with_bootstrap_sidecar(
16255 &archive.bytes,
16256 &non_covered_sidecar,
16257 &master_key(),
16258 )
16259 .unwrap();
16260 assert_eq!(
16261 opened.extract_file("hmac-boundary.txt").unwrap(),
16262 Some(b"boundary bytes".to_vec())
16263 );
16264 }
16265
16266 #[test]
16267 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
16268 let archive = write_archive(
16269 &[RegularFile::new("volume-index.txt", b"identity")],
16270 &master_key(),
16271 single_stream_options(),
16272 )
16273 .unwrap();
16274
16275 let mut bad_trailer = archive.bytes.clone();
16276 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
16277 trailer.volume_index = 1;
16278 });
16279 open_archive(&bad_trailer, &master_key()).unwrap();
16280
16281 let mut bad_manifest = archive.bytes;
16282 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
16283 footer.volume_index = 1;
16284 });
16285 open_archive(&bad_manifest, &master_key()).unwrap();
16286 }
16287
16288 #[test]
16289 fn rejects_same_key_header_terminal_material_splice() {
16290 let first = write_archive(
16291 &[RegularFile::new("splice.txt", b"same shape")],
16292 &master_key(),
16293 single_stream_options(),
16294 )
16295 .unwrap();
16296 let second = write_archive(
16297 &[RegularFile::new("splice.txt", b"same shape")],
16298 &master_key(),
16299 single_stream_options(),
16300 )
16301 .unwrap();
16302 assert_ne!(first.archive_uuid, second.archive_uuid);
16303 assert_eq!(
16304 terminal_material_offset(&first.bytes),
16305 terminal_material_offset(&second.bytes)
16306 );
16307 assert_eq!(first.bytes.len(), second.bytes.len());
16308
16309 let terminal_offset = terminal_material_offset(&first.bytes);
16310 let mut spliced = first.bytes.clone();
16311 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
16312
16313 assert_eq!(
16314 open_archive(&spliced, &master_key()).unwrap_err(),
16315 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16316 );
16317 }
16318
16319 #[test]
16320 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
16321 let kdf_params = crate::crypto::KdfParams::Argon2id {
16322 t_cost: 1,
16323 m_cost_kib: 8,
16324 parallelism: 1,
16325 salt: b"0123456789abcdef".to_vec(),
16326 };
16327 let archive = write_archive_with_kdf(
16328 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
16329 &master_key(),
16330 single_stream_options(),
16331 &kdf_params,
16332 )
16333 .unwrap();
16334 let mut mutated = archive.bytes.clone();
16335 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
16336 let subkeys = Subkeys::derive(
16337 &master_key(),
16338 &volume_header.archive_uuid,
16339 &volume_header.session_id,
16340 )
16341 .unwrap();
16342
16343 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
16344 let crypto_region = image
16345 .regions
16346 .iter_mut()
16347 .find(|region| region.region_type == 2)
16348 .unwrap();
16349 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
16350 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
16351 crypto_region.bytes[salt_start] ^= 0x01;
16352 let hmac = compute_hmac(
16353 HmacDomain::CryptoHeader,
16354 &subkeys.mac_key,
16355 &volume_header.archive_uuid,
16356 &volume_header.session_id,
16357 &crypto_region.bytes[..hmac_offset],
16358 );
16359 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
16360 });
16361
16362 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16363 let locator = final_recovery_locator(&mutated);
16364 let crypto_start = volume_header.crypto_header_offset as usize;
16365 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16366 let parsed_crypto = CryptoHeader::parse(
16367 &mutated[crypto_start..crypto_end],
16368 volume_header.crypto_header_length,
16369 )
16370 .unwrap();
16371 assert_eq!(
16372 parse_locator_cmra_candidate(
16373 &mutated,
16374 final_offset,
16375 locator,
16376 KeyHoldingTerminalContext {
16377 subkeys: &subkeys,
16378 volume_header: &volume_header,
16379 crypto_header: &parsed_crypto.fixed,
16380 crypto_header_bytes: &mutated[crypto_start..crypto_end],
16381 },
16382 )
16383 .unwrap_err(),
16384 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
16385 );
16386 assert!(open_archive(&mutated, &master_key()).is_err());
16387 }
16388
16389 #[test]
16390 fn recovers_physical_crypto_header_splice_from_cmra_authority() {
16391 let base = WriterOptions {
16392 archive_uuid: Some([0x11; 16]),
16393 session_id: Some([0x22; 16]),
16394 ..small_block_recovery_options()
16395 };
16396 let same_archive = WriterOptions {
16397 archive_uuid: Some([0x11; 16]),
16398 session_id: Some([0x33; 16]),
16399 ..small_block_recovery_options()
16400 };
16401
16402 let first = write_archive(
16403 &[RegularFile::new("splice.txt", b"same shape")],
16404 &master_key(),
16405 base,
16406 )
16407 .unwrap();
16408 let second = write_archive(
16409 &[RegularFile::new("splice.txt", b"same shape")],
16410 &master_key(),
16411 same_archive,
16412 )
16413 .unwrap();
16414
16415 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16416 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
16417 let crypto_start = volume_header.crypto_header_offset as usize;
16418 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
16419 let second_crypto_end = second_volume_header.crypto_header_offset as usize
16420 + second_volume_header.crypto_header_length as usize;
16421 assert_eq!(crypto_end, second_crypto_end);
16422
16423 let mut spliced = first.bytes.clone();
16424 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
16425
16426 let opened = open_archive(&spliced, &master_key()).unwrap();
16427 assert_eq!(
16428 opened.crypto_header_bytes,
16429 first.bytes[crypto_start..crypto_end].to_vec()
16430 );
16431 assert_eq!(
16432 opened.extract_file("splice.txt").unwrap(),
16433 Some(b"same shape".to_vec())
16434 );
16435 opened.verify().unwrap();
16436 }
16437
16438 #[test]
16439 fn rejects_same_key_object_splice_with_session_mismatch() {
16440 let first = write_archive(
16441 &[RegularFile::new("splice.txt", b"same shape")],
16442 &master_key(),
16443 WriterOptions {
16444 archive_uuid: Some([0x11; 16]),
16445 session_id: Some([0x22; 16]),
16446 ..single_stream_options()
16447 },
16448 )
16449 .unwrap();
16450 let second = write_archive(
16451 &[RegularFile::new("splice.txt", b"same shape")],
16452 &master_key(),
16453 WriterOptions {
16454 archive_uuid: Some([0x11; 16]),
16455 session_id: Some([0x33; 16]),
16456 ..single_stream_options()
16457 },
16458 )
16459 .unwrap();
16460
16461 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
16462 let crypto_end = volume_header.crypto_header_offset as usize
16463 + volume_header.crypto_header_length as usize;
16464 let terminal_offset = terminal_material_offset(&first.bytes);
16465 let second_terminal_offset = terminal_material_offset(&second.bytes);
16466 assert_eq!(terminal_offset, second_terminal_offset);
16467
16468 let mut spliced = first.bytes.clone();
16469 spliced[crypto_end..terminal_offset]
16470 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
16471
16472 assert_eq!(
16473 open_archive(&spliced, &master_key()).unwrap_err(),
16474 FormatError::AeadFailure
16475 );
16476 }
16477
16478 #[test]
16479 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
16480 let archive = write_archive(
16481 &[RegularFile::new(
16482 "trailer-range.txt",
16483 b"authenticated ranges",
16484 )],
16485 &master_key(),
16486 single_stream_options(),
16487 )
16488 .unwrap();
16489 let strict_options = ReaderOptions {
16490 max_trailing_garbage_scan: 0,
16491 ..ReaderOptions::default()
16492 };
16493 let bytes = archive.bytes;
16494 let manifest_offset = terminal_material_offset(&bytes);
16495 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
16496
16497 let mut wrong_footer_length = bytes.clone();
16498 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
16499 trailer.manifest_footer_length = 42;
16500 });
16501 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
16502 .unwrap();
16503
16504 for (label, offset) in [
16505 (
16506 "offset before trailer by 1",
16507 manifest_offset.saturating_sub(1),
16508 ),
16509 ("offset after trailer", manifest_offset + 1),
16510 ("offset at stream start", 0),
16511 ("offset at trailer", trailer_offset),
16512 ("offset beyond trailer", trailer_offset + 4),
16513 ] {
16514 let mut wrong_footer_offset = bytes.clone();
16515 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16516 trailer.manifest_footer_offset = offset as u64;
16517 });
16518 open_archive(&wrong_footer_offset, &master_key())
16519 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
16520 }
16521
16522 let mut wrong_bytes_written = bytes.clone();
16523 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
16524 trailer.bytes_written += 1;
16525 });
16526 open_archive(&wrong_bytes_written, &master_key()).unwrap();
16527
16528 let mut wrong_block_count = bytes.clone();
16529 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
16530 trailer.block_count += 1;
16531 });
16532 open_archive(&wrong_block_count, &master_key()).unwrap();
16533
16534 let mut wrong_footer_offset = bytes.clone();
16535 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
16536 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
16537 });
16538 open_archive(&wrong_footer_offset, &master_key()).unwrap();
16539 }
16540
16541 #[test]
16542 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
16543 let archive = write_archive(
16544 &[RegularFile::new(
16545 "trailer-trailing-scan.txt",
16546 b"trailer scan boundaries",
16547 )],
16548 &master_key(),
16549 single_stream_options(),
16550 )
16551 .unwrap();
16552 let options = ReaderOptions {
16553 max_trailing_garbage_scan: 8,
16554 ..ReaderOptions::default()
16555 };
16556
16557 let mut within_scan = archive.bytes.clone();
16558 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
16559 let opened =
16560 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
16561 assert_eq!(
16562 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
16563 Some(b"trailer scan boundaries".to_vec())
16564 );
16565
16566 let mut beyond_scan = archive.bytes.clone();
16567 beyond_scan.resize(
16568 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
16569 0xAA,
16570 );
16571 assert_eq!(
16572 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
16573 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
16574 );
16575 }
16576
16577 #[test]
16578 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
16579 let archive = write_archive(
16580 &[RegularFile::new("index-root-size.txt", b"extent size")],
16581 &master_key(),
16582 single_stream_options(),
16583 )
16584 .unwrap();
16585 let mut malformed = archive.bytes;
16586 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
16587 .expect("archive should contain IndexRootData");
16588 mutate_block_record_at_slot(&mut malformed, slot, |record| {
16589 record.payload[0] ^= 0x55;
16590 });
16591
16592 assert_eq!(
16593 open_archive(&malformed, &master_key()).unwrap_err(),
16594 FormatError::AeadFailure
16595 );
16596 }
16597
16598 #[test]
16599 fn rejects_block_record_at_wrong_stripe_position() {
16600 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
16601 let archive = write_archive(
16602 &files,
16603 &master_key(),
16604 WriterOptions {
16605 stripe_width: 2,
16606 volume_loss_tolerance: 1,
16607 ..single_stream_options()
16608 },
16609 )
16610 .unwrap();
16611 let mut volumes = archive.volumes.clone();
16612 mutate_first_block_record(&mut volumes[0], |record| {
16613 record.block_index += 2;
16614 });
16615
16616 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
16617 assert_eq!(
16618 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
16619 FormatError::InvalidArchive("BlockRecord index does not match volume position")
16620 );
16621 }
16622
16623 #[test]
16624 fn rejects_decreasing_block_record_index_in_required_region() {
16625 let archive = write_archive(
16626 &[RegularFile::new("alpha.txt", b"decreasing block index")],
16627 &master_key(),
16628 single_stream_options(),
16629 )
16630 .unwrap();
16631 assert!(block_record_slots(&archive.bytes).len() >= 2);
16632
16633 let mut malformed = archive.bytes;
16634 mutate_block_record_at_slot(&mut malformed, 1, |record| {
16635 record.block_index = 0;
16636 });
16637
16638 assert_eq!(
16639 open_archive(&malformed, &master_key()).unwrap_err(),
16640 FormatError::InvalidArchive("BlockRecord index does not match volume position")
16641 );
16642 }
16643
16644 #[test]
16645 fn rejects_duplicate_authenticated_volume_indexes() {
16646 let files = [RegularFile::new("alpha.txt", b"duplicates")];
16647 let archive = write_archive(
16648 &files,
16649 &master_key(),
16650 WriterOptions {
16651 stripe_width: 2,
16652 volume_loss_tolerance: 1,
16653 ..single_stream_options()
16654 },
16655 )
16656 .unwrap();
16657
16658 assert_eq!(
16659 open_archive_volumes(
16660 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
16661 &master_key()
16662 )
16663 .unwrap_err(),
16664 FormatError::InvalidArchive("duplicate authenticated volume index")
16665 );
16666 }
16667
16668 #[test]
16669 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
16670 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
16671 let archive = write_archive(
16672 &files,
16673 &master_key(),
16674 WriterOptions {
16675 stripe_width: 2,
16676 volume_loss_tolerance: 1,
16677 ..single_stream_options()
16678 },
16679 )
16680 .unwrap();
16681 let mut conflicting = archive.volumes[0].clone();
16682 corrupt_first_block_record_payload(&mut conflicting);
16683
16684 assert_eq!(
16685 open_archive_volumes(
16686 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
16687 &master_key()
16688 )
16689 .unwrap_err(),
16690 FormatError::InvalidArchive("duplicate authenticated volume index")
16691 );
16692 }
16693
16694 fn directory_hint_table_from_rows(
16695 hint_shard_index: u64,
16696 rows: &[(Vec<u8>, Vec<u32>)],
16697 shard_count: u32,
16698 ) -> DirectoryHintTable {
16699 let mut entries = Vec::new();
16700 let mut shard_row_indexes = Vec::new();
16701 let mut string_pool = Vec::new();
16702
16703 for (path, rows) in rows {
16704 let path_offset = if path.is_empty() {
16705 0
16706 } else {
16707 let offset = string_pool.len() as u64;
16708 string_pool.extend_from_slice(path);
16709 offset
16710 };
16711 let shard_list_start_index = shard_row_indexes.len() as u32;
16712 shard_row_indexes.extend_from_slice(rows);
16713 entries.push(DirectoryHintEntry {
16714 dir_hash: hash_prefix(path),
16715 path_offset,
16716 path_length: path.len() as u32,
16717 shard_list_start_index,
16718 shard_count: rows.len() as u32,
16719 });
16720 }
16721
16722 let table_bytes =
16723 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
16724 let locating = DirectoryHintShardEntry {
16725 hint_shard_index,
16726 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
16727 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
16728 first_block_index: 0,
16729 data_block_count: 1,
16730 parity_block_count: 0,
16731 encrypted_size: 4096,
16732 decompressed_size: table_bytes.len() as u32,
16733 entry_count: rows.len() as u64,
16734 };
16735 DirectoryHintTable::parse(
16736 &table_bytes,
16737 &locating,
16738 shard_count,
16739 MetadataLimits::default(),
16740 )
16741 .unwrap()
16742 }
16743
16744 fn directory_hint_table_bytes(
16745 hint_shard_index: u64,
16746 entries: Vec<DirectoryHintEntry>,
16747 shard_row_indexes: Vec<u32>,
16748 string_pool: Vec<u8>,
16749 ) -> Vec<u8> {
16750 let header_len = DirectoryHintTableHeader {
16751 version: 1,
16752 hint_shard_index,
16753 entry_count: 0,
16754 entry_table_offset: 0,
16755 shard_list_offset: 0,
16756 string_pool_offset: 0,
16757 string_pool_size: 0,
16758 }
16759 .to_bytes()
16760 .len();
16761 let entry_len = entries
16762 .first()
16763 .map(|entry| entry.to_bytes().len())
16764 .unwrap_or(0);
16765 let shard_list_offset = if entries.is_empty() {
16766 0
16767 } else {
16768 header_len + entries.len() * entry_len
16769 };
16770 let string_pool_offset = if string_pool.is_empty() {
16771 0
16772 } else {
16773 shard_list_offset + shard_row_indexes.len() * 4
16774 };
16775
16776 let header = DirectoryHintTableHeader {
16777 version: 1,
16778 hint_shard_index,
16779 entry_count: entries.len() as u64,
16780 entry_table_offset: if entries.is_empty() {
16781 0
16782 } else {
16783 header_len as u64
16784 },
16785 shard_list_offset: shard_list_offset as u64,
16786 string_pool_offset: string_pool_offset as u64,
16787 string_pool_size: string_pool.len() as u64,
16788 };
16789
16790 let mut out = Vec::new();
16791 out.extend_from_slice(&header.to_bytes());
16792 for entry in entries {
16793 out.extend_from_slice(&entry.to_bytes());
16794 }
16795 for row in shard_row_indexes {
16796 out.extend_from_slice(&row.to_le_bytes());
16797 }
16798 out.extend_from_slice(&string_pool);
16799 out
16800 }
16801
16802 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
16803 let (record_offset, _) = first_block_record(volume);
16804 volume[record_offset + 16] ^= 0x55;
16805 }
16806
16807 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
16808 let (record_offset, _) = block_record_at_slot(volume, slot);
16809 volume[record_offset + 16] ^= 0x55;
16810 }
16811
16812 fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
16813 for patch in patches {
16814 let offset = patch.record_offset as usize;
16815 let end = offset + patch.record_bytes.len();
16816 volume[offset..end].copy_from_slice(&patch.record_bytes);
16817 }
16818 }
16819
16820 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
16821 let (record_offset, _) = block_record_at_slot(volume, slot);
16822 volume[record_offset] ^= 0x55;
16823 }
16824
16825 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
16826 let (record_offset, _) = block_record_at_slot(volume, slot);
16827 volume[record_offset + 14] = 0x01;
16828 }
16829
16830 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
16831 let manifest_offset = terminal_material_offset(volume);
16832 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
16833 }
16834
16835 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
16836 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16837 CriticalRecoveryLocator::parse(
16838 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
16839 )
16840 .unwrap()
16841 }
16842
16843 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
16844 let locator = final_recovery_locator(volume);
16845 let tuple = CmraDecoderTuple::from(locator);
16846 assert!(parity_shard_count < tuple.parity_shard_count);
16847 let cmra_offset = locator.cmra_offset as usize;
16848 let shard_size = tuple.shard_size as usize;
16849 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
16850 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
16851 let mut header = CriticalMetadataRecoveryHeader::parse(
16852 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
16853 )
16854 .unwrap();
16855 header.parity_shard_count = parity_shard_count;
16856
16857 let mut cmra =
16858 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
16859 cmra.extend_from_slice(&header.to_bytes());
16860 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
16861 for row in 0..kept_rows {
16862 let start = rows_start + row * row_len;
16863 cmra.extend_from_slice(&volume[start..start + row_len]);
16864 }
16865
16866 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
16867 out.extend_from_slice(&volume[..cmra_offset]);
16868 out.extend_from_slice(&cmra);
16869 let mut mirror = locator;
16870 mirror.locator_sequence = 1;
16871 mirror.cmra_length = cmra.len() as u32;
16872 mirror.cmra_parity_shard_count = parity_shard_count;
16873 out.extend_from_slice(&mirror.to_bytes());
16874 let final_locator = CriticalRecoveryLocator {
16875 volume_format_rev: locator.volume_format_rev,
16876 locator_sequence: 0,
16877 ..mirror
16878 };
16879 out.extend_from_slice(&final_locator.to_bytes());
16880 out
16881 }
16882
16883 fn rewrite_public_cmra_image(
16884 volume: &mut [u8],
16885 mutate: impl FnOnce(&mut CriticalMetadataImage),
16886 ) {
16887 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
16888 }
16889
16890 fn rewrite_root_auth_footer_revision_bytes(bytes: &mut [u8], revision: u16) {
16891 bytes[72..74].copy_from_slice(&revision.to_le_bytes());
16892 let crc_offset = bytes.len() - 4;
16893 let crc = crc32c::crc32c(&bytes[..crc_offset]);
16894 bytes[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
16895 }
16896
16897 fn rewrite_cmra_image(
16898 volume: &mut [u8],
16899 mode: CmraRecoveryMode,
16900 mutate: impl FnOnce(&mut CriticalMetadataImage),
16901 ) {
16902 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
16903 let locator = final_recovery_locator(volume);
16904 let tuple = CmraDecoderTuple::from(locator);
16905 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16906 let mut image = recovered.image;
16907 mutate(&mut image);
16908 refresh_critical_image_region_digests(&mut image);
16909 let image_bytes = image.to_bytes().unwrap();
16910 assert_eq!(image_bytes.len(), tuple.image_length as usize);
16911
16912 let shard_size = tuple.shard_size as usize;
16913 let data_shard_count = tuple.data_shard_count as usize;
16914 let parity_shard_count = tuple.parity_shard_count as usize;
16915 assert!(image_bytes.len() <= data_shard_count * shard_size);
16916
16917 let mut data_shards = Vec::with_capacity(data_shard_count);
16918 for idx in 0..data_shard_count {
16919 let start = idx * shard_size;
16920 let end = (start + shard_size).min(image_bytes.len());
16921 let mut payload = vec![0u8; shard_size];
16922 if start < image_bytes.len() {
16923 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
16924 }
16925 data_shards.push(payload);
16926 }
16927 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
16928 let image_sha256 = sha256_bytes(&image_bytes);
16929
16930 let header = CriticalMetadataRecoveryHeader {
16931 shard_size: tuple.shard_size,
16932 data_shard_count: tuple.data_shard_count,
16933 parity_shard_count: tuple.parity_shard_count,
16934 image_length: tuple.image_length,
16935 archive_uuid_hint: locator.archive_uuid_hint,
16936 session_id_hint: locator.session_id_hint,
16937 volume_index_hint: locator.volume_index_hint,
16938 image_sha256,
16939 header_crc32c: 0,
16940 };
16941 let mut cmra = Vec::new();
16942 cmra.extend_from_slice(&header.to_bytes());
16943 for (idx, payload) in data_shards.into_iter().enumerate() {
16944 let payload_len = if idx + 1 == data_shard_count {
16945 image_bytes.len() - idx * shard_size
16946 } else {
16947 shard_size
16948 };
16949 cmra.extend_from_slice(
16950 &CriticalMetadataRecoveryShard {
16951 shard_index: idx as u16,
16952 shard_role: 0,
16953 shard_payload_length: payload_len as u32,
16954 payload,
16955 shard_crc32c: 0,
16956 }
16957 .to_bytes(shard_size)
16958 .unwrap(),
16959 );
16960 }
16961 for (idx, payload) in parity_shards.into_iter().enumerate() {
16962 cmra.extend_from_slice(
16963 &CriticalMetadataRecoveryShard {
16964 shard_index: (data_shard_count + idx) as u16,
16965 shard_role: 1,
16966 shard_payload_length: shard_size as u32,
16967 payload,
16968 shard_crc32c: 0,
16969 }
16970 .to_bytes(shard_size)
16971 .unwrap(),
16972 );
16973 }
16974 assert_eq!(cmra.len() as u64, recovered.cmra_length);
16975 let cmra_offset = locator.cmra_offset as usize;
16976 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
16977
16978 rewrite_locator_image_sha(volume, final_offset, image_sha256);
16979 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
16980 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
16981 }
16982
16983 fn rewrite_cmra_image_variable_len(
16984 volume: &[u8],
16985 mode: CmraRecoveryMode,
16986 mutate: impl FnOnce(&mut CriticalMetadataImage),
16987 ) -> Vec<u8> {
16988 let locator = final_recovery_locator(volume);
16989 let tuple = CmraDecoderTuple::from(locator);
16990 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
16991 let mut image = recovered.image;
16992 mutate(&mut image);
16993 refresh_critical_image_region_digests(&mut image);
16994 let image_bytes = image.to_bytes().unwrap();
16995
16996 let shard_size = tuple.shard_size as usize;
16997 let data_shard_count = image_bytes.len().div_ceil(shard_size);
16998 let parity_shard_count = tuple.parity_shard_count as usize;
16999 assert!(data_shard_count > 0);
17000 assert!(image_bytes.len() <= data_shard_count * shard_size);
17001
17002 let mut data_shards = Vec::with_capacity(data_shard_count);
17003 for idx in 0..data_shard_count {
17004 let start = idx * shard_size;
17005 let end = (start + shard_size).min(image_bytes.len());
17006 let mut payload = vec![0u8; shard_size];
17007 if start < image_bytes.len() {
17008 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
17009 }
17010 data_shards.push(payload);
17011 }
17012 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
17013 let image_sha256 = sha256_bytes(&image_bytes);
17014 let data_shard_count_u16 = u16::try_from(data_shard_count).unwrap();
17015 let image_length_u32 = u32::try_from(image_bytes.len()).unwrap();
17016
17017 let header = CriticalMetadataRecoveryHeader {
17018 shard_size: tuple.shard_size,
17019 data_shard_count: data_shard_count_u16,
17020 parity_shard_count: tuple.parity_shard_count,
17021 image_length: image_length_u32,
17022 archive_uuid_hint: locator.archive_uuid_hint,
17023 session_id_hint: locator.session_id_hint,
17024 volume_index_hint: locator.volume_index_hint,
17025 image_sha256,
17026 header_crc32c: 0,
17027 };
17028 let mut cmra = Vec::new();
17029 cmra.extend_from_slice(&header.to_bytes());
17030 for (idx, payload) in data_shards.into_iter().enumerate() {
17031 let payload_len = if idx + 1 == data_shard_count {
17032 image_bytes.len() - idx * shard_size
17033 } else {
17034 shard_size
17035 };
17036 cmra.extend_from_slice(
17037 &CriticalMetadataRecoveryShard {
17038 shard_index: idx as u16,
17039 shard_role: 0,
17040 shard_payload_length: payload_len as u32,
17041 payload,
17042 shard_crc32c: 0,
17043 }
17044 .to_bytes(shard_size)
17045 .unwrap(),
17046 );
17047 }
17048 for (idx, payload) in parity_shards.into_iter().enumerate() {
17049 cmra.extend_from_slice(
17050 &CriticalMetadataRecoveryShard {
17051 shard_index: (data_shard_count + idx) as u16,
17052 shard_role: 1,
17053 shard_payload_length: shard_size as u32,
17054 payload,
17055 shard_crc32c: 0,
17056 }
17057 .to_bytes(shard_size)
17058 .unwrap(),
17059 );
17060 }
17061
17062 let locator_base = CriticalRecoveryLocator {
17063 volume_format_rev: image.volume_format_rev,
17064 cmra_offset: locator.cmra_offset,
17065 cmra_length: cmra.len() as u32,
17066 volume_trailer_offset: locator.volume_trailer_offset,
17067 body_bytes_before_cmra: locator.body_bytes_before_cmra,
17068 archive_uuid_hint: locator.archive_uuid_hint,
17069 session_id_hint: locator.session_id_hint,
17070 volume_index_hint: locator.volume_index_hint,
17071 locator_sequence: 1,
17072 cmra_shard_size: tuple.shard_size,
17073 cmra_data_shard_count: data_shard_count_u16,
17074 cmra_parity_shard_count: tuple.parity_shard_count,
17075 cmra_image_length: image_length_u32,
17076 cmra_image_sha256: image_sha256,
17077 locator_crc32c: 0,
17078 };
17079
17080 let cmra_offset = locator.cmra_offset as usize;
17081 let mut out = Vec::new();
17082 out.extend_from_slice(&volume[..cmra_offset]);
17083 out.extend_from_slice(&cmra);
17084 out.extend_from_slice(&locator_base.to_bytes());
17085 out.extend_from_slice(
17086 &CriticalRecoveryLocator {
17087 locator_sequence: 0,
17088 ..locator_base
17089 }
17090 .to_bytes(),
17091 );
17092 out
17093 }
17094
17095 fn rewrite_recovery_locator(
17096 volume: &mut [u8],
17097 offset: usize,
17098 mutate: impl FnOnce(&mut CriticalRecoveryLocator),
17099 ) {
17100 let mut locator =
17101 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
17102 .unwrap();
17103 mutate(&mut locator);
17104 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
17105 }
17106
17107 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
17108 image.volume_header_sha256 = sha256_bytes(
17109 &image
17110 .regions
17111 .iter()
17112 .find(|region| region.region_type == 1)
17113 .unwrap()
17114 .bytes,
17115 );
17116 image.crypto_header_sha256 = sha256_bytes(
17117 &image
17118 .regions
17119 .iter()
17120 .find(|region| region.region_type == 2)
17121 .unwrap()
17122 .bytes,
17123 );
17124 image.key_wrap_table_sha256 = image
17125 .regions
17126 .iter()
17127 .find(|region| region.region_type == 6)
17128 .map(|region| sha256_bytes(®ion.bytes))
17129 .unwrap_or([0u8; 32]);
17130 image.manifest_footer_sha256 = sha256_bytes(
17131 &image
17132 .regions
17133 .iter()
17134 .find(|region| region.region_type == 3)
17135 .unwrap()
17136 .bytes,
17137 );
17138 image.root_auth_footer_sha256 = image
17139 .regions
17140 .iter()
17141 .find(|region| region.region_type == 4)
17142 .map(|region| sha256_bytes(®ion.bytes))
17143 .unwrap_or([0u8; 32]);
17144 image.volume_trailer_sha256 = sha256_bytes(
17145 &image
17146 .regions
17147 .iter()
17148 .find(|region| region.region_type == 5)
17149 .unwrap()
17150 .bytes,
17151 );
17152 }
17153
17154 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
17155 let mut locator =
17156 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
17157 .unwrap();
17158 locator.cmra_image_sha256 = image_sha256;
17159 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
17160 }
17161
17162 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
17163 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
17164 let final_locator = CriticalRecoveryLocator::parse(
17165 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
17166 )
17167 .unwrap();
17168 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
17169 volume[final_locator.cmra_offset as usize] ^= 0x55;
17170 volume[mirror_offset] ^= 0x55;
17171 volume[final_offset] ^= 0x55;
17172 }
17173
17174 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
17175 let (record_offset, record_len) = first_block_record(volume);
17176 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17177 let mut record = BlockRecord::parse(
17178 &volume[record_offset..record_offset + record_len],
17179 block_size,
17180 )
17181 .unwrap();
17182 mutate(&mut record);
17183 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
17184 }
17185
17186 fn mutate_block_record_at_slot(
17187 volume: &mut [u8],
17188 slot: usize,
17189 mutate: impl FnOnce(&mut BlockRecord),
17190 ) {
17191 let (record_offset, record_len) = block_record_at_slot(volume, slot);
17192 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17193 let mut record = BlockRecord::parse(
17194 &volume[record_offset..record_offset + record_len],
17195 block_size,
17196 )
17197 .unwrap();
17198 mutate(&mut record);
17199 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
17200 }
17201
17202 fn first_block_record(volume: &[u8]) -> (usize, usize) {
17203 block_record_at_slot(volume, 0)
17204 }
17205
17206 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
17207 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17208 let crypto_start = volume_header.crypto_header_offset as usize;
17209 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17210 let crypto_header = CryptoHeader::parse(
17211 &volume[crypto_start..crypto_end],
17212 volume_header.crypto_header_length,
17213 )
17214 .unwrap();
17215 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17216 let record_offset = crypto_end + slot * record_len;
17217 assert!(volume.len() >= record_offset + record_len);
17218 (record_offset, record_len)
17219 }
17220
17221 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
17222 block_record_slots(volume)
17223 .into_iter()
17224 .enumerate()
17225 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17226 }
17227
17228 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
17229 block_record_slots(volume)
17230 .into_iter()
17231 .enumerate()
17232 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
17233 .collect()
17234 }
17235
17236 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
17237 let mut slots = Vec::new();
17238 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
17239 if record.kind == BlockKind::PayloadData {
17240 slots.push(slot);
17241 } else if !slots.is_empty() {
17242 break;
17243 }
17244 }
17245 slots
17246 }
17247
17248 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
17249 envelope_entries_for_path(opened, path)
17250 .into_iter()
17251 .map(|entry| entry.envelope_index)
17252 .collect()
17253 }
17254
17255 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
17256 let normalized =
17257 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
17258 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
17259 let file = &located.shard.files[located.file_index];
17260 frame_range_for_file(&located.shard, file)
17261 .unwrap()
17262 .iter()
17263 .map(|frame| {
17264 located
17265 .shard
17266 .envelopes
17267 .iter()
17268 .find(|entry| entry.envelope_index == frame.envelope_index)
17269 .unwrap()
17270 .clone()
17271 })
17272 .collect()
17273 }
17274
17275 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
17276 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17277 let crypto_start = volume_header.crypto_header_offset as usize;
17278 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17279 let crypto_header = CryptoHeader::parse(
17280 &volume[crypto_start..crypto_end],
17281 volume_header.crypto_header_length,
17282 )
17283 .unwrap();
17284 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17285 let manifest_offset = terminal_material_offset(volume);
17286 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
17287 let record_count = (manifest_offset - crypto_end) / record_len;
17288 (0..record_count)
17289 .map(|slot| {
17290 let offset = crypto_end + slot * record_len;
17291 let record = BlockRecord::parse(
17292 &volume[offset..offset + record_len],
17293 record_len - BLOCK_RECORD_FRAMING_LEN,
17294 )
17295 .unwrap();
17296 (offset, record_len, record)
17297 })
17298 .collect()
17299 }
17300
17301 fn rewrite_manifest_footer(
17302 volume: &mut [u8],
17303 master_key: &MasterKey,
17304 mutate: impl FnOnce(&mut ManifestFooter),
17305 ) {
17306 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17307 let offset = terminal_material_offset(volume);
17308 let mut footer =
17309 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17310 mutate(&mut footer);
17311 footer.manifest_hmac = [0u8; 32];
17312 let mut footer_bytes = footer.to_bytes();
17313 let subkeys = Subkeys::derive(
17314 master_key,
17315 &volume_header.archive_uuid,
17316 &volume_header.session_id,
17317 )
17318 .unwrap();
17319 footer.manifest_hmac = compute_hmac(
17320 HmacDomain::ManifestFooter,
17321 &subkeys.mac_key,
17322 &volume_header.archive_uuid,
17323 &volume_header.session_id,
17324 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17325 );
17326 footer_bytes = footer.to_bytes();
17327 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17328 }
17329
17330 fn rewrite_volume_trailer(
17331 volume: &mut [u8],
17332 master_key: &MasterKey,
17333 mutate: impl FnOnce(&mut VolumeTrailer),
17334 ) {
17335 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17336 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
17337 let mut trailer =
17338 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
17339 mutate(&mut trailer);
17340 trailer.trailer_hmac = [0u8; 32];
17341 let mut trailer_bytes = trailer.to_bytes();
17342 let subkeys = Subkeys::derive(
17343 master_key,
17344 &volume_header.archive_uuid,
17345 &volume_header.session_id,
17346 )
17347 .unwrap();
17348 trailer.trailer_hmac = compute_hmac(
17349 HmacDomain::VolumeTrailer,
17350 &subkeys.mac_key,
17351 &volume_header.archive_uuid,
17352 &volume_header.session_id,
17353 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
17354 );
17355 trailer_bytes = trailer.to_bytes();
17356 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
17357 }
17358
17359 fn rewrite_sidecar_header(
17360 sidecar: &mut [u8],
17361 master_key: &MasterKey,
17362 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
17363 ) {
17364 let mut header =
17365 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17366 mutate(&mut header);
17367 write_signed_sidecar_header(sidecar, master_key, &mut header);
17368 }
17369
17370 fn write_signed_sidecar_header(
17371 sidecar: &mut [u8],
17372 master_key: &MasterKey,
17373 header: &mut BootstrapSidecarHeader,
17374 ) {
17375 header.sidecar_hmac = [0u8; 32];
17376 let mut header_bytes = header.to_bytes();
17377 let subkeys =
17378 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
17379 header.sidecar_hmac = compute_hmac(
17380 HmacDomain::BootstrapSidecar,
17381 &subkeys.mac_key,
17382 &header.archive_uuid,
17383 &header.session_id,
17384 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
17385 );
17386 header_bytes = header.to_bytes();
17387 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
17388 }
17389
17390 fn sparse_bootstrap_sidecar(
17391 source: &[u8],
17392 master_key: &MasterKey,
17393 include_manifest: bool,
17394 include_index_root: bool,
17395 include_dictionary: bool,
17396 ) -> Vec<u8> {
17397 let source_header =
17398 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17399 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
17400 let mut header = BootstrapSidecarHeader {
17401 archive_uuid: source_header.archive_uuid,
17402 session_id: source_header.session_id,
17403 flags: 0,
17404 manifest_footer_offset: 0,
17405 manifest_footer_length: 0,
17406 index_root_records_offset: 0,
17407 index_root_records_length: 0,
17408 dictionary_records_offset: 0,
17409 dictionary_records_length: 0,
17410 sidecar_hmac: [0u8; 32],
17411 header_crc32c: 0,
17412 };
17413
17414 if include_manifest {
17415 assert!(source_header.has_manifest_footer());
17416 let (offset, length) = append_sidecar_section(
17417 source,
17418 &mut sidecar,
17419 source_header.manifest_footer_offset,
17420 source_header.manifest_footer_length as u64,
17421 );
17422 header.flags |= 0x01;
17423 header.manifest_footer_offset = offset;
17424 header.manifest_footer_length = length as u32;
17425 }
17426 if include_index_root {
17427 assert!(source_header.has_index_root_records());
17428 let (offset, length) = append_sidecar_section(
17429 source,
17430 &mut sidecar,
17431 source_header.index_root_records_offset,
17432 source_header.index_root_records_length,
17433 );
17434 header.flags |= 0x02;
17435 header.index_root_records_offset = offset;
17436 header.index_root_records_length = length;
17437 }
17438 if include_dictionary {
17439 assert!(source_header.has_dictionary_records());
17440 let (offset, length) = append_sidecar_section(
17441 source,
17442 &mut sidecar,
17443 source_header.dictionary_records_offset,
17444 source_header.dictionary_records_length,
17445 );
17446 header.flags |= 0x04;
17447 header.dictionary_records_offset = offset;
17448 header.dictionary_records_length = length;
17449 }
17450
17451 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
17452 sidecar
17453 }
17454
17455 fn append_sidecar_section(
17456 source: &[u8],
17457 sidecar: &mut Vec<u8>,
17458 source_offset: u64,
17459 length: u64,
17460 ) -> (u64, u64) {
17461 let source_offset = source_offset as usize;
17462 let length = length as usize;
17463 let offset = sidecar.len() as u64;
17464 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
17465 (offset, length as u64)
17466 }
17467
17468 fn mutate_sidecar_manifest(
17469 sidecar: &mut [u8],
17470 master_key: &MasterKey,
17471 mutate: impl FnOnce(&mut ManifestFooter),
17472 ) {
17473 let header =
17474 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17475 let offset = header.manifest_footer_offset as usize;
17476 let mut footer =
17477 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
17478 mutate(&mut footer);
17479 footer.manifest_hmac = [0u8; 32];
17480 let mut footer_bytes = footer.to_bytes();
17481 let subkeys =
17482 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
17483 footer.manifest_hmac = compute_hmac(
17484 HmacDomain::ManifestFooter,
17485 &subkeys.mac_key,
17486 &footer.archive_uuid,
17487 &footer.session_id,
17488 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
17489 );
17490 footer_bytes = footer.to_bytes();
17491 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
17492 }
17493
17494 fn mutate_sidecar_index_record(
17495 sidecar: &mut [u8],
17496 record_index: usize,
17497 mutate: impl FnOnce(&mut BlockRecord),
17498 ) {
17499 let header =
17500 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17501 let record_len = sidecar_record_len(sidecar);
17502 let offset = header.index_root_records_offset as usize + record_index * record_len;
17503 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17504 let mut record =
17505 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17506 mutate(&mut record);
17507 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17508 }
17509
17510 fn mutate_sidecar_dictionary_record(
17511 sidecar: &mut [u8],
17512 record_index: usize,
17513 mutate: impl FnOnce(&mut BlockRecord),
17514 ) {
17515 let header =
17516 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17517 let record_len = sidecar_record_len(sidecar);
17518 let offset = header.dictionary_records_offset as usize + record_index * record_len;
17519 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
17520 let mut record =
17521 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
17522 mutate(&mut record);
17523 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
17524 }
17525
17526 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
17527 let header =
17528 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17529 let record_len = sidecar_record_len(sidecar);
17530 let left_offset = header.index_root_records_offset as usize + left * record_len;
17531 let right_offset = header.index_root_records_offset as usize + right * record_len;
17532 for idx in 0..record_len {
17533 sidecar.swap(left_offset + idx, right_offset + idx);
17534 }
17535 }
17536
17537 fn sidecar_record_len(sidecar: &[u8]) -> usize {
17538 let header =
17539 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
17540 let footer_offset = header.manifest_footer_offset as usize;
17541 let footer =
17542 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
17543 .unwrap();
17544 let index_record_count = footer.index_root_data_block_count as usize
17545 + footer.index_root_parity_block_count as usize;
17546 header.index_root_records_length as usize / index_record_count
17547 }
17548
17549 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
17550 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17551 assert_eq!(volume_header.volume_index, 0);
17552 assert_eq!(volume_header.stripe_width, 1);
17553 let crypto_start = volume_header.crypto_header_offset as usize;
17554 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17555 let crypto_header = CryptoHeader::parse(
17556 &volume[crypto_start..crypto_end],
17557 volume_header.crypto_header_length,
17558 )
17559 .unwrap();
17560 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
17561 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
17562 for offset in 0..record_count {
17563 let block_index = extent.first_block_index + offset;
17564 let record_offset = crypto_end + block_index as usize * record_len;
17565 volume[record_offset + 16] ^= 0x55;
17566 }
17567 }
17568
17569 fn terminal_material_offset(volume: &[u8]) -> usize {
17570 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
17571 let crypto_start = volume_header.crypto_header_offset as usize;
17572 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
17573 let crypto_header = CryptoHeader::parse(
17574 &volume[crypto_start..crypto_end],
17575 volume_header.crypto_header_length,
17576 )
17577 .unwrap();
17578 let (_, offset, _) = parse_stream_block_prefix(
17579 volume,
17580 crypto_end,
17581 crypto_header.fixed.block_size as usize,
17582 &volume_header,
17583 )
17584 .unwrap();
17585 offset
17586 }
17587
17588 #[derive(Debug)]
17589 struct TestObject {
17590 extent: ObjectExtent,
17591 records: Vec<BlockRecord>,
17592 }
17593
17594 #[derive(Debug)]
17595 struct TestFileMeta {
17596 path: Vec<u8>,
17597 frame_index: u64,
17598 tar_stream_offset: u64,
17599 member_group_size: u64,
17600 file_data_size: u64,
17601 }
17602
17603 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
17604 let volume_header = test_volume_header();
17605 let crypto_header = test_crypto_header();
17606 let subkeys = Subkeys::derive(
17607 &master_key(),
17608 &volume_header.archive_uuid,
17609 &volume_header.session_id,
17610 )
17611 .unwrap();
17612 let mut next_block_index = 0u64;
17613 let mut blocks = BTreeMap::new();
17614
17615 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
17616 let broken = test_member(b"broken.txt", b"broken payload\n");
17617 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
17618
17619 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
17620 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
17621
17622 let healthy_payload = encrypt_test_object(
17623 &healthy_frame,
17624 &subkeys.enc_key,
17625 &subkeys.nonce_seed,
17626 b"envelope",
17627 0,
17628 BlockKind::PayloadData,
17629 &mut next_block_index,
17630 &crypto_header,
17631 &volume_header,
17632 );
17633 let broken_payload = encrypt_test_object(
17634 &broken_frame,
17635 &subkeys.enc_key,
17636 &subkeys.nonce_seed,
17637 b"envelope",
17638 1,
17639 BlockKind::PayloadData,
17640 &mut next_block_index,
17641 &crypto_header,
17642 &volume_header,
17643 );
17644 let broken_payload_block = broken_payload.extent.first_block_index;
17645 insert_records(&mut blocks, &healthy_payload.records);
17646 insert_records(&mut blocks, &broken_payload.records);
17647
17648 let frames = vec![
17649 FrameEntry {
17650 frame_index: 0,
17651 envelope_index: 0,
17652 offset_in_envelope: 0,
17653 compressed_size: healthy_frame.len() as u32,
17654 decompressed_size: healthy.len() as u32,
17655 flags: 0x0000_0003,
17656 tar_stream_offset: 0,
17657 },
17658 FrameEntry {
17659 frame_index: 1,
17660 envelope_index: 1,
17661 offset_in_envelope: 0,
17662 compressed_size: broken_frame.len() as u32,
17663 decompressed_size: broken.len() as u32,
17664 flags: 0x0000_0003,
17665 tar_stream_offset: healthy.len() as u64,
17666 },
17667 ];
17668 let envelopes = vec![
17669 EnvelopeEntry {
17670 envelope_index: 0,
17671 first_block_index: healthy_payload.extent.first_block_index,
17672 data_block_count: healthy_payload.extent.data_block_count,
17673 parity_block_count: 0,
17674 encrypted_size: healthy_payload.extent.encrypted_size,
17675 plaintext_size: healthy_frame.len() as u32,
17676 first_frame_index: 0,
17677 frame_count: 1,
17678 },
17679 EnvelopeEntry {
17680 envelope_index: 1,
17681 first_block_index: broken_payload.extent.first_block_index,
17682 data_block_count: broken_payload.extent.data_block_count,
17683 parity_block_count: 0,
17684 encrypted_size: broken_payload.extent.encrypted_size,
17685 plaintext_size: broken_frame.len() as u32,
17686 first_frame_index: 1,
17687 frame_count: 1,
17688 },
17689 ];
17690 let files = vec![
17691 TestFileMeta {
17692 path: b"healthy.txt".to_vec(),
17693 frame_index: 0,
17694 tar_stream_offset: 0,
17695 member_group_size: healthy.len() as u64,
17696 file_data_size: b"healthy payload\n".len() as u64,
17697 },
17698 TestFileMeta {
17699 path: b"broken.txt".to_vec(),
17700 frame_index: 1,
17701 tar_stream_offset: healthy.len() as u64,
17702 member_group_size: broken.len() as u64,
17703 file_data_size: b"broken payload\n".len() as u64,
17704 },
17705 ];
17706
17707 let (index_shard_plaintext, first_path_hash, last_path_hash) =
17708 build_test_index_shard(&files, &frames, &envelopes);
17709 let index_shard = encrypt_test_object(
17710 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
17711 &subkeys.index_shard_key,
17712 &subkeys.index_nonce_seed,
17713 b"idxshard",
17714 0,
17715 BlockKind::IndexShardData,
17716 &mut next_block_index,
17717 &crypto_header,
17718 &volume_header,
17719 );
17720 insert_records(&mut blocks, &index_shard.records);
17721
17722 let shard_entry = ShardEntry {
17723 shard_index: 0,
17724 first_block_index: index_shard.extent.first_block_index,
17725 data_block_count: index_shard.extent.data_block_count,
17726 parity_block_count: 0,
17727 encrypted_size: index_shard.extent.encrypted_size,
17728 decompressed_size: index_shard_plaintext.len() as u32,
17729 file_count: files.len() as u32,
17730 first_path_hash,
17731 last_path_hash,
17732 };
17733 let mut root_header = IndexRootHeader::empty();
17734 root_header.frame_count = frames.len() as u64;
17735 root_header.envelope_count = envelopes.len() as u64;
17736 root_header.file_count = files.len() as u64;
17737 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
17738 + broken_payload.extent.data_block_count as u64;
17739 root_header.tar_total_size = tar_stream.len() as u64;
17740 root_header.content_sha256 = sha256_bytes(&tar_stream);
17741 let index_root = IndexRoot {
17742 header: root_header,
17743 shards: vec![shard_entry],
17744 directory_hint_shards: Vec::new(),
17745 };
17746
17747 let index_root_plaintext = index_root.to_bytes();
17748 let index_root_object = encrypt_test_object(
17749 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
17750 &subkeys.index_root_key,
17751 &subkeys.index_nonce_seed,
17752 b"idxroot",
17753 0,
17754 BlockKind::IndexRootData,
17755 &mut next_block_index,
17756 &crypto_header,
17757 &volume_header,
17758 );
17759 insert_records(&mut blocks, &index_root_object.records);
17760
17761 let archive_uuid = volume_header.archive_uuid;
17762 let session_id = volume_header.session_id;
17763 let opened = OpenedArchive {
17764 options: ReaderOptions::default(),
17765 observed_archive_bytes: 1_000_000,
17766 observed_volume_count: 1,
17767 subkeys,
17768 blocks,
17769 lazy_blocks: None,
17770 crypto_header_bytes: Vec::new(),
17771 volume_header,
17772 crypto_header,
17773 manifest_footer: ManifestFooter {
17774 archive_uuid,
17775 session_id,
17776 volume_index: 0,
17777 is_authoritative: 1,
17778 total_volumes: 1,
17779 index_root_first_block: index_root_object.extent.first_block_index,
17780 index_root_data_block_count: index_root_object.extent.data_block_count,
17781 index_root_parity_block_count: 0,
17782 index_root_encrypted_size: index_root_object.extent.encrypted_size,
17783 index_root_decompressed_size: index_root_plaintext.len() as u32,
17784 manifest_hmac: [0u8; 32],
17785 },
17786 volume_trailer: Some(VolumeTrailer {
17787 archive_uuid,
17788 session_id,
17789 volume_index: 0,
17790 block_count: next_block_index,
17791 bytes_written: 0,
17792 manifest_footer_offset: 0,
17793 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
17794 closed_at_ns: 0,
17795 root_auth_footer_offset: 0,
17796 root_auth_footer_length: 0,
17797 root_auth_flags: 0,
17798 trailer_hmac: [0u8; 32],
17799 }),
17800 root_auth_footer: None,
17801 index_root,
17802 payload_dictionary: None,
17803 };
17804 (opened, broken_payload_block)
17805 }
17806
17807 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
17808 let locating = opened.index_root.shards[0].clone();
17809 let mut shard = opened.load_index_shard(&locating).unwrap();
17810 mutate(&mut shard);
17811 let plaintext = shard.to_bytes();
17812 let mut next_block_index = opened
17813 .blocks
17814 .keys()
17815 .last()
17816 .copied()
17817 .map(|index| index + 1)
17818 .unwrap_or(0);
17819 let replacement = encrypt_test_object(
17820 &compress_zstd_frame(&plaintext, 1).unwrap(),
17821 &opened.subkeys.index_shard_key,
17822 &opened.subkeys.index_nonce_seed,
17823 b"idxshard",
17824 locating.shard_index,
17825 BlockKind::IndexShardData,
17826 &mut next_block_index,
17827 &opened.crypto_header,
17828 &opened.volume_header,
17829 );
17830 insert_records(&mut opened.blocks, &replacement.records);
17831 opened.index_root.shards[0] = ShardEntry {
17832 shard_index: locating.shard_index,
17833 first_block_index: replacement.extent.first_block_index,
17834 data_block_count: replacement.extent.data_block_count,
17835 parity_block_count: 0,
17836 encrypted_size: replacement.extent.encrypted_size,
17837 decompressed_size: plaintext.len() as u32,
17838 file_count: shard.files.len() as u32,
17839 first_path_hash: shard.files.first().unwrap().path_hash,
17840 last_path_hash: shard.files.last().unwrap().path_hash,
17841 };
17842 }
17843
17844 fn rewrite_as_single_healthy_file(
17845 opened: &mut OpenedArchive,
17846 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
17847 ) {
17848 let healthy_path = b"healthy.txt";
17849 let healthy_payload = b"healthy payload\n";
17850 let healthy_member = test_member(healthy_path, healthy_payload);
17851 replace_first_index_shard(opened, |shard| {
17852 let file_index = (0..shard.files.len())
17853 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
17854 .unwrap();
17855 let mut file = shard.files[file_index].clone();
17856 let frame = shard
17857 .frames
17858 .iter()
17859 .find(|entry| entry.frame_index == 0)
17860 .unwrap()
17861 .clone();
17862 let envelope = shard
17863 .envelopes
17864 .iter()
17865 .find(|entry| entry.envelope_index == 0)
17866 .unwrap()
17867 .clone();
17868 let mut path = healthy_path.to_vec();
17869
17870 file.path_offset = 0;
17871 file.path_length = path.len() as u32;
17872 file.first_frame_index = 0;
17873 file.frame_count = 1;
17874 file.offset_in_first_frame_plaintext = 0;
17875 file.tar_member_group_size = healthy_member.len() as u64;
17876 file.file_data_size = healthy_payload.len() as u64;
17877 file.flags = 0;
17878 mutate(&mut file, &mut path);
17879 file.path_offset = 0;
17880 file.path_length = path.len() as u32;
17881 file.path_hash = hash_prefix(&path);
17882
17883 shard.files = vec![file];
17884 shard.frames = vec![frame];
17885 shard.envelopes = vec![envelope];
17886 shard.string_pool = path;
17887 });
17888
17889 opened.index_root.header.file_count = 1;
17890 opened.index_root.header.frame_count = 1;
17891 opened.index_root.header.envelope_count = 1;
17892 opened.index_root.header.payload_block_count = 1;
17893 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
17894 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
17895 }
17896
17897 fn test_volume_header() -> VolumeHeader {
17898 VolumeHeader {
17899 format_version: FORMAT_VERSION,
17900 volume_format_rev: VOLUME_FORMAT_REV,
17901 volume_index: 0,
17902 stripe_width: 1,
17903 archive_uuid: [0x31; 16],
17904 session_id: [0x42; 16],
17905 crypto_header_offset: VOLUME_HEADER_LEN as u32,
17906 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
17907 header_crc32c: 0,
17908 }
17909 }
17910
17911 fn test_crypto_header() -> CryptoHeaderFixed {
17912 CryptoHeaderFixed {
17913 length: CRYPTO_HEADER_FIXED_LEN as u32,
17914 compression_algo: CompressionAlgo::ZstdFramed,
17915 aead_algo: AeadAlgo::AesGcmSiv256,
17916 fec_algo: FecAlgo::ReedSolomonGF16,
17917 kdf_algo: KdfAlgo::Raw,
17918 chunk_size: 4096,
17919 envelope_target_size: 8192,
17920 block_size: 4096,
17921 fec_data_shards: 4,
17922 fec_parity_shards: 0,
17923 index_fec_data_shards: 4,
17924 index_fec_parity_shards: 0,
17925 index_root_fec_data_shards: 4,
17926 index_root_fec_parity_shards: 0,
17927 stripe_width: 1,
17928 volume_loss_tolerance: 0,
17929 bit_rot_buffer_pct: 0,
17930 has_dictionary: 0,
17931 max_path_length: 4096,
17932 expected_volume_size: 0,
17933 }
17934 }
17935
17936 #[allow(clippy::too_many_arguments)]
17937 fn encrypt_test_object(
17938 plaintext: &[u8],
17939 key: &[u8; 32],
17940 nonce_seed: &[u8; 32],
17941 domain: &[u8],
17942 counter: u64,
17943 data_kind: BlockKind,
17944 next_block_index: &mut u64,
17945 crypto_header: &CryptoHeaderFixed,
17946 volume_header: &VolumeHeader,
17947 ) -> TestObject {
17948 let block_size = crypto_header.block_size as usize;
17949 let encrypted = encrypt_padded_aead_object(
17950 AeadObjectContext {
17951 algo: crypto_header.aead_algo,
17952 key,
17953 nonce_seed,
17954 domain,
17955 archive_uuid: &volume_header.archive_uuid,
17956 session_id: &volume_header.session_id,
17957 counter,
17958 },
17959 block_size,
17960 plaintext,
17961 )
17962 .unwrap();
17963 assert_eq!(encrypted.len() % block_size, 0);
17964
17965 let first_block_index = *next_block_index;
17966 let data_block_count = encrypted.len() / block_size;
17967 let records = encrypted
17968 .chunks(block_size)
17969 .enumerate()
17970 .map(|(index, payload)| BlockRecord {
17971 block_index: first_block_index + index as u64,
17972 kind: data_kind,
17973 flags: if index + 1 == data_block_count {
17974 0x01
17975 } else {
17976 0
17977 },
17978 payload: payload.to_vec(),
17979 record_crc32c: 0,
17980 })
17981 .collect::<Vec<_>>();
17982 *next_block_index += data_block_count as u64;
17983
17984 TestObject {
17985 extent: ObjectExtent {
17986 first_block_index,
17987 data_block_count: data_block_count as u32,
17988 parity_block_count: 0,
17989 encrypted_size: encrypted.len() as u32,
17990 },
17991 records,
17992 }
17993 }
17994
17995 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
17996 for record in records {
17997 assert!(blocks.insert(record.block_index, record.clone()).is_none());
17998 }
17999 }
18000
18001 #[allow(clippy::too_many_arguments)]
18002 fn build_metadata_object_from_payload(
18003 payload: &[u8],
18004 _subkeys: &Subkeys,
18005 volume_header: &VolumeHeader,
18006 crypto_header: &CryptoHeaderFixed,
18007 key: &[u8; 32],
18008 nonce_seed: &[u8; 32],
18009 domain: &[u8],
18010 counter: u64,
18011 data_kind: BlockKind,
18012 next_block_index: &mut u64,
18013 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
18014 let compressed = compress_zstd_frame(payload, 1).unwrap();
18015 build_metadata_object_from_compressed(
18016 &compressed,
18017 key,
18018 nonce_seed,
18019 domain,
18020 counter,
18021 data_kind,
18022 next_block_index,
18023 crypto_header,
18024 volume_header,
18025 )
18026 }
18027
18028 #[allow(clippy::too_many_arguments)]
18029 fn build_metadata_object_from_compressed(
18030 compressed: &[u8],
18031 key: &[u8; 32],
18032 nonce_seed: &[u8; 32],
18033 domain: &[u8],
18034 counter: u64,
18035 data_kind: BlockKind,
18036 next_block_index: &mut u64,
18037 crypto_header: &CryptoHeaderFixed,
18038 volume_header: &VolumeHeader,
18039 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
18040 let object = encrypt_test_object(
18041 compressed,
18042 key,
18043 nonce_seed,
18044 domain,
18045 counter,
18046 data_kind,
18047 next_block_index,
18048 crypto_header,
18049 volume_header,
18050 );
18051
18052 let mut blocks = BTreeMap::new();
18053 for record in object.records {
18054 blocks.insert(record.block_index, record);
18055 }
18056 (object.extent, blocks)
18057 }
18058
18059 #[allow(clippy::too_many_arguments)]
18060 fn assert_metadata_object_from_compressed(
18061 compressed: &[u8],
18062 decompressed_size: usize,
18063 _subkeys: &Subkeys,
18064 volume_header: &VolumeHeader,
18065 crypto_header: &CryptoHeaderFixed,
18066 key: &[u8; 32],
18067 nonce_seed: &[u8; 32],
18068 domain: &[u8],
18069 counter: u64,
18070 data_kind: BlockKind,
18071 parity_kind: BlockKind,
18072 class_data_shards: u16,
18073 class_parity_shards: u16,
18074 next_block_index: &mut u64,
18075 expected: FormatError,
18076 ) {
18077 let (extent, blocks) = build_metadata_object_from_compressed(
18078 compressed,
18079 key,
18080 nonce_seed,
18081 domain,
18082 counter,
18083 data_kind,
18084 next_block_index,
18085 crypto_header,
18086 volume_header,
18087 );
18088 let error = load_metadata_object_from_parts(
18089 &blocks,
18090 ObjectLoadContext {
18091 volume_header,
18092 crypto_header,
18093 extent,
18094 data_kind,
18095 parity_kind,
18096 key,
18097 nonce_seed,
18098 domain,
18099 counter,
18100 class_data_shard_max: class_data_shards,
18101 class_parity_shard_max: class_parity_shards,
18102 },
18103 decompressed_size as u32,
18104 )
18105 .unwrap_err();
18106 assert_eq!(error, expected);
18107 }
18108
18109 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
18110 let record = blocks.get_mut(&block_index).unwrap();
18111 assert_eq!(record.kind, BlockKind::PayloadData);
18112 record.payload[0] ^= 0x55;
18113 }
18114
18115 fn build_test_index_shard(
18116 files: &[TestFileMeta],
18117 frames: &[FrameEntry],
18118 envelopes: &[EnvelopeEntry],
18119 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
18120 let mut sorted = files
18121 .iter()
18122 .map(|file| (hash_prefix(&file.path), file))
18123 .collect::<Vec<_>>();
18124 sorted.sort_by(|left, right| {
18125 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
18126 right.0,
18127 right.1.path.as_slice(),
18128 right.1.tar_stream_offset,
18129 ))
18130 });
18131
18132 let mut string_pool = Vec::new();
18133 let mut file_entries = Vec::with_capacity(sorted.len());
18134 for (path_hash, file) in &sorted {
18135 let path_offset = string_pool.len() as u32;
18136 string_pool.extend_from_slice(&file.path);
18137 file_entries.push(FileEntry {
18138 path_hash: *path_hash,
18139 path_offset,
18140 path_length: file.path.len() as u32,
18141 first_frame_index: file.frame_index,
18142 frame_count: 1,
18143 offset_in_first_frame_plaintext: 0,
18144 tar_member_group_size: file.member_group_size,
18145 file_data_size: file.file_data_size,
18146 kind: crate::tar_model::TarEntryKind::Regular,
18147 mode: 0o644,
18148 mtime: 0,
18149 flags: 0,
18150 });
18151 }
18152
18153 let header = IndexShardHeader {
18154 version: 1,
18155 shard_index: 0,
18156 file_count: file_entries.len() as u32,
18157 frame_count: frames.len() as u32,
18158 envelope_count: envelopes.len() as u32,
18159 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
18160 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
18161 as u32,
18162 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
18163 + file_entries.len() * FILE_ENTRY_LEN
18164 + frames.len() * FRAME_ENTRY_LEN) as u32,
18165 string_pool_offset: (INDEX_SHARD_HEADER_LEN
18166 + file_entries.len() * FILE_ENTRY_LEN
18167 + frames.len() * FRAME_ENTRY_LEN
18168 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
18169 string_pool_size: string_pool.len() as u32,
18170 };
18171
18172 let mut bytes = Vec::new();
18173 bytes.extend_from_slice(&header.to_bytes());
18174 for entry in &file_entries {
18175 bytes.extend_from_slice(&entry.to_bytes());
18176 }
18177 for entry in frames {
18178 bytes.extend_from_slice(&entry.to_bytes());
18179 }
18180 for entry in envelopes {
18181 bytes.extend_from_slice(&entry.to_bytes());
18182 }
18183 bytes.extend_from_slice(&string_pool);
18184
18185 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
18186 }
18187
18188 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
18189 let mut out = Vec::new();
18190 out.extend_from_slice(&test_tar_header(path, data.len() as u64));
18191 out.extend_from_slice(data);
18192 out.resize(out.len() + padding_to_512(data.len()), 0);
18193 out
18194 }
18195
18196 fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
18197 let mut header = [0u8; 512];
18198 header[..path.len()].copy_from_slice(path);
18199 write_test_tar_octal(&mut header[100..108], 0o644);
18200 write_test_tar_octal(&mut header[108..116], 0);
18201 write_test_tar_octal(&mut header[116..124], 0);
18202 write_test_tar_octal(&mut header[124..136], size);
18203 write_test_tar_octal(&mut header[136..148], 0);
18204 header[148..156].fill(b' ');
18205 header[156] = b'0';
18206 header[257..263].copy_from_slice(b"ustar\0");
18207 header[263..265].copy_from_slice(b"00");
18208 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
18209 write_test_tar_checksum(&mut header[148..156], checksum);
18210 header
18211 }
18212
18213 fn write_test_tar_octal(field: &mut [u8], value: u64) {
18214 let digits = format!("{value:o}");
18215 field.fill(0);
18216 let start = field.len() - 1 - digits.len();
18217 field[..start].fill(b'0');
18218 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
18219 }
18220
18221 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
18222 let digits = format!("{value:06o}");
18223 field[0..6].copy_from_slice(digits.as_bytes());
18224 field[6] = 0;
18225 field[7] = b' ';
18226 }
18227
18228 fn padding_to_512(len: usize) -> usize {
18229 let remainder = len % 512;
18230 if remainder == 0 {
18231 0
18232 } else {
18233 512 - remainder
18234 }
18235 }
18236}