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, MasterKey,
12 Subkeys,
13};
14use crate::fec::{encode_parity_gf16, repair_data_gf16};
15use crate::format::{
16 AeadAlgo, BlockKind, ExtractError, FormatError, KdfAlgo, BLOCK_RECORD_FRAMING_LEN,
17 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, FORMAT_VERSION, IMAGE_CRC_LEN,
20 LOCATOR_PAIR_LEN, MANIFEST_FOOTER_LEN, READER_MAX_CMRA_PARITY_PCT,
21 READER_MAX_CRYPTO_HEADER_LEN, READER_MAX_ROOT_AUTH_FOOTER_LEN, SERIALIZED_REGION_HEADER_LEN,
22 VOLUME_FORMAT_REV, 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, critical_metadata_digest, data_block_merkle_root, fec_layout_digest,
34 index_digest, root_auth_descriptor_digest, signer_identity_digest, ArchiveRootInputs,
35 CriticalMetadataDigestInputs, DataBlockMerkleLeaf, FecLayoutObjectRow,
36};
37use crate::tar_model::{
38 parse_tar_member_group, restore_streaming_tar_member_group,
39 stream_regular_tar_member_group_to_writer, validate_tar_stream_total_extraction_size,
40 MetadataDiagnostic, NoopTarStreamObserver, OwnedTarMember, SafeExtractionOptions, TarEntryKind,
41 TarMemberGroupReader, TarStreamFilesystemRestoreObserver, TarStreamObserver,
42 TarStreamSummaryValidator, TarStreamTotalExtractionSizeValidator,
43};
44use crate::wire::{
45 BlockRecord, BootstrapSidecarHeader, CriticalMetadataImage, CriticalMetadataRecoveryHeader,
46 CriticalMetadataRecoveryShard, CriticalRecoveryLocator, CryptoHeader, CryptoHeaderFixed,
47 ExtensionTlv, ManifestFooter, RootAuthFooterV1, VolumeHeader, VolumeTrailer,
48};
49
50const TRAILER_HMAC_COVERED_LEN: usize = 96;
51const MANIFEST_HMAC_COVERED_LEN: usize = 104;
52const SIDECAR_HMAC_COVERED_LEN: usize = 92;
53const DEFAULT_MAX_VERIFY_TAR_SIZE: usize = 128 * 1024 * 1024;
54const DEFAULT_MAX_TRAILING_GARBAGE_SCAN: usize = 1024 * 1024;
55const DEFAULT_MAX_TOTAL_EXTRACTION_SIZE: u64 = 100 * 1024 * 1024 * 1024;
56const DIRECTORY_HINT_REQUIRED_FILE_COUNT: u64 = 100_000;
57
58fn default_jobs() -> usize {
59 std::thread::available_parallelism()
60 .map(|jobs| jobs.get())
61 .unwrap_or(1)
62}
63
64pub trait ArchiveReadAt: Send + Sync + 'static {
65 fn len(&self) -> Result<u64, FormatError>;
66 fn is_empty(&self) -> Result<bool, FormatError> {
67 Ok(self.len()? == 0)
68 }
69 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError>;
70}
71
72impl ArchiveReadAt for File {
73 fn len(&self) -> Result<u64, FormatError> {
74 self.metadata()
75 .map(|metadata| metadata.len())
76 .map_err(|_| FormatError::InvalidArchive("archive read metadata failed"))
77 }
78
79 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
80 file_read_exact_at(self, offset, buf)
81 }
82}
83
84#[cfg(unix)]
85fn file_read_exact_at(file: &File, mut offset: u64, mut buf: &mut [u8]) -> Result<(), FormatError> {
86 use std::os::unix::fs::FileExt;
87
88 while !buf.is_empty() {
89 let read = file
90 .read_at(buf, offset)
91 .map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
92 if read == 0 {
93 return Err(FormatError::InvalidArchive("archive read failed"));
94 }
95 offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
96 let rest = std::mem::take(&mut buf).split_at_mut(read).1;
97 buf = rest;
98 }
99 Ok(())
100}
101
102#[cfg(windows)]
103fn file_read_exact_at(file: &File, mut offset: u64, mut buf: &mut [u8]) -> Result<(), FormatError> {
104 use std::os::windows::fs::FileExt;
105
106 while !buf.is_empty() {
107 let read = file
108 .seek_read(buf, offset)
109 .map_err(|_| FormatError::InvalidArchive("archive read failed"))?;
110 if read == 0 {
111 return Err(FormatError::InvalidArchive("archive read failed"));
112 }
113 offset = checked_u64_add(offset, read as u64, "archive read offset overflow")?;
114 let rest = std::mem::take(&mut buf).split_at_mut(read).1;
115 buf = rest;
116 }
117 Ok(())
118}
119
120#[cfg(not(any(unix, windows)))]
121fn file_read_exact_at(file: &File, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
122 let mut file = file
123 .try_clone()
124 .map_err(|_| FormatError::InvalidArchive("archive read clone failed"))?;
125 std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(offset))
126 .map_err(|_| FormatError::InvalidArchive("archive read seek failed"))?;
127 file.read_exact(buf)
128 .map_err(|_| FormatError::InvalidArchive("archive read failed"))
129}
130
131impl ArchiveReadAt for Vec<u8> {
132 fn len(&self) -> Result<u64, FormatError> {
133 u64::try_from(self.len())
134 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
135 }
136
137 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
138 let offset = to_usize(offset, "archive")?;
139 let end = checked_add(offset, buf.len(), "archive")?;
140 let source = self.get(offset..end).ok_or(FormatError::InvalidLength {
141 structure: "archive",
142 expected: end,
143 actual: self.len(),
144 })?;
145 buf.copy_from_slice(source);
146 Ok(())
147 }
148}
149
150impl<T: ArchiveReadAt + ?Sized> ArchiveReadAt for Arc<T> {
151 fn len(&self) -> Result<u64, FormatError> {
152 (**self).len()
153 }
154
155 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
156 (**self).read_exact_at(offset, buf)
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub struct ReaderOptions {
162 pub max_trailing_garbage_scan: usize,
163 pub max_verify_tar_size: usize,
164 pub max_total_extraction_size: u64,
165 pub jobs: usize,
166}
167
168impl Default for ReaderOptions {
169 fn default() -> Self {
170 Self {
171 max_trailing_garbage_scan: DEFAULT_MAX_TRAILING_GARBAGE_SCAN,
172 max_verify_tar_size: DEFAULT_MAX_VERIFY_TAR_SIZE,
173 max_total_extraction_size: DEFAULT_MAX_TOTAL_EXTRACTION_SIZE,
174 jobs: default_jobs(),
175 }
176 }
177}
178
179pub(crate) fn validate_reader_options(options: ReaderOptions) -> Result<(), FormatError> {
180 if options.jobs == 0 {
181 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
182 }
183 Ok(())
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct ArchiveEntry {
188 pub path: String,
189 pub file_data_size: u64,
190 pub kind: TarEntryKind,
191 pub mode: u32,
192 pub mtime: u64,
193 pub diagnostics: Vec<MetadataDiagnostic>,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct ArchiveIndexEntry {
198 pub path: String,
199 pub file_data_size: u64,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct ExtractedArchiveMember {
204 pub path: String,
205 pub kind: TarEntryKind,
206 pub data: Vec<u8>,
207 pub link_target: Option<String>,
208 pub diagnostics: Vec<MetadataDiagnostic>,
209}
210
211pub trait ArchiveExtractProgressSink {
217 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64);
219}
220
221impl<F> ArchiveExtractProgressSink for F
222where
223 F: FnMut(&str, u64),
224{
225 fn file_bytes_extracted(&mut self, archive_path: &str, bytes: u64) {
226 self(archive_path, bytes);
227 }
228}
229
230#[derive(Debug, Clone)]
231pub struct OpenedArchive {
232 options: ReaderOptions,
233 observed_archive_bytes: u64,
234 observed_volume_count: u32,
235 subkeys: Subkeys,
236 blocks: BTreeMap<u64, BlockRecord>,
237 lazy_blocks: Option<Arc<SeekableBlockSource>>,
238 crypto_header_bytes: Vec<u8>,
239 pub volume_header: VolumeHeader,
240 pub crypto_header: CryptoHeaderFixed,
241 pub manifest_footer: ManifestFooter,
242 pub volume_trailer: Option<VolumeTrailer>,
243 pub root_auth_footer: Option<RootAuthFooterV1>,
244 pub index_root: IndexRoot,
245 payload_dictionary: Option<Vec<u8>>,
246}
247
248#[derive(Debug)]
249pub struct ArchiveContentVerification<'a> {
250 archive: &'a OpenedArchive,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct ArchiveRepairPatch {
255 pub volume_index: u32,
256 pub block_index: u64,
257 pub record_offset: u64,
258 pub record_bytes: Vec<u8>,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub enum RootAuthDiagnostic {
263 RootAuthContentVerified,
264 RootAuthDeferredFullArchiveScanRequired,
265 AuthenticatedMetadataNotRootSigned,
266 RecoveryMarginNotRootAuthenticated,
267 ReplicatedGlobalCopyUncheckedDueToVolumeLoss,
268 RecoveryMarginChecked,
269 RecoveryMarginFailed,
270 RecoveryMarginUnchecked,
271}
272
273impl RootAuthDiagnostic {
274 pub const fn label(self) -> &'static str {
275 match self {
276 Self::RootAuthContentVerified => "root_auth_content_verified",
277 Self::RootAuthDeferredFullArchiveScanRequired => {
278 "root_auth_deferred_full_archive_scan_required"
279 }
280 Self::AuthenticatedMetadataNotRootSigned => "authenticated_metadata_not_root_signed",
281 Self::RecoveryMarginNotRootAuthenticated => "recovery_margin_not_root_authenticated",
282 Self::ReplicatedGlobalCopyUncheckedDueToVolumeLoss => {
283 "replicated_global_copy_unchecked_due_to_volume_loss"
284 }
285 Self::RecoveryMarginChecked => "recovery_margin_checked",
286 Self::RecoveryMarginFailed => "recovery_margin_failed",
287 Self::RecoveryMarginUnchecked => "recovery_margin_unchecked",
288 }
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum PublicNoKeyDiagnostic {
294 PublicDataBlockCommitmentVerified,
295 PublicPhysicalCompletenessUnverified,
296 PublicRecoveryMarginUnchecked,
297}
298
299impl PublicNoKeyDiagnostic {
300 pub const fn label(self) -> &'static str {
301 match self {
302 Self::PublicDataBlockCommitmentVerified => "public_data_block_commitment_verified",
303 Self::PublicPhysicalCompletenessUnverified => "public_physical_completeness_unverified",
304 Self::PublicRecoveryMarginUnchecked => "public_recovery_margin_unchecked",
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct RootAuthVerification {
311 pub archive_root: [u8; 32],
312 pub authenticator_id: u16,
313 pub signer_identity_type: u16,
314 pub signer_identity_bytes: Vec<u8>,
315 pub total_data_block_count: u64,
316 pub diagnostics: Vec<RootAuthDiagnostic>,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct PublicNoKeyVerification {
321 pub archive_root: [u8; 32],
322 pub authenticator_id: u16,
323 pub signer_identity_type: u16,
324 pub signer_identity_bytes: Vec<u8>,
325 pub total_data_block_count: u64,
326 pub diagnostics: Vec<PublicNoKeyDiagnostic>,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330struct RootAuthMaterial {
331 critical_metadata_digest: [u8; 32],
332 index_digest: [u8; 32],
333 fec_layout_digest: [u8; 32],
334 data_block_merkle_root: [u8; 32],
335 signer_identity_digest: [u8; 32],
336 archive_root: [u8; 32],
337 total_data_block_count: u64,
338}
339
340#[derive(Debug, Clone, Copy)]
341struct ObjectExtent {
342 first_block_index: u64,
343 data_block_count: u32,
344 parity_block_count: u32,
345 encrypted_size: u32,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349enum ParityReadPolicy {
350 Always,
351 RepairOnly,
352}
353
354pub(crate) struct StreamedArchiveOpenParts {
355 pub(crate) options: ReaderOptions,
356 pub(crate) observed_archive_bytes: u64,
357 pub(crate) subkeys: Subkeys,
358 pub(crate) blocks: BTreeMap<u64, BlockRecord>,
359 pub(crate) crypto_header_bytes: Vec<u8>,
360 pub(crate) volume_header: VolumeHeader,
361 pub(crate) crypto_header: CryptoHeaderFixed,
362 pub(crate) manifest_footer: ManifestFooter,
363 pub(crate) volume_trailer: VolumeTrailer,
364 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
365}
366
367#[derive(Clone, Copy)]
368struct WinningIndexEntry {
369 start: u64,
370 file_data_size: u64,
371 shard_index: usize,
372 file_index: usize,
373}
374
375struct LocatedIndexFile {
376 shard: IndexShard,
377 file_index: usize,
378 start: u64,
379}
380
381struct ExtractProgressWriter<'a, W> {
382 inner: &'a mut W,
383 archive_path: &'a str,
384 file_data_size: u64,
385 reported_bytes: u64,
386 progress: &'a mut dyn ArchiveExtractProgressSink,
387}
388
389impl<'a, W> ExtractProgressWriter<'a, W> {
390 fn new(
391 inner: &'a mut W,
392 archive_path: &'a str,
393 file_data_size: u64,
394 progress: &'a mut dyn ArchiveExtractProgressSink,
395 ) -> Self {
396 Self {
397 inner,
398 archive_path,
399 file_data_size,
400 reported_bytes: 0,
401 progress,
402 }
403 }
404
405 fn report(&mut self, bytes: u64) {
406 if bytes == 0 || self.file_data_size == 0 {
407 return;
408 }
409 let capped_next = self
410 .reported_bytes
411 .saturating_add(bytes)
412 .min(self.file_data_size);
413 let delta = capped_next.saturating_sub(self.reported_bytes);
414 if delta == 0 {
415 return;
416 }
417 self.reported_bytes = capped_next;
418 self.progress.file_bytes_extracted(self.archive_path, delta);
419 }
420}
421
422impl<W: Write> Write for ExtractProgressWriter<'_, W> {
423 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
424 let written = self.inner.write(buf)?;
425 self.report(written as u64);
426 Ok(written)
427 }
428
429 fn flush(&mut self) -> std::io::Result<()> {
430 self.inner.flush()
431 }
432}
433
434struct DecodedTarMemberGroupReader<'a> {
435 archive: &'a OpenedArchive,
436 shard: &'a IndexShard,
437 file: &'a FileEntry,
438 decompressor: zstd::bulk::Decompressor<'static>,
439 next_frame_offset: u64,
440 cached_envelope_index: Option<u64>,
441 cached_envelope_plaintext: Vec<u8>,
442 current_frame: Vec<u8>,
443 current_frame_offset: usize,
444 remaining_group_bytes: u64,
445}
446
447struct SeekableVolumeSource {
448 reader: Arc<dyn ArchiveReadAt>,
449 volume_index: u32,
450 crypto_end: u64,
451 block_count: u64,
452 record_len: u64,
453 block_size: usize,
454}
455
456impl std::fmt::Debug for SeekableVolumeSource {
457 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458 f.debug_struct("SeekableVolumeSource")
459 .field("volume_index", &self.volume_index)
460 .field("crypto_end", &self.crypto_end)
461 .field("block_count", &self.block_count)
462 .field("record_len", &self.record_len)
463 .field("block_size", &self.block_size)
464 .finish_non_exhaustive()
465 }
466}
467
468#[derive(Debug)]
469struct SeekableBlockSource {
470 stripe_width: u32,
471 volumes: Vec<Option<SeekableVolumeSource>>,
472}
473
474trait BlockProvider {
475 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError>;
476}
477
478struct OpenedBlockProvider<'a> {
479 memory_blocks: &'a BTreeMap<u64, BlockRecord>,
480 lazy_blocks: Option<&'a SeekableBlockSource>,
481}
482
483impl SeekableBlockSource {
484 fn record_location(&self, block_index: u64) -> Result<(u32, u64), FormatError> {
485 if self.stripe_width == 0 {
486 return Err(FormatError::ZeroStripeWidth);
487 }
488 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
489 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
490 let Some(volume) = self
491 .volumes
492 .get(volume_index as usize)
493 .and_then(Option::as_ref)
494 else {
495 return Err(FormatError::InvalidArchive(
496 "repair output requires all archive volumes",
497 ));
498 };
499 let slot = block_index / self.stripe_width as u64;
500 if slot >= volume.block_count {
501 return Err(FormatError::InvalidArchive(
502 "BlockRecord global coverage has a gap",
503 ));
504 }
505 Ok((volume_index, volume.record_offset(slot)?))
506 }
507
508 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
509 if self.stripe_width == 0 {
510 return Err(FormatError::ZeroStripeWidth);
511 }
512 let volume_index = u32::try_from(block_index % self.stripe_width as u64)
513 .map_err(|_| FormatError::InvalidArchive("BlockRecord volume index overflow"))?;
514 let Some(volume) = self
515 .volumes
516 .get(volume_index as usize)
517 .and_then(Option::as_ref)
518 else {
519 return Ok(None);
520 };
521 let slot = block_index / self.stripe_width as u64;
522 if slot >= volume.block_count {
523 return Ok(None);
524 }
525 match volume.read_slot(slot, block_index) {
526 Ok(record) => Ok(Some(record)),
527 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
528 Err(err) => Err(err),
529 }
530 }
531
532 fn is_complete_volume_set(&self) -> bool {
533 self.volumes.iter().all(Option::is_some)
534 }
535
536 fn total_block_count(&self) -> Result<u64, FormatError> {
537 self.volumes
538 .iter()
539 .map(|volume| {
540 volume
541 .as_ref()
542 .map(|volume| volume.block_count)
543 .ok_or(FormatError::InvalidArchive(
544 "missing volume in complete set",
545 ))
546 })
547 .try_fold(0u64, |sum, count| {
548 checked_u64_add(sum, count?, "BlockRecord count overflow")
549 })
550 }
551}
552
553impl SeekableVolumeSource {
554 fn record_offset(&self, slot: u64) -> Result<u64, FormatError> {
555 self.crypto_end
556 .checked_add(checked_u64_mul(
557 slot,
558 self.record_len,
559 "BlockRecord offset overflow",
560 )?)
561 .ok_or(FormatError::InvalidArchive("BlockRecord offset overflow"))
562 }
563
564 fn read_slot(&self, slot: u64, expected_block_index: u64) -> Result<BlockRecord, FormatError> {
565 let record_offset = self.record_offset(slot)?;
566 let raw = read_at_vec(
567 self.reader.as_ref(),
568 record_offset,
569 usize::try_from(self.record_len)
570 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?,
571 "BlockRecord",
572 )?;
573 let record = BlockRecord::parse(&raw, self.block_size)?;
574 if record.block_index != expected_block_index {
575 return Err(FormatError::InvalidArchive(
576 "BlockRecord index does not match volume position",
577 ));
578 }
579 Ok(record)
580 }
581}
582
583impl BlockProvider for BTreeMap<u64, BlockRecord> {
584 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
585 Ok(self.get(&block_index).cloned())
586 }
587}
588
589impl BlockProvider for OpenedBlockProvider<'_> {
590 fn block(&self, block_index: u64) -> Result<Option<BlockRecord>, FormatError> {
591 if let Some(record) = self.memory_blocks.get(&block_index) {
592 return Ok(Some(record.clone()));
593 }
594 match self.lazy_blocks {
595 Some(source) => source.block(block_index),
596 None => Ok(None),
597 }
598 }
599}
600
601fn subkeys_for_open(
602 master_key: Option<&MasterKey>,
603 aead_algo: AeadAlgo,
604 archive_uuid: &[u8; 16],
605 session_id: &[u8; 16],
606) -> Result<Subkeys, FormatError> {
607 if aead_algo.is_encrypted() {
608 Subkeys::derive(
609 master_key.ok_or(FormatError::KeyMaterialMismatch)?,
610 archive_uuid,
611 session_id,
612 )
613 } else {
614 Ok(Subkeys::unencrypted_placeholder())
615 }
616}
617
618type DirectoryHintMap = BTreeMap<Vec<u8>, BTreeSet<u32>>;
619pub type ExtractedRegularFile = (Vec<u8>, Vec<MetadataDiagnostic>);
620const FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED: &str =
621 "fast full extract requires unique archive paths";
622
623#[derive(Debug)]
624struct PayloadIndexTables {
625 shards: Vec<IndexShard>,
626 file_count: u64,
627 frames: BTreeMap<u64, FrameEntry>,
628 envelopes: BTreeMap<u64, EnvelopeEntry>,
629}
630
631pub fn open_archive(bytes: &[u8], master_key: &MasterKey) -> Result<OpenedArchive, FormatError> {
632 OpenedArchive::open_with_options(bytes, master_key, ReaderOptions::default())
633}
634
635pub fn open_archive_unencrypted(bytes: &[u8]) -> Result<OpenedArchive, FormatError> {
636 require_unencrypted_volume_profile(bytes)?;
637 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
638 OpenedArchive::open_with_options(bytes, &placeholder, ReaderOptions::default())
639}
640
641pub fn open_archive_volumes(
642 volumes: &[&[u8]],
643 master_key: &MasterKey,
644) -> Result<OpenedArchive, FormatError> {
645 OpenedArchive::open_volumes_with_options(volumes, master_key, ReaderOptions::default())
646}
647
648pub fn open_archive_volumes_unencrypted(volumes: &[&[u8]]) -> Result<OpenedArchive, FormatError> {
649 for volume in volumes {
650 require_unencrypted_volume_profile(volume)?;
651 }
652 let placeholder = MasterKey::from_raw_key(&[0; 32])?;
653 OpenedArchive::open_volumes_with_options(volumes, &placeholder, ReaderOptions::default())
654}
655
656pub fn open_archive_with_bootstrap_sidecar(
657 bytes: &[u8],
658 bootstrap_sidecar: &[u8],
659 master_key: &MasterKey,
660) -> Result<OpenedArchive, FormatError> {
661 OpenedArchive::open_with_bootstrap_sidecar_options(
662 bytes,
663 bootstrap_sidecar,
664 master_key,
665 ReaderOptions::default(),
666 )
667}
668
669fn require_unencrypted_volume_profile(bytes: &[u8]) -> Result<(), FormatError> {
670 if bytes.len() < VOLUME_HEADER_LEN {
671 return Err(FormatError::InvalidLength {
672 structure: "archive",
673 expected: VOLUME_HEADER_LEN,
674 actual: bytes.len(),
675 });
676 }
677 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
678 let crypto_start = volume_header.crypto_header_offset as usize;
679 let crypto_len = volume_header.crypto_header_length as usize;
680 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
681 let crypto_header = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
682 if crypto_header.fixed.aead_algo == AeadAlgo::None
683 && crypto_header.fixed.kdf_algo == KdfAlgo::None
684 {
685 Ok(())
686 } else {
687 Err(FormatError::KeyMaterialMismatch)
688 }
689}
690
691pub fn open_seekable_archive<R: ArchiveReadAt>(
692 reader: R,
693 master_key: &MasterKey,
694) -> Result<OpenedArchive, FormatError> {
695 OpenedArchive::open_seekable_volumes_with_options(
696 vec![reader],
697 master_key,
698 ReaderOptions::default(),
699 )
700}
701
702pub fn open_seekable_archive_volumes<R: ArchiveReadAt>(
703 readers: Vec<R>,
704 master_key: &MasterKey,
705) -> Result<OpenedArchive, FormatError> {
706 OpenedArchive::open_seekable_volumes_with_options(readers, master_key, ReaderOptions::default())
707}
708
709pub fn open_seekable_archive_with_bootstrap_sidecar<R: ArchiveReadAt>(
710 reader: R,
711 bootstrap_sidecar: &[u8],
712 master_key: &MasterKey,
713) -> Result<OpenedArchive, FormatError> {
714 open_seekable_archive_with_bootstrap_sidecar_options(
715 reader,
716 bootstrap_sidecar,
717 master_key,
718 ReaderOptions::default(),
719 )
720}
721
722pub fn open_seekable_archive_with_bootstrap_sidecar_options<R: ArchiveReadAt>(
723 reader: R,
724 bootstrap_sidecar: &[u8],
725 master_key: &MasterKey,
726 options: ReaderOptions,
727) -> Result<OpenedArchive, FormatError> {
728 OpenedArchive::open_seekable_volumes_with_options_for_mode(
729 vec![Arc::new(reader) as Arc<dyn ArchiveReadAt>],
730 master_key,
731 options,
732 Some(bootstrap_sidecar),
733 )
734}
735
736pub fn open_non_seekable_archive(
737 bytes: &[u8],
738 master_key: &MasterKey,
739 bootstrap_sidecar: Option<&[u8]>,
740) -> Result<OpenedArchive, FormatError> {
741 match bootstrap_sidecar {
742 Some(sidecar) => OpenedArchive::open_with_bootstrap_sidecar_options_for_mode(
743 bytes,
744 sidecar,
745 master_key,
746 ReaderOptions::default(),
747 BootstrapSidecarUse::NonSeekableRandomAccess,
748 ),
749 None => Err(FormatError::ReaderUnsupported(
750 "non-seekable random access requires a bootstrap sidecar",
751 )),
752 }
753}
754
755pub fn public_no_key_verify_archive_with<F>(
756 bytes: &[u8],
757 verifier: F,
758) -> Result<PublicNoKeyVerification, FormatError>
759where
760 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
761{
762 public_no_key_verify_volumes_with_options(&[bytes], verifier, ReaderOptions::default())
763}
764
765pub fn public_no_key_verify_volumes_with<F>(
766 volumes: &[&[u8]],
767 verifier: F,
768) -> Result<PublicNoKeyVerification, FormatError>
769where
770 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
771{
772 public_no_key_verify_volumes_with_options(volumes, verifier, ReaderOptions::default())
773}
774
775pub fn sequential_extract_tar_stream(
781 bytes: &[u8],
782 master_key: &MasterKey,
783) -> Result<Vec<u8>, FormatError> {
784 sequential_extract_tar_stream_with_options(bytes, master_key, ReaderOptions::default())
785}
786
787impl OpenedArchive {
788 fn block_provider(&self) -> OpenedBlockProvider<'_> {
789 OpenedBlockProvider {
790 memory_blocks: &self.blocks,
791 lazy_blocks: self.lazy_blocks.as_deref(),
792 }
793 }
794
795 fn missing_volume_count(&self) -> u32 {
796 self.crypto_header
797 .stripe_width
798 .saturating_sub(self.observed_volume_count)
799 }
800
801 fn root_auth_success_diagnostics(&self) -> Vec<RootAuthDiagnostic> {
802 let mut diagnostics = vec![
803 RootAuthDiagnostic::RootAuthContentVerified,
804 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
805 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
806 ];
807 if self.missing_volume_count() > 0 {
808 diagnostics.push(RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss);
809 }
810 diagnostics.push(RootAuthDiagnostic::RecoveryMarginUnchecked);
811 diagnostics
812 }
813
814 pub fn open_with_options(
815 bytes: &[u8],
816 master_key: &MasterKey,
817 options: ReaderOptions,
818 ) -> Result<Self, FormatError> {
819 Self::open_volumes_with_options(&[bytes], master_key, options)
820 }
821
822 pub fn open_volumes_with_options(
823 volumes: &[&[u8]],
824 master_key: &MasterKey,
825 options: ReaderOptions,
826 ) -> Result<Self, FormatError> {
827 validate_reader_options(options)?;
828 if volumes.is_empty() {
829 return Err(FormatError::InvalidArchive("no volumes supplied"));
830 }
831
832 let observed_archive_bytes =
833 observed_archive_size(volumes.iter().map(|volume| volume.len() as u64))?;
834 let mut first: Option<ParsedSeekableVolume> = None;
835 let mut manifest_authority: Option<ManifestFooter> = None;
836 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
837 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
838 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
839 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
840 let mut saw_root_auth_absent = false;
841 let mut first_manifest_footer_error: Option<FormatError> = None;
842 let mut seen_volume_indexes = BTreeSet::new();
843 let mut blocks = BTreeMap::new();
844 let mut erased_block_indices = BTreeSet::new();
845
846 for volume_bytes in volumes {
847 let mut parsed = parse_seekable_volume(volume_bytes, master_key, options)?;
848 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
849 return Err(FormatError::InvalidArchive(
850 "duplicate authenticated volume index",
851 ));
852 }
853
854 if let Some(first) = &first {
855 validate_volume_set_member(first, &parsed)?;
856 }
857
858 if let Some(footer) = &parsed.manifest_footer {
859 if let Some(authority) = &manifest_authority {
860 if !manifest_bootstrap_fields_match(authority, footer) {
861 return Err(FormatError::InvalidArchive(
862 "ManifestFooter bootstrap fields differ",
863 ));
864 }
865 } else {
866 manifest_authority = Some(footer.clone());
867 manifest_authority_volume_header = Some(parsed.volume_header.clone());
868 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
869 }
870 } else if first_manifest_footer_error.is_none() {
871 first_manifest_footer_error = parsed.manifest_footer_error.take();
872 }
873
874 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
875 (Some(footer), Some(bytes)) => {
876 if saw_root_auth_absent {
877 return Err(FormatError::InvalidArchive(
878 "root-auth footer presence differs across volumes",
879 ));
880 }
881 if let Some(authority_bytes) = &root_auth_authority_bytes {
882 if authority_bytes != bytes {
883 return Err(FormatError::InvalidArchive(
884 "RootAuthFooter copies differ",
885 ));
886 }
887 } else {
888 root_auth_authority = Some(footer.clone());
889 root_auth_authority_bytes = Some(bytes.clone());
890 }
891 }
892 (None, None) => {
893 if root_auth_authority_bytes.is_some() {
894 return Err(FormatError::InvalidArchive(
895 "root-auth footer presence differs across volumes",
896 ));
897 }
898 saw_root_auth_absent = true;
899 }
900 _ => {
901 return Err(FormatError::InvalidArchive(
902 "root-auth footer terminal state is inconsistent",
903 ));
904 }
905 }
906
907 for (block_index, record) in &parsed.blocks {
908 if blocks.insert(*block_index, record.clone()).is_some() {
909 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
910 }
911 }
912 for block_index in &parsed.erased_block_indices {
913 erased_block_indices.insert(*block_index);
914 }
915
916 if first.is_none() {
917 first = Some(parsed);
918 }
919 }
920
921 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
922 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
923 Some(err) => err,
924 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
925 })?;
926 let authority_volume_header = manifest_authority_volume_header.ok_or(
927 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
928 )?;
929 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
930 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
931 )?;
932 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
933 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
934 let missing_volume_count = first
935 .crypto_header
936 .stripe_width
937 .checked_sub(observed_volume_count)
938 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
939 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
940 return Err(FormatError::InvalidArchive(
941 "missing volume count exceeds volume_loss_tolerance",
942 ));
943 }
944 if seen_volume_indexes.len() == first.crypto_header.stripe_width as usize {
945 validate_complete_global_block_coverage(&blocks, &erased_block_indices)?;
946 }
947
948 let limits = metadata_limits(&first.crypto_header);
949 let index_root_plaintext = load_metadata_object_from_parts(
950 &blocks,
951 ObjectLoadContext::index_root(
952 &first.volume_header,
953 &first.crypto_header,
954 &first.subkeys,
955 ObjectExtent {
956 first_block_index: manifest_footer.index_root_first_block,
957 data_block_count: manifest_footer.index_root_data_block_count,
958 parity_block_count: manifest_footer.index_root_parity_block_count,
959 encrypted_size: manifest_footer.index_root_encrypted_size,
960 },
961 ),
962 manifest_footer.index_root_decompressed_size,
963 )?;
964 let index_root = IndexRoot::parse(
965 &index_root_plaintext,
966 first.crypto_header.has_dictionary != 0,
967 limits,
968 )?;
969 let payload_dictionary = load_archive_dictionary(
970 &blocks,
971 &first.subkeys,
972 &first.volume_header,
973 &first.crypto_header,
974 &index_root,
975 )?;
976
977 Ok(Self {
978 options,
979 observed_archive_bytes,
980 observed_volume_count,
981 subkeys: first.subkeys,
982 blocks,
983 lazy_blocks: None,
984 crypto_header_bytes: first.crypto_header_bytes,
985 volume_header: authority_volume_header,
986 crypto_header: first.crypto_header,
987 manifest_footer,
988 volume_trailer: Some(authority_volume_trailer),
989 root_auth_footer: root_auth_authority,
990 index_root,
991 payload_dictionary,
992 })
993 }
994
995 pub fn open_seekable_volumes_with_options<R: ArchiveReadAt>(
996 readers: Vec<R>,
997 master_key: &MasterKey,
998 options: ReaderOptions,
999 ) -> Result<Self, FormatError> {
1000 let readers = readers
1001 .into_iter()
1002 .map(|reader| Arc::new(reader) as Arc<dyn ArchiveReadAt>)
1003 .collect::<Vec<_>>();
1004 Self::open_seekable_volumes_with_options_for_mode(readers, master_key, options, None)
1005 }
1006
1007 fn open_seekable_volumes_with_options_for_mode(
1008 readers: Vec<Arc<dyn ArchiveReadAt>>,
1009 master_key: &MasterKey,
1010 options: ReaderOptions,
1011 bootstrap_sidecar: Option<&[u8]>,
1012 ) -> Result<Self, FormatError> {
1013 validate_reader_options(options)?;
1014 if readers.is_empty() {
1015 return Err(FormatError::InvalidArchive("no volumes supplied"));
1016 }
1017 if bootstrap_sidecar.is_some() && readers.len() > 1 {
1018 return Err(FormatError::ReaderUnsupported(
1019 "multi-volume inputs with bootstrap sidecar are not supported",
1020 ));
1021 }
1022
1023 let observed_archive_bytes = observed_archive_size(
1024 readers
1025 .iter()
1026 .map(|reader| reader.len())
1027 .collect::<Result<Vec<_>, _>>()?
1028 .into_iter()
1029 .chain(bootstrap_sidecar.map(|sidecar| sidecar.len() as u64)),
1030 )?;
1031 let mut first: Option<ParsedSeekableReadAtVolume> = None;
1032 let mut manifest_authority: Option<ManifestFooter> = None;
1033 let mut manifest_authority_volume_header: Option<VolumeHeader> = None;
1034 let mut manifest_authority_volume_trailer: Option<VolumeTrailer> = None;
1035 let mut root_auth_authority: Option<RootAuthFooterV1> = None;
1036 let mut root_auth_authority_bytes: Option<Vec<u8>> = None;
1037 let mut saw_root_auth_absent = false;
1038 let mut first_manifest_footer_error: Option<FormatError> = None;
1039 let mut seen_volume_indexes = BTreeSet::new();
1040 let mut lazy_volume_slots: Vec<Option<SeekableVolumeSource>> = Vec::new();
1041
1042 for reader in readers {
1043 let mut parsed = parse_seekable_read_at_volume(reader, master_key, options)?;
1044 if bootstrap_sidecar.is_some() {
1045 validate_bootstrap_single_volume_input(
1046 &parsed.volume_header,
1047 &parsed.crypto_header,
1048 )?;
1049 }
1050 if !seen_volume_indexes.insert(parsed.volume_header.volume_index) {
1051 return Err(FormatError::InvalidArchive(
1052 "duplicate authenticated volume index",
1053 ));
1054 }
1055
1056 if let Some(first) = &first {
1057 validate_volume_set_member_metadata(
1058 &first.volume_header,
1059 &first.crypto_header,
1060 &first.crypto_header_bytes,
1061 &parsed.volume_header,
1062 &parsed.crypto_header,
1063 &parsed.crypto_header_bytes,
1064 )?;
1065 } else {
1066 lazy_volume_slots.resize_with(parsed.crypto_header.stripe_width as usize, || None);
1067 }
1068
1069 if let Some(footer) = &parsed.manifest_footer {
1070 if let Some(authority) = &manifest_authority {
1071 if !manifest_bootstrap_fields_match(authority, footer) {
1072 return Err(FormatError::InvalidArchive(
1073 "ManifestFooter bootstrap fields differ",
1074 ));
1075 }
1076 } else {
1077 manifest_authority = Some(footer.clone());
1078 manifest_authority_volume_header = Some(parsed.volume_header.clone());
1079 manifest_authority_volume_trailer = Some(parsed.volume_trailer.clone());
1080 }
1081 } else if first_manifest_footer_error.is_none() {
1082 first_manifest_footer_error = parsed.manifest_footer_error.take();
1083 }
1084
1085 match (&parsed.root_auth_footer, &parsed.root_auth_footer_bytes) {
1086 (Some(footer), Some(bytes)) => {
1087 if saw_root_auth_absent {
1088 return Err(FormatError::InvalidArchive(
1089 "root-auth footer presence differs across volumes",
1090 ));
1091 }
1092 if let Some(authority_bytes) = &root_auth_authority_bytes {
1093 if authority_bytes != bytes {
1094 return Err(FormatError::InvalidArchive(
1095 "RootAuthFooter copies differ",
1096 ));
1097 }
1098 } else {
1099 root_auth_authority = Some(footer.clone());
1100 root_auth_authority_bytes = Some(bytes.clone());
1101 }
1102 }
1103 (None, None) => {
1104 if root_auth_authority_bytes.is_some() {
1105 return Err(FormatError::InvalidArchive(
1106 "root-auth footer presence differs across volumes",
1107 ));
1108 }
1109 saw_root_auth_absent = true;
1110 }
1111 _ => {
1112 return Err(FormatError::InvalidArchive(
1113 "root-auth footer terminal state is inconsistent",
1114 ));
1115 }
1116 }
1117
1118 let record_len = block_record_len(parsed.crypto_header.block_size as usize)?;
1119 let source = SeekableVolumeSource {
1120 reader: parsed.reader.clone(),
1121 volume_index: parsed.volume_header.volume_index,
1122 crypto_end: parsed.crypto_end,
1123 block_count: parsed.volume_trailer.block_count,
1124 record_len,
1125 block_size: parsed.crypto_header.block_size as usize,
1126 };
1127 let slot = parsed.volume_header.volume_index as usize;
1128 if slot >= lazy_volume_slots.len() || lazy_volume_slots[slot].replace(source).is_some()
1129 {
1130 return Err(FormatError::InvalidArchive(
1131 "duplicate authenticated volume index",
1132 ));
1133 }
1134
1135 if first.is_none() {
1136 first = Some(parsed);
1137 }
1138 }
1139
1140 let first = first.ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
1141 let manifest_footer = manifest_authority.ok_or(match first_manifest_footer_error {
1142 Some(err) => err,
1143 None => FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1144 })?;
1145 let authority_volume_header = manifest_authority_volume_header.ok_or(
1146 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1147 )?;
1148 let authority_volume_trailer = manifest_authority_volume_trailer.ok_or(
1149 FormatError::InvalidArchive("no authenticated ManifestFooter found"),
1150 )?;
1151 let observed_volume_count = u32::try_from(seen_volume_indexes.len())
1152 .map_err(|_| FormatError::InvalidArchive("volume count overflow"))?;
1153 let missing_volume_count = first
1154 .crypto_header
1155 .stripe_width
1156 .checked_sub(observed_volume_count)
1157 .ok_or(FormatError::InvalidArchive("volume count overflow"))?;
1158 if missing_volume_count > first.crypto_header.volume_loss_tolerance as u32 {
1159 return Err(FormatError::InvalidArchive(
1160 "missing volume count exceeds volume_loss_tolerance",
1161 ));
1162 }
1163
1164 let mut blocks = BTreeMap::new();
1165 let sidecar = if let Some(bytes) = bootstrap_sidecar {
1166 let sidecar = parse_bootstrap_sidecar(
1167 bytes,
1168 &first.volume_header,
1169 &first.crypto_header,
1170 &first.subkeys,
1171 )?;
1172 sidecar
1173 .require_sections_for(BootstrapSidecarUse::SeekableAssist, &first.crypto_header)?;
1174 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1175 if !manifest_bootstrap_fields_match(&manifest_footer, sidecar_manifest) {
1176 return Err(FormatError::InvalidArchive(
1177 "bootstrap sidecar conflicts with terminal ManifestFooter",
1178 ));
1179 }
1180 }
1181 Some((bytes, sidecar))
1182 } else {
1183 None
1184 };
1185
1186 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1187 if let Some((offset, length)) = sidecar.index_root_records_section {
1188 let index_root_records = parse_sidecar_block_records(
1189 sidecar_bytes,
1190 first.crypto_header.block_size as usize,
1191 SidecarBlockRecordsSection {
1192 offset,
1193 length,
1194 extent: index_root_extent_from_manifest(&manifest_footer),
1195 data_kind: BlockKind::IndexRootData,
1196 parity_kind: BlockKind::IndexRootParity,
1197 structure: "IndexRoot",
1198 },
1199 )?;
1200 insert_sidecar_records(&mut blocks, index_root_records)?;
1201 }
1202 }
1203
1204 let lazy_source = Arc::new(SeekableBlockSource {
1205 stripe_width: first.crypto_header.stripe_width,
1206 volumes: lazy_volume_slots,
1207 });
1208 let block_provider = OpenedBlockProvider {
1209 memory_blocks: &blocks,
1210 lazy_blocks: Some(lazy_source.as_ref()),
1211 };
1212 let limits = metadata_limits(&first.crypto_header);
1213 let index_root_plaintext = load_metadata_object_from_parts(
1214 &block_provider,
1215 ObjectLoadContext::index_root(
1216 &first.volume_header,
1217 &first.crypto_header,
1218 &first.subkeys,
1219 index_root_extent_from_manifest(&manifest_footer),
1220 ),
1221 manifest_footer.index_root_decompressed_size,
1222 )?;
1223 let index_root = IndexRoot::parse(
1224 &index_root_plaintext,
1225 first.crypto_header.has_dictionary != 0,
1226 limits,
1227 )?;
1228 if first.crypto_header.has_dictionary != 0 {
1229 if let Some((sidecar_bytes, sidecar)) = &sidecar {
1230 if let Some((offset, length)) = sidecar.dictionary_records_section {
1231 let dictionary_records = parse_sidecar_block_records(
1232 sidecar_bytes,
1233 first.crypto_header.block_size as usize,
1234 SidecarBlockRecordsSection {
1235 offset,
1236 length,
1237 extent: dictionary_extent_from_index_root(&index_root)?,
1238 data_kind: BlockKind::DictionaryData,
1239 parity_kind: BlockKind::DictionaryParity,
1240 structure: "Dictionary",
1241 },
1242 )?;
1243 insert_sidecar_records(&mut blocks, dictionary_records)?;
1244 }
1245 }
1246 }
1247 let block_provider = OpenedBlockProvider {
1248 memory_blocks: &blocks,
1249 lazy_blocks: Some(lazy_source.as_ref()),
1250 };
1251 let payload_dictionary = load_archive_dictionary(
1252 &block_provider,
1253 &first.subkeys,
1254 &first.volume_header,
1255 &first.crypto_header,
1256 &index_root,
1257 )?;
1258
1259 Ok(Self {
1260 options,
1261 observed_archive_bytes,
1262 observed_volume_count,
1263 subkeys: first.subkeys,
1264 blocks,
1265 lazy_blocks: Some(lazy_source),
1266 crypto_header_bytes: first.crypto_header_bytes,
1267 volume_header: authority_volume_header,
1268 crypto_header: first.crypto_header,
1269 manifest_footer,
1270 volume_trailer: Some(authority_volume_trailer),
1271 root_auth_footer: root_auth_authority,
1272 index_root,
1273 payload_dictionary,
1274 })
1275 }
1276
1277 pub fn open_with_bootstrap_sidecar_options(
1278 bytes: &[u8],
1279 bootstrap_sidecar: &[u8],
1280 master_key: &MasterKey,
1281 options: ReaderOptions,
1282 ) -> Result<Self, FormatError> {
1283 Self::open_with_bootstrap_sidecar_options_for_mode(
1284 bytes,
1285 bootstrap_sidecar,
1286 master_key,
1287 options,
1288 BootstrapSidecarUse::SeekableAssist,
1289 )
1290 }
1291
1292 fn open_with_bootstrap_sidecar_options_for_mode(
1293 bytes: &[u8],
1294 bootstrap_sidecar: &[u8],
1295 master_key: &MasterKey,
1296 options: ReaderOptions,
1297 sidecar_use: BootstrapSidecarUse,
1298 ) -> Result<Self, FormatError> {
1299 let observed_archive_bytes =
1300 observed_archive_size([bytes.len() as u64, bootstrap_sidecar.len() as u64])?;
1301 if bytes.len() < VOLUME_HEADER_LEN {
1302 return Err(FormatError::InvalidLength {
1303 structure: "archive",
1304 expected: VOLUME_HEADER_LEN,
1305 actual: bytes.len(),
1306 });
1307 }
1308
1309 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
1310 let crypto_start = volume_header.crypto_header_offset as usize;
1311 let crypto_len = volume_header.crypto_header_length as usize;
1312 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
1313 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
1314 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
1315 let subkeys = subkeys_for_open(
1316 Some(master_key),
1317 parsed_crypto.fixed.aead_algo,
1318 &volume_header.archive_uuid,
1319 &volume_header.session_id,
1320 )?;
1321 verify_integrity_tag(
1322 HmacDomain::CryptoHeader,
1323 parsed_crypto.fixed.aead_algo,
1324 Some(&subkeys.mac_key),
1325 &volume_header.archive_uuid,
1326 &volume_header.session_id,
1327 parsed_crypto.hmac_covered_bytes,
1328 &parsed_crypto.header_hmac,
1329 )?;
1330 parsed_crypto.validate_extension_semantics()?;
1331 reject_unsupported_raw_stream_profile(&parsed_crypto.extensions)?;
1332 validate_bootstrap_single_volume_input(&volume_header, &parsed_crypto.fixed)?;
1333 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
1334
1335 let sidecar = parse_bootstrap_sidecar(
1336 bootstrap_sidecar,
1337 &volume_header,
1338 &parsed_crypto.fixed,
1339 &subkeys,
1340 )?;
1341 sidecar.require_sections_for(sidecar_use, &parsed_crypto.fixed)?;
1342
1343 let (mut blocks, terminal_offset, observed_block_count) = parse_stream_block_prefix(
1344 bytes,
1345 crypto_end,
1346 parsed_crypto.fixed.block_size as usize,
1347 &volume_header,
1348 )?;
1349 let terminal_material = match sidecar_use {
1350 BootstrapSidecarUse::SeekableAssist => Some(parse_terminal_material(
1351 bytes,
1352 terminal_offset,
1353 observed_block_count,
1354 KeyHoldingTerminalContext {
1355 subkeys: &subkeys,
1356 volume_header: &volume_header,
1357 crypto_header: &parsed_crypto.fixed,
1358 crypto_header_bytes: crypto_bytes,
1359 },
1360 options,
1361 )?),
1362 BootstrapSidecarUse::NonSeekableRandomAccess => parse_terminal_material(
1363 bytes,
1364 terminal_offset,
1365 observed_block_count,
1366 KeyHoldingTerminalContext {
1367 subkeys: &subkeys,
1368 volume_header: &volume_header,
1369 crypto_header: &parsed_crypto.fixed,
1370 crypto_header_bytes: crypto_bytes,
1371 },
1372 options,
1373 )
1374 .ok(),
1375 };
1376 let terminal_manifest = terminal_material.as_ref().map(|(manifest, _, _)| manifest);
1377 let manifest_authority = match sidecar_use {
1378 BootstrapSidecarUse::SeekableAssist => {
1379 let terminal_manifest = terminal_manifest.ok_or(FormatError::InvalidArchive(
1380 "terminal ManifestFooter/VolumeTrailer is required",
1381 ))?;
1382 if let Some(sidecar_manifest) = &sidecar.manifest_footer {
1383 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1384 return Err(FormatError::InvalidArchive(
1385 "bootstrap sidecar conflicts with terminal ManifestFooter",
1386 ));
1387 }
1388 }
1389 terminal_manifest.clone()
1390 }
1391 BootstrapSidecarUse::NonSeekableRandomAccess => {
1392 let sidecar_manifest = sidecar
1393 .manifest_footer
1394 .as_ref()
1395 .ok_or(FormatError::ReaderUnsupported(
1396 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
1397 ))?;
1398 if let Some(terminal_manifest) = terminal_manifest {
1399 if !manifest_bootstrap_fields_match(terminal_manifest, sidecar_manifest) {
1400 return Err(FormatError::InvalidArchive(
1401 "bootstrap sidecar conflicts with terminal ManifestFooter",
1402 ));
1403 }
1404 }
1405 sidecar_manifest.clone()
1406 }
1407 };
1408 manifest_authority.validate_index_root_extent(parsed_crypto.fixed.block_size)?;
1409
1410 if let Some((offset, length)) = sidecar.index_root_records_section {
1411 let index_root_records = parse_sidecar_block_records(
1412 bootstrap_sidecar,
1413 parsed_crypto.fixed.block_size as usize,
1414 SidecarBlockRecordsSection {
1415 offset,
1416 length,
1417 extent: index_root_extent_from_manifest(&manifest_authority),
1418 data_kind: BlockKind::IndexRootData,
1419 parity_kind: BlockKind::IndexRootParity,
1420 structure: "IndexRoot",
1421 },
1422 )?;
1423 insert_sidecar_records(&mut blocks, index_root_records)?;
1424 }
1425
1426 let limits = metadata_limits(&parsed_crypto.fixed);
1427 let index_root_plaintext = load_metadata_object_from_parts(
1428 &blocks,
1429 ObjectLoadContext::index_root(
1430 &volume_header,
1431 &parsed_crypto.fixed,
1432 &subkeys,
1433 index_root_extent_from_manifest(&manifest_authority),
1434 ),
1435 manifest_authority.index_root_decompressed_size,
1436 )?;
1437 let index_root = IndexRoot::parse(
1438 &index_root_plaintext,
1439 parsed_crypto.fixed.has_dictionary != 0,
1440 limits,
1441 )?;
1442 if parsed_crypto.fixed.has_dictionary != 0 {
1443 if let Some((offset, length)) = sidecar.dictionary_records_section {
1444 let dictionary_records = parse_sidecar_block_records(
1445 bootstrap_sidecar,
1446 parsed_crypto.fixed.block_size as usize,
1447 SidecarBlockRecordsSection {
1448 offset,
1449 length,
1450 extent: dictionary_extent_from_index_root(&index_root)?,
1451 data_kind: BlockKind::DictionaryData,
1452 parity_kind: BlockKind::DictionaryParity,
1453 structure: "dictionary",
1454 },
1455 )?;
1456 insert_sidecar_records(&mut blocks, dictionary_records)?;
1457 }
1458 }
1459 let payload_dictionary = load_archive_dictionary(
1460 &blocks,
1461 &subkeys,
1462 &volume_header,
1463 &parsed_crypto.fixed,
1464 &index_root,
1465 )?;
1466
1467 Ok(Self {
1468 options,
1469 observed_archive_bytes,
1470 observed_volume_count: 1,
1471 subkeys,
1472 blocks,
1473 lazy_blocks: None,
1474 crypto_header_bytes: crypto_bytes.to_vec(),
1475 volume_header,
1476 crypto_header: parsed_crypto.fixed,
1477 manifest_footer: manifest_authority,
1478 volume_trailer: terminal_material
1479 .as_ref()
1480 .map(|(_, trailer, _)| trailer.clone()),
1481 root_auth_footer: terminal_material.and_then(|(_, _, root_auth)| root_auth),
1482 index_root,
1483 payload_dictionary,
1484 })
1485 }
1486
1487 pub fn list_index_entries(&self) -> Result<Vec<ArchiveIndexEntry>, FormatError> {
1493 let shards = self.load_all_index_shards()?;
1494 final_index_entry_winners(&shards)?
1495 .into_iter()
1496 .map(|(path, winner)| {
1497 Ok(ArchiveIndexEntry {
1498 path,
1499 file_data_size: winner.file_data_size,
1500 })
1501 })
1502 .collect()
1503 }
1504
1505 pub fn lookup_index_entry(&self, path: &str) -> Result<Option<ArchiveIndexEntry>, FormatError> {
1507 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1508 self.locate_index_file(&normalized)?
1509 .map(|located| archive_index_entry_from_loaded_file(&located.shard, located.file_index))
1510 .transpose()
1511 }
1512
1513 pub fn list_files(&self) -> Result<Vec<ArchiveEntry>, FormatError> {
1514 let shards = self.load_all_index_shards()?;
1515 final_index_entry_winners(&shards)?
1516 .into_iter()
1517 .map(|(path, winner)| {
1518 let shard = &shards[winner.shard_index];
1519 let member =
1520 self.decode_loaded_owned_tar_member(shard, winner.file_index, false)?;
1521 Ok(ArchiveEntry {
1522 path,
1523 file_data_size: winner.file_data_size,
1524 kind: member.kind,
1525 mode: member.mode,
1526 mtime: member.mtime,
1527 diagnostics: member.diagnostics,
1528 })
1529 })
1530 .collect()
1531 }
1532
1533 pub fn extract_file(&self, path: &str) -> Result<Option<Vec<u8>>, FormatError> {
1540 self.extract_member(path)?
1541 .map(|member| {
1542 if member.kind != TarEntryKind::Regular {
1543 return Err(FormatError::ReaderUnsupported(
1544 "extract_file returns only regular file payloads",
1545 ));
1546 }
1547 Ok(member.data)
1548 })
1549 .transpose()
1550 }
1551
1552 pub fn extract_file_with_diagnostics(
1555 &self,
1556 path: &str,
1557 ) -> Result<Option<ExtractedRegularFile>, FormatError> {
1558 self.extract_member(path)?
1559 .map(|member| {
1560 if member.kind != TarEntryKind::Regular {
1561 return Err(FormatError::ReaderUnsupported(
1562 "extract_file_with_diagnostics returns only regular file payloads",
1563 ));
1564 }
1565 Ok((member.data, member.diagnostics))
1566 })
1567 .transpose()
1568 }
1569
1570 pub fn extract_file_to_writer<W: Write>(
1576 &self,
1577 path: &str,
1578 writer: &mut W,
1579 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1580 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1581 self.locate_index_file(&normalized)?
1582 .map(|located| {
1583 self.stream_loaded_file_to_writer(&located.shard, located.file_index, writer)
1584 })
1585 .transpose()
1586 }
1587
1588 pub fn extract_file_to_writer_with_progress<W: Write>(
1591 &self,
1592 path: &str,
1593 writer: &mut W,
1594 progress: &mut dyn ArchiveExtractProgressSink,
1595 ) -> Result<Option<Vec<MetadataDiagnostic>>, ExtractError> {
1596 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1597 self.locate_index_file(&normalized)?
1598 .map(|located| {
1599 self.stream_loaded_file_to_writer_with_progress(
1600 &located.shard,
1601 located.file_index,
1602 writer,
1603 progress,
1604 )
1605 })
1606 .transpose()
1607 }
1608
1609 pub fn extract_member(
1610 &self,
1611 path: &str,
1612 ) -> Result<Option<ExtractedArchiveMember>, FormatError> {
1613 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1614 self.locate_index_file(&normalized)?
1615 .map(|located| self.extract_loaded_member(&located.shard, located.file_index))
1616 .transpose()
1617 }
1618
1619 pub fn extract_file_to(
1620 &self,
1621 path: &str,
1622 root: &std::path::Path,
1623 options: SafeExtractionOptions,
1624 ) -> Result<Option<Vec<MetadataDiagnostic>>, FormatError> {
1625 let normalized = normalize_lookup_file_path(path, self.crypto_header.max_path_length)?;
1626 self.locate_index_file(&normalized)?
1627 .map(|located| {
1628 self.stream_loaded_file_to_path(&located.shard, located.file_index, root, options)
1629 })
1630 .transpose()
1631 }
1632
1633 pub fn extract_indexed_files_to(
1634 &self,
1635 root: &std::path::Path,
1636 options: SafeExtractionOptions,
1637 jobs: usize,
1638 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1639 if jobs == 0 {
1640 return Err(FormatError::ReaderUnsupported("jobs must be at least 1"));
1641 }
1642
1643 let shards = self.load_all_index_shards()?;
1644 let entries = final_index_entry_winners(&shards)?.into_iter().collect();
1645 self.extract_winning_index_entries_to(&shards, entries, root, options, jobs)
1646 }
1647
1648 pub fn verify(&self) -> Result<(), FormatError> {
1649 self.verify_content().map(|_| ())
1650 }
1651
1652 pub fn verify_content(&self) -> Result<ArchiveContentVerification<'_>, FormatError> {
1653 let tables = self.load_payload_index_tables()?;
1654 let streamed = self.scan_seekable_payload(
1655 &tables,
1656 u64::MAX,
1657 NoopTarStreamObserver,
1658 true,
1659 ParityReadPolicy::Always,
1660 )?;
1661 self.validate_streamed_payload_summary(&tables, &streamed, false, true)?;
1662 Ok(ArchiveContentVerification { archive: self })
1663 }
1664
1665 pub fn repair_patches(&self) -> Result<Vec<ArchiveRepairPatch>, FormatError> {
1666 let lazy_source = self
1667 .lazy_blocks
1668 .as_ref()
1669 .ok_or(FormatError::ReaderUnsupported(
1670 "repair output requires seekable archive input",
1671 ))?;
1672 if !lazy_source.is_complete_volume_set() {
1673 return Err(FormatError::ReaderUnsupported(
1674 "repair output requires all archive volumes",
1675 ));
1676 }
1677
1678 let shards = self.load_all_index_shards()?;
1679 let rows = self.root_auth_fec_layout_rows(&shards)?;
1680 let block_provider = self.block_provider();
1681 let mut patches = BTreeMap::<u64, ArchiveRepairPatch>::new();
1682 for row in rows.into_iter().filter(|row| row.present) {
1683 self.collect_repair_patches_for_object(
1684 &block_provider,
1685 lazy_source,
1686 row,
1687 &mut patches,
1688 )?;
1689 }
1690 Ok(patches.into_values().collect())
1691 }
1692
1693 pub fn extract_all_to(
1694 &self,
1695 root: &std::path::Path,
1696 options: SafeExtractionOptions,
1697 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
1698 let tables = self.load_payload_index_tables()?;
1699 if final_index_entry_winners(&tables.shards)?.len() as u64 != tables.file_count {
1700 return Err(FormatError::ReaderUnsupported(
1701 FAST_FULL_EXTRACT_UNIQUE_PATHS_UNSUPPORTED,
1702 ));
1703 }
1704
1705 let observer = TarStreamFilesystemRestoreObserver::new(root, options);
1706 let streamed = self.scan_seekable_payload(
1707 &tables,
1708 total_extraction_size_cap(self.options, self.observed_archive_bytes),
1709 observer,
1710 false,
1711 ParityReadPolicy::RepairOnly,
1712 )?;
1713 self.validate_streamed_payload_summary(&tables, &streamed, true, false)?;
1714 streamed
1715 .tar
1716 .members
1717 .into_iter()
1718 .map(|member| Ok((utf8_path(&member.path)?, member.diagnostics)))
1719 .collect()
1720 }
1721
1722 fn collect_repair_patches_for_object(
1723 &self,
1724 blocks: &impl BlockProvider,
1725 source: &SeekableBlockSource,
1726 row: FecLayoutObjectRow,
1727 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1728 ) -> Result<(), FormatError> {
1729 let (data_kind, parity_kind, data_max, parity_max) =
1730 self.fec_object_class_shape(row.object_class)?;
1731 let extent = ObjectExtent {
1732 first_block_index: row.first_block_index,
1733 data_block_count: row.data_block_count,
1734 parity_block_count: row.parity_block_count,
1735 encrypted_size: row.encrypted_size,
1736 };
1737 validate_object_extent(extent, &self.crypto_header, data_max, parity_max)?;
1738
1739 let block_size = self.crypto_header.block_size as usize;
1740 let data_count = extent.data_block_count as usize;
1741 let parity_count = extent.parity_block_count as usize;
1742 let mut data_shards = Vec::with_capacity(data_count);
1743 let mut parity_shards = Vec::with_capacity(parity_count);
1744
1745 for offset in 0..data_count {
1746 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1747 match blocks.block(block_index)? {
1748 Some(record) => {
1749 if record.kind != data_kind {
1750 return Err(FormatError::InvalidArchive(
1751 "object data block has unexpected kind",
1752 ));
1753 }
1754 let should_be_last = offset + 1 == data_count;
1755 if record.is_last_data() != should_be_last {
1756 return Err(FormatError::InvalidArchive(
1757 "object last-data flag is not on the final data block",
1758 ));
1759 }
1760 data_shards.push(Some(record.payload.clone()));
1761 }
1762 None => data_shards.push(None),
1763 }
1764 }
1765
1766 for offset in 0..parity_count {
1767 let block_index = checked_u64_add(
1768 extent.first_block_index,
1769 data_count as u64 + offset as u64,
1770 "object",
1771 )?;
1772 match blocks.block(block_index)? {
1773 Some(record) => {
1774 if record.kind != parity_kind {
1775 return Err(FormatError::InvalidArchive(
1776 "object parity block has unexpected kind",
1777 ));
1778 }
1779 if record.is_last_data() {
1780 return Err(FormatError::InvalidArchive(
1781 "object parity block has last-data flag",
1782 ));
1783 }
1784 parity_shards.push(Some(record.payload.clone()));
1785 }
1786 None => parity_shards.push(None),
1787 }
1788 }
1789
1790 let repaired_data = repair_data_gf16(&data_shards, &parity_shards, block_size)?;
1791 for (offset, payload) in repaired_data.iter().enumerate() {
1792 if data_shards[offset].is_none() {
1793 let block_index =
1794 checked_u64_add(extent.first_block_index, offset as u64, "object")?;
1795 let flags = if offset + 1 == data_count { 0x01 } else { 0 };
1796 self.insert_repair_patch(
1797 patches,
1798 source,
1799 block_index,
1800 data_kind,
1801 flags,
1802 payload.clone(),
1803 )?;
1804 }
1805 }
1806
1807 if parity_count > 0 {
1808 let repaired_parity = encode_parity_gf16(&repaired_data, parity_count)?;
1809 for (offset, payload) in repaired_parity.into_iter().enumerate() {
1810 if parity_shards[offset].as_ref() != Some(&payload) {
1811 let block_index = checked_u64_add(
1812 extent.first_block_index,
1813 data_count as u64 + offset as u64,
1814 "object",
1815 )?;
1816 self.insert_repair_patch(
1817 patches,
1818 source,
1819 block_index,
1820 parity_kind,
1821 0,
1822 payload,
1823 )?;
1824 }
1825 }
1826 }
1827
1828 Ok(())
1829 }
1830
1831 fn insert_repair_patch(
1832 &self,
1833 patches: &mut BTreeMap<u64, ArchiveRepairPatch>,
1834 source: &SeekableBlockSource,
1835 block_index: u64,
1836 kind: BlockKind,
1837 flags: u8,
1838 payload: Vec<u8>,
1839 ) -> Result<(), FormatError> {
1840 let (volume_index, record_offset) = source.record_location(block_index)?;
1841 let record = BlockRecord {
1842 block_index,
1843 kind,
1844 flags,
1845 payload,
1846 record_crc32c: 0,
1847 };
1848 let patch = ArchiveRepairPatch {
1849 volume_index,
1850 block_index,
1851 record_offset,
1852 record_bytes: record.to_bytes(),
1853 };
1854 if let Some(existing) = patches.insert(block_index, patch.clone()) {
1855 if existing != patch {
1856 return Err(FormatError::InvalidArchive(
1857 "conflicting repair patch for BlockRecord",
1858 ));
1859 }
1860 }
1861 Ok(())
1862 }
1863
1864 fn load_payload_index_tables(&self) -> Result<PayloadIndexTables, FormatError> {
1865 if self.index_root.header.file_count > DIRECTORY_HINT_REQUIRED_FILE_COUNT
1866 && self.index_root.directory_hint_shards.is_empty()
1867 {
1868 return Err(FormatError::InvalidArchive(
1869 "IndexRoot file_count requires directory hints",
1870 ));
1871 }
1872
1873 let shards = self.load_all_index_shards()?;
1874 let mut file_count = 0u64;
1875 let mut frames = BTreeMap::<u64, FrameEntry>::new();
1876 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
1877
1878 for shard in &shards {
1879 file_count = file_count
1880 .checked_add(shard.files.len() as u64)
1881 .ok_or(FormatError::InvalidArchive("file count overflow"))?;
1882 for frame in &shard.frames {
1883 if let Some(existing) = frames.insert(frame.frame_index, frame.clone()) {
1884 if existing != *frame {
1885 return Err(FormatError::InvalidArchive(
1886 "duplicate FrameEntry rows do not match",
1887 ));
1888 }
1889 }
1890 }
1891 for envelope in &shard.envelopes {
1892 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
1893 {
1894 if existing != *envelope {
1895 return Err(FormatError::InvalidArchive(
1896 "duplicate EnvelopeEntry rows do not match",
1897 ));
1898 }
1899 }
1900 }
1901 }
1902 validate_global_file_table_order(&shards)?;
1903
1904 if file_count != self.index_root.header.file_count {
1905 return Err(FormatError::InvalidArchive(
1906 "IndexRoot file_count does not match decoded shards",
1907 ));
1908 }
1909 verify_dense_keys(&frames, self.index_root.header.frame_count, "FrameEntry")?;
1910 verify_dense_keys(
1911 &envelopes,
1912 self.index_root.header.envelope_count,
1913 "EnvelopeEntry",
1914 )?;
1915 validate_envelope_frame_coverage(&frames, &envelopes)?;
1916 self.validate_encrypted_object_block_ranges(&envelopes)?;
1917
1918 let payload_block_count = envelopes.values().try_fold(0u64, |sum, envelope| {
1919 sum.checked_add(envelope.data_block_count as u64)
1920 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
1921 })?;
1922 if payload_block_count != self.index_root.header.payload_block_count {
1923 return Err(FormatError::InvalidArchive(
1924 "IndexRoot payload_block_count does not match envelopes",
1925 ));
1926 }
1927
1928 Ok(PayloadIndexTables {
1929 shards,
1930 file_count,
1931 frames,
1932 envelopes,
1933 })
1934 }
1935
1936 fn scan_seekable_payload<O: TarStreamObserver>(
1937 &self,
1938 tables: &PayloadIndexTables,
1939 extraction_cap: u64,
1940 observer: O,
1941 hash_content: bool,
1942 parity_policy: ParityReadPolicy,
1943 ) -> Result<StreamedPayloadSummary, FormatError> {
1944 let mut tar = TarStreamSummaryValidator::with_observer(
1945 self.crypto_header.max_path_length,
1946 extraction_cap,
1947 usize::MAX,
1948 self.index_root.header.file_count,
1949 observer,
1950 );
1951 let mut content_hasher = hash_content.then(Sha256::new);
1952 let mut streamed_frames = Vec::with_capacity(tables.frames.len());
1953 let streamed_envelopes = tables
1954 .envelopes
1955 .values()
1956 .map(|envelope| StreamedEnvelopeSummary {
1957 envelope_index: envelope.envelope_index,
1958 first_block_index: envelope.first_block_index,
1959 data_block_count: envelope.data_block_count,
1960 parity_block_count: envelope.parity_block_count,
1961 encrypted_size: envelope.encrypted_size,
1962 plaintext_size: envelope.plaintext_size,
1963 first_frame_index: envelope.first_frame_index,
1964 frame_count: envelope.frame_count,
1965 })
1966 .collect::<Vec<_>>();
1967 let mut cached_envelope_index = None;
1968 let mut cached_envelope_plaintext = Vec::new();
1969 let mut decompressor = self.new_payload_decompressor()?;
1970
1971 for frame in tables.frames.values() {
1972 let envelope =
1973 tables
1974 .envelopes
1975 .get(&frame.envelope_index)
1976 .ok_or(FormatError::InvalidArchive(
1977 "FrameEntry references missing EnvelopeEntry",
1978 ))?;
1979 if cached_envelope_index != Some(envelope.envelope_index) {
1980 cached_envelope_plaintext = self.load_payload_envelope(envelope, parity_policy)?;
1981 cached_envelope_index = Some(envelope.envelope_index);
1982 }
1983 let compressed = slice(
1984 &cached_envelope_plaintext,
1985 frame.offset_in_envelope as usize,
1986 frame.compressed_size as usize,
1987 "FrameEntry",
1988 )?;
1989 let tar_stream_offset = tar.tar_total_size();
1990 let decoded = self.decompress_payload_frame_with(
1991 &mut decompressor,
1992 compressed,
1993 frame.decompressed_size,
1994 )?;
1995 if decoded.is_empty() {
1996 return Err(FormatError::InvalidArchive(
1997 "zstd payload frame decompressed to zero bytes",
1998 ));
1999 }
2000 if let Some(hasher) = &mut content_hasher {
2001 hasher.update(&decoded);
2002 }
2003 tar.observe(&decoded)?;
2004 streamed_frames.push(StreamedFrameSummary {
2005 frame_index: frame.frame_index,
2006 envelope_index: frame.envelope_index,
2007 offset_in_envelope: frame.offset_in_envelope,
2008 compressed_size: u32::try_from(compressed.len()).map_err(|_| {
2009 FormatError::InvalidArchive("FrameEntry.compressed_size overflow")
2010 })?,
2011 decompressed_size: u32::try_from(decoded.len()).map_err(|_| {
2012 FormatError::InvalidArchive("FrameEntry.decompressed_size overflow")
2013 })?,
2014 tar_stream_offset,
2015 });
2016 }
2017
2018 let mut content_sha256 = [0u8; 32];
2019 if let Some(hasher) = content_hasher {
2020 let digest = hasher.finalize();
2021 content_sha256.copy_from_slice(&digest);
2022 }
2023 Ok(StreamedPayloadSummary {
2024 tar: tar.finish()?,
2025 content_sha256,
2026 envelopes: streamed_envelopes,
2027 frames: streamed_frames,
2028 })
2029 }
2030
2031 fn validate_streamed_payload_summary(
2032 &self,
2033 tables: &PayloadIndexTables,
2034 streamed: &StreamedPayloadSummary,
2035 enforce_total_extraction_cap: bool,
2036 enforce_content_sha256: bool,
2037 ) -> Result<(), FormatError> {
2038 if enforce_total_extraction_cap
2039 && streamed.tar.total_extraction_size
2040 > total_extraction_size_cap(self.options, self.observed_archive_bytes)
2041 {
2042 return Err(FormatError::ReaderUnsupported(
2043 "total extraction size exceeds configured cap",
2044 ));
2045 }
2046
2047 let streamed_payload_block_count =
2048 streamed.envelopes.iter().try_fold(0u64, |sum, envelope| {
2049 sum.checked_add(envelope.data_block_count as u64)
2050 .ok_or(FormatError::InvalidArchive("payload block count overflow"))
2051 })?;
2052 if streamed_payload_block_count != self.index_root.header.payload_block_count {
2053 return Err(FormatError::InvalidArchive(
2054 "streamed payload block count does not match IndexRoot",
2055 ));
2056 }
2057
2058 if streamed.tar.tar_total_size != self.index_root.header.tar_total_size {
2059 return Err(FormatError::InvalidArchive(
2060 "IndexRoot tar_total_size does not match streamed tar stream",
2061 ));
2062 }
2063 if enforce_content_sha256
2064 && streamed.content_sha256 != self.index_root.header.content_sha256
2065 {
2066 return Err(FormatError::InvalidArchive(
2067 "IndexRoot content_sha256 does not match decoded tar stream",
2068 ));
2069 }
2070
2071 let streamed_envelopes = streamed.envelope_map()?;
2072 for envelope in tables.envelopes.values() {
2073 let actual = streamed_envelopes.get(&envelope.envelope_index).ok_or(
2074 FormatError::InvalidArchive(
2075 "metadata references missing streamed payload envelope",
2076 ),
2077 )?;
2078 if actual.first_block_index != envelope.first_block_index
2079 || actual.data_block_count != envelope.data_block_count
2080 || actual.parity_block_count != envelope.parity_block_count
2081 || actual.encrypted_size != envelope.encrypted_size
2082 || actual.plaintext_size != envelope.plaintext_size
2083 || actual.first_frame_index != envelope.first_frame_index
2084 || actual.frame_count != envelope.frame_count
2085 {
2086 return Err(FormatError::InvalidArchive(
2087 "EnvelopeEntry does not match streamed payload envelope",
2088 ));
2089 }
2090 }
2091
2092 let streamed_frames = streamed.frame_map()?;
2093 for frame in tables.frames.values() {
2094 let actual =
2095 streamed_frames
2096 .get(&frame.frame_index)
2097 .ok_or(FormatError::InvalidArchive(
2098 "metadata references missing streamed payload frame",
2099 ))?;
2100 if actual.envelope_index != frame.envelope_index
2101 || actual.offset_in_envelope != frame.offset_in_envelope
2102 || actual.compressed_size != frame.compressed_size
2103 || actual.decompressed_size != frame.decompressed_size
2104 || actual.tar_stream_offset != frame.tar_stream_offset
2105 || streamed.frame_flags(actual)? != frame.flags
2106 {
2107 return Err(FormatError::InvalidArchive(
2108 "FrameEntry does not match streamed payload frame",
2109 ));
2110 }
2111 }
2112
2113 let streamed_members = streamed.member_start_map()?;
2114 if streamed.tar.members.len() as u64 != tables.file_count {
2115 return Err(FormatError::InvalidArchive(
2116 "streamed tar member count does not match decoded shards",
2117 ));
2118 }
2119 let mut file_extents = Vec::new();
2120 let mut directory_hint_map = DirectoryHintMap::new();
2121 for (shard_row_index, shard) in tables.shards.iter().enumerate() {
2122 let shard_row_index = u32::try_from(shard_row_index)
2123 .map_err(|_| FormatError::InvalidArchive("shard row index overflow"))?;
2124 for idx in 0..shard.files.len() {
2125 let file = &shard.files[idx];
2126 let start =
2127 shard
2128 .tar_member_group_start(idx)
2129 .ok_or(FormatError::InvalidArchive(
2130 "FileEntry tar member start is missing",
2131 ))?;
2132 file_extents.push((start, file.tar_member_group_size));
2133 let path = shard
2134 .file_path(idx)
2135 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2136 let member = streamed_members
2137 .get(&start)
2138 .ok_or(FormatError::InvalidArchive(
2139 "FileEntry tar member start is missing from streamed tar",
2140 ))?;
2141 if member.path != path {
2142 return Err(FormatError::InvalidArchive(
2143 "tar member path does not match FileEntry path",
2144 ));
2145 }
2146 if member.logical_size != file.file_data_size {
2147 return Err(FormatError::InvalidArchive(
2148 "tar member size does not match FileEntry file_data_size",
2149 ));
2150 }
2151 if member.group_size != file.tar_member_group_size {
2152 return Err(FormatError::InvalidArchive(
2153 "FileEntry does not match streamed tar member",
2154 ));
2155 }
2156 add_expected_directory_hint_rows(
2157 &mut directory_hint_map,
2158 shard_row_index,
2159 path,
2160 member.kind,
2161 );
2162 }
2163 }
2164 validate_file_extent_coverage_ranges(&file_extents, self.index_root.header.tar_total_size)?;
2165 if !self.index_root.directory_hint_shards.is_empty() {
2166 let hint_tables = self.load_all_directory_hint_tables()?;
2167 validate_directory_hint_tables_against_expected(&hint_tables, &directory_hint_map)?;
2168 }
2169
2170 Ok(())
2171 }
2172
2173 pub(crate) fn from_streamed_parts(
2174 parts: StreamedArchiveOpenParts,
2175 ) -> Result<Self, FormatError> {
2176 let limits = metadata_limits(&parts.crypto_header);
2177 let index_root_plaintext = load_metadata_object_from_parts(
2178 &parts.blocks,
2179 ObjectLoadContext::index_root(
2180 &parts.volume_header,
2181 &parts.crypto_header,
2182 &parts.subkeys,
2183 ObjectExtent {
2184 first_block_index: parts.manifest_footer.index_root_first_block,
2185 data_block_count: parts.manifest_footer.index_root_data_block_count,
2186 parity_block_count: parts.manifest_footer.index_root_parity_block_count,
2187 encrypted_size: parts.manifest_footer.index_root_encrypted_size,
2188 },
2189 ),
2190 parts.manifest_footer.index_root_decompressed_size,
2191 )?;
2192 let index_root = IndexRoot::parse(
2193 &index_root_plaintext,
2194 parts.crypto_header.has_dictionary != 0,
2195 limits,
2196 )?;
2197 let payload_dictionary = load_archive_dictionary(
2198 &parts.blocks,
2199 &parts.subkeys,
2200 &parts.volume_header,
2201 &parts.crypto_header,
2202 &index_root,
2203 )?;
2204
2205 Ok(Self {
2206 options: parts.options,
2207 observed_archive_bytes: parts.observed_archive_bytes,
2208 observed_volume_count: 1,
2209 subkeys: parts.subkeys,
2210 blocks: parts.blocks,
2211 lazy_blocks: None,
2212 crypto_header_bytes: parts.crypto_header_bytes,
2213 volume_header: parts.volume_header,
2214 crypto_header: parts.crypto_header,
2215 manifest_footer: parts.manifest_footer,
2216 volume_trailer: Some(parts.volume_trailer),
2217 root_auth_footer: parts.root_auth_footer,
2218 index_root,
2219 payload_dictionary,
2220 })
2221 }
2222
2223 pub(crate) fn verify_streamed_payload_summary(
2224 &self,
2225 streamed: &StreamedPayloadSummary,
2226 ) -> Result<(), FormatError> {
2227 let tables = self.load_payload_index_tables()?;
2228 self.validate_streamed_payload_summary(&tables, streamed, true, true)
2229 }
2230
2231 pub fn verify_root_auth_with<F>(&self, verifier: F) -> Result<RootAuthVerification, FormatError>
2232 where
2233 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2234 {
2235 let content_verification = self.verify_content()?;
2236 self.verify_root_auth_with_verified_content(&content_verification, verifier)
2237 }
2238
2239 pub fn verify_root_auth_with_verified_content<F>(
2240 &self,
2241 content_verification: &ArchiveContentVerification<'_>,
2242 mut verifier: F,
2243 ) -> Result<RootAuthVerification, FormatError>
2244 where
2245 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
2246 {
2247 if !std::ptr::eq(content_verification.archive, self) {
2248 return Err(FormatError::InvalidArchive(
2249 "content verification does not match archive",
2250 ));
2251 }
2252 let footer = self
2253 .root_auth_footer
2254 .as_ref()
2255 .ok_or(FormatError::ReaderUnsupported("root-auth footer is absent"))?;
2256 let material = self.recompute_root_auth_material(footer)?;
2257 if material.critical_metadata_digest != footer.critical_metadata_digest
2258 || material.index_digest != footer.index_digest
2259 || material.fec_layout_digest != footer.fec_layout_digest
2260 || material.data_block_merkle_root != footer.data_block_merkle_root
2261 || material.signer_identity_digest != footer.signer_identity_digest
2262 || material.archive_root != footer.archive_root
2263 || material.total_data_block_count != footer.total_data_block_count
2264 {
2265 return Err(FormatError::InvalidArchive(
2266 "RootAuthFooter commitments do not match recomputed archive root",
2267 ));
2268 }
2269 if !verifier(footer, &material.archive_root)? {
2270 return Err(FormatError::InvalidArchive(
2271 "root-auth authenticator verification failed",
2272 ));
2273 }
2274 Ok(RootAuthVerification {
2275 archive_root: material.archive_root,
2276 authenticator_id: footer.authenticator_id,
2277 signer_identity_type: footer.signer_identity_type,
2278 signer_identity_bytes: footer.signer_identity_bytes.clone(),
2279 total_data_block_count: footer.total_data_block_count,
2280 diagnostics: self.root_auth_success_diagnostics(),
2281 })
2282 }
2283
2284 fn load_all_index_shards(&self) -> Result<Vec<IndexShard>, FormatError> {
2285 parallel_map_ref(&self.index_root.shards, self.options.jobs, |entry| {
2286 self.load_index_shard(entry)
2287 })
2288 }
2289
2290 fn load_index_shard(&self, entry: &ShardEntry) -> Result<IndexShard, FormatError> {
2291 let block_provider = self.block_provider();
2292 let plaintext = load_metadata_object_from_parts(
2293 &block_provider,
2294 ObjectLoadContext::index_shard(
2295 &self.volume_header,
2296 &self.crypto_header,
2297 &self.subkeys,
2298 entry,
2299 ),
2300 entry.decompressed_size,
2301 )?;
2302 IndexShard::parse(&plaintext, entry, self.metadata_limits())
2303 }
2304
2305 fn load_all_directory_hint_tables(&self) -> Result<Vec<DirectoryHintTable>, FormatError> {
2306 parallel_map_ref(
2307 &self.index_root.directory_hint_shards,
2308 self.options.jobs,
2309 |entry| self.load_directory_hint_table(entry),
2310 )
2311 }
2312
2313 fn load_directory_hint_table(
2314 &self,
2315 entry: &DirectoryHintShardEntry,
2316 ) -> Result<DirectoryHintTable, FormatError> {
2317 let block_provider = self.block_provider();
2318 let plaintext = load_metadata_object_from_parts(
2319 &block_provider,
2320 ObjectLoadContext::directory_hint(
2321 &self.volume_header,
2322 &self.crypto_header,
2323 &self.subkeys,
2324 entry,
2325 ),
2326 entry.decompressed_size,
2327 )?;
2328 DirectoryHintTable::parse(
2329 &plaintext,
2330 entry,
2331 self.index_root.header.shard_count,
2332 self.metadata_limits(),
2333 )
2334 }
2335
2336 fn load_payload_envelope(
2337 &self,
2338 envelope: &EnvelopeEntry,
2339 parity_policy: ParityReadPolicy,
2340 ) -> Result<Vec<u8>, FormatError> {
2341 let block_provider = self.block_provider();
2342 let plaintext = load_decrypted_object_from_parts_with_parity_policy(
2343 &block_provider,
2344 ObjectLoadContext::payload(
2345 &self.volume_header,
2346 &self.crypto_header,
2347 &self.subkeys,
2348 envelope,
2349 ),
2350 parity_policy,
2351 )?;
2352 if plaintext.len() != envelope.plaintext_size as usize {
2353 return Err(FormatError::InvalidArchive(
2354 "payload envelope plaintext_size mismatch",
2355 ));
2356 }
2357 Ok(plaintext)
2358 }
2359
2360 fn locate_index_file(
2361 &self,
2362 normalized: &[u8],
2363 ) -> Result<Option<LocatedIndexFile>, FormatError> {
2364 let candidate_indexes = self
2365 .index_root
2366 .candidate_shards_for_path(normalized, self.metadata_limits())?;
2367 let mut winner: Option<LocatedIndexFile> = None;
2368
2369 for row_index in candidate_indexes {
2370 let locating =
2371 self.index_root
2372 .shards
2373 .get(row_index)
2374 .ok_or(FormatError::InvalidArchive(
2375 "candidate shard row is out of bounds",
2376 ))?;
2377 let shard = self.load_index_shard(locating)?;
2378 if let Some(file_index) = shard.lookup_file_index(normalized) {
2379 let start =
2380 shard
2381 .tar_member_group_start(file_index)
2382 .ok_or(FormatError::InvalidArchive(
2383 "FileEntry tar member start is missing",
2384 ))?;
2385 if winner
2386 .as_ref()
2387 .map(|existing| start > existing.start)
2388 .unwrap_or(true)
2389 {
2390 winner = Some(LocatedIndexFile {
2391 shard,
2392 file_index,
2393 start,
2394 });
2395 }
2396 }
2397 }
2398
2399 Ok(winner)
2400 }
2401
2402 fn extract_loaded_member(
2403 &self,
2404 shard: &IndexShard,
2405 file_index: usize,
2406 ) -> Result<ExtractedArchiveMember, FormatError> {
2407 let member = self.extract_loaded_owned_tar_member(shard, file_index)?;
2408 Ok(ExtractedArchiveMember {
2409 path: utf8_path(&member.path)?,
2410 kind: member.kind,
2411 data: member.data,
2412 link_target: member
2413 .link_target
2414 .map(|target| utf8_path(&target))
2415 .transpose()?,
2416 diagnostics: member.diagnostics,
2417 })
2418 }
2419
2420 fn extract_loaded_owned_tar_member(
2421 &self,
2422 shard: &IndexShard,
2423 file_index: usize,
2424 ) -> Result<OwnedTarMember, FormatError> {
2425 self.decode_loaded_owned_tar_member(shard, file_index, true)
2426 }
2427
2428 fn stream_loaded_file_to_writer<W: Write>(
2429 &self,
2430 shard: &IndexShard,
2431 file_index: usize,
2432 writer: &mut W,
2433 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
2434 let file = shard
2435 .files
2436 .get(file_index)
2437 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2438 self.validate_total_extraction_size(file.file_data_size)?;
2439 let expected_path = shard
2440 .file_path(file_index)
2441 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2442 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2443 stream_regular_tar_member_group_to_writer(
2444 &mut reader,
2445 expected_path,
2446 file.file_data_size,
2447 file.tar_member_group_size,
2448 self.crypto_header.max_path_length,
2449 writer,
2450 )
2451 }
2452
2453 fn stream_loaded_file_to_writer_with_progress<W: Write>(
2454 &self,
2455 shard: &IndexShard,
2456 file_index: usize,
2457 writer: &mut W,
2458 progress: &mut dyn ArchiveExtractProgressSink,
2459 ) -> Result<Vec<MetadataDiagnostic>, ExtractError> {
2460 let file = shard
2461 .files
2462 .get(file_index)
2463 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2464 self.validate_total_extraction_size(file.file_data_size)?;
2465 let expected_path = shard
2466 .file_path(file_index)
2467 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2468 let archive_path = utf8_path(expected_path)?;
2469 let mut progress_writer =
2470 ExtractProgressWriter::new(writer, &archive_path, file.file_data_size, progress);
2471 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2472 stream_regular_tar_member_group_to_writer(
2473 &mut reader,
2474 expected_path,
2475 file.file_data_size,
2476 file.tar_member_group_size,
2477 self.crypto_header.max_path_length,
2478 &mut progress_writer,
2479 )
2480 }
2481
2482 fn stream_loaded_file_to_path(
2483 &self,
2484 shard: &IndexShard,
2485 file_index: usize,
2486 root: &std::path::Path,
2487 options: SafeExtractionOptions,
2488 ) -> Result<Vec<MetadataDiagnostic>, FormatError> {
2489 let file = shard
2490 .files
2491 .get(file_index)
2492 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2493 self.validate_total_extraction_size(file.file_data_size)?;
2494 let expected_path = shard
2495 .file_path(file_index)
2496 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2497 let mut reader = DecodedTarMemberGroupReader::new(self, shard, file)?;
2498 restore_streaming_tar_member_group(
2499 root,
2500 expected_path,
2501 file.file_data_size,
2502 file.tar_member_group_size,
2503 self.crypto_header.max_path_length,
2504 options,
2505 &mut reader,
2506 )
2507 .map_err(format_error_from_extract_error)
2508 }
2509
2510 fn extract_winning_index_entries_to(
2511 &self,
2512 shards: &[IndexShard],
2513 entries: Vec<(String, WinningIndexEntry)>,
2514 root: &std::path::Path,
2515 options: SafeExtractionOptions,
2516 jobs: usize,
2517 ) -> Result<Vec<(String, Vec<MetadataDiagnostic>)>, FormatError> {
2518 if entries.is_empty() {
2519 return Ok(Vec::new());
2520 }
2521 if jobs <= 1 || entries.len() <= 1 {
2522 return entries
2523 .into_iter()
2524 .map(|(path, entry)| {
2525 let shard =
2526 shards
2527 .get(entry.shard_index)
2528 .ok_or(FormatError::InvalidArchive(
2529 "winning FileEntry shard is out of bounds",
2530 ))?;
2531 let diagnostics =
2532 self.stream_loaded_file_to_path(shard, entry.file_index, root, options)?;
2533 Ok((path, diagnostics))
2534 })
2535 .collect();
2536 }
2537
2538 let worker_count = jobs.min(entries.len());
2539 let chunk_size = entries.len().div_ceil(worker_count);
2540 std::thread::scope(|scope| {
2541 let handles = entries
2542 .chunks(chunk_size)
2543 .map(|chunk| {
2544 scope.spawn(move || {
2545 let mut out = Vec::with_capacity(chunk.len());
2546 for (path, entry) in chunk {
2547 let shard = shards.get(entry.shard_index).ok_or(
2548 FormatError::InvalidArchive(
2549 "winning FileEntry shard is out of bounds",
2550 ),
2551 )?;
2552 let diagnostics = self.stream_loaded_file_to_path(
2553 shard,
2554 entry.file_index,
2555 root,
2556 options,
2557 )?;
2558 out.push((path.clone(), diagnostics));
2559 }
2560 Ok(out)
2561 })
2562 })
2563 .collect::<Vec<_>>();
2564 let mut out = Vec::new();
2565 for handle in handles {
2566 let mut chunk = handle
2567 .join()
2568 .map_err(|_| FormatError::ReaderUnsupported("extract worker panicked"))??;
2569 out.append(&mut chunk);
2570 }
2571 Ok(out)
2572 })
2573 }
2574
2575 fn decode_loaded_owned_tar_member(
2576 &self,
2577 shard: &IndexShard,
2578 file_index: usize,
2579 enforce_extraction_cap: bool,
2580 ) -> Result<OwnedTarMember, FormatError> {
2581 let file = shard
2582 .files
2583 .get(file_index)
2584 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
2585 if enforce_extraction_cap {
2586 self.validate_total_extraction_size(file.file_data_size)?;
2587 }
2588 let expected_path = shard
2589 .file_path(file_index)
2590 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?;
2591 let frames = frame_range_for_file(shard, file)?;
2592 let mut envelope_cache = HashMap::<u64, Vec<u8>>::new();
2593 let mut decoded = Vec::new();
2594
2595 for frame in frames {
2596 let envelope = shard
2597 .envelopes
2598 .iter()
2599 .find(|entry| entry.envelope_index == frame.envelope_index)
2600 .ok_or(FormatError::InvalidArchive(
2601 "FrameEntry references missing EnvelopeEntry",
2602 ))?;
2603 if let Entry::Vacant(entry) = envelope_cache.entry(envelope.envelope_index) {
2604 entry.insert(self.load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?);
2605 }
2606 let envelope_plaintext = envelope_cache
2607 .get(&envelope.envelope_index)
2608 .expect("inserted above");
2609 let compressed = slice(
2610 envelope_plaintext,
2611 frame.offset_in_envelope as usize,
2612 frame.compressed_size as usize,
2613 "FrameEntry",
2614 )?;
2615 decoded.extend_from_slice(
2616 &self.decompress_payload_frame(compressed, frame.decompressed_size)?,
2617 );
2618 }
2619
2620 let offset = file.offset_in_first_frame_plaintext as usize;
2621 let group_len = to_usize(file.tar_member_group_size, "FileEntry")?;
2622 let group = slice(&decoded, offset, group_len, "FileEntry")?;
2623 let member = parse_tar_member_group(group, self.crypto_header.max_path_length)?;
2624 if member.path != expected_path {
2625 return Err(FormatError::InvalidArchive(
2626 "tar member path does not match FileEntry path",
2627 ));
2628 }
2629 if member.logical_size != file.file_data_size {
2630 return Err(FormatError::InvalidArchive(
2631 "tar member size does not match FileEntry file_data_size",
2632 ));
2633 }
2634 Ok(member.to_owned_member())
2635 }
2636
2637 fn metadata_limits(&self) -> MetadataLimits {
2638 metadata_limits(&self.crypto_header)
2639 }
2640
2641 fn recompute_root_auth_material(
2642 &self,
2643 footer: &RootAuthFooterV1,
2644 ) -> Result<RootAuthMaterial, FormatError> {
2645 let footer_length = footer.footer_length()?;
2646 let root_auth_descriptor_digest = root_auth_descriptor_digest(
2647 footer.authenticator_id,
2648 footer.signer_identity_type,
2649 &footer.signer_identity_bytes,
2650 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
2651 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
2652 })?,
2653 footer_length,
2654 )?;
2655 let signer_identity_digest =
2656 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
2657 let manifest_pre_hmac = manifest_footer_global_pre_hmac_bytes(&self.manifest_footer);
2658 let crypto_pre_hmac_len = self
2659 .crypto_header_bytes
2660 .len()
2661 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
2662 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
2663 let critical_metadata_digest = critical_metadata_digest(CriticalMetadataDigestInputs {
2664 archive_uuid: self.volume_header.archive_uuid,
2665 session_id: self.volume_header.session_id,
2666 stripe_width: self.crypto_header.stripe_width,
2667 total_volumes: self.manifest_footer.total_volumes,
2668 compression_algo: self.crypto_header.compression_algo,
2669 aead_algo: self.crypto_header.aead_algo,
2670 fec_algo: self.crypto_header.fec_algo,
2671 kdf_algo: self.crypto_header.kdf_algo,
2672 crypto_header_pre_hmac_bytes: &self.crypto_header_bytes[..crypto_pre_hmac_len],
2673 chunk_size: self.crypto_header.chunk_size,
2674 envelope_target_size: self.crypto_header.envelope_target_size,
2675 block_size: self.crypto_header.block_size,
2676 fec_data_shards: self.crypto_header.fec_data_shards,
2677 fec_parity_shards: self.crypto_header.fec_parity_shards,
2678 index_fec_data_shards: self.crypto_header.index_fec_data_shards,
2679 index_fec_parity_shards: self.crypto_header.index_fec_parity_shards,
2680 index_root_fec_data_shards: self.crypto_header.index_root_fec_data_shards,
2681 index_root_fec_parity_shards: self.crypto_header.index_root_fec_parity_shards,
2682 volume_loss_tolerance: self.crypto_header.volume_loss_tolerance,
2683 bit_rot_buffer_pct: self.crypto_header.bit_rot_buffer_pct,
2684 has_dictionary: self.crypto_header.has_dictionary,
2685 manifest_footer_global_pre_hmac_bytes: &manifest_pre_hmac,
2686 index_root_first_block: self.manifest_footer.index_root_first_block,
2687 index_root_data_block_count: self.manifest_footer.index_root_data_block_count,
2688 index_root_parity_block_count: self.manifest_footer.index_root_parity_block_count,
2689 index_root_encrypted_size: self.manifest_footer.index_root_encrypted_size,
2690 index_root_decompressed_size: self.manifest_footer.index_root_decompressed_size,
2691 root_auth_descriptor_digest,
2692 })?;
2693 let index_root_plaintext = self.index_root.to_bytes();
2694 let index_digest = index_digest(&index_root_plaintext);
2695 let shards = self.load_all_index_shards()?;
2696 let fec_layout_rows = self.root_auth_fec_layout_rows(&shards)?;
2697 let fec_layout_digest = fec_layout_digest(&fec_layout_rows)?;
2698 let data_leaves = self.root_auth_data_block_leaves(&fec_layout_rows)?;
2699 let total_data_block_count = u64::try_from(data_leaves.len())
2700 .map_err(|_| FormatError::InvalidArchive("root-auth data block count overflow"))?;
2701 let data_block_merkle_root = data_block_merkle_root(&data_leaves);
2702 let archive_root = archive_root(ArchiveRootInputs {
2703 archive_uuid: self.volume_header.archive_uuid,
2704 session_id: self.volume_header.session_id,
2705 format_version: FORMAT_VERSION,
2706 volume_format_rev: VOLUME_FORMAT_REV,
2707 compression_algo: self.crypto_header.compression_algo,
2708 aead_algo: self.crypto_header.aead_algo,
2709 fec_algo: self.crypto_header.fec_algo,
2710 kdf_algo: self.crypto_header.kdf_algo,
2711 critical_metadata_digest,
2712 index_digest,
2713 fec_layout_digest,
2714 total_data_block_count,
2715 data_block_merkle_root,
2716 root_auth_descriptor_digest,
2717 signer_identity_digest,
2718 });
2719 Ok(RootAuthMaterial {
2720 critical_metadata_digest,
2721 index_digest,
2722 fec_layout_digest,
2723 data_block_merkle_root,
2724 signer_identity_digest,
2725 archive_root,
2726 total_data_block_count,
2727 })
2728 }
2729
2730 fn root_auth_fec_layout_rows(
2731 &self,
2732 shards: &[IndexShard],
2733 ) -> Result<Vec<FecLayoutObjectRow>, FormatError> {
2734 let mut rows = Vec::new();
2735 rows.push(FecLayoutObjectRow {
2736 object_class: 1,
2737 present: true,
2738 object_id: 0,
2739 first_block_index: self.manifest_footer.index_root_first_block,
2740 data_block_count: self.manifest_footer.index_root_data_block_count,
2741 parity_block_count: self.manifest_footer.index_root_parity_block_count,
2742 encrypted_size: self.manifest_footer.index_root_encrypted_size,
2743 plain_size: self.manifest_footer.index_root_decompressed_size,
2744 });
2745 if self.crypto_header.has_dictionary != 0 {
2746 rows.push(FecLayoutObjectRow {
2747 object_class: 2,
2748 present: true,
2749 object_id: 0,
2750 first_block_index: self.index_root.header.dictionary_first_block,
2751 data_block_count: self.index_root.header.dictionary_data_block_count,
2752 parity_block_count: self.index_root.header.dictionary_parity_block_count,
2753 encrypted_size: self.index_root.header.dictionary_encrypted_size,
2754 plain_size: self.index_root.header.dictionary_decompressed_size,
2755 });
2756 } else {
2757 rows.push(FecLayoutObjectRow {
2758 object_class: 2,
2759 present: false,
2760 object_id: 0,
2761 first_block_index: 0,
2762 data_block_count: 0,
2763 parity_block_count: 0,
2764 encrypted_size: 0,
2765 plain_size: 0,
2766 });
2767 }
2768 for entry in &self.index_root.shards {
2769 rows.push(FecLayoutObjectRow {
2770 object_class: 3,
2771 present: true,
2772 object_id: entry.shard_index,
2773 first_block_index: entry.first_block_index,
2774 data_block_count: entry.data_block_count,
2775 parity_block_count: entry.parity_block_count,
2776 encrypted_size: entry.encrypted_size,
2777 plain_size: entry.decompressed_size,
2778 });
2779 }
2780 let mut envelopes = BTreeMap::<u64, EnvelopeEntry>::new();
2781 for shard in shards {
2782 for envelope in &shard.envelopes {
2783 if let Some(existing) = envelopes.insert(envelope.envelope_index, envelope.clone())
2784 {
2785 if existing != *envelope {
2786 return Err(FormatError::InvalidArchive(
2787 "duplicate EnvelopeEntry rows do not match",
2788 ));
2789 }
2790 }
2791 }
2792 }
2793 for envelope in envelopes.values() {
2794 rows.push(FecLayoutObjectRow {
2795 object_class: 4,
2796 present: true,
2797 object_id: envelope.envelope_index,
2798 first_block_index: envelope.first_block_index,
2799 data_block_count: envelope.data_block_count,
2800 parity_block_count: envelope.parity_block_count,
2801 encrypted_size: envelope.encrypted_size,
2802 plain_size: envelope.plaintext_size,
2803 });
2804 }
2805 for entry in &self.index_root.directory_hint_shards {
2806 rows.push(FecLayoutObjectRow {
2807 object_class: 5,
2808 present: true,
2809 object_id: entry.hint_shard_index,
2810 first_block_index: entry.first_block_index,
2811 data_block_count: entry.data_block_count,
2812 parity_block_count: entry.parity_block_count,
2813 encrypted_size: entry.encrypted_size,
2814 plain_size: entry.decompressed_size,
2815 });
2816 }
2817 Ok(rows)
2818 }
2819
2820 fn fec_object_class_shape(
2821 &self,
2822 object_class: u8,
2823 ) -> Result<(BlockKind, BlockKind, u16, u16), FormatError> {
2824 match object_class {
2825 1 => Ok((
2826 BlockKind::IndexRootData,
2827 BlockKind::IndexRootParity,
2828 self.crypto_header.index_root_fec_data_shards,
2829 self.crypto_header.index_root_fec_parity_shards,
2830 )),
2831 2 => Ok((
2832 BlockKind::DictionaryData,
2833 BlockKind::DictionaryParity,
2834 self.crypto_header.index_root_fec_data_shards,
2835 self.crypto_header.index_root_fec_parity_shards,
2836 )),
2837 3 => Ok((
2838 BlockKind::IndexShardData,
2839 BlockKind::IndexShardParity,
2840 self.crypto_header.index_fec_data_shards,
2841 self.crypto_header.index_fec_parity_shards,
2842 )),
2843 4 => Ok((
2844 BlockKind::PayloadData,
2845 BlockKind::PayloadParity,
2846 self.crypto_header.fec_data_shards,
2847 self.crypto_header.fec_parity_shards,
2848 )),
2849 5 => Ok((
2850 BlockKind::DirectoryHintData,
2851 BlockKind::DirectoryHintParity,
2852 self.crypto_header.index_fec_data_shards,
2853 self.crypto_header.index_fec_parity_shards,
2854 )),
2855 _ => Err(FormatError::InvalidArchive(
2856 "unknown root-auth FEC row class",
2857 )),
2858 }
2859 }
2860
2861 fn root_auth_data_block_leaves(
2862 &self,
2863 rows: &[FecLayoutObjectRow],
2864 ) -> Result<Vec<DataBlockMerkleLeaf>, FormatError> {
2865 let block_provider = self.block_provider();
2866 let present_rows = rows.iter().filter(|row| row.present).collect::<Vec<_>>();
2867 let chunks = parallel_map_ref(&present_rows, self.options.jobs, |row| {
2868 let row = **row;
2869 let (data_kind, parity_kind, data_max, parity_max) =
2870 self.fec_object_class_shape(row.object_class)?;
2871 let extent = ObjectExtent {
2872 first_block_index: row.first_block_index,
2873 data_block_count: row.data_block_count,
2874 parity_block_count: row.parity_block_count,
2875 encrypted_size: row.encrypted_size,
2876 };
2877 let repaired = load_repaired_object_data_shards_from_parts(
2878 &block_provider,
2879 &self.crypto_header,
2880 extent,
2881 data_kind,
2882 parity_kind,
2883 data_max,
2884 parity_max,
2885 )?;
2886 let mut leaves = Vec::new();
2887 for (offset, payload) in repaired.into_iter().enumerate() {
2888 leaves.push(DataBlockMerkleLeaf {
2889 block_index: checked_u64_add(
2890 row.first_block_index,
2891 offset as u64,
2892 "root-auth data block",
2893 )?,
2894 kind: data_kind,
2895 flags: if offset + 1 == row.data_block_count as usize {
2896 0x01
2897 } else {
2898 0
2899 },
2900 payload,
2901 });
2902 }
2903 Ok(leaves)
2904 })?;
2905 let mut leaves = Vec::new();
2906 for mut chunk in chunks {
2907 leaves.append(&mut chunk);
2908 }
2909 leaves.sort_by_key(|leaf| leaf.block_index);
2910 Ok(leaves)
2911 }
2912
2913 fn validate_total_extraction_size(&self, logical_size: u64) -> Result<(), FormatError> {
2914 let cap = total_extraction_size_cap(self.options, self.observed_archive_bytes);
2915 if logical_size > cap {
2916 return Err(FormatError::ReaderUnsupported(
2917 "total extraction size exceeds configured cap",
2918 ));
2919 }
2920 Ok(())
2921 }
2922
2923 fn decompress_payload_frame(
2924 &self,
2925 compressed: &[u8],
2926 decompressed_size: u32,
2927 ) -> Result<Vec<u8>, FormatError> {
2928 let mut decompressor = self.new_payload_decompressor()?;
2929 self.decompress_payload_frame_with(&mut decompressor, compressed, decompressed_size)
2930 }
2931
2932 fn new_payload_decompressor(&self) -> Result<zstd::bulk::Decompressor<'static>, FormatError> {
2933 match &self.payload_dictionary {
2934 Some(dictionary) => zstd::bulk::Decompressor::with_dictionary(dictionary),
2935 None => zstd::bulk::Decompressor::new(),
2936 }
2937 .map_err(|_| FormatError::ZstdDecompressionFailure)
2938 }
2939
2940 fn decompress_payload_frame_with(
2941 &self,
2942 decompressor: &mut zstd::bulk::Decompressor<'static>,
2943 compressed: &[u8],
2944 decompressed_size: u32,
2945 ) -> Result<Vec<u8>, FormatError> {
2946 validate_exact_zstd_frame(compressed)?;
2947 let expected = decompressed_size as usize;
2948 let decoded = decompressor
2949 .decompress(compressed, expected)
2950 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
2951 if decoded.len() != expected {
2952 return Err(FormatError::ZstdDecompressedSizeMismatch {
2953 expected,
2954 actual: decoded.len(),
2955 });
2956 }
2957 Ok(decoded)
2958 }
2959
2960 fn validate_encrypted_object_block_ranges(
2961 &self,
2962 envelopes: &BTreeMap<u64, EnvelopeEntry>,
2963 ) -> Result<(), FormatError> {
2964 let mut ranges = Vec::new();
2965 ranges.push(object_block_range(
2966 self.manifest_footer.index_root_first_block,
2967 self.manifest_footer.index_root_data_block_count,
2968 self.manifest_footer.index_root_parity_block_count,
2969 "IndexRoot",
2970 )?);
2971 for shard in &self.index_root.shards {
2972 ranges.push(object_block_range(
2973 shard.first_block_index,
2974 shard.data_block_count,
2975 shard.parity_block_count,
2976 "IndexShard",
2977 )?);
2978 }
2979 for hint in &self.index_root.directory_hint_shards {
2980 ranges.push(object_block_range(
2981 hint.first_block_index,
2982 hint.data_block_count,
2983 hint.parity_block_count,
2984 "DirectoryHintShardEntry",
2985 )?);
2986 }
2987 if self.crypto_header.has_dictionary != 0 {
2988 ranges.push(object_block_range(
2989 self.index_root.header.dictionary_first_block,
2990 self.index_root.header.dictionary_data_block_count,
2991 self.index_root.header.dictionary_parity_block_count,
2992 "dictionary",
2993 )?);
2994 }
2995 for envelope in envelopes.values() {
2996 ranges.push(object_block_range(
2997 envelope.first_block_index,
2998 envelope.data_block_count,
2999 envelope.parity_block_count,
3000 "EnvelopeEntry",
3001 )?);
3002 }
3003 validate_non_overlapping_object_ranges(&mut ranges)?;
3004 if let Some(source) = &self.lazy_blocks {
3005 if source.is_complete_volume_set() {
3006 validate_exact_coverage_ranges_u64(
3007 &mut ranges,
3008 source.total_block_count()?,
3009 "encrypted object block ranges do not cover complete archive exactly",
3010 )?;
3011 }
3012 }
3013 Ok(())
3014 }
3015}
3016
3017impl<'a> DecodedTarMemberGroupReader<'a> {
3018 fn new(
3019 archive: &'a OpenedArchive,
3020 shard: &'a IndexShard,
3021 file: &'a FileEntry,
3022 ) -> Result<Self, FormatError> {
3023 Ok(Self {
3024 archive,
3025 shard,
3026 file,
3027 decompressor: archive.new_payload_decompressor()?,
3028 next_frame_offset: 0,
3029 cached_envelope_index: None,
3030 cached_envelope_plaintext: Vec::new(),
3031 current_frame: Vec::new(),
3032 current_frame_offset: 0,
3033 remaining_group_bytes: file.tar_member_group_size,
3034 })
3035 }
3036
3037 fn ensure_frame_available(&mut self) -> Result<(), ExtractError> {
3038 while self.current_frame_offset >= self.current_frame.len() {
3039 if self.next_frame_offset >= self.file.frame_count as u64 {
3040 return Err(
3041 FormatError::InvalidArchive("tar member group exceeds frame range").into(),
3042 );
3043 }
3044 let frame_index = self
3045 .file
3046 .first_frame_index
3047 .checked_add(self.next_frame_offset)
3048 .ok_or(FormatError::InvalidArchive(
3049 "FileEntry frame range overflow",
3050 ))?;
3051 let frame = frame_by_index(self.shard, frame_index)?;
3052 let envelope = envelope_by_index(self.shard, frame.envelope_index)?;
3053 if self.cached_envelope_index != Some(envelope.envelope_index) {
3054 self.cached_envelope_plaintext = self
3055 .archive
3056 .load_payload_envelope(envelope, ParityReadPolicy::RepairOnly)?;
3057 self.cached_envelope_index = Some(envelope.envelope_index);
3058 }
3059 let compressed = slice(
3060 &self.cached_envelope_plaintext,
3061 frame.offset_in_envelope as usize,
3062 frame.compressed_size as usize,
3063 "FrameEntry",
3064 )?;
3065 let decoded = self.archive.decompress_payload_frame_with(
3066 &mut self.decompressor,
3067 compressed,
3068 frame.decompressed_size,
3069 )?;
3070 let offset = if self.next_frame_offset == 0 {
3071 self.file.offset_in_first_frame_plaintext as usize
3072 } else {
3073 0
3074 };
3075 if offset > decoded.len() {
3076 return Err(FormatError::InvalidArchive(
3077 "offset in first frame is outside the first referenced frame",
3078 )
3079 .into());
3080 }
3081 self.next_frame_offset += 1;
3082 self.current_frame = decoded;
3083 self.current_frame_offset = offset;
3084 }
3085 Ok(())
3086 }
3087}
3088
3089impl TarMemberGroupReader for DecodedTarMemberGroupReader<'_> {
3090 fn read_some_member_bytes(&mut self, buf: &mut [u8]) -> Result<usize, ExtractError> {
3091 if buf.is_empty() {
3092 return Ok(0);
3093 }
3094 if self.remaining_group_bytes == 0 {
3095 return Ok(0);
3096 }
3097 self.ensure_frame_available()?;
3098 let available = self.current_frame.len() - self.current_frame_offset;
3099 let len = available
3100 .min(buf.len())
3101 .min(to_usize(self.remaining_group_bytes, "FileEntry")?);
3102 if len == 0 {
3103 return Err(FormatError::InvalidArchive("tar member group exceeds frame range").into());
3104 }
3105 buf[..len].copy_from_slice(
3106 &self.current_frame[self.current_frame_offset..self.current_frame_offset + len],
3107 );
3108 self.current_frame_offset += len;
3109 self.remaining_group_bytes -= len as u64;
3110 Ok(len)
3111 }
3112}
3113
3114fn frame_by_index(shard: &IndexShard, frame_index: u64) -> Result<&FrameEntry, FormatError> {
3115 shard
3116 .frames
3117 .binary_search_by_key(&frame_index, |entry| entry.frame_index)
3118 .map(|idx| &shard.frames[idx])
3119 .map_err(|_| FormatError::InvalidArchive("FileEntry references missing FrameEntry"))
3120}
3121
3122fn envelope_by_index(
3123 shard: &IndexShard,
3124 envelope_index: u64,
3125) -> Result<&EnvelopeEntry, FormatError> {
3126 shard
3127 .envelopes
3128 .binary_search_by_key(&envelope_index, |entry| entry.envelope_index)
3129 .map(|idx| &shard.envelopes[idx])
3130 .map_err(|_| FormatError::InvalidArchive("FrameEntry references missing EnvelopeEntry"))
3131}
3132
3133fn format_error_from_extract_error(err: ExtractError) -> FormatError {
3134 match err {
3135 ExtractError::Format(err) => err,
3136 ExtractError::Output(_) => {
3137 FormatError::FilesystemExtractionFailed("failed to write regular file")
3138 }
3139 }
3140}
3141
3142fn final_index_entry_winners(
3143 shards: &[IndexShard],
3144) -> Result<BTreeMap<String, WinningIndexEntry>, FormatError> {
3145 let mut final_entries = BTreeMap::<String, WinningIndexEntry>::new();
3146 for (shard_index, shard) in shards.iter().enumerate() {
3147 for (idx, file) in shard.files.iter().enumerate() {
3148 let path = utf8_path(
3149 shard
3150 .file_path(idx)
3151 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3152 )?;
3153 let start = shard
3154 .tar_member_group_start(idx)
3155 .ok_or(FormatError::InvalidArchive(
3156 "FileEntry tar member start is missing",
3157 ))?;
3158 if let Some(winner) = final_entries.get_mut(&path) {
3159 if start >= winner.start {
3160 winner.start = start;
3161 winner.file_data_size = file.file_data_size;
3162 winner.shard_index = shard_index;
3163 winner.file_index = idx;
3164 }
3165 } else {
3166 final_entries.insert(
3167 path,
3168 WinningIndexEntry {
3169 start,
3170 file_data_size: file.file_data_size,
3171 shard_index,
3172 file_index: idx,
3173 },
3174 );
3175 }
3176 }
3177 }
3178 Ok(final_entries)
3179}
3180
3181fn archive_index_entry_from_loaded_file(
3182 shard: &IndexShard,
3183 file_index: usize,
3184) -> Result<ArchiveIndexEntry, FormatError> {
3185 let file = shard
3186 .files
3187 .get(file_index)
3188 .ok_or(FormatError::InvalidArchive("FileEntry index out of bounds"))?;
3189 let path = utf8_path(
3190 shard
3191 .file_path(file_index)
3192 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?,
3193 )?;
3194 Ok(ArchiveIndexEntry {
3195 path,
3196 file_data_size: file.file_data_size,
3197 })
3198}
3199
3200#[derive(Debug)]
3201struct ParsedSeekableVolume {
3202 volume_header: VolumeHeader,
3203 crypto_header: CryptoHeaderFixed,
3204 crypto_header_bytes: Vec<u8>,
3205 subkeys: Subkeys,
3206 manifest_footer: Option<ManifestFooter>,
3207 manifest_footer_error: Option<FormatError>,
3208 root_auth_footer: Option<RootAuthFooterV1>,
3209 root_auth_footer_bytes: Option<Vec<u8>>,
3210 volume_trailer: VolumeTrailer,
3211 blocks: BTreeMap<u64, BlockRecord>,
3212 erased_block_indices: BTreeSet<u64>,
3213}
3214
3215struct ParsedSeekableReadAtVolume {
3216 reader: Arc<dyn ArchiveReadAt>,
3217 volume_header: VolumeHeader,
3218 crypto_header: CryptoHeaderFixed,
3219 crypto_header_bytes: Vec<u8>,
3220 subkeys: Subkeys,
3221 manifest_footer: Option<ManifestFooter>,
3222 manifest_footer_error: Option<FormatError>,
3223 root_auth_footer: Option<RootAuthFooterV1>,
3224 root_auth_footer_bytes: Option<Vec<u8>>,
3225 volume_trailer: VolumeTrailer,
3226 crypto_end: u64,
3227}
3228
3229struct ParsedOpenPrefix {
3230 volume_header: VolumeHeader,
3231 crypto_header: CryptoHeaderFixed,
3232 crypto_header_bytes: Vec<u8>,
3233 crypto_end: usize,
3234 subkeys: Subkeys,
3235}
3236
3237struct ParsedReadAtOpenPrefix {
3238 volume_header: VolumeHeader,
3239 crypto_header: CryptoHeaderFixed,
3240 crypto_header_bytes: Vec<u8>,
3241 crypto_end: u64,
3242 subkeys: Subkeys,
3243}
3244
3245fn parse_seekable_volume(
3246 bytes: &[u8],
3247 master_key: &MasterKey,
3248 options: ReaderOptions,
3249) -> Result<ParsedSeekableVolume, FormatError> {
3250 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3251 return Err(FormatError::InvalidLength {
3252 structure: "archive",
3253 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3254 actual: bytes.len(),
3255 });
3256 }
3257
3258 let prefix = match parse_open_prefix(bytes, master_key) {
3259 Ok(prefix) => prefix,
3260 Err(prefix_err) => {
3261 return parse_seekable_volume_from_recovered_terminal(bytes, master_key, options)
3262 .or(Err(prefix_err));
3263 }
3264 };
3265 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3266 match parse_seekable_volume_with_prefix(bytes, prefix, options) {
3267 Ok(parsed) => Ok(parsed),
3268 Err(prefix_err) => {
3269 match parse_seekable_volume_from_recovered_terminal(bytes, master_key, options) {
3270 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3271 Ok(recovered)
3272 }
3273 Ok(_) | Err(_) => Err(prefix_err),
3274 }
3275 }
3276 }
3277}
3278
3279fn parse_seekable_volume_with_prefix(
3280 bytes: &[u8],
3281 prefix: ParsedOpenPrefix,
3282 options: ReaderOptions,
3283) -> Result<ParsedSeekableVolume, FormatError> {
3284 let ParsedOpenPrefix {
3285 volume_header,
3286 crypto_header,
3287 crypto_header_bytes,
3288 crypto_end,
3289 subkeys,
3290 } = prefix;
3291 let crypto_bytes = crypto_header_bytes.as_slice();
3292
3293 let terminal = locate_v41_terminal(
3294 bytes,
3295 KeyHoldingTerminalContext {
3296 subkeys: &subkeys,
3297 volume_header: &volume_header,
3298 crypto_header: &crypto_header,
3299 crypto_header_bytes: crypto_bytes,
3300 },
3301 options,
3302 )?;
3303 finish_parse_seekable_volume(
3304 bytes,
3305 volume_header,
3306 crypto_header,
3307 crypto_header_bytes,
3308 crypto_end,
3309 subkeys,
3310 terminal,
3311 )
3312}
3313
3314fn parse_seekable_volume_from_recovered_terminal(
3315 bytes: &[u8],
3316 master_key: &MasterKey,
3317 options: ReaderOptions,
3318) -> Result<ParsedSeekableVolume, FormatError> {
3319 let authority = locate_v41_terminal_authority(bytes, master_key, options)?;
3320 let crypto_end = checked_add(
3321 authority.volume_header.crypto_header_offset as usize,
3322 authority.volume_header.crypto_header_length as usize,
3323 "CryptoHeader",
3324 )?;
3325 finish_parse_seekable_volume(
3326 bytes,
3327 authority.volume_header,
3328 authority.crypto_header,
3329 authority.crypto_header_bytes,
3330 crypto_end,
3331 authority.subkeys,
3332 authority.terminal,
3333 )
3334}
3335
3336fn parse_open_prefix(
3337 bytes: &[u8],
3338 master_key: &MasterKey,
3339) -> Result<ParsedOpenPrefix, FormatError> {
3340 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3341 let crypto_start = volume_header.crypto_header_offset as usize;
3342 let crypto_len = volume_header.crypto_header_length as usize;
3343 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3344 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3345 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3346 let subkeys = subkeys_for_open(
3347 Some(master_key),
3348 parsed_crypto.fixed.aead_algo,
3349 &volume_header.archive_uuid,
3350 &volume_header.session_id,
3351 )?;
3352 verify_integrity_tag(
3353 HmacDomain::CryptoHeader,
3354 parsed_crypto.fixed.aead_algo,
3355 Some(&subkeys.mac_key),
3356 &volume_header.archive_uuid,
3357 &volume_header.session_id,
3358 parsed_crypto.hmac_covered_bytes,
3359 &parsed_crypto.header_hmac,
3360 )?;
3361 parsed_crypto.validate_extension_semantics()?;
3362 validate_seekable_supported_volume(
3363 &volume_header,
3364 &parsed_crypto.fixed,
3365 &parsed_crypto.extensions,
3366 )?;
3367 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3368 let crypto_header = parsed_crypto.fixed.clone();
3369 Ok(ParsedOpenPrefix {
3370 volume_header,
3371 crypto_header,
3372 crypto_header_bytes: crypto_bytes.to_vec(),
3373 crypto_end,
3374 subkeys,
3375 })
3376}
3377
3378fn finish_parse_seekable_volume(
3379 bytes: &[u8],
3380 volume_header: VolumeHeader,
3381 crypto_header: CryptoHeaderFixed,
3382 crypto_header_bytes: Vec<u8>,
3383 crypto_end: usize,
3384 subkeys: Subkeys,
3385 terminal: V41Terminal,
3386) -> Result<ParsedSeekableVolume, FormatError> {
3387 let trailer_offset = to_usize(terminal.image.volume_trailer_offset, "VolumeTrailer")?;
3388 let volume_trailer = terminal.volume_trailer.clone();
3389 validate_trailer_identity(&volume_header, &volume_trailer)?;
3390
3391 let manifest_offset = to_usize(volume_trailer.manifest_footer_offset, "ManifestFooter")?;
3392 let manifest_end = checked_add(manifest_offset, MANIFEST_FOOTER_LEN, "ManifestFooter")?;
3393 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3394 if to_usize(volume_trailer.root_auth_footer_offset, "RootAuthFooter")? != manifest_end
3395 || volume_trailer
3396 .root_auth_footer_offset
3397 .checked_add(volume_trailer.root_auth_footer_length as u64)
3398 .ok_or(FormatError::InvalidArchive(
3399 "RootAuthFooter terminal boundary overflow",
3400 ))?
3401 != trailer_offset as u64
3402 {
3403 return Err(FormatError::InvalidArchive(
3404 "RootAuthFooter does not sit before selected trailer",
3405 ));
3406 }
3407 } else if manifest_end != trailer_offset {
3408 return Err(FormatError::InvalidArchive(
3409 "ManifestFooter does not end at selected trailer",
3410 ));
3411 }
3412 let manifest_bytes = &terminal.manifest_footer_bytes;
3413 let (manifest_footer, manifest_footer_error) =
3414 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3415 {
3416 Ok(footer) => (Some(footer), None),
3417 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3418 Err(err) => return Err(err),
3419 };
3420
3421 let block_region = parse_block_region(
3422 bytes,
3423 crypto_end,
3424 manifest_offset,
3425 crypto_header.block_size as usize,
3426 &volume_header,
3427 &volume_trailer,
3428 )?;
3429
3430 Ok(ParsedSeekableVolume {
3431 volume_header,
3432 crypto_header,
3433 crypto_header_bytes,
3434 subkeys,
3435 manifest_footer,
3436 manifest_footer_error,
3437 root_auth_footer: terminal.root_auth_footer,
3438 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3439 volume_trailer,
3440 blocks: block_region.blocks,
3441 erased_block_indices: block_region.erased_block_indices,
3442 })
3443}
3444
3445fn parse_seekable_read_at_volume(
3446 reader: Arc<dyn ArchiveReadAt>,
3447 master_key: &MasterKey,
3448 options: ReaderOptions,
3449) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3450 let observed_len = reader.len()?;
3451 if observed_len < (VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN) as u64 {
3452 return Err(FormatError::InvalidLength {
3453 structure: "archive",
3454 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3455 actual: to_usize(observed_len, "archive")?,
3456 });
3457 }
3458
3459 let prefix = match parse_read_at_open_prefix(reader.as_ref(), master_key) {
3460 Ok(prefix) => prefix,
3461 Err(prefix_err) => {
3462 return parse_seekable_read_at_volume_from_recovered_terminal(
3463 reader,
3464 observed_len,
3465 master_key,
3466 options,
3467 )
3468 .or(Err(prefix_err));
3469 }
3470 };
3471 let physical_crypto_header_bytes = prefix.crypto_header_bytes.clone();
3472 match parse_seekable_read_at_volume_with_prefix(reader.clone(), observed_len, prefix, options) {
3473 Ok(parsed) => Ok(parsed),
3474 Err(prefix_err) => match parse_seekable_read_at_volume_from_recovered_terminal(
3475 reader,
3476 observed_len,
3477 master_key,
3478 options,
3479 ) {
3480 Ok(recovered) if recovered.crypto_header_bytes == physical_crypto_header_bytes => {
3481 Ok(recovered)
3482 }
3483 Ok(_) | Err(_) => Err(prefix_err),
3484 },
3485 }
3486}
3487
3488fn parse_seekable_read_at_volume_with_prefix(
3489 reader: Arc<dyn ArchiveReadAt>,
3490 observed_len: u64,
3491 prefix: ParsedReadAtOpenPrefix,
3492 options: ReaderOptions,
3493) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3494 let ParsedReadAtOpenPrefix {
3495 volume_header,
3496 crypto_header,
3497 crypto_header_bytes,
3498 crypto_end,
3499 subkeys,
3500 } = prefix;
3501
3502 let terminal = locate_v41_terminal_read_at(
3503 reader.as_ref(),
3504 observed_len,
3505 KeyHoldingTerminalContext {
3506 subkeys: &subkeys,
3507 volume_header: &volume_header,
3508 crypto_header: &crypto_header,
3509 crypto_header_bytes: &crypto_header_bytes,
3510 },
3511 options,
3512 )?;
3513 finish_parse_seekable_read_at_volume(
3514 reader,
3515 volume_header,
3516 crypto_header,
3517 crypto_header_bytes,
3518 crypto_end,
3519 subkeys,
3520 terminal,
3521 )
3522}
3523
3524fn parse_seekable_read_at_volume_from_recovered_terminal(
3525 reader: Arc<dyn ArchiveReadAt>,
3526 observed_len: u64,
3527 master_key: &MasterKey,
3528 options: ReaderOptions,
3529) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3530 let authority =
3531 locate_v41_terminal_authority_read_at(reader.as_ref(), observed_len, master_key, options)?;
3532 let crypto_end = checked_u64_add(
3533 authority.volume_header.crypto_header_offset as u64,
3534 authority.volume_header.crypto_header_length as u64,
3535 "CryptoHeader",
3536 )?;
3537 finish_parse_seekable_read_at_volume(
3538 reader,
3539 authority.volume_header,
3540 authority.crypto_header,
3541 authority.crypto_header_bytes,
3542 crypto_end,
3543 authority.subkeys,
3544 authority.terminal,
3545 )
3546}
3547
3548fn parse_read_at_open_prefix(
3549 reader: &dyn ArchiveReadAt,
3550 master_key: &MasterKey,
3551) -> Result<ParsedReadAtOpenPrefix, FormatError> {
3552 let volume_header_bytes = read_at_vec(reader, 0, VOLUME_HEADER_LEN, "archive")?;
3553 let volume_header = VolumeHeader::parse(&volume_header_bytes)?;
3554 let crypto_start = volume_header.crypto_header_offset as u64;
3555 let crypto_len = volume_header.crypto_header_length as u64;
3556 let crypto_end = checked_u64_add(crypto_start, crypto_len, "CryptoHeader")?;
3557 let crypto_bytes = read_at_vec(
3558 reader,
3559 crypto_start,
3560 to_usize(crypto_len, "CryptoHeader")?,
3561 "CryptoHeader",
3562 )?;
3563 let parsed_crypto = CryptoHeader::parse(&crypto_bytes, volume_header.crypto_header_length)?;
3564 let subkeys = subkeys_for_open(
3565 Some(master_key),
3566 parsed_crypto.fixed.aead_algo,
3567 &volume_header.archive_uuid,
3568 &volume_header.session_id,
3569 )?;
3570 verify_integrity_tag(
3571 HmacDomain::CryptoHeader,
3572 parsed_crypto.fixed.aead_algo,
3573 Some(&subkeys.mac_key),
3574 &volume_header.archive_uuid,
3575 &volume_header.session_id,
3576 parsed_crypto.hmac_covered_bytes,
3577 &parsed_crypto.header_hmac,
3578 )?;
3579 parsed_crypto.validate_extension_semantics()?;
3580 validate_seekable_supported_volume(
3581 &volume_header,
3582 &parsed_crypto.fixed,
3583 &parsed_crypto.extensions,
3584 )?;
3585 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3586 let crypto_header = parsed_crypto.fixed.clone();
3587 drop(parsed_crypto);
3588 Ok(ParsedReadAtOpenPrefix {
3589 volume_header,
3590 crypto_header,
3591 crypto_header_bytes: crypto_bytes,
3592 crypto_end,
3593 subkeys,
3594 })
3595}
3596
3597fn finish_parse_seekable_read_at_volume(
3598 reader: Arc<dyn ArchiveReadAt>,
3599 volume_header: VolumeHeader,
3600 crypto_header: CryptoHeaderFixed,
3601 crypto_header_bytes: Vec<u8>,
3602 crypto_end: u64,
3603 subkeys: Subkeys,
3604 terminal: V41Terminal,
3605) -> Result<ParsedSeekableReadAtVolume, FormatError> {
3606 let volume_trailer = terminal.volume_trailer.clone();
3607 validate_trailer_identity(&volume_header, &volume_trailer)?;
3608
3609 let manifest_offset = volume_trailer.manifest_footer_offset;
3610 let manifest_end = checked_u64_add(
3611 manifest_offset,
3612 MANIFEST_FOOTER_LEN as u64,
3613 "ManifestFooter",
3614 )?;
3615 if volume_trailer.root_auth_flags & 0x0000_0001 != 0 {
3616 if volume_trailer.root_auth_footer_offset != manifest_end
3617 || volume_trailer
3618 .root_auth_footer_offset
3619 .checked_add(volume_trailer.root_auth_footer_length as u64)
3620 .ok_or(FormatError::InvalidArchive(
3621 "RootAuthFooter terminal boundary overflow",
3622 ))?
3623 != terminal.image.volume_trailer_offset
3624 {
3625 return Err(FormatError::InvalidArchive(
3626 "RootAuthFooter does not sit before selected trailer",
3627 ));
3628 }
3629 } else if manifest_end != terminal.image.volume_trailer_offset {
3630 return Err(FormatError::InvalidArchive(
3631 "ManifestFooter does not end at selected trailer",
3632 ));
3633 }
3634 validate_seekable_block_region_layout(
3635 crypto_end,
3636 manifest_offset,
3637 crypto_header.block_size as usize,
3638 &volume_trailer,
3639 )?;
3640
3641 let manifest_bytes = &terminal.manifest_footer_bytes;
3642 let (manifest_footer, manifest_footer_error) =
3643 match parse_valid_manifest_footer(&volume_header, &crypto_header, &subkeys, manifest_bytes)
3644 {
3645 Ok(footer) => (Some(footer), None),
3646 Err(err) if manifest_footer_copy_error_is_recoverable(&err) => (None, Some(err)),
3647 Err(err) => return Err(err),
3648 };
3649
3650 Ok(ParsedSeekableReadAtVolume {
3651 reader,
3652 volume_header,
3653 crypto_header,
3654 crypto_header_bytes,
3655 subkeys,
3656 manifest_footer,
3657 manifest_footer_error,
3658 root_auth_footer: terminal.root_auth_footer,
3659 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3660 volume_trailer,
3661 crypto_end,
3662 })
3663}
3664
3665#[derive(Debug)]
3666struct ParsedPublicNoKeyVolume {
3667 volume_header: VolumeHeader,
3668 crypto_header: CryptoHeaderFixed,
3669 root_auth_footer: RootAuthFooterV1,
3670 root_auth_footer_bytes: Vec<u8>,
3671 blocks: BTreeMap<u64, BlockRecord>,
3672}
3673
3674pub fn public_no_key_verify_volumes_with_options<F>(
3675 volumes: &[&[u8]],
3676 mut verifier: F,
3677 options: ReaderOptions,
3678) -> Result<PublicNoKeyVerification, FormatError>
3679where
3680 F: FnMut(&RootAuthFooterV1, &[u8; 32]) -> Result<bool, FormatError>,
3681{
3682 validate_reader_options(options)?;
3683 if volumes.is_empty() {
3684 return Err(FormatError::InvalidArchive("no volumes supplied"));
3685 }
3686 let mut parsed = Vec::with_capacity(volumes.len());
3687 for volume in volumes {
3688 parsed.push(parse_public_no_key_volume(volume, options)?);
3689 }
3690 let first = parsed
3691 .first()
3692 .ok_or(FormatError::InvalidArchive("no volumes supplied"))?;
3693 if parsed.len() != first.crypto_header.stripe_width as usize {
3694 return Err(FormatError::ReaderUnsupported(
3695 "public no-key verification requires a complete volume set",
3696 ));
3697 }
3698
3699 let mut seen_volume_indexes = BTreeSet::new();
3700 let mut blocks = BTreeMap::new();
3701 for volume in &parsed {
3702 if volume.volume_header.archive_uuid != first.volume_header.archive_uuid
3703 || volume.volume_header.session_id != first.volume_header.session_id
3704 || !public_crypto_headers_agree(&volume.crypto_header, &first.crypto_header)
3705 {
3706 return Err(FormatError::InvalidArchive(
3707 "public no-key volume global metadata differs",
3708 ));
3709 }
3710 if volume.root_auth_footer_bytes != first.root_auth_footer_bytes {
3711 return Err(FormatError::InvalidArchive(
3712 "public no-key RootAuthFooter copies differ",
3713 ));
3714 }
3715 if !seen_volume_indexes.insert(volume.volume_header.volume_index) {
3716 return Err(FormatError::InvalidArchive(
3717 "duplicate public no-key volume index",
3718 ));
3719 }
3720 for (block_index, record) in &volume.blocks {
3721 if blocks.insert(*block_index, record.clone()).is_some() {
3722 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
3723 }
3724 }
3725 }
3726 validate_complete_global_block_coverage(&blocks, &BTreeSet::new())?;
3727
3728 let footer = &first.root_auth_footer;
3729 let mut data_leaves = blocks
3730 .values()
3731 .filter(|record| record.kind.is_data())
3732 .map(|record| DataBlockMerkleLeaf {
3733 block_index: record.block_index,
3734 kind: record.kind,
3735 flags: record.flags,
3736 payload: record.payload.clone(),
3737 })
3738 .collect::<Vec<_>>();
3739 data_leaves.sort_by_key(|leaf| leaf.block_index);
3740 let total_data_block_count = u64::try_from(data_leaves.len())
3741 .map_err(|_| FormatError::InvalidArchive("public no-key data block count overflow"))?;
3742 let observed_data_root = data_block_merkle_root(&data_leaves);
3743 if total_data_block_count != footer.total_data_block_count
3744 || observed_data_root != footer.data_block_merkle_root
3745 {
3746 return Err(FormatError::InvalidArchive(
3747 "public no-key data-block commitment mismatch",
3748 ));
3749 }
3750 let archive_root = recompute_public_archive_root(footer, &first.crypto_header)?;
3751 if archive_root != footer.archive_root {
3752 return Err(FormatError::InvalidArchive(
3753 "public no-key archive_root mismatch",
3754 ));
3755 }
3756 if !verifier(footer, &archive_root)? {
3757 return Err(FormatError::InvalidArchive(
3758 "public no-key authenticator verification failed",
3759 ));
3760 }
3761 Ok(PublicNoKeyVerification {
3762 archive_root,
3763 authenticator_id: footer.authenticator_id,
3764 signer_identity_type: footer.signer_identity_type,
3765 signer_identity_bytes: footer.signer_identity_bytes.clone(),
3766 total_data_block_count,
3767 diagnostics: vec![
3768 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
3769 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
3770 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
3771 ],
3772 })
3773}
3774
3775fn parse_public_no_key_volume(
3776 bytes: &[u8],
3777 options: ReaderOptions,
3778) -> Result<ParsedPublicNoKeyVolume, FormatError> {
3779 if bytes.len() < VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN {
3780 return Err(FormatError::InvalidLength {
3781 structure: "archive",
3782 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
3783 actual: bytes.len(),
3784 });
3785 }
3786 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
3787 let crypto_start = volume_header.crypto_header_offset as usize;
3788 let crypto_len = volume_header.crypto_header_length as usize;
3789 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
3790 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
3791 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
3792 parsed_crypto.validate_extension_semantics()?;
3793 validate_seekable_supported_volume(
3794 &volume_header,
3795 &parsed_crypto.fixed,
3796 &parsed_crypto.extensions,
3797 )?;
3798 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
3799
3800 let terminal =
3801 locate_v41_public_terminal(bytes, &volume_header, &parsed_crypto.fixed, options)?;
3802 let block_region = parse_public_block_observation(
3803 bytes,
3804 crypto_end,
3805 &terminal.image,
3806 parsed_crypto.fixed.block_size as usize,
3807 &volume_header,
3808 )?;
3809 Ok(ParsedPublicNoKeyVolume {
3810 volume_header,
3811 crypto_header: parsed_crypto.fixed,
3812 root_auth_footer: terminal.root_auth_footer,
3813 root_auth_footer_bytes: terminal.root_auth_footer_bytes,
3814 blocks: block_region,
3815 })
3816}
3817
3818fn public_crypto_headers_agree(left: &CryptoHeaderFixed, right: &CryptoHeaderFixed) -> bool {
3819 left.length == right.length
3820 && left.stripe_width == right.stripe_width
3821 && left.block_size == right.block_size
3822 && left.compression_algo == right.compression_algo
3823 && left.aead_algo == right.aead_algo
3824 && left.fec_algo == right.fec_algo
3825 && left.kdf_algo == right.kdf_algo
3826}
3827
3828fn recompute_public_archive_root(
3829 footer: &RootAuthFooterV1,
3830 crypto_header: &CryptoHeaderFixed,
3831) -> Result<[u8; 32], FormatError> {
3832 let descriptor_digest = root_auth_descriptor_digest(
3833 footer.authenticator_id,
3834 footer.signer_identity_type,
3835 &footer.signer_identity_bytes,
3836 u32::try_from(footer.authenticator_value.len()).map_err(|_| {
3837 FormatError::InvalidArchive("RootAuthFooter authenticator length overflow")
3838 })?,
3839 footer.footer_length()?,
3840 )?;
3841 let signer_digest =
3842 signer_identity_digest(footer.signer_identity_type, &footer.signer_identity_bytes)?;
3843 if signer_digest != footer.signer_identity_digest {
3844 return Err(FormatError::InvalidArchive(
3845 "public no-key signer identity digest mismatch",
3846 ));
3847 }
3848 Ok(archive_root(ArchiveRootInputs {
3849 archive_uuid: footer.archive_uuid,
3850 session_id: footer.session_id,
3851 format_version: FORMAT_VERSION,
3852 volume_format_rev: VOLUME_FORMAT_REV,
3853 compression_algo: crypto_header.compression_algo,
3854 aead_algo: crypto_header.aead_algo,
3855 fec_algo: crypto_header.fec_algo,
3856 kdf_algo: crypto_header.kdf_algo,
3857 critical_metadata_digest: footer.critical_metadata_digest,
3858 index_digest: footer.index_digest,
3859 fec_layout_digest: footer.fec_layout_digest,
3860 total_data_block_count: footer.total_data_block_count,
3861 data_block_merkle_root: footer.data_block_merkle_root,
3862 root_auth_descriptor_digest: descriptor_digest,
3863 signer_identity_digest: signer_digest,
3864 }))
3865}
3866
3867fn parse_valid_manifest_footer(
3868 volume_header: &VolumeHeader,
3869 crypto_header: &CryptoHeaderFixed,
3870 subkeys: &Subkeys,
3871 manifest_bytes: &[u8],
3872) -> Result<ManifestFooter, FormatError> {
3873 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
3874 validate_manifest_footer(
3875 volume_header,
3876 crypto_header,
3877 &manifest_footer,
3878 subkeys,
3879 manifest_bytes,
3880 )?;
3881 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
3882 Ok(manifest_footer)
3883}
3884
3885fn manifest_footer_copy_error_is_recoverable(error: &FormatError) -> bool {
3886 matches!(
3887 error,
3888 FormatError::BadMagic {
3889 structure: "ManifestFooter",
3890 } | FormatError::NonZeroReserved {
3891 structure: "ManifestFooter",
3892 } | FormatError::InvalidAuthoritativeFlag(_)
3893 | FormatError::HmacMismatch {
3894 structure: "ManifestFooter",
3895 }
3896 | FormatError::IntegrityDigestMismatch {
3897 structure: "ManifestFooter",
3898 }
3899 )
3900}
3901
3902fn validate_seekable_supported_volume(
3903 volume_header: &VolumeHeader,
3904 crypto_header: &CryptoHeaderFixed,
3905 extensions: &[ExtensionTlv<'_>],
3906) -> Result<(), FormatError> {
3907 reject_unsupported_raw_stream_profile(extensions)?;
3908 if crypto_header.stripe_width != volume_header.stripe_width {
3909 return Err(FormatError::InvalidArchive(
3910 "VolumeHeader and CryptoHeader stripe_width differ",
3911 ));
3912 }
3913 Ok(())
3914}
3915
3916pub(crate) fn validate_crypto_class_parity_exactness(
3917 crypto_header: &CryptoHeaderFixed,
3918) -> Result<(), FormatError> {
3919 let fec = required_object_parity(crypto_header.fec_data_shards as u64, crypto_header)?;
3920 if crypto_header.fec_parity_shards as u32 != fec {
3921 return Err(FormatError::InvalidArchive(
3922 "fec_parity_shards does not match v41 compute_parity",
3923 ));
3924 }
3925 let index = required_object_parity(crypto_header.index_fec_data_shards as u64, crypto_header)?;
3926 if crypto_header.index_fec_parity_shards as u32 != index {
3927 return Err(FormatError::InvalidArchive(
3928 "index_fec_parity_shards does not match v41 compute_parity",
3929 ));
3930 }
3931 let index_root = required_object_parity(
3932 crypto_header.index_root_fec_data_shards as u64,
3933 crypto_header,
3934 )?;
3935 if crypto_header.index_root_fec_parity_shards as u32 != index_root {
3936 return Err(FormatError::InvalidArchive(
3937 "index_root_fec_parity_shards does not match v41 compute_parity",
3938 ));
3939 }
3940 Ok(())
3941}
3942
3943fn validate_volume_set_member(
3944 first: &ParsedSeekableVolume,
3945 candidate: &ParsedSeekableVolume,
3946) -> Result<(), FormatError> {
3947 validate_volume_set_member_metadata(
3948 &first.volume_header,
3949 &first.crypto_header,
3950 &first.crypto_header_bytes,
3951 &candidate.volume_header,
3952 &candidate.crypto_header,
3953 &candidate.crypto_header_bytes,
3954 )
3955}
3956
3957fn validate_volume_set_member_metadata(
3958 first_volume_header: &VolumeHeader,
3959 first_crypto_header: &CryptoHeaderFixed,
3960 first_crypto_header_bytes: &[u8],
3961 candidate_volume_header: &VolumeHeader,
3962 candidate_crypto_header: &CryptoHeaderFixed,
3963 candidate_crypto_header_bytes: &[u8],
3964) -> Result<(), FormatError> {
3965 if candidate_volume_header.archive_uuid != first_volume_header.archive_uuid
3966 || candidate_volume_header.session_id != first_volume_header.session_id
3967 {
3968 return Err(FormatError::InvalidArchive(
3969 "mixed archive or session IDs in volume set",
3970 ));
3971 }
3972 if candidate_crypto_header_bytes != first_crypto_header_bytes
3973 || candidate_crypto_header != first_crypto_header
3974 {
3975 return Err(FormatError::InvalidArchive("CryptoHeader copies differ"));
3976 }
3977 Ok(())
3978}
3979
3980pub(crate) fn manifest_bootstrap_fields_match(
3981 left: &ManifestFooter,
3982 right: &ManifestFooter,
3983) -> bool {
3984 left.archive_uuid == right.archive_uuid
3985 && left.session_id == right.session_id
3986 && left.is_authoritative == right.is_authoritative
3987 && left.total_volumes == right.total_volumes
3988 && left.index_root_first_block == right.index_root_first_block
3989 && left.index_root_data_block_count == right.index_root_data_block_count
3990 && left.index_root_parity_block_count == right.index_root_parity_block_count
3991 && left.index_root_encrypted_size == right.index_root_encrypted_size
3992 && left.index_root_decompressed_size == right.index_root_decompressed_size
3993}
3994
3995fn validate_complete_global_block_coverage(
3996 blocks: &BTreeMap<u64, BlockRecord>,
3997 erased_block_indices: &BTreeSet<u64>,
3998) -> Result<(), FormatError> {
3999 let mut expected = 0u64;
4000 let mut block_iter = blocks.keys().copied().peekable();
4001 let mut erasure_iter = erased_block_indices.iter().copied().peekable();
4002
4003 loop {
4004 let next_block = block_iter.peek().copied();
4005 let next_erasure = erasure_iter.peek().copied();
4006 let next = match (next_block, next_erasure) {
4007 (Some(block), Some(erasure)) if block == erasure => {
4008 return Err(FormatError::InvalidArchive(
4009 "BlockRecord index is both present and erased",
4010 ));
4011 }
4012 (Some(block), Some(erasure)) => block.min(erasure),
4013 (Some(block), None) => block,
4014 (None, Some(erasure)) => erasure,
4015 (None, None) => return Ok(()),
4016 };
4017
4018 if next != expected {
4019 return Err(FormatError::InvalidArchive(
4020 "complete volume set has missing global blocks",
4021 ));
4022 }
4023 if next_block == Some(next) {
4024 block_iter.next();
4025 }
4026 if next_erasure == Some(next) {
4027 erasure_iter.next();
4028 }
4029 expected = expected
4030 .checked_add(1)
4031 .ok_or(FormatError::InvalidArchive("global block index overflow"))?;
4032 }
4033}
4034
4035#[derive(Debug)]
4036struct V41Terminal {
4037 image: CriticalMetadataImage,
4038 manifest_footer_bytes: Vec<u8>,
4039 root_auth_footer_bytes: Option<Vec<u8>>,
4040 root_auth_footer: Option<RootAuthFooterV1>,
4041 volume_trailer: VolumeTrailer,
4042}
4043
4044pub(crate) struct SequentialTerminalMaterial {
4045 pub(crate) manifest_footer: ManifestFooter,
4046 pub(crate) volume_trailer: VolumeTrailer,
4047 pub(crate) root_auth_footer: Option<RootAuthFooterV1>,
4048}
4049
4050#[derive(Debug)]
4051struct V41PublicTerminal {
4052 image: CriticalMetadataImage,
4053 root_auth_footer_bytes: Vec<u8>,
4054 root_auth_footer: RootAuthFooterV1,
4055}
4056
4057#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4058struct CmraDecoderTuple {
4059 shard_size: u32,
4060 data_shard_count: u16,
4061 parity_shard_count: u16,
4062 image_length: u32,
4063 image_sha256: [u8; 32],
4064}
4065
4066#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4067struct CmraIdentityHints {
4068 archive_uuid: [u8; 16],
4069 session_id: [u8; 16],
4070 volume_index: u32,
4071}
4072
4073impl From<CriticalMetadataRecoveryHeader> for CmraDecoderTuple {
4074 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
4075 Self {
4076 shard_size: header.shard_size,
4077 data_shard_count: header.data_shard_count,
4078 parity_shard_count: header.parity_shard_count,
4079 image_length: header.image_length,
4080 image_sha256: header.image_sha256,
4081 }
4082 }
4083}
4084
4085impl From<CriticalMetadataRecoveryHeader> for CmraIdentityHints {
4086 fn from(header: CriticalMetadataRecoveryHeader) -> Self {
4087 Self {
4088 archive_uuid: header.archive_uuid_hint,
4089 session_id: header.session_id_hint,
4090 volume_index: header.volume_index_hint,
4091 }
4092 }
4093}
4094
4095impl From<CriticalRecoveryLocator> for CmraDecoderTuple {
4096 fn from(locator: CriticalRecoveryLocator) -> Self {
4097 Self {
4098 shard_size: locator.cmra_shard_size,
4099 data_shard_count: locator.cmra_data_shard_count,
4100 parity_shard_count: locator.cmra_parity_shard_count,
4101 image_length: locator.cmra_image_length,
4102 image_sha256: locator.cmra_image_sha256,
4103 }
4104 }
4105}
4106
4107impl From<CriticalRecoveryLocator> for CmraIdentityHints {
4108 fn from(locator: CriticalRecoveryLocator) -> Self {
4109 Self {
4110 archive_uuid: locator.archive_uuid_hint,
4111 session_id: locator.session_id_hint,
4112 volume_index: locator.volume_index_hint,
4113 }
4114 }
4115}
4116
4117#[derive(Debug)]
4118struct RecoveredCmra {
4119 image: CriticalMetadataImage,
4120 tuple: CmraDecoderTuple,
4121 header_hints: Option<CmraIdentityHints>,
4122 cmra_length: u64,
4123}
4124
4125#[derive(Debug)]
4126struct TerminalCandidate {
4127 terminal: V41Terminal,
4128 anchor: usize,
4129 locator_sequence: Option<u32>,
4130 cmra_offset: u64,
4131 cmra_length: u64,
4132}
4133
4134#[derive(Debug)]
4135struct PublicTerminalCandidate {
4136 terminal: V41PublicTerminal,
4137 anchor: usize,
4138 cmra_offset: u64,
4139 cmra_length: u64,
4140}
4141
4142#[derive(Debug)]
4143struct RecoveredTerminalAuthority {
4144 terminal: V41Terminal,
4145 volume_header: VolumeHeader,
4146 crypto_header: CryptoHeaderFixed,
4147 crypto_header_bytes: Vec<u8>,
4148 subkeys: Subkeys,
4149}
4150
4151#[derive(Debug)]
4152struct TerminalAuthorityCandidate {
4153 authority: RecoveredTerminalAuthority,
4154 anchor: usize,
4155 cmra_offset: u64,
4156 cmra_length: u64,
4157}
4158
4159#[derive(Debug, Clone, Copy)]
4160enum CmraRecoveryMode {
4161 KeyHolding,
4162 PublicNoKey,
4163}
4164
4165#[derive(Clone, Copy)]
4166pub(crate) struct KeyHoldingTerminalContext<'a> {
4167 pub(crate) subkeys: &'a Subkeys,
4168 pub(crate) volume_header: &'a VolumeHeader,
4169 pub(crate) crypto_header: &'a CryptoHeaderFixed,
4170 pub(crate) crypto_header_bytes: &'a [u8],
4171}
4172
4173fn locate_v41_terminal(
4174 bytes: &[u8],
4175 context: KeyHoldingTerminalContext<'_>,
4176 options: ReaderOptions,
4177) -> Result<V41Terminal, FormatError> {
4178 locate_v41_terminal_candidate(bytes, context, options).map(|candidate| candidate.terminal)
4179}
4180
4181fn locate_v41_terminal_read_at(
4182 reader: &dyn ArchiveReadAt,
4183 len: u64,
4184 context: KeyHoldingTerminalContext<'_>,
4185 options: ReaderOptions,
4186) -> Result<V41Terminal, FormatError> {
4187 let mut candidates = Vec::new();
4188 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4189 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4190 collect_v41_locator_candidate_read_at(reader, final_offset, 0, context, &mut candidates);
4191 }
4192 if len >= LOCATOR_PAIR_LEN as u64 {
4193 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4194 collect_v41_locator_candidate_read_at(reader, mirror_offset, 1, context, &mut candidates);
4195 }
4196
4197 if candidates.is_empty() {
4198 let scan = max_critical_recovery_scan(options)? as u64;
4199 let scan_start = len.saturating_sub(scan);
4200 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4201 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4202 let mut offset = tail.len().saturating_sub(4);
4203 while offset < tail.len() {
4204 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4205 if tail.get(offset..offset + 4) == Some(b"TZCL") {
4206 collect_v41_locator_candidate_read_at(
4207 reader,
4208 absolute_offset,
4209 2,
4210 context,
4211 &mut candidates,
4212 );
4213 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4214 if let Ok(candidate) =
4215 parse_locatorless_cmra_candidate_read_at(reader, absolute_offset, context)
4216 {
4217 candidates.push(candidate);
4218 }
4219 }
4220 if offset == 0 {
4221 break;
4222 }
4223 offset -= 1;
4224 }
4225 }
4226
4227 choose_v41_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4228}
4229
4230fn locate_v41_terminal_authority(
4231 bytes: &[u8],
4232 master_key: &MasterKey,
4233 options: ReaderOptions,
4234) -> Result<RecoveredTerminalAuthority, FormatError> {
4235 let mut candidates = Vec::new();
4236 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4237 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4238 collect_v41_locator_authority_candidate(
4239 bytes,
4240 final_offset,
4241 0,
4242 master_key,
4243 &mut candidates,
4244 );
4245 }
4246 if bytes.len() >= LOCATOR_PAIR_LEN {
4247 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4248 collect_v41_locator_authority_candidate(
4249 bytes,
4250 mirror_offset,
4251 1,
4252 master_key,
4253 &mut candidates,
4254 );
4255 }
4256
4257 if candidates.is_empty() {
4258 let scan = max_critical_recovery_scan(options)?;
4259 let scan_start = bytes.len().saturating_sub(scan);
4260 let mut offset = bytes.len().saturating_sub(4);
4261 while offset >= scan_start {
4262 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4263 collect_v41_locator_authority_candidate(
4264 bytes,
4265 offset,
4266 2,
4267 master_key,
4268 &mut candidates,
4269 );
4270 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4271 if let Ok(candidate) =
4272 parse_locatorless_cmra_authority_candidate(bytes, offset, master_key)
4273 {
4274 candidates.push(candidate);
4275 }
4276 }
4277 if offset == 0 {
4278 break;
4279 }
4280 offset -= 1;
4281 }
4282 }
4283
4284 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4285}
4286
4287fn locate_v41_terminal_authority_read_at(
4288 reader: &dyn ArchiveReadAt,
4289 len: u64,
4290 master_key: &MasterKey,
4291 options: ReaderOptions,
4292) -> Result<RecoveredTerminalAuthority, FormatError> {
4293 let mut candidates = Vec::new();
4294 if len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
4295 let final_offset = len - CRITICAL_RECOVERY_LOCATOR_LEN as u64;
4296 collect_v41_locator_authority_candidate_read_at(
4297 reader,
4298 final_offset,
4299 0,
4300 master_key,
4301 &mut candidates,
4302 );
4303 }
4304 if len >= LOCATOR_PAIR_LEN as u64 {
4305 let mirror_offset = len - LOCATOR_PAIR_LEN as u64;
4306 collect_v41_locator_authority_candidate_read_at(
4307 reader,
4308 mirror_offset,
4309 1,
4310 master_key,
4311 &mut candidates,
4312 );
4313 }
4314
4315 if candidates.is_empty() {
4316 let scan = max_critical_recovery_scan(options)? as u64;
4317 let scan_start = len.saturating_sub(scan);
4318 let scan_len = to_usize(len.saturating_sub(scan_start), "CMRA scan")?;
4319 let tail = read_at_vec(reader, scan_start, scan_len, "CMRA scan")?;
4320 let mut offset = tail.len().saturating_sub(4);
4321 while offset < tail.len() {
4322 let absolute_offset = checked_u64_add(scan_start, offset as u64, "CMRA scan")?;
4323 if tail.get(offset..offset + 4) == Some(b"TZCL") {
4324 collect_v41_locator_authority_candidate_read_at(
4325 reader,
4326 absolute_offset,
4327 2,
4328 master_key,
4329 &mut candidates,
4330 );
4331 } else if tail.get(offset..offset + 4) == Some(b"TZCR") {
4332 if let Ok(candidate) = parse_locatorless_cmra_authority_candidate_read_at(
4333 reader,
4334 absolute_offset,
4335 master_key,
4336 ) {
4337 candidates.push(candidate);
4338 }
4339 }
4340 if offset == 0 {
4341 break;
4342 }
4343 offset -= 1;
4344 }
4345 }
4346
4347 choose_v41_terminal_authority_candidate(candidates).map(|candidate| candidate.authority)
4348}
4349
4350fn locate_v41_terminal_candidate(
4351 bytes: &[u8],
4352 context: KeyHoldingTerminalContext<'_>,
4353 options: ReaderOptions,
4354) -> Result<TerminalCandidate, FormatError> {
4355 let mut candidates = Vec::new();
4356 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4357 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4358 collect_v41_locator_candidate(bytes, final_offset, 0, context, &mut candidates);
4359 }
4360 if bytes.len() >= LOCATOR_PAIR_LEN {
4361 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4362 collect_v41_locator_candidate(bytes, mirror_offset, 1, context, &mut candidates);
4363 }
4364
4365 if candidates.is_empty() {
4366 let scan = max_critical_recovery_scan(options)?;
4367 let scan_start = bytes.len().saturating_sub(scan);
4368 let mut offset = bytes.len().saturating_sub(4);
4369 while offset >= scan_start {
4370 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4371 collect_v41_locator_candidate(bytes, offset, 2, context, &mut candidates);
4372 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4373 if let Ok(candidate) = parse_locatorless_cmra_candidate(bytes, offset, context) {
4374 candidates.push(candidate);
4375 }
4376 }
4377 if offset == 0 {
4378 break;
4379 }
4380 offset -= 1;
4381 }
4382 }
4383
4384 choose_v41_terminal_candidate(candidates)
4385}
4386
4387fn locate_v41_public_terminal(
4388 bytes: &[u8],
4389 volume_header: &VolumeHeader,
4390 crypto_header: &CryptoHeaderFixed,
4391 options: ReaderOptions,
4392) -> Result<V41PublicTerminal, FormatError> {
4393 let mut candidates = Vec::new();
4394 if bytes.len() >= CRITICAL_RECOVERY_LOCATOR_LEN {
4395 let final_offset = bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
4396 collect_v41_public_locator_candidate(
4397 bytes,
4398 final_offset,
4399 0,
4400 volume_header,
4401 crypto_header,
4402 &mut candidates,
4403 );
4404 }
4405 if bytes.len() >= LOCATOR_PAIR_LEN {
4406 let mirror_offset = bytes.len() - LOCATOR_PAIR_LEN;
4407 collect_v41_public_locator_candidate(
4408 bytes,
4409 mirror_offset,
4410 1,
4411 volume_header,
4412 crypto_header,
4413 &mut candidates,
4414 );
4415 }
4416
4417 if candidates.is_empty() {
4418 let scan = max_critical_recovery_scan(options)?;
4419 let scan_start = bytes.len().saturating_sub(scan);
4420 let mut offset = bytes.len().saturating_sub(4);
4421 while offset >= scan_start {
4422 if bytes.get(offset..offset + 4) == Some(b"TZCL") {
4423 collect_v41_public_locator_candidate(
4424 bytes,
4425 offset,
4426 2,
4427 volume_header,
4428 crypto_header,
4429 &mut candidates,
4430 );
4431 } else if bytes.get(offset..offset + 4) == Some(b"TZCR") {
4432 if let Ok(candidate) = parse_public_locatorless_cmra_candidate(
4433 bytes,
4434 offset,
4435 volume_header,
4436 crypto_header,
4437 ) {
4438 candidates.push(candidate);
4439 }
4440 }
4441 if offset == 0 {
4442 break;
4443 }
4444 offset -= 1;
4445 }
4446 }
4447
4448 choose_v41_public_terminal_candidate(candidates).map(|candidate| candidate.terminal)
4449}
4450
4451fn collect_v41_locator_candidate(
4452 bytes: &[u8],
4453 offset: usize,
4454 expected_sequence: u32,
4455 context: KeyHoldingTerminalContext<'_>,
4456 candidates: &mut Vec<TerminalCandidate>,
4457) {
4458 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4459 return;
4460 };
4461 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4462 return;
4463 };
4464 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4465 return;
4466 }
4467 if let Ok(candidate) = parse_locator_cmra_candidate(bytes, offset, locator, context) {
4468 candidates.push(candidate);
4469 }
4470}
4471
4472fn collect_v41_locator_candidate_read_at(
4473 reader: &dyn ArchiveReadAt,
4474 offset: u64,
4475 expected_sequence: u32,
4476 context: KeyHoldingTerminalContext<'_>,
4477 candidates: &mut Vec<TerminalCandidate>,
4478) {
4479 let Ok(raw) = read_at_vec(
4480 reader,
4481 offset,
4482 CRITICAL_RECOVERY_LOCATOR_LEN,
4483 "CriticalRecoveryLocator",
4484 ) else {
4485 return;
4486 };
4487 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4488 return;
4489 };
4490 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4491 return;
4492 }
4493 if let Ok(candidate) = parse_locator_cmra_candidate_read_at(reader, offset, locator, context) {
4494 candidates.push(candidate);
4495 }
4496}
4497
4498fn collect_v41_locator_authority_candidate(
4499 bytes: &[u8],
4500 offset: usize,
4501 expected_sequence: u32,
4502 master_key: &MasterKey,
4503 candidates: &mut Vec<TerminalAuthorityCandidate>,
4504) {
4505 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4506 return;
4507 };
4508 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4509 return;
4510 };
4511 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4512 return;
4513 }
4514 if let Ok(candidate) =
4515 parse_locator_cmra_authority_candidate(bytes, offset, locator, master_key)
4516 {
4517 candidates.push(candidate);
4518 }
4519}
4520
4521fn collect_v41_locator_authority_candidate_read_at(
4522 reader: &dyn ArchiveReadAt,
4523 offset: u64,
4524 expected_sequence: u32,
4525 master_key: &MasterKey,
4526 candidates: &mut Vec<TerminalAuthorityCandidate>,
4527) {
4528 let Ok(raw) = read_at_vec(
4529 reader,
4530 offset,
4531 CRITICAL_RECOVERY_LOCATOR_LEN,
4532 "CriticalRecoveryLocator",
4533 ) else {
4534 return;
4535 };
4536 let Ok(locator) = CriticalRecoveryLocator::parse(&raw) else {
4537 return;
4538 };
4539 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4540 return;
4541 }
4542 if let Ok(candidate) =
4543 parse_locator_cmra_authority_candidate_read_at(reader, offset, locator, master_key)
4544 {
4545 candidates.push(candidate);
4546 }
4547}
4548
4549fn collect_v41_public_locator_candidate(
4550 bytes: &[u8],
4551 offset: usize,
4552 expected_sequence: u32,
4553 volume_header: &VolumeHeader,
4554 crypto_header: &CryptoHeaderFixed,
4555 candidates: &mut Vec<PublicTerminalCandidate>,
4556) {
4557 let Some(raw) = bytes.get(offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN) else {
4558 return;
4559 };
4560 let Ok(locator) = CriticalRecoveryLocator::parse(raw) else {
4561 return;
4562 };
4563 if expected_sequence <= 1 && locator.locator_sequence != expected_sequence {
4564 return;
4565 }
4566 if let Ok(candidate) =
4567 parse_public_locator_cmra_candidate(bytes, offset, locator, volume_header, crypto_header)
4568 {
4569 candidates.push(candidate);
4570 }
4571}
4572
4573fn choose_v41_terminal_candidate(
4574 mut candidates: Vec<TerminalCandidate>,
4575) -> Result<TerminalCandidate, FormatError> {
4576 candidates.sort_by_key(|candidate| candidate.anchor);
4577 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4578 "no valid v41 CMRA candidate found",
4579 ))?;
4580 if let Some(previous) = candidates.last() {
4581 if previous.anchor == winner.anchor
4582 && (previous.cmra_offset != winner.cmra_offset
4583 || previous.cmra_length != winner.cmra_length)
4584 {
4585 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4586 }
4587 }
4588 Ok(winner)
4589}
4590
4591fn choose_v41_terminal_authority_candidate(
4592 mut candidates: Vec<TerminalAuthorityCandidate>,
4593) -> Result<TerminalAuthorityCandidate, FormatError> {
4594 candidates.sort_by_key(|candidate| candidate.anchor);
4595 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4596 "no valid v41 CMRA candidate found",
4597 ))?;
4598 if let Some(previous) = candidates.last() {
4599 if previous.anchor == winner.anchor
4600 && (previous.cmra_offset != winner.cmra_offset
4601 || previous.cmra_length != winner.cmra_length)
4602 {
4603 return Err(FormatError::InvalidArchive("ambiguous v41 CMRA candidates"));
4604 }
4605 }
4606 Ok(winner)
4607}
4608
4609fn choose_v41_public_terminal_candidate(
4610 mut candidates: Vec<PublicTerminalCandidate>,
4611) -> Result<PublicTerminalCandidate, FormatError> {
4612 candidates.sort_by_key(|candidate| candidate.anchor);
4613 let winner = candidates.pop().ok_or(FormatError::InvalidArchive(
4614 "no valid v41 public CMRA candidate found",
4615 ))?;
4616 if let Some(previous) = candidates.last() {
4617 if previous.anchor == winner.anchor
4618 && (previous.cmra_offset != winner.cmra_offset
4619 || previous.cmra_length != winner.cmra_length)
4620 {
4621 return Err(FormatError::InvalidArchive(
4622 "ambiguous v41 public CMRA candidates",
4623 ));
4624 }
4625 }
4626 Ok(winner)
4627}
4628
4629fn parse_locator_cmra_candidate(
4630 bytes: &[u8],
4631 locator_offset: usize,
4632 locator: CriticalRecoveryLocator,
4633 context: KeyHoldingTerminalContext<'_>,
4634) -> Result<TerminalCandidate, FormatError> {
4635 let tuple = CmraDecoderTuple::from(locator);
4636 validate_cmra_decoder_tuple(tuple)?;
4637 let expected_cmra_length = cmra_serialized_length(tuple)?;
4638 if locator.cmra_length as u64 != expected_cmra_length {
4639 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4640 }
4641 validate_locator_position(locator_offset, locator)?;
4642 let recovered = recover_cmra(
4643 bytes,
4644 locator.cmra_offset,
4645 Some(tuple),
4646 CmraRecoveryMode::KeyHolding,
4647 )?;
4648 if recovered.tuple != tuple {
4649 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4650 }
4651 if expected_cmra_length != recovered.cmra_length {
4652 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4653 }
4654 validate_locator_image_boundary(locator, &recovered.image)?;
4655 validate_cmra_identity_hints(
4656 recovered.header_hints,
4657 Some(CmraIdentityHints::from(locator)),
4658 &recovered.image,
4659 )?;
4660 let terminal =
4661 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, false)?;
4662 Ok(TerminalCandidate {
4663 terminal,
4664 anchor: locator_offset
4665 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4666 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4667 locator_sequence: Some(locator.locator_sequence),
4668 cmra_offset: locator.cmra_offset,
4669 cmra_length: recovered.cmra_length,
4670 })
4671}
4672
4673fn parse_locator_cmra_candidate_read_at(
4674 reader: &dyn ArchiveReadAt,
4675 locator_offset: u64,
4676 locator: CriticalRecoveryLocator,
4677 context: KeyHoldingTerminalContext<'_>,
4678) -> Result<TerminalCandidate, FormatError> {
4679 let tuple = CmraDecoderTuple::from(locator);
4680 validate_cmra_decoder_tuple(tuple)?;
4681 let expected_cmra_length = cmra_serialized_length(tuple)?;
4682 if locator.cmra_length as u64 != expected_cmra_length {
4683 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4684 }
4685 validate_locator_position(
4686 to_usize(locator_offset, "CriticalRecoveryLocator")?,
4687 locator,
4688 )?;
4689 let recovered = recover_cmra_read_at(
4690 reader,
4691 locator.cmra_offset,
4692 Some(tuple),
4693 CmraRecoveryMode::KeyHolding,
4694 )?;
4695 if recovered.tuple != tuple {
4696 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4697 }
4698 if expected_cmra_length != recovered.cmra_length {
4699 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4700 }
4701 validate_locator_image_boundary(locator, &recovered.image)?;
4702 validate_cmra_identity_hints(
4703 recovered.header_hints,
4704 Some(CmraIdentityHints::from(locator)),
4705 &recovered.image,
4706 )?;
4707 let terminal = validate_recovered_terminal_read_at(
4708 recovered.image,
4709 recovered.tuple,
4710 reader,
4711 context,
4712 false,
4713 )?;
4714 Ok(TerminalCandidate {
4715 terminal,
4716 anchor: to_usize(
4717 checked_u64_add(
4718 locator_offset,
4719 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4720 "locator anchor overflow",
4721 )?,
4722 "locator anchor overflow",
4723 )?,
4724 locator_sequence: Some(locator.locator_sequence),
4725 cmra_offset: locator.cmra_offset,
4726 cmra_length: recovered.cmra_length,
4727 })
4728}
4729
4730fn parse_locator_cmra_authority_candidate(
4731 bytes: &[u8],
4732 locator_offset: usize,
4733 locator: CriticalRecoveryLocator,
4734 master_key: &MasterKey,
4735) -> Result<TerminalAuthorityCandidate, FormatError> {
4736 let tuple = CmraDecoderTuple::from(locator);
4737 validate_cmra_decoder_tuple(tuple)?;
4738 let expected_cmra_length = cmra_serialized_length(tuple)?;
4739 if locator.cmra_length as u64 != expected_cmra_length {
4740 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4741 }
4742 validate_locator_position(locator_offset, locator)?;
4743 let recovered = recover_cmra(
4744 bytes,
4745 locator.cmra_offset,
4746 Some(tuple),
4747 CmraRecoveryMode::KeyHolding,
4748 )?;
4749 if recovered.tuple != tuple {
4750 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4751 }
4752 if expected_cmra_length != recovered.cmra_length {
4753 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4754 }
4755 validate_locator_image_boundary(locator, &recovered.image)?;
4756 validate_cmra_identity_hints(
4757 recovered.header_hints,
4758 Some(CmraIdentityHints::from(locator)),
4759 &recovered.image,
4760 )?;
4761 let cmra_length = recovered.cmra_length;
4762 let authority =
4763 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4764 Ok(TerminalAuthorityCandidate {
4765 authority,
4766 anchor: locator_offset
4767 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4768 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4769 cmra_offset: locator.cmra_offset,
4770 cmra_length,
4771 })
4772}
4773
4774fn parse_locator_cmra_authority_candidate_read_at(
4775 reader: &dyn ArchiveReadAt,
4776 locator_offset: u64,
4777 locator: CriticalRecoveryLocator,
4778 master_key: &MasterKey,
4779) -> Result<TerminalAuthorityCandidate, FormatError> {
4780 let tuple = CmraDecoderTuple::from(locator);
4781 validate_cmra_decoder_tuple(tuple)?;
4782 let expected_cmra_length = cmra_serialized_length(tuple)?;
4783 if locator.cmra_length as u64 != expected_cmra_length {
4784 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4785 }
4786 validate_locator_position(
4787 to_usize(locator_offset, "CriticalRecoveryLocator")?,
4788 locator,
4789 )?;
4790 let recovered = recover_cmra_read_at(
4791 reader,
4792 locator.cmra_offset,
4793 Some(tuple),
4794 CmraRecoveryMode::KeyHolding,
4795 )?;
4796 if recovered.tuple != tuple {
4797 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4798 }
4799 if expected_cmra_length != recovered.cmra_length {
4800 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4801 }
4802 validate_locator_image_boundary(locator, &recovered.image)?;
4803 validate_cmra_identity_hints(
4804 recovered.header_hints,
4805 Some(CmraIdentityHints::from(locator)),
4806 &recovered.image,
4807 )?;
4808 let cmra_length = recovered.cmra_length;
4809 let authority =
4810 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, false)?;
4811 Ok(TerminalAuthorityCandidate {
4812 authority,
4813 anchor: to_usize(
4814 checked_u64_add(
4815 locator_offset,
4816 CRITICAL_RECOVERY_LOCATOR_LEN as u64,
4817 "locator anchor overflow",
4818 )?,
4819 "locator anchor overflow",
4820 )?,
4821 cmra_offset: locator.cmra_offset,
4822 cmra_length,
4823 })
4824}
4825
4826fn parse_public_locator_cmra_candidate(
4827 bytes: &[u8],
4828 locator_offset: usize,
4829 locator: CriticalRecoveryLocator,
4830 volume_header: &VolumeHeader,
4831 crypto_header: &CryptoHeaderFixed,
4832) -> Result<PublicTerminalCandidate, FormatError> {
4833 let tuple = CmraDecoderTuple::from(locator);
4834 validate_cmra_decoder_tuple(tuple)?;
4835 let expected_cmra_length = cmra_serialized_length(tuple)?;
4836 if locator.cmra_length as u64 != expected_cmra_length {
4837 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4838 }
4839 validate_locator_position(locator_offset, locator)?;
4840 let recovered = recover_cmra(
4841 bytes,
4842 locator.cmra_offset,
4843 Some(tuple),
4844 CmraRecoveryMode::PublicNoKey,
4845 )?;
4846 if recovered.tuple != tuple {
4847 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
4848 }
4849 if expected_cmra_length != recovered.cmra_length {
4850 return Err(FormatError::InvalidArchive("locator CMRA length mismatch"));
4851 }
4852 validate_locator_image_boundary(locator, &recovered.image)?;
4853 validate_cmra_identity_hints(
4854 recovered.header_hints,
4855 Some(CmraIdentityHints::from(locator)),
4856 &recovered.image,
4857 )?;
4858 let terminal = validate_recovered_public_terminal(
4859 recovered.image,
4860 bytes,
4861 volume_header,
4862 crypto_header,
4863 false,
4864 )?;
4865 Ok(PublicTerminalCandidate {
4866 terminal,
4867 anchor: locator_offset
4868 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
4869 .ok_or(FormatError::InvalidArchive("locator anchor overflow"))?,
4870 cmra_offset: locator.cmra_offset,
4871 cmra_length: recovered.cmra_length,
4872 })
4873}
4874
4875fn parse_locatorless_cmra_candidate(
4876 bytes: &[u8],
4877 cmra_offset: usize,
4878 context: KeyHoldingTerminalContext<'_>,
4879) -> Result<TerminalCandidate, FormatError> {
4880 let recovered = recover_cmra(
4881 bytes,
4882 cmra_offset as u64,
4883 None,
4884 CmraRecoveryMode::KeyHolding,
4885 )?;
4886 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4887 return Err(FormatError::InvalidArchive(
4888 "locatorless CMRA boundary mismatch",
4889 ));
4890 }
4891 if recovered
4892 .image
4893 .volume_trailer_offset
4894 .checked_add(VOLUME_TRAILER_LEN as u64)
4895 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4896 != cmra_offset as u64
4897 {
4898 return Err(FormatError::InvalidArchive(
4899 "locatorless trailer boundary mismatch",
4900 ));
4901 }
4902 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4903 let terminal =
4904 validate_recovered_terminal(recovered.image, recovered.tuple, bytes, context, true)?;
4905 Ok(TerminalCandidate {
4906 terminal,
4907 anchor: cmra_offset
4908 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
4909 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4910 locator_sequence: None,
4911 cmra_offset: cmra_offset as u64,
4912 cmra_length: recovered.cmra_length,
4913 })
4914}
4915
4916fn parse_locatorless_cmra_candidate_read_at(
4917 reader: &dyn ArchiveReadAt,
4918 cmra_offset: u64,
4919 context: KeyHoldingTerminalContext<'_>,
4920) -> Result<TerminalCandidate, FormatError> {
4921 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
4922 if recovered.image.body_bytes_before_cmra != cmra_offset {
4923 return Err(FormatError::InvalidArchive(
4924 "locatorless CMRA boundary mismatch",
4925 ));
4926 }
4927 if recovered
4928 .image
4929 .volume_trailer_offset
4930 .checked_add(VOLUME_TRAILER_LEN as u64)
4931 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4932 != cmra_offset
4933 {
4934 return Err(FormatError::InvalidArchive(
4935 "locatorless trailer boundary mismatch",
4936 ));
4937 }
4938 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4939 let terminal = validate_recovered_terminal_read_at(
4940 recovered.image,
4941 recovered.tuple,
4942 reader,
4943 context,
4944 true,
4945 )?;
4946 Ok(TerminalCandidate {
4947 terminal,
4948 anchor: to_usize(
4949 checked_u64_add(cmra_offset, recovered.cmra_length, "CMRA anchor overflow")?,
4950 "CMRA anchor overflow",
4951 )?,
4952 locator_sequence: None,
4953 cmra_offset,
4954 cmra_length: recovered.cmra_length,
4955 })
4956}
4957
4958fn parse_locatorless_cmra_authority_candidate(
4959 bytes: &[u8],
4960 cmra_offset: usize,
4961 master_key: &MasterKey,
4962) -> Result<TerminalAuthorityCandidate, FormatError> {
4963 let recovered = recover_cmra(
4964 bytes,
4965 cmra_offset as u64,
4966 None,
4967 CmraRecoveryMode::KeyHolding,
4968 )?;
4969 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
4970 return Err(FormatError::InvalidArchive(
4971 "locatorless CMRA boundary mismatch",
4972 ));
4973 }
4974 if recovered
4975 .image
4976 .volume_trailer_offset
4977 .checked_add(VOLUME_TRAILER_LEN as u64)
4978 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
4979 != cmra_offset as u64
4980 {
4981 return Err(FormatError::InvalidArchive(
4982 "locatorless trailer boundary mismatch",
4983 ));
4984 }
4985 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
4986 let cmra_length = recovered.cmra_length;
4987 let authority =
4988 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
4989 Ok(TerminalAuthorityCandidate {
4990 authority,
4991 anchor: cmra_offset
4992 .checked_add(to_usize(cmra_length, "CMRA")?)
4993 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
4994 cmra_offset: cmra_offset as u64,
4995 cmra_length,
4996 })
4997}
4998
4999fn parse_locatorless_cmra_authority_candidate_read_at(
5000 reader: &dyn ArchiveReadAt,
5001 cmra_offset: u64,
5002 master_key: &MasterKey,
5003) -> Result<TerminalAuthorityCandidate, FormatError> {
5004 let recovered = recover_cmra_read_at(reader, cmra_offset, None, CmraRecoveryMode::KeyHolding)?;
5005 if recovered.image.body_bytes_before_cmra != cmra_offset {
5006 return Err(FormatError::InvalidArchive(
5007 "locatorless CMRA boundary mismatch",
5008 ));
5009 }
5010 if recovered
5011 .image
5012 .volume_trailer_offset
5013 .checked_add(VOLUME_TRAILER_LEN as u64)
5014 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
5015 != cmra_offset
5016 {
5017 return Err(FormatError::InvalidArchive(
5018 "locatorless trailer boundary mismatch",
5019 ));
5020 }
5021 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
5022 let cmra_length = recovered.cmra_length;
5023 let authority =
5024 validate_recovered_terminal_authority(recovered.image, recovered.tuple, master_key, true)?;
5025 Ok(TerminalAuthorityCandidate {
5026 authority,
5027 anchor: to_usize(
5028 checked_u64_add(cmra_offset, cmra_length, "CMRA anchor overflow")?,
5029 "CMRA anchor overflow",
5030 )?,
5031 cmra_offset,
5032 cmra_length,
5033 })
5034}
5035
5036fn parse_public_locatorless_cmra_candidate(
5037 bytes: &[u8],
5038 cmra_offset: usize,
5039 volume_header: &VolumeHeader,
5040 crypto_header: &CryptoHeaderFixed,
5041) -> Result<PublicTerminalCandidate, FormatError> {
5042 let recovered = recover_cmra(
5043 bytes,
5044 cmra_offset as u64,
5045 None,
5046 CmraRecoveryMode::PublicNoKey,
5047 )?;
5048 if recovered.image.body_bytes_before_cmra != cmra_offset as u64 {
5049 return Err(FormatError::InvalidArchive(
5050 "locatorless CMRA boundary mismatch",
5051 ));
5052 }
5053 if recovered
5054 .image
5055 .volume_trailer_offset
5056 .checked_add(VOLUME_TRAILER_LEN as u64)
5057 .ok_or(FormatError::InvalidArchive("CMRA boundary overflow"))?
5058 != cmra_offset as u64
5059 {
5060 return Err(FormatError::InvalidArchive(
5061 "locatorless trailer boundary mismatch",
5062 ));
5063 }
5064 validate_cmra_identity_hints(recovered.header_hints, None, &recovered.image)?;
5065 let terminal = validate_recovered_public_terminal(
5066 recovered.image,
5067 bytes,
5068 volume_header,
5069 crypto_header,
5070 true,
5071 )?;
5072 Ok(PublicTerminalCandidate {
5073 terminal,
5074 anchor: cmra_offset
5075 .checked_add(to_usize(recovered.cmra_length, "CMRA")?)
5076 .ok_or(FormatError::InvalidArchive("CMRA anchor overflow"))?,
5077 cmra_offset: cmra_offset as u64,
5078 cmra_length: recovered.cmra_length,
5079 })
5080}
5081
5082fn validate_locator_position(
5083 locator_offset: usize,
5084 locator: CriticalRecoveryLocator,
5085) -> Result<(), FormatError> {
5086 if locator.cmra_offset != locator.body_bytes_before_cmra {
5087 return Err(FormatError::InvalidArchive(
5088 "locator CMRA boundary mismatch",
5089 ));
5090 }
5091 if locator
5092 .volume_trailer_offset
5093 .checked_add(VOLUME_TRAILER_LEN as u64)
5094 .ok_or(FormatError::InvalidArchive("locator trailer overflow"))?
5095 != locator.cmra_offset
5096 {
5097 return Err(FormatError::InvalidArchive(
5098 "locator trailer boundary mismatch",
5099 ));
5100 }
5101 let expected_offset = match locator.locator_sequence {
5102 1 => locator.cmra_offset.checked_add(locator.cmra_length as u64),
5103 0 => locator
5104 .cmra_offset
5105 .checked_add(locator.cmra_length as u64)
5106 .and_then(|value| value.checked_add(CRITICAL_RECOVERY_LOCATOR_LEN as u64)),
5107 _ => None,
5108 }
5109 .ok_or(FormatError::InvalidArchive("locator position overflow"))?;
5110 if expected_offset != locator_offset as u64 {
5111 return Err(FormatError::InvalidArchive(
5112 "locator position does not match sequence",
5113 ));
5114 }
5115 Ok(())
5116}
5117
5118fn validate_locator_image_boundary(
5119 locator: CriticalRecoveryLocator,
5120 image: &CriticalMetadataImage,
5121) -> Result<(), FormatError> {
5122 if locator.volume_trailer_offset != image.volume_trailer_offset
5123 || locator.body_bytes_before_cmra != image.body_bytes_before_cmra
5124 || image
5125 .volume_trailer_offset
5126 .checked_add(VOLUME_TRAILER_LEN as u64)
5127 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5128 != locator.cmra_offset
5129 {
5130 return Err(FormatError::InvalidArchive(
5131 "locator and CMRA image boundaries differ",
5132 ));
5133 }
5134 Ok(())
5135}
5136
5137fn validate_cmra_identity_hints(
5138 header_hints: Option<CmraIdentityHints>,
5139 locator_hints: Option<CmraIdentityHints>,
5140 image: &CriticalMetadataImage,
5141) -> Result<(), FormatError> {
5142 if let (Some(header), Some(locator)) = (header_hints, locator_hints) {
5143 if header != locator {
5144 return Err(FormatError::InvalidArchive(
5145 "CMRA header and locator identity hints differ",
5146 ));
5147 }
5148 }
5149 for hints in [header_hints, locator_hints].into_iter().flatten() {
5150 if hints.archive_uuid != image.archive_uuid
5151 || hints.session_id != image.session_id
5152 || hints.volume_index != image.volume_index
5153 {
5154 return Err(FormatError::InvalidArchive(
5155 "CMRA identity hints do not match recovered image",
5156 ));
5157 }
5158 }
5159 Ok(())
5160}
5161
5162fn recover_cmra(
5163 bytes: &[u8],
5164 cmra_offset: u64,
5165 locator_tuple: Option<CmraDecoderTuple>,
5166 mode: CmraRecoveryMode,
5167) -> Result<RecoveredCmra, FormatError> {
5168 let offset = to_usize(cmra_offset, "CMRA")?;
5169 let header_bytes = slice(
5170 bytes,
5171 offset,
5172 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5173 "CriticalMetadataRecoveryHeader",
5174 )?;
5175 let (tuple, header_hints) = recover_cmra_header_tuple(header_bytes, locator_tuple)?;
5176 validate_cmra_decoder_tuple(tuple)?;
5177 let cmra_length = cmra_serialized_length(tuple)?;
5178 let cmra_len = to_usize(cmra_length, "CMRA")?;
5179 let cmra_bytes = slice(bytes, offset, cmra_len, "CMRA")?;
5180 recover_cmra_from_bytes(cmra_bytes, tuple, header_hints, cmra_length, mode)
5181}
5182
5183fn recover_cmra_read_at(
5184 reader: &dyn ArchiveReadAt,
5185 cmra_offset: u64,
5186 locator_tuple: Option<CmraDecoderTuple>,
5187 mode: CmraRecoveryMode,
5188) -> Result<RecoveredCmra, FormatError> {
5189 let header_bytes = read_at_vec(
5190 reader,
5191 cmra_offset,
5192 CRITICAL_METADATA_RECOVERY_HEADER_LEN,
5193 "CriticalMetadataRecoveryHeader",
5194 )?;
5195 let (tuple, header_hints) = recover_cmra_header_tuple(&header_bytes, locator_tuple)?;
5196 validate_cmra_decoder_tuple(tuple)?;
5197 let cmra_length = cmra_serialized_length(tuple)?;
5198 let cmra_bytes = read_at_vec(reader, cmra_offset, to_usize(cmra_length, "CMRA")?, "CMRA")?;
5199 recover_cmra_from_bytes(&cmra_bytes, tuple, header_hints, cmra_length, mode)
5200}
5201
5202fn recover_cmra_header_tuple(
5203 header_bytes: &[u8],
5204 locator_tuple: Option<CmraDecoderTuple>,
5205) -> Result<(CmraDecoderTuple, Option<CmraIdentityHints>), FormatError> {
5206 let parsed_header = CriticalMetadataRecoveryHeader::parse(header_bytes);
5207 Ok(match (parsed_header, locator_tuple) {
5208 (Ok(header), Some(locator_tuple)) => {
5209 let header_tuple = CmraDecoderTuple::from(header);
5210 if header_tuple != locator_tuple {
5211 return Err(FormatError::InvalidArchive("CMRA decoder tuple mismatch"));
5212 }
5213 (locator_tuple, Some(CmraIdentityHints::from(header)))
5214 }
5215 (Ok(header), None) => (
5216 CmraDecoderTuple::from(header),
5217 Some(CmraIdentityHints::from(header)),
5218 ),
5219 (Err(_), Some(tuple)) => (tuple, None),
5220 (Err(err), _) => return Err(err),
5221 })
5222}
5223
5224fn recover_cmra_from_bytes(
5225 cmra_bytes: &[u8],
5226 tuple: CmraDecoderTuple,
5227 header_hints: Option<CmraIdentityHints>,
5228 cmra_length: u64,
5229 mode: CmraRecoveryMode,
5230) -> Result<RecoveredCmra, FormatError> {
5231 let shard_size = tuple.shard_size as usize;
5232 let mut data_shards = vec![None; tuple.data_shard_count as usize];
5233 let mut parity_shards = vec![None; tuple.parity_shard_count as usize];
5234 let mut cursor = CRITICAL_METADATA_RECOVERY_HEADER_LEN;
5235 for idx in 0..(tuple.data_shard_count as usize + tuple.parity_shard_count as usize) {
5236 let raw = slice(
5237 cmra_bytes,
5238 cursor,
5239 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5240 "CriticalMetadataRecoveryShard",
5241 )?;
5242 let shard = CriticalMetadataRecoveryShard::parse(raw, shard_size).ok();
5243 if let Some(shard) = shard {
5244 validate_cmra_shard(&shard, idx, tuple)?;
5245 if shard.shard_role == 0 {
5246 let data_slot = data_shards
5247 .get_mut(idx)
5248 .ok_or(FormatError::InvalidArchive("CMRA data shard out of range"))?;
5249 *data_slot = Some(shard.payload);
5250 } else {
5251 let parity_idx = idx - tuple.data_shard_count as usize;
5252 let parity_slot =
5253 parity_shards
5254 .get_mut(parity_idx)
5255 .ok_or(FormatError::InvalidArchive(
5256 "CMRA parity shard out of range",
5257 ))?;
5258 *parity_slot = Some(shard.payload);
5259 }
5260 }
5261 cursor = checked_add(
5262 cursor,
5263 CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size,
5264 "CriticalMetadataRecoveryShard",
5265 )?;
5266 }
5267 let repaired = repair_data_gf16(&data_shards, &parity_shards, shard_size)?;
5268 let mut image_bytes = Vec::with_capacity(tuple.image_length as usize);
5269 for shard in repaired {
5270 image_bytes.extend_from_slice(&shard);
5271 }
5272 image_bytes.truncate(tuple.image_length as usize);
5273 if sha256_bytes(&image_bytes) != tuple.image_sha256 {
5274 return Err(FormatError::InvalidArchive("CMRA image SHA-256 mismatch"));
5275 }
5276 let image = CriticalMetadataImage::parse(&image_bytes)?;
5277 validate_critical_metadata_image(&image, mode)?;
5278 Ok(RecoveredCmra {
5279 image,
5280 tuple,
5281 header_hints,
5282 cmra_length,
5283 })
5284}
5285
5286fn validate_cmra_decoder_tuple(tuple: CmraDecoderTuple) -> Result<(), FormatError> {
5287 let shard_size = tuple.shard_size as u64;
5288 if !(512..=4096).contains(&shard_size) || shard_size % 2 != 0 {
5289 return Err(FormatError::InvalidArchive("CMRA shard_size is invalid"));
5290 }
5291 let image_length = tuple.image_length as u64;
5292 let min = critical_image_min();
5293 let cap = critical_image_cap()?;
5294 if image_length < min || image_length > cap {
5295 return Err(FormatError::InvalidArchive(
5296 "CMRA image_length is outside bounds",
5297 ));
5298 }
5299 let expected_data_shards = ceil_div_u64(image_length, shard_size)?;
5300 if expected_data_shards == 0 || expected_data_shards != tuple.data_shard_count as u64 {
5301 return Err(FormatError::InvalidArchive(
5302 "CMRA data_shard_count does not match image length",
5303 ));
5304 }
5305 let max_parity = 2u64.max(ceil_div_u64(
5306 checked_u64_mul(
5307 expected_data_shards,
5308 READER_MAX_CMRA_PARITY_PCT as u64,
5309 "CMRA parity overflow",
5310 )?,
5311 100,
5312 )?);
5313 if tuple.parity_shard_count as u64 > max_parity {
5314 return Err(FormatError::ReaderResourceLimitExceeded {
5315 field: "CMRA parity shard count",
5316 cap: max_parity,
5317 actual: tuple.parity_shard_count as u64,
5318 });
5319 }
5320 let total = expected_data_shards
5321 .checked_add(tuple.parity_shard_count as u64)
5322 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
5323 if total > 65_535 {
5324 return Err(FormatError::FecTooManyShards(total as usize));
5325 }
5326 Ok(())
5327}
5328
5329fn validate_cmra_writer_parity_lower_bound(
5330 tuple: CmraDecoderTuple,
5331 bit_rot_buffer_pct: u8,
5332) -> Result<(), FormatError> {
5333 let min_parity = 2u64.max(ceil_div_u64(
5334 checked_u64_mul(
5335 tuple.data_shard_count as u64,
5336 bit_rot_buffer_pct as u64,
5337 "CMRA parity lower-bound overflow",
5338 )?,
5339 100,
5340 )?);
5341 if (tuple.parity_shard_count as u64) < min_parity {
5342 return Err(FormatError::InvalidArchive(
5343 "CMRA parity shard count is below authenticated bit-rot lower bound",
5344 ));
5345 }
5346 Ok(())
5347}
5348
5349fn validate_cmra_shard(
5350 shard: &CriticalMetadataRecoveryShard,
5351 serialized_idx: usize,
5352 tuple: CmraDecoderTuple,
5353) -> Result<(), FormatError> {
5354 if shard.shard_index as usize != serialized_idx {
5355 return Err(FormatError::InvalidArchive(
5356 "CMRA shards are not in canonical order",
5357 ));
5358 }
5359 let data_count = tuple.data_shard_count as usize;
5360 let shard_size = tuple.shard_size as usize;
5361 if serialized_idx < data_count {
5362 if shard.shard_role != 0 {
5363 return Err(FormatError::InvalidArchive(
5364 "CMRA data shard has wrong role",
5365 ));
5366 }
5367 let expected_len = if serialized_idx + 1 == data_count {
5368 let used = tuple.image_length as usize - serialized_idx * shard_size;
5369 if used == 0 {
5370 shard_size
5371 } else {
5372 used
5373 }
5374 } else {
5375 shard_size
5376 };
5377 if shard.shard_payload_length as usize != expected_len {
5378 return Err(FormatError::InvalidArchive(
5379 "CMRA data shard payload length is non-canonical",
5380 ));
5381 }
5382 if serialized_idx + 1 == data_count
5383 && shard.payload[expected_len..].iter().any(|byte| *byte != 0)
5384 {
5385 return Err(FormatError::InvalidArchive(
5386 "CMRA final data shard padding is non-zero",
5387 ));
5388 }
5389 } else {
5390 if shard.shard_role != 1 {
5391 return Err(FormatError::InvalidArchive(
5392 "CMRA parity shard has wrong role",
5393 ));
5394 }
5395 if shard.shard_payload_length as usize != shard_size {
5396 return Err(FormatError::InvalidArchive(
5397 "CMRA parity shard payload length is non-canonical",
5398 ));
5399 }
5400 }
5401 Ok(())
5402}
5403
5404fn validate_critical_metadata_image(
5405 image: &CriticalMetadataImage,
5406 mode: CmraRecoveryMode,
5407) -> Result<(), FormatError> {
5408 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5409 if image.volume_header_offset != 0
5410 || image.volume_header_length != VOLUME_HEADER_LEN as u32
5411 || image.crypto_header_offset != VOLUME_HEADER_LEN as u64
5412 || image.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5413 || image.volume_trailer_length != VOLUME_TRAILER_LEN as u32
5414 || image.body_bytes_before_cmra
5415 != image
5416 .volume_trailer_offset
5417 .checked_add(VOLUME_TRAILER_LEN as u64)
5418 .ok_or(FormatError::InvalidArchive("CMRA image boundary overflow"))?
5419 {
5420 return Err(FormatError::InvalidArchive(
5421 "CriticalMetadataImage fixed layout is invalid",
5422 ));
5423 }
5424 if root_auth_present {
5425 if image.root_auth_footer_offset == 0
5426 || image.root_auth_footer_length == 0
5427 || image.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5428 {
5429 return Err(FormatError::InvalidArchive(
5430 "CriticalMetadataImage root-auth range is invalid",
5431 ));
5432 }
5433 } else if image.root_auth_footer_offset != 0
5434 || image.root_auth_footer_length != 0
5435 || image.root_auth_footer_sha256 != [0u8; 32]
5436 {
5437 return Err(FormatError::InvalidArchive(
5438 "CriticalMetadataImage root-auth fields must be zero when absent",
5439 ));
5440 }
5441 let block_record_len = image_block_record_len_from_region(image)?;
5442 let block_record_len_u64 = u64::try_from(block_record_len)
5443 .map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))?;
5444 match mode {
5445 CmraRecoveryMode::KeyHolding => {
5446 let expected_len = image.block_count.checked_mul(block_record_len_u64).ok_or(
5447 FormatError::InvalidArchive("BlockRecord region length overflow"),
5448 )?;
5449 if image.block_records_length != expected_len {
5450 return Err(FormatError::InvalidArchive(
5451 "CriticalMetadataImage terminal equations are invalid",
5452 ));
5453 }
5454 }
5455 CmraRecoveryMode::PublicNoKey => {
5456 if image.block_records_length % block_record_len_u64 != 0 {
5457 return Err(FormatError::InvalidArchive(
5458 "CriticalMetadataImage BlockRecord region is not aligned",
5459 ));
5460 }
5461 }
5462 }
5463 if image.block_records_offset
5464 != image
5465 .crypto_header_offset
5466 .checked_add(image.crypto_header_length as u64)
5467 .ok_or(FormatError::InvalidArchive(
5468 "CryptoHeader boundary overflow",
5469 ))?
5470 || image.manifest_footer_offset
5471 != image
5472 .block_records_offset
5473 .checked_add(image.block_records_length)
5474 .ok_or(FormatError::InvalidArchive(
5475 "ManifestFooter boundary overflow",
5476 ))?
5477 {
5478 return Err(FormatError::InvalidArchive(
5479 "CriticalMetadataImage terminal equations are invalid",
5480 ));
5481 }
5482 let manifest_end = image
5483 .manifest_footer_offset
5484 .checked_add(MANIFEST_FOOTER_LEN as u64)
5485 .ok_or(FormatError::InvalidArchive(
5486 "RootAuthFooter boundary overflow",
5487 ))?;
5488 if root_auth_present {
5489 if image.root_auth_footer_offset != manifest_end
5490 || image
5491 .root_auth_footer_offset
5492 .checked_add(image.root_auth_footer_length as u64)
5493 .ok_or(FormatError::InvalidArchive(
5494 "VolumeTrailer boundary overflow",
5495 ))?
5496 != image.volume_trailer_offset
5497 {
5498 return Err(FormatError::InvalidArchive(
5499 "CriticalMetadataImage root-auth terminal equations are invalid",
5500 ));
5501 }
5502 } else if image.volume_trailer_offset != manifest_end {
5503 return Err(FormatError::InvalidArchive(
5504 "CriticalMetadataImage unsigned terminal equations are invalid",
5505 ));
5506 }
5507 let expected_types: &[u16] = if root_auth_present {
5508 &[1, 2, 3, 4, 5]
5509 } else {
5510 &[1, 2, 3, 5]
5511 };
5512 if image.regions.len() != expected_types.len()
5513 || image
5514 .regions
5515 .iter()
5516 .map(|region| region.region_type)
5517 .ne(expected_types.iter().copied())
5518 {
5519 return Err(FormatError::InvalidArchive(
5520 "CriticalMetadataImage regions are not canonical",
5521 ));
5522 }
5523 validate_image_region(
5524 image,
5525 1,
5526 image.volume_header_offset,
5527 image.volume_header_length,
5528 )?;
5529 validate_image_region(
5530 image,
5531 2,
5532 image.crypto_header_offset,
5533 image.crypto_header_length,
5534 )?;
5535 validate_image_region(
5536 image,
5537 3,
5538 image.manifest_footer_offset,
5539 image.manifest_footer_length,
5540 )?;
5541 if root_auth_present {
5542 validate_image_region(
5543 image,
5544 4,
5545 image.root_auth_footer_offset,
5546 image.root_auth_footer_length,
5547 )?;
5548 }
5549 validate_image_region(
5550 image,
5551 5,
5552 image.volume_trailer_offset,
5553 image.volume_trailer_length,
5554 )?;
5555 if sha256_region(image, 1)? != image.volume_header_sha256
5556 || sha256_region(image, 2)? != image.crypto_header_sha256
5557 || sha256_region(image, 3)? != image.manifest_footer_sha256
5558 || (root_auth_present && sha256_region(image, 4)? != image.root_auth_footer_sha256)
5559 || (!root_auth_present && image.root_auth_footer_sha256 != [0u8; 32])
5560 || sha256_region(image, 5)? != image.volume_trailer_sha256
5561 {
5562 return Err(FormatError::InvalidArchive(
5563 "CriticalMetadataImage region digest mismatch",
5564 ));
5565 }
5566 Ok(())
5567}
5568
5569fn image_block_record_len_from_region(image: &CriticalMetadataImage) -> Result<usize, FormatError> {
5570 let crypto_region = image
5571 .region(2)
5572 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5573 let crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5574 crypto.fixed.validate_supported_profile()?;
5575 Ok(crypto.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN)
5576}
5577
5578fn validate_image_region(
5579 image: &CriticalMetadataImage,
5580 region_type: u16,
5581 offset: u64,
5582 length: u32,
5583) -> Result<(), FormatError> {
5584 let region = image
5585 .region(region_type)
5586 .ok_or(FormatError::InvalidArchive(
5587 "missing CriticalMetadataImage region",
5588 ))?;
5589 if region.offset != offset || region.bytes.len() != length as usize {
5590 return Err(FormatError::InvalidArchive(
5591 "CriticalMetadataImage region range mismatch",
5592 ));
5593 }
5594 Ok(())
5595}
5596
5597fn validate_image_identity(
5598 image: &CriticalMetadataImage,
5599 volume_header: &VolumeHeader,
5600 crypto_header: &CryptoHeaderFixed,
5601) -> Result<(), FormatError> {
5602 if image.archive_uuid != volume_header.archive_uuid
5603 || image.session_id != volume_header.session_id
5604 || image.volume_index != volume_header.volume_index
5605 || image.stripe_width != volume_header.stripe_width
5606 || image.stripe_width != crypto_header.stripe_width
5607 {
5608 return Err(FormatError::InvalidArchive(
5609 "CriticalMetadataImage identity does not match selected volume",
5610 ));
5611 }
5612 Ok(())
5613}
5614
5615fn sha256_region(image: &CriticalMetadataImage, region_type: u16) -> Result<[u8; 32], FormatError> {
5616 Ok(sha256_bytes(
5617 &image
5618 .region(region_type)
5619 .ok_or(FormatError::InvalidArchive(
5620 "missing CriticalMetadataImage region",
5621 ))?
5622 .bytes,
5623 ))
5624}
5625
5626fn validate_recovered_terminal_authority(
5627 image: CriticalMetadataImage,
5628 tuple: CmraDecoderTuple,
5629 master_key: &MasterKey,
5630 require_cmra_boundary_magic: bool,
5631) -> Result<RecoveredTerminalAuthority, FormatError> {
5632 let volume_header_region = image
5633 .region(1)
5634 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5635 let volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5636 let crypto_region = image
5637 .region(2)
5638 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5639 let crypto_header_bytes = crypto_region.bytes.clone();
5640 let parsed_crypto = CryptoHeader::parse(&crypto_header_bytes, image.crypto_header_length)?;
5641 let subkeys = subkeys_for_open(
5642 Some(master_key),
5643 parsed_crypto.fixed.aead_algo,
5644 &volume_header.archive_uuid,
5645 &volume_header.session_id,
5646 )?;
5647 verify_integrity_tag(
5648 HmacDomain::CryptoHeader,
5649 parsed_crypto.fixed.aead_algo,
5650 Some(&subkeys.mac_key),
5651 &volume_header.archive_uuid,
5652 &volume_header.session_id,
5653 parsed_crypto.hmac_covered_bytes,
5654 &parsed_crypto.header_hmac,
5655 )?;
5656 parsed_crypto.validate_extension_semantics()?;
5657 validate_seekable_supported_volume(
5658 &volume_header,
5659 &parsed_crypto.fixed,
5660 &parsed_crypto.extensions,
5661 )?;
5662 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
5663 let crypto_header = parsed_crypto.fixed.clone();
5664 if crypto_header.bit_rot_buffer_pct == 0 {
5665 return Err(FormatError::InvalidArchive(
5666 "CMRA startup recovery requires a nonzero bit-rot budget",
5667 ));
5668 }
5669 drop(parsed_crypto);
5670
5671 let terminal = validate_recovered_terminal_inner(
5672 image,
5673 tuple,
5674 require_cmra_boundary_magic,
5675 true,
5676 KeyHoldingTerminalContext {
5677 subkeys: &subkeys,
5678 volume_header: &volume_header,
5679 crypto_header: &crypto_header,
5680 crypto_header_bytes: &crypto_header_bytes,
5681 },
5682 )?;
5683 Ok(RecoveredTerminalAuthority {
5684 terminal,
5685 volume_header,
5686 crypto_header,
5687 crypto_header_bytes,
5688 subkeys,
5689 })
5690}
5691
5692fn validate_recovered_terminal(
5693 image: CriticalMetadataImage,
5694 tuple: CmraDecoderTuple,
5695 bytes: &[u8],
5696 context: KeyHoldingTerminalContext<'_>,
5697 require_cmra_boundary_magic: bool,
5698) -> Result<V41Terminal, FormatError> {
5699 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5700 let cmra_boundary_magic_ok = bytes.get(cmra_offset..cmra_offset + 4) == Some(b"TZCR");
5701 validate_recovered_terminal_inner(
5702 image,
5703 tuple,
5704 require_cmra_boundary_magic,
5705 cmra_boundary_magic_ok,
5706 context,
5707 )
5708}
5709
5710fn validate_recovered_terminal_read_at(
5711 image: CriticalMetadataImage,
5712 tuple: CmraDecoderTuple,
5713 reader: &dyn ArchiveReadAt,
5714 context: KeyHoldingTerminalContext<'_>,
5715 require_cmra_boundary_magic: bool,
5716) -> Result<V41Terminal, FormatError> {
5717 let mut magic = [0u8; 4];
5718 reader.read_exact_at(image.body_bytes_before_cmra, &mut magic)?;
5719 validate_recovered_terminal_inner(
5720 image,
5721 tuple,
5722 require_cmra_boundary_magic,
5723 magic == *b"TZCR",
5724 context,
5725 )
5726}
5727
5728fn validate_recovered_terminal_inner(
5729 image: CriticalMetadataImage,
5730 tuple: CmraDecoderTuple,
5731 require_cmra_boundary_magic: bool,
5732 cmra_boundary_magic_ok: bool,
5733 context: KeyHoldingTerminalContext<'_>,
5734) -> Result<V41Terminal, FormatError> {
5735 let subkeys = context.subkeys;
5736 let volume_header = context.volume_header;
5737 let crypto_header = context.crypto_header;
5738 let volume_header_region = image
5739 .region(1)
5740 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5741 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5742 if &recovered_volume_header != volume_header {
5743 return Err(FormatError::InvalidArchive(
5744 "CMRA VolumeHeader differs from parsed VolumeHeader",
5745 ));
5746 }
5747 validate_image_identity(&image, volume_header, crypto_header)?;
5748 let crypto_region = image
5749 .region(2)
5750 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5751 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5752 if recovered_crypto.fixed != *crypto_header {
5753 return Err(FormatError::InvalidArchive(
5754 "CMRA CryptoHeader differs from parsed CryptoHeader",
5755 ));
5756 }
5757 let recovered_pre_hmac_len = crypto_region
5758 .bytes
5759 .len()
5760 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5761 .ok_or(FormatError::InvalidArchive(
5762 "CMRA CryptoHeader is too short",
5763 ))?;
5764 let parsed_pre_hmac_len = context
5765 .crypto_header_bytes
5766 .len()
5767 .checked_sub(CRYPTO_HEADER_HMAC_LEN)
5768 .ok_or(FormatError::InvalidArchive("CryptoHeader is too short"))?;
5769 if recovered_pre_hmac_len != parsed_pre_hmac_len
5770 || crypto_region.bytes[..recovered_pre_hmac_len]
5771 != context.crypto_header_bytes[..parsed_pre_hmac_len]
5772 {
5773 return Err(FormatError::InvalidArchive(
5774 "CMRA CryptoHeader differs from parsed CryptoHeader",
5775 ));
5776 }
5777 verify_integrity_tag(
5778 HmacDomain::CryptoHeader,
5779 recovered_crypto.fixed.aead_algo,
5780 Some(&subkeys.mac_key),
5781 &volume_header.archive_uuid,
5782 &volume_header.session_id,
5783 recovered_crypto.hmac_covered_bytes,
5784 &recovered_crypto.header_hmac,
5785 )?;
5786 validate_cmra_writer_parity_lower_bound(tuple, recovered_crypto.fixed.bit_rot_buffer_pct)?;
5787 recovered_crypto.validate_extension_semantics()?;
5788
5789 let manifest_region = image
5790 .region(3)
5791 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5792 let manifest_footer = ManifestFooter::parse(&manifest_region.bytes)?;
5793 validate_manifest_footer(
5794 volume_header,
5795 crypto_header,
5796 &manifest_footer,
5797 subkeys,
5798 &manifest_region.bytes,
5799 )?;
5800 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
5801
5802 let root_auth_footer = if image.layout_flags & 0x0000_0001 != 0 {
5803 let root_auth_region = image
5804 .region(4)
5805 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5806 let footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5807 if footer.archive_uuid != volume_header.archive_uuid
5808 || footer.session_id != volume_header.session_id
5809 || footer.footer_length()? != image.root_auth_footer_length
5810 {
5811 return Err(FormatError::InvalidArchive(
5812 "RootAuthFooter identity or length does not match terminal image",
5813 ));
5814 }
5815 Some(footer)
5816 } else {
5817 None
5818 };
5819
5820 let trailer_region = image
5821 .region(5)
5822 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5823 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5824 verify_integrity_tag(
5825 HmacDomain::VolumeTrailer,
5826 crypto_header.aead_algo,
5827 Some(&subkeys.mac_key),
5828 &volume_header.archive_uuid,
5829 &volume_header.session_id,
5830 &trailer_region.bytes[..TRAILER_HMAC_COVERED_LEN],
5831 &trailer.trailer_hmac,
5832 )?;
5833 validate_trailer_identity(volume_header, &trailer)?;
5834 validate_v41_trailer_equations(&image, &trailer)?;
5835
5836 if require_cmra_boundary_magic && !cmra_boundary_magic_ok {
5837 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5838 }
5839
5840 let manifest_footer_bytes = manifest_region.bytes.clone();
5841 let root_auth_footer_bytes = image.region(4).map(|region| region.bytes.clone());
5842 Ok(V41Terminal {
5843 image,
5844 manifest_footer_bytes,
5845 root_auth_footer_bytes,
5846 root_auth_footer,
5847 volume_trailer: trailer,
5848 })
5849}
5850
5851fn validate_recovered_public_terminal(
5852 image: CriticalMetadataImage,
5853 bytes: &[u8],
5854 volume_header: &VolumeHeader,
5855 crypto_header: &CryptoHeaderFixed,
5856 require_cmra_boundary_magic: bool,
5857) -> Result<V41PublicTerminal, FormatError> {
5858 if image.layout_flags & 0x0000_0001 == 0 {
5859 return Err(FormatError::ReaderUnsupported(
5860 "public no-key verification requires root-auth",
5861 ));
5862 }
5863 let volume_header_region = image
5864 .region(1)
5865 .ok_or(FormatError::InvalidArchive("missing VolumeHeader region"))?;
5866 let recovered_volume_header = VolumeHeader::parse(&volume_header_region.bytes)?;
5867 if &recovered_volume_header != volume_header {
5868 return Err(FormatError::InvalidArchive(
5869 "CMRA VolumeHeader differs from parsed VolumeHeader",
5870 ));
5871 }
5872 validate_image_identity(&image, volume_header, crypto_header)?;
5873 let crypto_region = image
5874 .region(2)
5875 .ok_or(FormatError::InvalidArchive("missing CryptoHeader region"))?;
5876 let recovered_crypto = CryptoHeader::parse(&crypto_region.bytes, image.crypto_header_length)?;
5877 if recovered_crypto.fixed != *crypto_header {
5878 return Err(FormatError::InvalidArchive(
5879 "CMRA CryptoHeader differs from parsed CryptoHeader",
5880 ));
5881 }
5882 recovered_crypto.validate_extension_semantics()?;
5883
5884 image
5885 .region(3)
5886 .ok_or(FormatError::InvalidArchive("missing ManifestFooter region"))?;
5887
5888 let root_auth_region = image
5889 .region(4)
5890 .ok_or(FormatError::InvalidArchive("missing RootAuthFooter region"))?;
5891 let root_auth_footer = RootAuthFooterV1::parse(&root_auth_region.bytes)?;
5892 if root_auth_footer.archive_uuid != volume_header.archive_uuid
5893 || root_auth_footer.session_id != volume_header.session_id
5894 || root_auth_footer.footer_length()? != image.root_auth_footer_length
5895 {
5896 return Err(FormatError::InvalidArchive(
5897 "public RootAuthFooter identity or length does not match terminal image",
5898 ));
5899 }
5900
5901 let trailer_region = image
5902 .region(5)
5903 .ok_or(FormatError::InvalidArchive("missing VolumeTrailer region"))?;
5904 let trailer = VolumeTrailer::parse(&trailer_region.bytes)?;
5905 validate_trailer_identity(volume_header, &trailer)?;
5906 validate_v41_public_trailer_profile(&image, &trailer)?;
5907
5908 let cmra_offset = to_usize(image.body_bytes_before_cmra, "CMRA")?;
5909 if require_cmra_boundary_magic && bytes.get(cmra_offset..cmra_offset + 4) != Some(b"TZCR") {
5910 return Err(FormatError::InvalidArchive("CMRA is not at image boundary"));
5911 }
5912
5913 let root_auth_footer_bytes = root_auth_region.bytes.clone();
5914 Ok(V41PublicTerminal {
5915 image,
5916 root_auth_footer_bytes,
5917 root_auth_footer,
5918 })
5919}
5920
5921fn validate_v41_trailer_equations(
5922 image: &CriticalMetadataImage,
5923 trailer: &VolumeTrailer,
5924) -> Result<(), FormatError> {
5925 let root_auth_present = image.layout_flags & 0x0000_0001 != 0;
5926 if trailer.bytes_written != image.volume_trailer_offset
5927 || trailer.manifest_footer_offset != image.manifest_footer_offset
5928 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5929 || trailer.block_count != image.block_count
5930 {
5931 return Err(FormatError::InvalidArchive(
5932 "VolumeTrailer does not match v41 terminal layout",
5933 ));
5934 }
5935 if root_auth_present {
5936 if trailer.root_auth_flags != 0x0000_0001
5937 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5938 || trailer.root_auth_footer_length != image.root_auth_footer_length
5939 || image.root_auth_footer_offset
5940 != image
5941 .manifest_footer_offset
5942 .checked_add(MANIFEST_FOOTER_LEN as u64)
5943 .ok_or(FormatError::InvalidArchive(
5944 "RootAuthFooter trailer boundary overflow",
5945 ))?
5946 || image
5947 .root_auth_footer_offset
5948 .checked_add(image.root_auth_footer_length as u64)
5949 .ok_or(FormatError::InvalidArchive(
5950 "RootAuthFooter trailer boundary overflow",
5951 ))?
5952 != image.volume_trailer_offset
5953 {
5954 return Err(FormatError::InvalidArchive(
5955 "VolumeTrailer root-auth fields do not match v41 terminal layout",
5956 ));
5957 }
5958 } else if trailer.root_auth_footer_offset != 0
5959 || trailer.root_auth_footer_length != 0
5960 || trailer.root_auth_flags != 0
5961 {
5962 return Err(FormatError::InvalidArchive(
5963 "VolumeTrailer root-auth fields must be zero when absent",
5964 ));
5965 }
5966 Ok(())
5967}
5968
5969fn validate_v41_public_trailer_profile(
5970 image: &CriticalMetadataImage,
5971 trailer: &VolumeTrailer,
5972) -> Result<(), FormatError> {
5973 if trailer.bytes_written != image.volume_trailer_offset
5974 || trailer.manifest_footer_offset != image.manifest_footer_offset
5975 || trailer.manifest_footer_length != MANIFEST_FOOTER_LEN as u32
5976 {
5977 return Err(FormatError::InvalidArchive(
5978 "VolumeTrailer does not match v41 public terminal layout",
5979 ));
5980 }
5981 if trailer.root_auth_flags != 0x0000_0001
5982 || trailer.root_auth_footer_offset == 0
5983 || trailer.root_auth_footer_length == 0
5984 || trailer.root_auth_footer_length > READER_MAX_ROOT_AUTH_FOOTER_LEN
5985 || trailer.root_auth_footer_offset != image.root_auth_footer_offset
5986 || trailer.root_auth_footer_length != image.root_auth_footer_length
5987 || image.root_auth_footer_offset
5988 != image
5989 .manifest_footer_offset
5990 .checked_add(MANIFEST_FOOTER_LEN as u64)
5991 .ok_or(FormatError::InvalidArchive(
5992 "RootAuthFooter trailer boundary overflow",
5993 ))?
5994 || image
5995 .root_auth_footer_offset
5996 .checked_add(image.root_auth_footer_length as u64)
5997 .ok_or(FormatError::InvalidArchive(
5998 "RootAuthFooter trailer boundary overflow",
5999 ))?
6000 != image.volume_trailer_offset
6001 {
6002 return Err(FormatError::InvalidArchive(
6003 "VolumeTrailer root-auth fields do not match v41 public terminal layout",
6004 ));
6005 }
6006 Ok(())
6007}
6008
6009fn critical_image_min() -> u64 {
6010 const MIN_CRYPTO_HEADER_LEN: u64 = 116;
6011 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64
6012 + 4 * SERIALIZED_REGION_HEADER_LEN as u64
6013 + VOLUME_HEADER_LEN as u64
6014 + MIN_CRYPTO_HEADER_LEN
6015 + MANIFEST_FOOTER_LEN as u64
6016 + VOLUME_TRAILER_LEN as u64
6017 + IMAGE_CRC_LEN as u64
6018}
6019
6020fn critical_image_cap() -> Result<u64, FormatError> {
6021 [
6022 CRITICAL_METADATA_IMAGE_FIXED_LEN as u64,
6023 5 * SERIALIZED_REGION_HEADER_LEN as u64,
6024 VOLUME_HEADER_LEN as u64,
6025 READER_MAX_CRYPTO_HEADER_LEN as u64,
6026 MANIFEST_FOOTER_LEN as u64,
6027 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
6028 VOLUME_TRAILER_LEN as u64,
6029 IMAGE_CRC_LEN as u64,
6030 ]
6031 .into_iter()
6032 .try_fold(0u64, |total, value| {
6033 total
6034 .checked_add(value)
6035 .ok_or(FormatError::InvalidArchive("critical image cap overflow"))
6036 })
6037}
6038
6039fn cmra_serialized_length(tuple: CmraDecoderTuple) -> Result<u64, FormatError> {
6040 let shard_total = (tuple.data_shard_count as u64)
6041 .checked_add(tuple.parity_shard_count as u64)
6042 .ok_or(FormatError::InvalidArchive("CMRA shard count overflow"))?;
6043 let row_len = (CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN as u64)
6044 .checked_add(tuple.shard_size as u64)
6045 .ok_or(FormatError::InvalidArchive("CMRA row length overflow"))?;
6046 checked_u64_mul(shard_total, row_len, "CMRA length overflow")?
6047 .checked_add(CRITICAL_METADATA_RECOVERY_HEADER_LEN as u64)
6048 .ok_or(FormatError::InvalidArchive("CMRA length overflow"))
6049}
6050
6051fn cmra_worst_case_cap() -> Result<u64, FormatError> {
6052 let cap = critical_image_cap()?;
6053 let mut worst = 0u64;
6054 let mut shard_size = 512u64;
6055 while shard_size <= 4096 {
6056 let data = ceil_div_u64(cap, shard_size)?;
6057 let parity = 2u64.max(ceil_div_u64(
6058 checked_u64_mul(data, READER_MAX_CMRA_PARITY_PCT as u64, "CMRA cap overflow")?,
6059 100,
6060 )?);
6061 let tuple = CmraDecoderTuple {
6062 shard_size: shard_size as u32,
6063 data_shard_count: u16::try_from(data)
6064 .map_err(|_| FormatError::InvalidArchive("CMRA cap data shard overflow"))?,
6065 parity_shard_count: u16::try_from(parity)
6066 .map_err(|_| FormatError::InvalidArchive("CMRA cap parity shard overflow"))?,
6067 image_length: u32::try_from(cap)
6068 .map_err(|_| FormatError::InvalidArchive("CMRA cap image overflow"))?,
6069 image_sha256: [0u8; 32],
6070 };
6071 worst = worst.max(cmra_serialized_length(tuple)?);
6072 shard_size += 2;
6073 }
6074 Ok(worst)
6075}
6076
6077pub(crate) fn v41_terminal_tail_cap() -> Result<usize, FormatError> {
6078 let total = [
6079 MANIFEST_FOOTER_LEN as u64,
6080 READER_MAX_ROOT_AUTH_FOOTER_LEN as u64,
6081 VOLUME_TRAILER_LEN as u64,
6082 cmra_worst_case_cap()?,
6083 LOCATOR_PAIR_LEN as u64,
6084 ]
6085 .into_iter()
6086 .try_fold(0u64, |sum, value| {
6087 sum.checked_add(value)
6088 .ok_or(FormatError::InvalidArchive("terminal tail cap overflow"))
6089 })?;
6090 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("terminal tail cap overflow"))
6091}
6092
6093fn max_critical_recovery_scan(options: ReaderOptions) -> Result<usize, FormatError> {
6094 let worst = cmra_worst_case_cap()?;
6095 let total = options
6096 .max_trailing_garbage_scan
6097 .try_into()
6098 .map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
6099 .and_then(|scan: u64| {
6100 scan.checked_add(worst)
6101 .and_then(|value| value.checked_add(LOCATOR_PAIR_LEN as u64))
6102 .ok_or(FormatError::InvalidArchive("scan cap overflow"))
6103 })?;
6104 usize::try_from(total).map_err(|_| FormatError::InvalidArchive("scan cap overflow"))
6105}
6106
6107fn validate_bootstrap_single_volume_input(
6108 volume_header: &VolumeHeader,
6109 crypto_header: &CryptoHeaderFixed,
6110) -> Result<(), FormatError> {
6111 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
6112 return Err(FormatError::ReaderUnsupported(
6113 "bootstrap sidecar reader supports only single-volume archive input",
6114 ));
6115 }
6116 if crypto_header.stripe_width != volume_header.stripe_width {
6117 return Err(FormatError::InvalidArchive(
6118 "VolumeHeader and CryptoHeader stripe_width differ",
6119 ));
6120 }
6121 Ok(())
6122}
6123
6124#[derive(Debug)]
6125struct ParsedBootstrapSidecar {
6126 manifest_footer: Option<ManifestFooter>,
6127 index_root_records_section: Option<(u64, u64)>,
6128 dictionary_records_section: Option<(u64, u64)>,
6129}
6130
6131pub(crate) struct NonSeekableBootstrapMaterial {
6132 pub(crate) manifest_footer: ManifestFooter,
6133 pub(crate) payload_dictionary: Option<Vec<u8>>,
6134}
6135
6136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6137enum BootstrapSidecarUse {
6138 SeekableAssist,
6139 NonSeekableRandomAccess,
6140}
6141
6142impl ParsedBootstrapSidecar {
6143 fn require_sections_for(
6144 &self,
6145 sidecar_use: BootstrapSidecarUse,
6146 crypto_header: &CryptoHeaderFixed,
6147 ) -> Result<(), FormatError> {
6148 if sidecar_use == BootstrapSidecarUse::NonSeekableRandomAccess {
6149 if self.manifest_footer.is_none() || self.index_root_records_section.is_none() {
6150 return Err(FormatError::ReaderUnsupported(
6151 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6152 ));
6153 }
6154 if crypto_header.has_dictionary != 0 && self.dictionary_records_section.is_none() {
6155 return Err(FormatError::ReaderUnsupported(
6156 "dictionary bootstrap required",
6157 ));
6158 }
6159 }
6160 Ok(())
6161 }
6162}
6163
6164pub(crate) fn parse_non_seekable_bootstrap_material(
6165 bootstrap_sidecar: &[u8],
6166 volume_header: &VolumeHeader,
6167 crypto_header: &CryptoHeaderFixed,
6168 subkeys: &Subkeys,
6169) -> Result<NonSeekableBootstrapMaterial, FormatError> {
6170 validate_bootstrap_single_volume_input(volume_header, crypto_header)?;
6171 let sidecar =
6172 parse_bootstrap_sidecar(bootstrap_sidecar, volume_header, crypto_header, subkeys)?;
6173 sidecar.require_sections_for(BootstrapSidecarUse::NonSeekableRandomAccess, crypto_header)?;
6174 let manifest_footer = sidecar
6175 .manifest_footer
6176 .clone()
6177 .ok_or(FormatError::ReaderUnsupported(
6178 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6179 ))?;
6180
6181 let mut blocks = BTreeMap::new();
6182 let (offset, length) =
6183 sidecar
6184 .index_root_records_section
6185 .ok_or(FormatError::ReaderUnsupported(
6186 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections",
6187 ))?;
6188 let index_root_records = parse_sidecar_block_records(
6189 bootstrap_sidecar,
6190 crypto_header.block_size as usize,
6191 SidecarBlockRecordsSection {
6192 offset,
6193 length,
6194 extent: index_root_extent_from_manifest(&manifest_footer),
6195 data_kind: BlockKind::IndexRootData,
6196 parity_kind: BlockKind::IndexRootParity,
6197 structure: "IndexRoot",
6198 },
6199 )?;
6200 insert_sidecar_records(&mut blocks, index_root_records)?;
6201
6202 let limits = metadata_limits(crypto_header);
6203 let index_root_plaintext = load_metadata_object_from_parts(
6204 &blocks,
6205 ObjectLoadContext::index_root(
6206 volume_header,
6207 crypto_header,
6208 subkeys,
6209 index_root_extent_from_manifest(&manifest_footer),
6210 ),
6211 manifest_footer.index_root_decompressed_size,
6212 )?;
6213 let index_root = IndexRoot::parse(
6214 &index_root_plaintext,
6215 crypto_header.has_dictionary != 0,
6216 limits,
6217 )?;
6218
6219 if crypto_header.has_dictionary != 0 {
6220 let (offset, length) =
6221 sidecar
6222 .dictionary_records_section
6223 .ok_or(FormatError::ReaderUnsupported(
6224 "dictionary bootstrap required",
6225 ))?;
6226 let dictionary_records = parse_sidecar_block_records(
6227 bootstrap_sidecar,
6228 crypto_header.block_size as usize,
6229 SidecarBlockRecordsSection {
6230 offset,
6231 length,
6232 extent: dictionary_extent_from_index_root(&index_root)?,
6233 data_kind: BlockKind::DictionaryData,
6234 parity_kind: BlockKind::DictionaryParity,
6235 structure: "dictionary",
6236 },
6237 )?;
6238 insert_sidecar_records(&mut blocks, dictionary_records)?;
6239 }
6240 let payload_dictionary =
6241 load_archive_dictionary(&blocks, subkeys, volume_header, crypto_header, &index_root)?;
6242
6243 Ok(NonSeekableBootstrapMaterial {
6244 manifest_footer,
6245 payload_dictionary,
6246 })
6247}
6248
6249fn parse_bootstrap_sidecar(
6250 bytes: &[u8],
6251 volume_header: &VolumeHeader,
6252 crypto_header: &CryptoHeaderFixed,
6253 subkeys: &Subkeys,
6254) -> Result<ParsedBootstrapSidecar, FormatError> {
6255 let header_bytes = slice(
6256 bytes,
6257 0,
6258 BOOTSTRAP_SIDECAR_HEADER_LEN,
6259 "BootstrapSidecarHeader",
6260 )?;
6261 let header = BootstrapSidecarHeader::parse(header_bytes)?;
6262 if header.archive_uuid != volume_header.archive_uuid
6263 || header.session_id != volume_header.session_id
6264 {
6265 return Err(FormatError::InvalidArchive(
6266 "bootstrap sidecar identity does not match VolumeHeader",
6267 ));
6268 }
6269 verify_integrity_tag(
6270 HmacDomain::BootstrapSidecar,
6271 crypto_header.aead_algo,
6272 Some(&subkeys.mac_key),
6273 &volume_header.archive_uuid,
6274 &volume_header.session_id,
6275 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
6276 &header.sidecar_hmac,
6277 )?;
6278 header.validate_packed_layout(bytes.len() as u64)?;
6279 validate_sidecar_size_cap(&header, crypto_header, bytes.len() as u64)?;
6280
6281 if header.has_dictionary_records() && crypto_header.has_dictionary == 0 {
6282 return Err(FormatError::InvalidArchive(
6283 "bootstrap sidecar has dictionary records while has_dictionary is false",
6284 ));
6285 }
6286
6287 let manifest_footer = if header.has_manifest_footer() {
6288 let manifest_offset = to_usize(header.manifest_footer_offset, "BootstrapSidecarHeader")?;
6289 let manifest_bytes = slice(
6290 bytes,
6291 manifest_offset,
6292 MANIFEST_FOOTER_LEN,
6293 "ManifestFooter",
6294 )?;
6295 let manifest_footer = ManifestFooter::parse(manifest_bytes)?;
6296 validate_sidecar_manifest_footer(
6297 volume_header,
6298 crypto_header,
6299 &manifest_footer,
6300 subkeys,
6301 manifest_bytes,
6302 )?;
6303 manifest_footer.validate_index_root_extent(crypto_header.block_size)?;
6304 Some(manifest_footer)
6305 } else {
6306 None
6307 };
6308
6309 Ok(ParsedBootstrapSidecar {
6310 manifest_footer,
6311 index_root_records_section: header.has_index_root_records().then_some((
6312 header.index_root_records_offset,
6313 header.index_root_records_length,
6314 )),
6315 dictionary_records_section: header.has_dictionary_records().then_some((
6316 header.dictionary_records_offset,
6317 header.dictionary_records_length,
6318 )),
6319 })
6320}
6321
6322fn index_root_extent_from_manifest(manifest_footer: &ManifestFooter) -> ObjectExtent {
6323 ObjectExtent {
6324 first_block_index: manifest_footer.index_root_first_block,
6325 data_block_count: manifest_footer.index_root_data_block_count,
6326 parity_block_count: manifest_footer.index_root_parity_block_count,
6327 encrypted_size: manifest_footer.index_root_encrypted_size,
6328 }
6329}
6330
6331fn insert_sidecar_records(
6332 blocks: &mut BTreeMap<u64, BlockRecord>,
6333 records: Vec<BlockRecord>,
6334) -> Result<(), FormatError> {
6335 for record in records {
6336 if let Some(existing) = blocks.insert(record.block_index, record.clone()) {
6337 if existing != record {
6338 return Err(FormatError::InvalidArchive(
6339 "bootstrap sidecar conflicts with volume BlockRecord",
6340 ));
6341 }
6342 }
6343 }
6344 Ok(())
6345}
6346
6347fn validate_sidecar_manifest_footer(
6348 volume_header: &VolumeHeader,
6349 crypto_header: &CryptoHeaderFixed,
6350 footer: &ManifestFooter,
6351 subkeys: &Subkeys,
6352 raw: &[u8],
6353) -> Result<(), FormatError> {
6354 if footer.archive_uuid != volume_header.archive_uuid
6355 || footer.session_id != volume_header.session_id
6356 {
6357 return Err(FormatError::InvalidArchive(
6358 "sidecar ManifestFooter identity does not match VolumeHeader",
6359 ));
6360 }
6361 if footer.volume_index != 0 {
6362 return Err(FormatError::InvalidArchive(
6363 "sidecar ManifestFooter volume_index must be zero",
6364 ));
6365 }
6366 if footer.total_volumes != crypto_header.stripe_width {
6367 return Err(FormatError::InvalidArchive(
6368 "sidecar ManifestFooter total_volumes does not match stripe_width",
6369 ));
6370 }
6371 if footer.is_authoritative != 1 {
6372 return Err(FormatError::InvalidArchive(
6373 "sidecar ManifestFooter is not authoritative",
6374 ));
6375 }
6376 verify_integrity_tag(
6377 HmacDomain::ManifestFooter,
6378 crypto_header.aead_algo,
6379 Some(&subkeys.mac_key),
6380 &volume_header.archive_uuid,
6381 &volume_header.session_id,
6382 &raw[..MANIFEST_HMAC_COVERED_LEN],
6383 &footer.manifest_hmac,
6384 )
6385}
6386
6387fn validate_sidecar_size_cap(
6388 header: &BootstrapSidecarHeader,
6389 crypto_header: &CryptoHeaderFixed,
6390 file_size: u64,
6391) -> Result<(), FormatError> {
6392 let record_len = checked_u64_add(
6393 crypto_header.block_size as u64,
6394 BLOCK_RECORD_FRAMING_LEN as u64,
6395 "bootstrap sidecar cap overflow",
6396 )?;
6397 let max_index_records = crypto_header.index_root_fec_data_shards as u64
6398 + crypto_header.index_root_fec_parity_shards as u64;
6399 let max_record_section_bytes = checked_u64_mul(
6400 max_index_records,
6401 record_len,
6402 "bootstrap sidecar cap overflow",
6403 )?;
6404 if header.index_root_records_length % record_len != 0 {
6405 return Err(FormatError::InvalidArchive(
6406 "bootstrap sidecar IndexRoot records length is not aligned",
6407 ));
6408 }
6409 if header.index_root_records_length / record_len > max_index_records {
6410 return Err(FormatError::InvalidArchive(
6411 "bootstrap sidecar IndexRoot records exceed resource cap",
6412 ));
6413 }
6414 if header.dictionary_records_length % record_len != 0 {
6415 return Err(FormatError::InvalidArchive(
6416 "bootstrap sidecar dictionary records length is not aligned",
6417 ));
6418 }
6419 if header.dictionary_records_length / record_len > max_index_records {
6420 return Err(FormatError::InvalidArchive(
6421 "bootstrap sidecar dictionary records exceed resource cap",
6422 ));
6423 }
6424
6425 let mut cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64;
6426 if header.has_manifest_footer() {
6427 cap = cap
6428 .checked_add(MANIFEST_FOOTER_LEN as u64)
6429 .ok_or(FormatError::InvalidArchive(
6430 "bootstrap sidecar cap overflow",
6431 ))?;
6432 }
6433 if header.has_index_root_records() {
6434 cap = checked_u64_add(
6435 cap,
6436 max_record_section_bytes,
6437 "bootstrap sidecar cap overflow",
6438 )?;
6439 }
6440 if header.has_dictionary_records() {
6441 cap = checked_u64_add(
6442 cap,
6443 max_record_section_bytes,
6444 "bootstrap sidecar cap overflow",
6445 )?;
6446 }
6447 if file_size > cap {
6448 return Err(FormatError::InvalidArchive(
6449 "bootstrap sidecar exceeds resource cap",
6450 ));
6451 }
6452 Ok(())
6453}
6454
6455#[derive(Debug, Clone, Copy)]
6456struct SidecarBlockRecordsSection {
6457 offset: u64,
6458 length: u64,
6459 extent: ObjectExtent,
6460 data_kind: BlockKind,
6461 parity_kind: BlockKind,
6462 structure: &'static str,
6463}
6464
6465fn parse_sidecar_block_records(
6466 sidecar_bytes: &[u8],
6467 block_size: usize,
6468 section: SidecarBlockRecordsSection,
6469) -> Result<Vec<BlockRecord>, FormatError> {
6470 let record_len = block_size
6471 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6472 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6473 if section.length % record_len as u64 != 0 {
6474 return Err(FormatError::InvalidArchive(
6475 "sidecar BlockRecord section is not aligned",
6476 ));
6477 }
6478 let expected_count =
6479 section.extent.data_block_count as usize + section.extent.parity_block_count as usize;
6480 let actual_count = usize::try_from(section.length / record_len as u64)
6481 .map_err(|_| FormatError::InvalidArchive("sidecar BlockRecord count overflow"))?;
6482 if actual_count != expected_count {
6483 return Err(FormatError::InvalidArchive(
6484 "sidecar BlockRecord section does not match declared extent",
6485 ));
6486 }
6487 let start = to_usize(section.offset, "BootstrapSidecarHeader")?;
6488 let raw = slice(
6489 sidecar_bytes,
6490 start,
6491 to_usize(section.length, "BootstrapSidecarHeader")?,
6492 "BootstrapSidecarHeader",
6493 )?;
6494 let mut records = Vec::with_capacity(expected_count);
6495
6496 for idx in 0..expected_count {
6497 let record = BlockRecord::parse(
6498 slice(raw, idx * record_len, record_len, "BlockRecord")?,
6499 block_size,
6500 )?;
6501 let expected_block_index = checked_u64_add(
6502 section.extent.first_block_index,
6503 idx as u64,
6504 section.structure,
6505 )?;
6506 if record.block_index != expected_block_index {
6507 return Err(FormatError::InvalidArchive(
6508 "sidecar BlockRecord section has missing or duplicate blocks",
6509 ));
6510 }
6511 let expected_kind = if idx < section.extent.data_block_count as usize {
6512 section.data_kind
6513 } else {
6514 section.parity_kind
6515 };
6516 if record.kind != expected_kind {
6517 return Err(FormatError::InvalidArchive(
6518 "sidecar BlockRecord section has wrong kind",
6519 ));
6520 }
6521 let should_be_last = idx + 1 == section.extent.data_block_count as usize;
6522 if idx < section.extent.data_block_count as usize && record.is_last_data() != should_be_last
6523 {
6524 return Err(FormatError::InvalidArchive(
6525 "sidecar BlockRecord section has wrong last-data flag",
6526 ));
6527 }
6528 records.push(record);
6529 }
6530
6531 Ok(records)
6532}
6533
6534fn validate_trailer_identity(
6535 volume_header: &VolumeHeader,
6536 trailer: &VolumeTrailer,
6537) -> Result<(), FormatError> {
6538 if trailer.archive_uuid != volume_header.archive_uuid
6539 || trailer.session_id != volume_header.session_id
6540 || trailer.volume_index != volume_header.volume_index
6541 {
6542 return Err(FormatError::InvalidArchive(
6543 "VolumeTrailer identity does not match VolumeHeader",
6544 ));
6545 }
6546 Ok(())
6547}
6548
6549fn validate_manifest_footer(
6550 volume_header: &VolumeHeader,
6551 crypto_header: &CryptoHeaderFixed,
6552 footer: &ManifestFooter,
6553 subkeys: &Subkeys,
6554 raw: &[u8],
6555) -> Result<(), FormatError> {
6556 if footer.archive_uuid != volume_header.archive_uuid
6557 || footer.session_id != volume_header.session_id
6558 || footer.volume_index != volume_header.volume_index
6559 {
6560 return Err(FormatError::InvalidArchive(
6561 "ManifestFooter identity does not match VolumeHeader",
6562 ));
6563 }
6564 if footer.total_volumes != volume_header.stripe_width {
6565 return Err(FormatError::InvalidArchive(
6566 "ManifestFooter total_volumes does not match stripe_width",
6567 ));
6568 }
6569 if footer.is_authoritative != 1 {
6570 return Err(FormatError::InvalidArchive(
6571 "ManifestFooter is not authoritative",
6572 ));
6573 }
6574 verify_integrity_tag(
6575 HmacDomain::ManifestFooter,
6576 crypto_header.aead_algo,
6577 Some(&subkeys.mac_key),
6578 &volume_header.archive_uuid,
6579 &volume_header.session_id,
6580 &raw[..MANIFEST_HMAC_COVERED_LEN],
6581 &footer.manifest_hmac,
6582 )
6583}
6584
6585#[derive(Debug)]
6586struct ParsedBlockRegion {
6587 blocks: BTreeMap<u64, BlockRecord>,
6588 erased_block_indices: BTreeSet<u64>,
6589}
6590
6591fn parse_block_region(
6592 bytes: &[u8],
6593 start: usize,
6594 end: usize,
6595 block_size: usize,
6596 volume_header: &VolumeHeader,
6597 trailer: &VolumeTrailer,
6598) -> Result<ParsedBlockRegion, FormatError> {
6599 if end < start {
6600 return Err(FormatError::InvalidArchive(
6601 "ManifestFooter starts before BlockRecord region",
6602 ));
6603 }
6604 let record_len = block_size
6605 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6606 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6607 let region_len = end - start;
6608 if region_len % record_len != 0 {
6609 return Err(FormatError::InvalidArchive(
6610 "BlockRecord region length is not aligned",
6611 ));
6612 }
6613 let observed_count = region_len / record_len;
6614 if observed_count as u64 != trailer.block_count {
6615 return Err(FormatError::InvalidArchive(
6616 "VolumeTrailer block_count does not match BlockRecord region",
6617 ));
6618 }
6619
6620 let mut blocks = BTreeMap::new();
6621 let mut erased_block_indices = BTreeSet::new();
6622 for idx in 0..observed_count {
6623 let offset = start + idx * record_len;
6624 let expected_block_index = checked_u64_add(
6625 volume_header.volume_index as u64,
6626 checked_u64_mul(
6627 idx as u64,
6628 volume_header.stripe_width as u64,
6629 "BlockRecord index overflow",
6630 )?,
6631 "BlockRecord index overflow",
6632 )?;
6633 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6634 match BlockRecord::parse(raw, block_size) {
6635 Ok(record) => {
6636 if record.block_index != expected_block_index {
6637 return Err(FormatError::InvalidArchive(
6638 "BlockRecord index does not match volume position",
6639 ));
6640 }
6641 if blocks.insert(record.block_index, record).is_some() {
6642 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6643 }
6644 }
6645 Err(err) if block_record_error_is_recoverable_erasure(&err) => {
6646 if !erased_block_indices.insert(expected_block_index) {
6647 return Err(FormatError::InvalidArchive(
6648 "duplicate erased BlockRecord index",
6649 ));
6650 }
6651 }
6652 Err(err) => return Err(err),
6653 }
6654 }
6655
6656 Ok(ParsedBlockRegion {
6657 blocks,
6658 erased_block_indices,
6659 })
6660}
6661
6662fn validate_seekable_block_region_layout(
6663 start: u64,
6664 end: u64,
6665 block_size: usize,
6666 trailer: &VolumeTrailer,
6667) -> Result<(), FormatError> {
6668 if end < start {
6669 return Err(FormatError::InvalidArchive(
6670 "ManifestFooter starts before BlockRecord region",
6671 ));
6672 }
6673 let record_len = block_record_len(block_size)?;
6674 let region_len = end - start;
6675 if region_len % record_len != 0 {
6676 return Err(FormatError::InvalidArchive(
6677 "BlockRecord region length is not aligned",
6678 ));
6679 }
6680 let observed_count = region_len / record_len;
6681 if observed_count != trailer.block_count {
6682 return Err(FormatError::InvalidArchive(
6683 "VolumeTrailer block_count does not match BlockRecord region",
6684 ));
6685 }
6686 Ok(())
6687}
6688
6689fn parse_public_block_observation(
6690 bytes: &[u8],
6691 start: usize,
6692 image: &CriticalMetadataImage,
6693 block_size: usize,
6694 volume_header: &VolumeHeader,
6695) -> Result<BTreeMap<u64, BlockRecord>, FormatError> {
6696 let image_start = to_usize(image.block_records_offset, "BlockRecord")?;
6697 if start != image_start {
6698 return Err(FormatError::InvalidArchive(
6699 "public BlockRecord observation start mismatch",
6700 ));
6701 }
6702 let scan_limit_u64 = image
6703 .block_records_offset
6704 .checked_add(image.block_records_length)
6705 .ok_or(FormatError::InvalidArchive(
6706 "public BlockRecord observation limit overflow",
6707 ))?;
6708 if scan_limit_u64 != image.manifest_footer_offset {
6709 return Err(FormatError::InvalidArchive(
6710 "public BlockRecord observation limit mismatch",
6711 ));
6712 }
6713 let scan_limit = to_usize(scan_limit_u64, "BlockRecord")?;
6714 if scan_limit < start {
6715 return Err(FormatError::InvalidArchive(
6716 "public BlockRecord observation limit before start",
6717 ));
6718 }
6719 let record_len = block_size
6720 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6721 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6722 let region_len = scan_limit - start;
6723 if region_len % record_len != 0 {
6724 return Err(FormatError::InvalidArchive(
6725 "public BlockRecord observation window is not aligned",
6726 ));
6727 }
6728
6729 let mut blocks = BTreeMap::new();
6730 let mut offset = start;
6731 let mut observed_slot = 0u64;
6732 while offset < scan_limit {
6733 let magic_end = checked_add(offset, 4, "BlockRecord")?;
6734 if magic_end > scan_limit || bytes.get(offset..magic_end) != Some(b"TZBK") {
6735 break;
6736 }
6737 let record_end = checked_add(offset, record_len, "BlockRecord")?;
6738 if record_end > scan_limit {
6739 return Err(FormatError::InvalidArchive(
6740 "public BlockRecord observation slot is incomplete",
6741 ));
6742 }
6743 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6744 let record = BlockRecord::parse(raw, block_size)?;
6745 let expected_block_index = checked_u64_add(
6746 volume_header.volume_index as u64,
6747 checked_u64_mul(
6748 observed_slot,
6749 volume_header.stripe_width as u64,
6750 "BlockRecord index overflow",
6751 )?,
6752 "BlockRecord index overflow",
6753 )?;
6754 if record.block_index != expected_block_index {
6755 return Err(FormatError::InvalidArchive(
6756 "public BlockRecord index does not match volume position",
6757 ));
6758 }
6759 if blocks.insert(record.block_index, record).is_some() {
6760 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6761 }
6762 offset = record_end;
6763 observed_slot = observed_slot
6764 .checked_add(1)
6765 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6766 }
6767
6768 let mut scan = if offset < scan_limit {
6769 checked_add(offset, record_len, "BlockRecord")?
6770 } else {
6771 scan_limit
6772 };
6773 while scan < scan_limit {
6774 let magic_end = checked_add(scan, 4, "BlockRecord")?;
6775 let record_end = checked_add(scan, record_len, "BlockRecord")?;
6776 if record_end <= scan_limit && bytes.get(scan..magic_end) == Some(b"TZBK") {
6777 let raw = slice(bytes, scan, record_len, "BlockRecord")?;
6778 if BlockRecord::parse(raw, block_size).is_ok() {
6779 return Err(FormatError::InvalidArchive(
6780 "public observation has ambiguous extra BlockRecord",
6781 ));
6782 }
6783 }
6784 scan = record_end;
6785 }
6786
6787 Ok(blocks)
6788}
6789
6790pub(crate) fn block_record_error_is_recoverable_erasure(error: &FormatError) -> bool {
6791 matches!(
6792 error,
6793 FormatError::BadCrc {
6794 structure: "BlockRecord",
6795 } | FormatError::BadMagic {
6796 structure: "BlockRecord",
6797 } | FormatError::NonZeroReserved {
6798 structure: "BlockRecord",
6799 }
6800 )
6801}
6802
6803fn block_record_len(block_size: usize) -> Result<u64, FormatError> {
6804 let len = block_size
6805 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6806 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6807 u64::try_from(len).map_err(|_| FormatError::InvalidArchive("BlockRecord length overflow"))
6808}
6809
6810fn checked_u64_mul(lhs: u64, rhs: u64, reason: &'static str) -> Result<u64, FormatError> {
6811 lhs.checked_mul(rhs)
6812 .ok_or(FormatError::InvalidArchive(reason))
6813}
6814
6815fn parse_stream_block_prefix(
6816 bytes: &[u8],
6817 start: usize,
6818 block_size: usize,
6819 volume_header: &VolumeHeader,
6820) -> Result<(BTreeMap<u64, BlockRecord>, usize, u64), FormatError> {
6821 let record_len = block_size
6822 .checked_add(BLOCK_RECORD_FRAMING_LEN)
6823 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
6824 let mut blocks = BTreeMap::new();
6825 let mut offset = start;
6826 let mut observed_block_count = 0u64;
6827
6828 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
6829 let expected_block_index =
6830 expected_stream_block_index(volume_header, observed_block_count)?;
6831 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6832 match BlockRecord::parse(raw, block_size) {
6833 Ok(record) => {
6834 if record.block_index != expected_block_index {
6835 return Err(FormatError::InvalidArchive(
6836 "BlockRecord index does not match stream position",
6837 ));
6838 }
6839 if blocks.insert(record.block_index, record).is_some() {
6840 return Err(FormatError::InvalidArchive("duplicate BlockRecord index"));
6841 }
6842 }
6843 Err(err) if block_record_error_is_recoverable_erasure(&err) => {}
6844 Err(err) => return Err(err),
6845 }
6846 offset = checked_add(offset, record_len, "BlockRecord")?;
6847 observed_block_count = observed_block_count
6848 .checked_add(1)
6849 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
6850 }
6851
6852 Ok((blocks, offset, observed_block_count))
6853}
6854
6855pub(crate) fn expected_stream_block_index(
6856 volume_header: &VolumeHeader,
6857 observed_block_count: u64,
6858) -> Result<u64, FormatError> {
6859 checked_u64_add(
6860 volume_header.volume_index as u64,
6861 checked_u64_mul(
6862 observed_block_count,
6863 volume_header.stripe_width as u64,
6864 "BlockRecord index overflow",
6865 )?,
6866 "BlockRecord index overflow",
6867 )
6868}
6869
6870fn parse_sequential_block_or_erasure(
6871 bytes: &[u8],
6872 offset: usize,
6873 record_len: usize,
6874 block_size: usize,
6875 volume_header: &VolumeHeader,
6876 observed_block_count: u64,
6877) -> Result<Option<BlockRecord>, FormatError> {
6878 let expected_block_index = expected_stream_block_index(volume_header, observed_block_count)?;
6879 let raw = slice(bytes, offset, record_len, "BlockRecord")?;
6880 match BlockRecord::parse(raw, block_size) {
6881 Ok(record) => {
6882 if record.block_index != expected_block_index {
6883 return Err(FormatError::InvalidArchive(
6884 "BlockRecord index does not match stream position",
6885 ));
6886 }
6887 Ok(Some(record))
6888 }
6889 Err(err) if block_record_error_is_recoverable_erasure(&err) => Ok(None),
6890 Err(err) => Err(err),
6891 }
6892}
6893
6894fn parse_terminal_material(
6895 bytes: &[u8],
6896 manifest_offset: usize,
6897 observed_block_count: u64,
6898 context: KeyHoldingTerminalContext<'_>,
6899 options: ReaderOptions,
6900) -> Result<(ManifestFooter, VolumeTrailer, Option<RootAuthFooterV1>), FormatError> {
6901 let candidate = locate_v41_terminal_candidate(bytes, context, options)?;
6902 if !terminal_candidate_reaches_eof(&candidate, bytes.len())? {
6903 return Err(FormatError::InvalidArchive(
6904 "sequential terminal does not end at EOF",
6905 ));
6906 }
6907 let terminal = candidate.terminal;
6908 if terminal.image.manifest_footer_offset != manifest_offset as u64 {
6909 return Err(FormatError::InvalidArchive(
6910 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6911 ));
6912 }
6913 if terminal.volume_trailer.block_count != observed_block_count {
6914 return Err(FormatError::InvalidArchive(
6915 "VolumeTrailer block_count does not match observed stream",
6916 ));
6917 }
6918 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6919 Ok((
6920 manifest_footer,
6921 terminal.volume_trailer,
6922 terminal.root_auth_footer,
6923 ))
6924}
6925
6926pub(crate) fn parse_terminal_material_read_at(
6927 reader: &dyn ArchiveReadAt,
6928 input_len: u64,
6929 manifest_offset: u64,
6930 observed_block_count: u64,
6931 context: KeyHoldingTerminalContext<'_>,
6932) -> Result<SequentialTerminalMaterial, FormatError> {
6933 let mut candidates = Vec::new();
6934 if input_len >= CRITICAL_RECOVERY_LOCATOR_LEN as u64 {
6935 collect_v41_locator_candidate_read_at(
6936 reader,
6937 input_len - CRITICAL_RECOVERY_LOCATOR_LEN as u64,
6938 0,
6939 context,
6940 &mut candidates,
6941 );
6942 }
6943 if input_len >= LOCATOR_PAIR_LEN as u64 {
6944 collect_v41_locator_candidate_read_at(
6945 reader,
6946 input_len - LOCATOR_PAIR_LEN as u64,
6947 1,
6948 context,
6949 &mut candidates,
6950 );
6951 }
6952
6953 let candidate = choose_v41_terminal_candidate(candidates)?;
6954 if !terminal_candidate_reaches_eof(&candidate, to_usize(input_len, "terminal EOF")?)? {
6955 return Err(FormatError::InvalidArchive(
6956 "sequential terminal does not end at EOF",
6957 ));
6958 }
6959 let terminal = candidate.terminal;
6960 if terminal.image.manifest_footer_offset != manifest_offset {
6961 return Err(FormatError::InvalidArchive(
6962 "VolumeTrailer ManifestFooter offset does not match observed stream offset",
6963 ));
6964 }
6965 if terminal.volume_trailer.block_count != observed_block_count {
6966 return Err(FormatError::InvalidArchive(
6967 "VolumeTrailer block_count does not match observed stream",
6968 ));
6969 }
6970 let manifest_footer = ManifestFooter::parse(&terminal.manifest_footer_bytes)?;
6971 Ok(SequentialTerminalMaterial {
6972 manifest_footer,
6973 volume_trailer: terminal.volume_trailer,
6974 root_auth_footer: terminal.root_auth_footer,
6975 })
6976}
6977
6978fn terminal_candidate_reaches_eof(
6979 candidate: &TerminalCandidate,
6980 input_len: usize,
6981) -> Result<bool, FormatError> {
6982 let expected_end =
6983 match candidate.locator_sequence {
6984 Some(0) => candidate.anchor,
6985 Some(1) => candidate
6986 .anchor
6987 .checked_add(CRITICAL_RECOVERY_LOCATOR_LEN)
6988 .ok_or(FormatError::InvalidArchive(
6989 "terminal EOF boundary overflow",
6990 ))?,
6991 None => candidate.anchor.checked_add(LOCATOR_PAIR_LEN).ok_or(
6992 FormatError::InvalidArchive("terminal EOF boundary overflow"),
6993 )?,
6994 Some(_) => {
6995 return Err(FormatError::InvalidArchive(
6996 "invalid terminal locator sequence",
6997 ))
6998 }
6999 };
7000 Ok(expected_end == input_len)
7001}
7002
7003#[derive(Debug, Default)]
7004struct PendingSequentialEnvelope {
7005 data_shards: Vec<Option<Vec<u8>>>,
7006 parity_shards: Vec<Option<Vec<u8>>>,
7007 saw_last_data: bool,
7008 awaiting_tentative_parity: bool,
7009}
7010
7011impl PendingSequentialEnvelope {
7012 fn is_empty(&self) -> bool {
7013 self.data_shards.is_empty() && self.parity_shards.is_empty()
7014 }
7015}
7016
7017fn handle_sequential_payload_erasure(
7018 pending: &mut PendingSequentialEnvelope,
7019 crypto_header: &CryptoHeaderFixed,
7020 metadata_seen: bool,
7021) -> Result<(), FormatError> {
7022 if metadata_seen || pending.saw_last_data {
7023 return Err(FormatError::BadCrc {
7024 structure: "BlockRecord",
7025 });
7026 }
7027 if !sequential_payload_parity_is_guaranteed(crypto_header) {
7028 return Err(FormatError::BadCrc {
7029 structure: "BlockRecord",
7030 });
7031 }
7032 pending.data_shards.push(None);
7033 pending.awaiting_tentative_parity = true;
7034 if pending.data_shards.len() > crypto_header.fec_data_shards as usize {
7035 return Err(FormatError::InvalidArchive(
7036 "sequential payload envelope exceeds data-shard cap",
7037 ));
7038 }
7039 Ok(())
7040}
7041
7042fn sequential_payload_parity_is_guaranteed(crypto_header: &CryptoHeaderFixed) -> bool {
7043 crypto_header.fec_parity_shards > 0
7044 && (crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0)
7045}
7046
7047fn sequential_extract_tar_stream_with_options(
7048 bytes: &[u8],
7049 master_key: &MasterKey,
7050 options: ReaderOptions,
7051) -> Result<Vec<u8>, FormatError> {
7052 validate_reader_options(options)?;
7053 if bytes.len() < VOLUME_HEADER_LEN {
7054 return Err(FormatError::InvalidLength {
7055 structure: "archive",
7056 expected: VOLUME_HEADER_LEN,
7057 actual: bytes.len(),
7058 });
7059 }
7060
7061 let volume_header = VolumeHeader::parse(slice(bytes, 0, VOLUME_HEADER_LEN, "archive")?)?;
7062 let crypto_start = volume_header.crypto_header_offset as usize;
7063 let crypto_len = volume_header.crypto_header_length as usize;
7064 let crypto_end = checked_add(crypto_start, crypto_len, "CryptoHeader")?;
7065 let crypto_bytes = slice(bytes, crypto_start, crypto_len, "CryptoHeader")?;
7066 let parsed_crypto = CryptoHeader::parse(crypto_bytes, volume_header.crypto_header_length)?;
7067 let subkeys = subkeys_for_open(
7068 Some(master_key),
7069 parsed_crypto.fixed.aead_algo,
7070 &volume_header.archive_uuid,
7071 &volume_header.session_id,
7072 )?;
7073 verify_integrity_tag(
7074 HmacDomain::CryptoHeader,
7075 parsed_crypto.fixed.aead_algo,
7076 Some(&subkeys.mac_key),
7077 &volume_header.archive_uuid,
7078 &volume_header.session_id,
7079 parsed_crypto.hmac_covered_bytes,
7080 &parsed_crypto.header_hmac,
7081 )?;
7082 parsed_crypto.validate_extension_semantics()?;
7083 validate_sequential_supported_volume(
7084 &volume_header,
7085 &parsed_crypto.fixed,
7086 &parsed_crypto.extensions,
7087 )?;
7088 validate_crypto_class_parity_exactness(&parsed_crypto.fixed)?;
7089
7090 let block_size = parsed_crypto.fixed.block_size as usize;
7091 let record_len = block_size
7092 .checked_add(BLOCK_RECORD_FRAMING_LEN)
7093 .ok_or(FormatError::InvalidArchive("BlockRecord length overflow"))?;
7094 let mut offset = crypto_end;
7095 let mut observed_block_count = 0u64;
7096 let mut metadata_seen = false;
7097 let mut pending = PendingSequentialEnvelope::default();
7098 let mut next_envelope_index = 0u64;
7099 let mut tar_stream = Vec::new();
7100 let max_tar_stream_size = options.max_verify_tar_size;
7101 let observed_archive_bytes = observed_archive_size([bytes.len() as u64])?;
7102 let total_extraction_cap = total_extraction_size_cap(options, observed_archive_bytes);
7103 let mut tar_stream_total_validator = TarStreamTotalExtractionSizeValidator::new(
7104 parsed_crypto.fixed.max_path_length,
7105 total_extraction_cap,
7106 );
7107
7108 while bytes.get(offset..offset + 4) == Some(b"TZBK") {
7109 let record = parse_sequential_block_or_erasure(
7110 bytes,
7111 offset,
7112 record_len,
7113 block_size,
7114 &volume_header,
7115 observed_block_count,
7116 )?;
7117 observed_block_count = observed_block_count
7118 .checked_add(1)
7119 .ok_or(FormatError::InvalidArchive("BlockRecord count overflow"))?;
7120 let Some(record) = record else {
7121 handle_sequential_payload_erasure(&mut pending, &parsed_crypto.fixed, metadata_seen)?;
7122 offset = checked_add(offset, record_len, "BlockRecord")?;
7123 continue;
7124 };
7125
7126 match record.kind {
7127 BlockKind::PayloadData => {
7128 if metadata_seen {
7129 return Err(FormatError::InvalidArchive(
7130 "payload BlockRecord appears after metadata",
7131 ));
7132 }
7133 if pending.awaiting_tentative_parity {
7134 return Err(FormatError::InvalidArchive(
7135 "sequential payload envelope boundary is ambiguous after CRC erasure",
7136 ));
7137 }
7138 if pending.saw_last_data {
7139 finalize_sequential_envelope(
7140 &mut pending,
7141 SequentialEnvelopeDecodeContext {
7142 crypto_header: &parsed_crypto.fixed,
7143 subkeys: &subkeys,
7144 volume_header: &volume_header,
7145 next_envelope_index: &mut next_envelope_index,
7146 tar_stream: &mut tar_stream,
7147 max_tar_stream_size,
7148 tar_stream_total_validator: &mut tar_stream_total_validator,
7149 },
7150 )?;
7151 }
7152 let is_last_data = record.is_last_data();
7153 pending.data_shards.push(Some(record.payload));
7154 if is_last_data {
7155 pending.saw_last_data = true;
7156 }
7157 if pending.data_shards.len() > parsed_crypto.fixed.fec_data_shards as usize {
7158 return Err(FormatError::InvalidArchive(
7159 "sequential payload envelope exceeds data-shard cap",
7160 ));
7161 }
7162 }
7163 BlockKind::PayloadParity => {
7164 if metadata_seen {
7165 return Err(FormatError::InvalidArchive(
7166 "payload parity BlockRecord appears after metadata",
7167 ));
7168 }
7169 if pending.awaiting_tentative_parity {
7170 pending.awaiting_tentative_parity = false;
7171 pending.saw_last_data = true;
7172 } else if pending.data_shards.is_empty() || !pending.saw_last_data {
7173 return Err(FormatError::InvalidArchive(
7174 "payload parity appears before envelope data is complete",
7175 ));
7176 }
7177 pending.parity_shards.push(Some(record.payload));
7178 if pending.parity_shards.len() > parsed_crypto.fixed.fec_parity_shards as usize {
7179 return Err(FormatError::InvalidArchive(
7180 "sequential payload envelope exceeds parity-shard cap",
7181 ));
7182 }
7183 }
7184 _ => {
7185 if !pending.is_empty() {
7186 finalize_sequential_envelope(
7187 &mut pending,
7188 SequentialEnvelopeDecodeContext {
7189 crypto_header: &parsed_crypto.fixed,
7190 subkeys: &subkeys,
7191 volume_header: &volume_header,
7192 next_envelope_index: &mut next_envelope_index,
7193 tar_stream: &mut tar_stream,
7194 max_tar_stream_size,
7195 tar_stream_total_validator: &mut tar_stream_total_validator,
7196 },
7197 )?;
7198 }
7199 metadata_seen = true;
7200 }
7201 }
7202
7203 offset = checked_add(offset, record_len, "BlockRecord")?;
7204 }
7205
7206 if !pending.is_empty() {
7207 finalize_sequential_envelope(
7208 &mut pending,
7209 SequentialEnvelopeDecodeContext {
7210 crypto_header: &parsed_crypto.fixed,
7211 subkeys: &subkeys,
7212 volume_header: &volume_header,
7213 next_envelope_index: &mut next_envelope_index,
7214 tar_stream: &mut tar_stream,
7215 max_tar_stream_size,
7216 tar_stream_total_validator: &mut tar_stream_total_validator,
7217 },
7218 )?;
7219 }
7220
7221 parse_terminal_material(
7222 bytes,
7223 offset,
7224 observed_block_count,
7225 KeyHoldingTerminalContext {
7226 subkeys: &subkeys,
7227 volume_header: &volume_header,
7228 crypto_header: &parsed_crypto.fixed,
7229 crypto_header_bytes: crypto_bytes,
7230 },
7231 options,
7232 )?;
7233 validate_tar_stream_total_extraction_size(
7236 &tar_stream,
7237 parsed_crypto.fixed.max_path_length,
7238 total_extraction_cap,
7239 )?;
7240 Ok(tar_stream)
7241}
7242
7243fn validate_sequential_supported_volume(
7244 volume_header: &VolumeHeader,
7245 crypto_header: &CryptoHeaderFixed,
7246 extensions: &[ExtensionTlv<'_>],
7247) -> Result<(), FormatError> {
7248 reject_unsupported_raw_stream_profile(extensions)?;
7249 if volume_header.stripe_width != 1 || volume_header.volume_index != 0 {
7250 return Err(FormatError::ReaderUnsupported(
7251 "sequential reader supports only single-volume archive input",
7252 ));
7253 }
7254 if crypto_header.stripe_width != volume_header.stripe_width {
7255 return Err(FormatError::InvalidArchive(
7256 "VolumeHeader and CryptoHeader stripe_width differ",
7257 ));
7258 }
7259 if crypto_header.has_dictionary != 0 {
7260 return Err(FormatError::ReaderUnsupported(
7261 "dictionary bootstrap required for non-seekable sequential extraction",
7262 ));
7263 }
7264 Ok(())
7265}
7266
7267struct SequentialEnvelopeDecodeContext<'a> {
7268 crypto_header: &'a CryptoHeaderFixed,
7269 subkeys: &'a Subkeys,
7270 volume_header: &'a VolumeHeader,
7271 next_envelope_index: &'a mut u64,
7272 tar_stream: &'a mut Vec<u8>,
7273 max_tar_stream_size: usize,
7274 tar_stream_total_validator: &'a mut TarStreamTotalExtractionSizeValidator,
7275}
7276
7277fn finalize_sequential_envelope(
7278 pending: &mut PendingSequentialEnvelope,
7279 context: SequentialEnvelopeDecodeContext<'_>,
7280) -> Result<(), FormatError> {
7281 if !pending.saw_last_data {
7282 return Err(FormatError::InvalidArchive(
7283 "sequential payload envelope is missing last-data flag",
7284 ));
7285 }
7286 if pending.data_shards.len() > context.crypto_header.fec_data_shards as usize {
7287 return Err(FormatError::InvalidArchive(
7288 "sequential payload envelope exceeds data-shard cap",
7289 ));
7290 }
7291 if pending.parity_shards.len() > context.crypto_header.fec_parity_shards as usize {
7292 return Err(FormatError::InvalidArchive(
7293 "sequential payload envelope exceeds parity-shard cap",
7294 ));
7295 }
7296 let required_parity =
7297 required_object_parity(pending.data_shards.len() as u64, context.crypto_header)?;
7298 if pending.parity_shards.len() < required_parity as usize {
7299 return Err(FormatError::InvalidArchive(
7300 "sequential payload envelope has insufficient parity for recovery settings",
7301 ));
7302 }
7303
7304 let repaired = repair_data_gf16(
7305 &pending.data_shards,
7306 &pending.parity_shards,
7307 context.crypto_header.block_size as usize,
7308 )?;
7309 let mut encrypted =
7310 Vec::with_capacity(repaired.len() * context.crypto_header.block_size as usize);
7311 for shard in repaired {
7312 encrypted.extend_from_slice(&shard);
7313 }
7314 let plaintext = decrypt_padded_aead_object(
7315 AeadObjectContext {
7316 algo: context.crypto_header.aead_algo,
7317 key: &context.subkeys.enc_key,
7318 nonce_seed: &context.subkeys.nonce_seed,
7319 domain: b"envelope",
7320 archive_uuid: &context.volume_header.archive_uuid,
7321 session_id: &context.volume_header.session_id,
7322 counter: *context.next_envelope_index,
7323 },
7324 &encrypted,
7325 )?;
7326 decode_concatenated_zstd_frames_with_cap(
7327 &plaintext,
7328 None,
7329 context.tar_stream,
7330 context.max_tar_stream_size,
7331 Some(context.tar_stream_total_validator),
7332 )?;
7333 *context.next_envelope_index = (*context.next_envelope_index)
7334 .checked_add(1)
7335 .ok_or(FormatError::InvalidArchive("envelope counter overflow"))?;
7336 *pending = PendingSequentialEnvelope::default();
7337 Ok(())
7338}
7339
7340fn decode_concatenated_zstd_frames_with_cap(
7341 plaintext: &[u8],
7342 dictionary: Option<&[u8]>,
7343 output: &mut Vec<u8>,
7344 max_output_len: usize,
7345 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7346) -> Result<(), FormatError> {
7347 let mut cursor = 0usize;
7348 while cursor < plaintext.len() {
7349 let frame_len = zstd_safe::find_frame_compressed_size(&plaintext[cursor..])
7350 .map_err(|_| FormatError::InvalidZstdFrame)?;
7351 if frame_len == 0 {
7352 return Err(FormatError::InvalidZstdFrame);
7353 }
7354 let end = checked_add(cursor, frame_len, "zstd frame")?;
7355 validate_exact_zstd_frame(&plaintext[cursor..end])?;
7356 if let Some(dictionary) = dictionary {
7357 let mut decoder =
7358 zstd::stream::Decoder::with_dictionary(&plaintext[cursor..end], dictionary)
7359 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7360 read_zstd_frame_to_capped_output(
7361 &mut decoder,
7362 output,
7363 max_output_len,
7364 tar_stream_total_validator.as_deref_mut(),
7365 )?;
7366 } else {
7367 let mut decoder = zstd::stream::Decoder::new(&plaintext[cursor..end])
7368 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7369 read_zstd_frame_to_capped_output(
7370 &mut decoder,
7371 output,
7372 max_output_len,
7373 tar_stream_total_validator.as_deref_mut(),
7374 )?;
7375 }
7376 cursor = end;
7377 }
7378 Ok(())
7379}
7380
7381fn read_zstd_frame_to_capped_output<R: Read>(
7382 decoder: &mut R,
7383 output: &mut Vec<u8>,
7384 max_output_len: usize,
7385 mut tar_stream_total_validator: Option<&mut TarStreamTotalExtractionSizeValidator>,
7386) -> Result<(), FormatError> {
7387 let mut buf = [0u8; 64 * 1024];
7388 loop {
7389 let read = decoder
7390 .read(&mut buf)
7391 .map_err(|_| FormatError::ZstdDecompressionFailure)?;
7392 if read == 0 {
7393 return Ok(());
7394 }
7395 let next_len = output
7396 .len()
7397 .checked_add(read)
7398 .ok_or(FormatError::ReaderUnsupported(
7399 "sequential tar stream exceeds configured verification cap",
7400 ))?;
7401 if next_len > max_output_len {
7402 return Err(FormatError::ReaderUnsupported(
7403 "sequential tar stream exceeds configured verification cap",
7404 ));
7405 }
7406 output.extend_from_slice(&buf[..read]);
7407 if let Some(validator) = tar_stream_total_validator.as_mut() {
7408 validator.observe(output)?;
7409 }
7410 }
7411}
7412
7413fn load_archive_dictionary(
7414 blocks: &impl BlockProvider,
7415 subkeys: &Subkeys,
7416 volume_header: &VolumeHeader,
7417 crypto_header: &CryptoHeaderFixed,
7418 index_root: &IndexRoot,
7419) -> Result<Option<Vec<u8>>, FormatError> {
7420 if crypto_header.has_dictionary == 0 {
7421 return Ok(None);
7422 }
7423 let plaintext = load_metadata_object_from_parts(
7424 blocks,
7425 ObjectLoadContext::dictionary(volume_header, crypto_header, subkeys, index_root)?,
7426 index_root.header.dictionary_decompressed_size,
7427 )?;
7428 Ok(Some(plaintext))
7429}
7430
7431#[derive(Clone, Copy)]
7432struct ObjectLoadContext<'a> {
7433 volume_header: &'a VolumeHeader,
7434 crypto_header: &'a CryptoHeaderFixed,
7435 extent: ObjectExtent,
7436 data_kind: BlockKind,
7437 parity_kind: BlockKind,
7438 key: &'a [u8; 32],
7439 nonce_seed: &'a [u8; 32],
7440 domain: &'a [u8],
7441 counter: u64,
7442 class_data_shard_max: u16,
7443 class_parity_shard_max: u16,
7444}
7445
7446impl<'a> ObjectLoadContext<'a> {
7447 fn index_root(
7448 volume_header: &'a VolumeHeader,
7449 crypto_header: &'a CryptoHeaderFixed,
7450 subkeys: &'a Subkeys,
7451 extent: ObjectExtent,
7452 ) -> Self {
7453 Self {
7454 volume_header,
7455 crypto_header,
7456 extent,
7457 data_kind: BlockKind::IndexRootData,
7458 parity_kind: BlockKind::IndexRootParity,
7459 key: &subkeys.index_root_key,
7460 nonce_seed: &subkeys.index_nonce_seed,
7461 domain: b"idxroot",
7462 counter: 0,
7463 class_data_shard_max: crypto_header.index_root_fec_data_shards,
7464 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7465 }
7466 }
7467
7468 fn index_shard(
7469 volume_header: &'a VolumeHeader,
7470 crypto_header: &'a CryptoHeaderFixed,
7471 subkeys: &'a Subkeys,
7472 entry: &ShardEntry,
7473 ) -> Self {
7474 Self {
7475 volume_header,
7476 crypto_header,
7477 extent: ObjectExtent {
7478 first_block_index: entry.first_block_index,
7479 data_block_count: entry.data_block_count,
7480 parity_block_count: entry.parity_block_count,
7481 encrypted_size: entry.encrypted_size,
7482 },
7483 data_kind: BlockKind::IndexShardData,
7484 parity_kind: BlockKind::IndexShardParity,
7485 key: &subkeys.index_shard_key,
7486 nonce_seed: &subkeys.index_nonce_seed,
7487 domain: b"idxshard",
7488 counter: entry.shard_index,
7489 class_data_shard_max: crypto_header.index_fec_data_shards,
7490 class_parity_shard_max: crypto_header.index_fec_parity_shards,
7491 }
7492 }
7493
7494 fn directory_hint(
7495 volume_header: &'a VolumeHeader,
7496 crypto_header: &'a CryptoHeaderFixed,
7497 subkeys: &'a Subkeys,
7498 entry: &DirectoryHintShardEntry,
7499 ) -> Self {
7500 Self {
7501 volume_header,
7502 crypto_header,
7503 extent: ObjectExtent {
7504 first_block_index: entry.first_block_index,
7505 data_block_count: entry.data_block_count,
7506 parity_block_count: entry.parity_block_count,
7507 encrypted_size: entry.encrypted_size,
7508 },
7509 data_kind: BlockKind::DirectoryHintData,
7510 parity_kind: BlockKind::DirectoryHintParity,
7511 key: &subkeys.dir_hint_key,
7512 nonce_seed: &subkeys.index_nonce_seed,
7513 domain: b"dirhint",
7514 counter: entry.hint_shard_index,
7515 class_data_shard_max: crypto_header.index_fec_data_shards,
7516 class_parity_shard_max: crypto_header.index_fec_parity_shards,
7517 }
7518 }
7519
7520 fn dictionary(
7521 volume_header: &'a VolumeHeader,
7522 crypto_header: &'a CryptoHeaderFixed,
7523 subkeys: &'a Subkeys,
7524 index_root: &IndexRoot,
7525 ) -> Result<Self, FormatError> {
7526 Ok(Self {
7527 volume_header,
7528 crypto_header,
7529 extent: dictionary_extent_from_index_root(index_root)?,
7530 data_kind: BlockKind::DictionaryData,
7531 parity_kind: BlockKind::DictionaryParity,
7532 key: &subkeys.dictionary_key,
7533 nonce_seed: &subkeys.index_nonce_seed,
7534 domain: b"dict",
7535 counter: 0,
7536 class_data_shard_max: crypto_header.index_root_fec_data_shards,
7537 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
7538 })
7539 }
7540
7541 fn payload(
7542 volume_header: &'a VolumeHeader,
7543 crypto_header: &'a CryptoHeaderFixed,
7544 subkeys: &'a Subkeys,
7545 envelope: &EnvelopeEntry,
7546 ) -> Self {
7547 Self {
7548 volume_header,
7549 crypto_header,
7550 extent: ObjectExtent {
7551 first_block_index: envelope.first_block_index,
7552 data_block_count: envelope.data_block_count,
7553 parity_block_count: envelope.parity_block_count,
7554 encrypted_size: envelope.encrypted_size,
7555 },
7556 data_kind: BlockKind::PayloadData,
7557 parity_kind: BlockKind::PayloadParity,
7558 key: &subkeys.enc_key,
7559 nonce_seed: &subkeys.nonce_seed,
7560 domain: b"envelope",
7561 counter: envelope.envelope_index,
7562 class_data_shard_max: crypto_header.fec_data_shards,
7563 class_parity_shard_max: crypto_header.fec_parity_shards,
7564 }
7565 }
7566}
7567
7568fn dictionary_extent_from_index_root(index_root: &IndexRoot) -> Result<ObjectExtent, FormatError> {
7569 if index_root.header.dictionary_data_block_count == 0
7570 || index_root.header.dictionary_encrypted_size == 0
7571 || index_root.header.dictionary_decompressed_size == 0
7572 {
7573 return Err(FormatError::InvalidArchive("dictionary bootstrap required"));
7574 }
7575 Ok(ObjectExtent {
7576 first_block_index: index_root.header.dictionary_first_block,
7577 data_block_count: index_root.header.dictionary_data_block_count,
7578 parity_block_count: index_root.header.dictionary_parity_block_count,
7579 encrypted_size: index_root.header.dictionary_encrypted_size,
7580 })
7581}
7582
7583fn load_metadata_object_from_parts(
7584 blocks: &impl BlockProvider,
7585 context: ObjectLoadContext<'_>,
7586 decompressed_size: u32,
7587) -> Result<Vec<u8>, FormatError> {
7588 let compressed = load_decrypted_object_from_parts(blocks, context)?;
7589 decompress_exact_zstd_frame(&compressed, decompressed_size as usize)
7590}
7591
7592fn load_decrypted_object_from_parts(
7593 blocks: &impl BlockProvider,
7594 context: ObjectLoadContext<'_>,
7595) -> Result<Vec<u8>, FormatError> {
7596 load_decrypted_object_from_parts_with_parity_policy(blocks, context, ParityReadPolicy::Always)
7597}
7598
7599fn load_decrypted_object_from_parts_with_parity_policy(
7600 blocks: &impl BlockProvider,
7601 context: ObjectLoadContext<'_>,
7602 parity_policy: ParityReadPolicy,
7603) -> Result<Vec<u8>, FormatError> {
7604 let repaired = load_repaired_object_data_shards_from_parts_with_parity_policy(
7605 blocks,
7606 context.crypto_header,
7607 context.extent,
7608 context.data_kind,
7609 context.parity_kind,
7610 context.class_data_shard_max,
7611 context.class_parity_shard_max,
7612 parity_policy,
7613 )?;
7614 let mut encrypted = Vec::with_capacity(context.extent.encrypted_size as usize);
7615 for shard in repaired {
7616 encrypted.extend_from_slice(&shard);
7617 }
7618 if encrypted.len() != context.extent.encrypted_size as usize {
7619 return Err(FormatError::InvalidArchive(
7620 "object encrypted size does not match repaired shards",
7621 ));
7622 }
7623
7624 decrypt_padded_aead_object(
7625 AeadObjectContext {
7626 algo: context.crypto_header.aead_algo,
7627 key: context.key,
7628 nonce_seed: context.nonce_seed,
7629 domain: context.domain,
7630 archive_uuid: &context.volume_header.archive_uuid,
7631 session_id: &context.volume_header.session_id,
7632 counter: context.counter,
7633 },
7634 &encrypted,
7635 )
7636}
7637
7638fn load_repaired_object_data_shards_from_parts(
7639 blocks: &impl BlockProvider,
7640 crypto_header: &CryptoHeaderFixed,
7641 extent: ObjectExtent,
7642 data_kind: BlockKind,
7643 parity_kind: BlockKind,
7644 class_data_shard_max: u16,
7645 class_parity_shard_max: u16,
7646) -> Result<Vec<Vec<u8>>, FormatError> {
7647 load_repaired_object_data_shards_from_parts_with_parity_policy(
7648 blocks,
7649 crypto_header,
7650 extent,
7651 data_kind,
7652 parity_kind,
7653 class_data_shard_max,
7654 class_parity_shard_max,
7655 ParityReadPolicy::Always,
7656 )
7657}
7658
7659#[allow(clippy::too_many_arguments)]
7660fn load_repaired_object_data_shards_from_parts_with_parity_policy(
7661 blocks: &impl BlockProvider,
7662 crypto_header: &CryptoHeaderFixed,
7663 extent: ObjectExtent,
7664 data_kind: BlockKind,
7665 parity_kind: BlockKind,
7666 class_data_shard_max: u16,
7667 class_parity_shard_max: u16,
7668 parity_policy: ParityReadPolicy,
7669) -> Result<Vec<Vec<u8>>, FormatError> {
7670 validate_object_extent(
7671 extent,
7672 crypto_header,
7673 class_data_shard_max,
7674 class_parity_shard_max,
7675 )?;
7676 let block_size = crypto_header.block_size as usize;
7677 let data_count = extent.data_block_count as usize;
7678 let parity_count = extent.parity_block_count as usize;
7679 let mut data_shards = Vec::with_capacity(data_count);
7680 let mut parity_shards = Vec::with_capacity(parity_count);
7681
7682 for offset in 0..data_count {
7683 let block_index = checked_u64_add(extent.first_block_index, offset as u64, "object")?;
7684 if let Some(record) = blocks.block(block_index)? {
7685 if record.kind != data_kind {
7686 return Err(FormatError::InvalidArchive(
7687 "object data block has unexpected kind",
7688 ));
7689 }
7690 let should_be_last = offset + 1 == data_count;
7691 if record.is_last_data() != should_be_last {
7692 return Err(FormatError::InvalidArchive(
7693 "object last-data flag is not on the final data block",
7694 ));
7695 }
7696 data_shards.push(Some(record.payload.clone()));
7697 } else {
7698 data_shards.push(None);
7699 }
7700 }
7701
7702 if parity_policy == ParityReadPolicy::RepairOnly && data_shards.iter().all(Option::is_some) {
7703 return repair_data_gf16(&data_shards, &[], block_size);
7704 }
7705
7706 for offset in 0..parity_count {
7707 let block_index = checked_u64_add(
7708 extent.first_block_index,
7709 data_count as u64 + offset as u64,
7710 "object",
7711 )?;
7712 if let Some(record) = blocks.block(block_index)? {
7713 if record.kind != parity_kind {
7714 return Err(FormatError::InvalidArchive(
7715 "object parity block has unexpected kind",
7716 ));
7717 }
7718 if record.is_last_data() {
7719 return Err(FormatError::InvalidArchive(
7720 "object parity block has last-data flag",
7721 ));
7722 }
7723 parity_shards.push(Some(record.payload.clone()));
7724 } else {
7725 parity_shards.push(None);
7726 }
7727 }
7728
7729 repair_data_gf16(&data_shards, &parity_shards, block_size)
7730}
7731
7732fn validate_object_extent(
7733 extent: ObjectExtent,
7734 crypto_header: &CryptoHeaderFixed,
7735 class_data_shard_max: u16,
7736 class_parity_shard_max: u16,
7737) -> Result<(), FormatError> {
7738 if extent.data_block_count == 0 || extent.encrypted_size == 0 {
7739 return Err(FormatError::InvalidArchive(
7740 "encrypted object has zero data blocks or size",
7741 ));
7742 }
7743 if extent.data_block_count > class_data_shard_max as u32 {
7744 return Err(FormatError::InvalidArchive(
7745 "encrypted object exceeds its class data-shard maximum",
7746 ));
7747 }
7748 if extent.parity_block_count > class_parity_shard_max as u32 {
7749 return Err(FormatError::InvalidArchive(
7750 "encrypted object exceeds its class parity-shard maximum",
7751 ));
7752 }
7753 let required_parity = required_object_parity(extent.data_block_count as u64, crypto_header)?;
7754 if extent.parity_block_count != required_parity {
7755 return Err(FormatError::InvalidArchive(
7756 "encrypted object parity does not match v41 compute_parity",
7757 ));
7758 }
7759 let total = checked_u64_add(
7760 extent.data_block_count as u64,
7761 extent.parity_block_count as u64,
7762 "encrypted object shard count overflow",
7763 )?;
7764 if total > 65_535 {
7765 return Err(FormatError::FecTooManyShards(total as usize));
7766 }
7767 let expected = checked_u64_mul(
7768 extent.data_block_count as u64,
7769 crypto_header.block_size as u64,
7770 "encrypted object size overflow",
7771 )?;
7772 if expected != extent.encrypted_size as u64 {
7773 return Err(FormatError::InvalidArchive(
7774 "encrypted object size is not data_block_count * block_size",
7775 ));
7776 }
7777 if extent.encrypted_size as usize <= crypto_header.aead_algo.tag_len() {
7778 return Err(FormatError::InvalidArchive(
7779 "encrypted object is too small for AEAD tag",
7780 ));
7781 }
7782 Ok(())
7783}
7784
7785pub(crate) fn required_object_parity(
7786 data_block_count: u64,
7787 crypto_header: &CryptoHeaderFixed,
7788) -> Result<u32, FormatError> {
7789 let min_parity =
7790 if crypto_header.volume_loss_tolerance > 0 || crypto_header.bit_rot_buffer_pct > 0 {
7791 1
7792 } else {
7793 0
7794 };
7795 let mut parity = 0u64;
7796 for _ in 0..100 {
7797 let total = data_block_count
7798 .checked_add(parity)
7799 .ok_or(FormatError::InvalidArchive("parity total overflow"))?;
7800 let by_volume = checked_u64_mul(
7801 crypto_header.volume_loss_tolerance as u64,
7802 ceil_div_u64(total, crypto_header.stripe_width as u64)?,
7803 "volume-loss parity overflow",
7804 )?;
7805 let by_bitrot = ceil_div_u64(
7806 checked_u64_mul(
7807 total,
7808 crypto_header.bit_rot_buffer_pct as u64,
7809 "bit-rot parity overflow",
7810 )?,
7811 100,
7812 )?;
7813 let next = by_volume
7814 .checked_add(by_bitrot)
7815 .ok_or(FormatError::InvalidArchive("parity overflow"))?
7816 .max(min_parity);
7817 if next == parity {
7818 return u32::try_from(next)
7819 .map_err(|_| FormatError::InvalidArchive("parity count overflow"));
7820 }
7821 parity = next;
7822 }
7823 Err(FormatError::InvalidArchive(
7824 "parity calculation did not converge",
7825 ))
7826}
7827
7828fn ceil_div_u64(numerator: u64, denominator: u64) -> Result<u64, FormatError> {
7829 if denominator == 0 {
7830 return Err(FormatError::InvalidArchive("division by zero"));
7831 }
7832 numerator
7833 .checked_add(denominator - 1)
7834 .ok_or(FormatError::InvalidArchive("ceiling division overflow"))
7835 .map(|value| value / denominator)
7836}
7837
7838fn frame_range_for_file<'b>(
7839 shard: &'b IndexShard,
7840 file: &FileEntry,
7841) -> Result<Vec<&'b FrameEntry>, FormatError> {
7842 let mut frames = Vec::with_capacity(file.frame_count as usize);
7843 for offset in 0..file.frame_count as u64 {
7844 let frame_index =
7845 file.first_frame_index
7846 .checked_add(offset)
7847 .ok_or(FormatError::InvalidArchive(
7848 "FileEntry frame range overflow",
7849 ))?;
7850 let frame = shard
7851 .frames
7852 .iter()
7853 .find(|entry| entry.frame_index == frame_index)
7854 .ok_or(FormatError::InvalidArchive(
7855 "FileEntry references missing FrameEntry",
7856 ))?;
7857 frames.push(frame);
7858 }
7859 Ok(frames)
7860}
7861
7862fn metadata_limits(crypto_header: &CryptoHeaderFixed) -> MetadataLimits {
7863 MetadataLimits {
7864 block_size: crypto_header.block_size,
7865 max_path_length: crypto_header.max_path_length,
7866 max_payload_data_shards: crypto_header.fec_data_shards,
7867 max_payload_parity_shards: crypto_header.fec_parity_shards,
7868 max_index_data_shards: crypto_header.index_fec_data_shards,
7869 max_index_parity_shards: crypto_header.index_fec_parity_shards,
7870 max_index_root_data_shards: crypto_header.index_root_fec_data_shards,
7871 max_index_root_parity_shards: crypto_header.index_root_fec_parity_shards,
7872 ..MetadataLimits::default()
7873 }
7874}
7875
7876fn verify_dense_keys<T>(
7877 entries: &BTreeMap<u64, T>,
7878 expected_count: u64,
7879 structure: &'static str,
7880) -> Result<(), FormatError> {
7881 if entries.len() as u64 != expected_count {
7882 return Err(FormatError::InvalidArchive(
7883 "decoded table count does not match IndexRoot",
7884 ));
7885 }
7886 for expected in 0..expected_count {
7887 if !entries.contains_key(&expected) {
7888 return Err(FormatError::InvalidMetadata {
7889 structure,
7890 reason: "global index coverage has a gap",
7891 });
7892 }
7893 }
7894 Ok(())
7895}
7896
7897fn validate_envelope_frame_coverage(
7898 frames: &BTreeMap<u64, FrameEntry>,
7899 envelopes: &BTreeMap<u64, EnvelopeEntry>,
7900) -> Result<(), FormatError> {
7901 let mut accounted_frames = BTreeSet::new();
7902 for envelope in envelopes.values() {
7903 let first = envelope.first_frame_index;
7904 let end =
7905 first
7906 .checked_add(envelope.frame_count as u64)
7907 .ok_or(FormatError::InvalidArchive(
7908 "EnvelopeEntry frame range overflow",
7909 ))?;
7910 let mut ranges = Vec::with_capacity(envelope.frame_count as usize);
7911 for frame_index in first..end {
7912 let frame = frames.get(&frame_index).ok_or(FormatError::InvalidArchive(
7913 "EnvelopeEntry references missing FrameEntry",
7914 ))?;
7915 if frame.envelope_index != envelope.envelope_index {
7916 return Err(FormatError::InvalidArchive(
7917 "FrameEntry envelope_index does not match containing EnvelopeEntry",
7918 ));
7919 }
7920 if !accounted_frames.insert(frame_index) {
7921 return Err(FormatError::InvalidArchive(
7922 "FrameEntry is covered by multiple EnvelopeEntries",
7923 ));
7924 }
7925 let start = frame.offset_in_envelope as usize;
7926 let end = checked_add(start, frame.compressed_size as usize, "FrameEntry")?;
7927 if end > envelope.plaintext_size as usize {
7928 return Err(FormatError::InvalidArchive(
7929 "FrameEntry exceeds EnvelopeEntry plaintext_size",
7930 ));
7931 }
7932 ranges.push((start, end));
7933 }
7934 validate_exact_coverage_ranges(
7935 &mut ranges,
7936 envelope.plaintext_size as usize,
7937 "EnvelopeEntry frame coverage has a gap or overlap",
7938 )?;
7939 }
7940
7941 for frame_index in frames.keys() {
7942 if !accounted_frames.contains(frame_index) {
7943 return Err(FormatError::InvalidArchive(
7944 "FrameEntry is not covered by any EnvelopeEntry",
7945 ));
7946 }
7947 }
7948 Ok(())
7949}
7950
7951fn validate_global_file_table_order(shards: &[IndexShard]) -> Result<(), FormatError> {
7952 let mut previous = None::<([u8; 8], Vec<u8>, u64)>;
7953 for shard in shards {
7954 for (idx, file) in shard.files.iter().enumerate() {
7955 let path = shard
7956 .file_path(idx)
7957 .ok_or(FormatError::InvalidArchive("FileEntry path is missing"))?
7958 .to_vec();
7959 let start = shard
7960 .tar_member_group_start(idx)
7961 .ok_or(FormatError::InvalidArchive(
7962 "FileEntry tar member start is missing",
7963 ))?;
7964 let key = (file.path_hash, path, start);
7965 validate_global_file_table_key_step(previous.as_ref(), &key)?;
7966 previous = Some(key);
7967 }
7968 }
7969 Ok(())
7970}
7971
7972fn validate_global_file_table_key_step(
7973 previous: Option<&([u8; 8], Vec<u8>, u64)>,
7974 current: &([u8; 8], Vec<u8>, u64),
7975) -> Result<(), FormatError> {
7976 if let Some(previous) = previous {
7977 if previous >= current {
7978 return Err(FormatError::InvalidArchive(
7979 "global FileEntry rows are not sorted and unique",
7980 ));
7981 }
7982 }
7983 Ok(())
7984}
7985
7986fn validate_file_extent_coverage_ranges(
7987 extents: &[(u64, u64)],
7988 tar_len: u64,
7989) -> Result<(), FormatError> {
7990 let mut ranges = Vec::with_capacity(extents.len());
7991 for (start, len) in extents {
7992 let end = checked_u64_add(*start, *len, "FileEntry")?;
7993 if end > tar_len {
7994 return Err(FormatError::InvalidArchive(
7995 "FileEntry extent exceeds IndexRoot tar_total_size",
7996 ));
7997 }
7998 ranges.push((*start, end));
7999 }
8000 validate_exact_coverage_ranges_u64(
8001 &mut ranges,
8002 tar_len,
8003 "FileEntry extents do not cover tar stream exactly",
8004 )
8005}
8006
8007fn add_expected_directory_hint_rows(
8008 map: &mut DirectoryHintMap,
8009 shard_row_index: u32,
8010 path: &[u8],
8011 kind: TarEntryKind,
8012) {
8013 map.entry(Vec::new()).or_default().insert(shard_row_index);
8014 for (idx, byte) in path.iter().enumerate() {
8015 if *byte == b'/' {
8016 map.entry(path[..idx].to_vec())
8017 .or_default()
8018 .insert(shard_row_index);
8019 }
8020 }
8021 if kind == TarEntryKind::Directory {
8022 map.entry(path.to_vec())
8023 .or_default()
8024 .insert(shard_row_index);
8025 }
8026}
8027
8028fn validate_directory_hint_tables_against_expected(
8029 tables: &[DirectoryHintTable],
8030 expected: &DirectoryHintMap,
8031) -> Result<(), FormatError> {
8032 let mut actual = Vec::new();
8033 let mut previous_key: Option<([u8; 8], Vec<u8>)> = None;
8034
8035 for table in tables {
8036 for entry_index in 0..table.entries.len() {
8037 let path = table
8038 .entry_path(entry_index)
8039 .ok_or(FormatError::InvalidArchive(
8040 "DirectoryHintEntry path is missing",
8041 ))?;
8042 let key = (hash_prefix(path), path.to_vec());
8043 if let Some(previous) = &previous_key {
8044 if previous >= &key {
8045 return Err(FormatError::InvalidArchive(
8046 "DirectoryHintEntry rows are not globally sorted",
8047 ));
8048 }
8049 }
8050 previous_key = Some(key);
8051
8052 let rows =
8053 table
8054 .shard_rows_for_entry(entry_index)
8055 .ok_or(FormatError::InvalidArchive(
8056 "DirectoryHintEntry shard rows are missing",
8057 ))?;
8058 actual.push((path.to_vec(), rows.to_vec()));
8059 }
8060 }
8061
8062 if actual != sorted_directory_hint_rows(expected) {
8063 return Err(FormatError::InvalidArchive(
8064 "directory hint map does not match decoded files",
8065 ));
8066 }
8067 Ok(())
8068}
8069
8070fn sorted_directory_hint_rows(map: &DirectoryHintMap) -> Vec<(Vec<u8>, Vec<u32>)> {
8071 let mut rows = map
8072 .iter()
8073 .map(|(path, shard_rows)| {
8074 (
8075 path.clone(),
8076 shard_rows.iter().copied().collect::<Vec<u32>>(),
8077 )
8078 })
8079 .collect::<Vec<_>>();
8080 rows.sort_by(|(left_path, _), (right_path, _)| {
8081 hash_prefix(left_path)
8082 .cmp(&hash_prefix(right_path))
8083 .then_with(|| left_path.cmp(right_path))
8084 });
8085 rows
8086}
8087
8088fn validate_exact_coverage_ranges(
8089 ranges: &mut [(usize, usize)],
8090 expected_end: usize,
8091 reason: &'static str,
8092) -> Result<(), FormatError> {
8093 ranges.sort_unstable();
8094 let mut cursor = 0usize;
8095 for (start, end) in ranges.iter().copied() {
8096 if start != cursor || end < start {
8097 return Err(FormatError::InvalidArchive(reason));
8098 }
8099 cursor = end;
8100 }
8101 if cursor != expected_end {
8102 return Err(FormatError::InvalidArchive(reason));
8103 }
8104 Ok(())
8105}
8106
8107fn validate_exact_coverage_ranges_u64(
8108 ranges: &mut [(u64, u64)],
8109 expected_end: u64,
8110 reason: &'static str,
8111) -> Result<(), FormatError> {
8112 ranges.sort_unstable();
8113 let mut cursor = 0u64;
8114 for (start, end) in ranges.iter().copied() {
8115 if start != cursor || end < start {
8116 return Err(FormatError::InvalidArchive(reason));
8117 }
8118 cursor = end;
8119 }
8120 if cursor != expected_end {
8121 return Err(FormatError::InvalidArchive(reason));
8122 }
8123 Ok(())
8124}
8125
8126fn object_block_range(
8127 first_block_index: u64,
8128 data_block_count: u32,
8129 parity_block_count: u32,
8130 structure: &'static str,
8131) -> Result<(u64, u64), FormatError> {
8132 let total = data_block_count as u64 + parity_block_count as u64;
8133 if total == 0 {
8134 return Err(FormatError::InvalidArchive(structure));
8135 }
8136 let end = checked_u64_add(first_block_index, total, structure)?;
8137 Ok((first_block_index, end))
8138}
8139
8140fn validate_non_overlapping_object_ranges(ranges: &mut [(u64, u64)]) -> Result<(), FormatError> {
8141 ranges.sort_unstable();
8142 for pair in ranges.windows(2) {
8143 if pair[0].1 > pair[1].0 {
8144 return Err(FormatError::InvalidArchive(
8145 "encrypted object block ranges overlap",
8146 ));
8147 }
8148 }
8149 Ok(())
8150}
8151
8152pub(crate) fn observed_archive_size(
8153 sizes: impl IntoIterator<Item = u64>,
8154) -> Result<u64, FormatError> {
8155 sizes.into_iter().try_fold(0u64, |sum, size| {
8156 sum.checked_add(size).ok_or(FormatError::InvalidArchive(
8157 "observed archive size overflow",
8158 ))
8159 })
8160}
8161
8162pub(crate) fn total_extraction_size_cap(
8163 options: ReaderOptions,
8164 observed_archive_bytes: u64,
8165) -> u64 {
8166 options
8167 .max_total_extraction_size
8168 .min(observed_archive_bytes.saturating_mul(10))
8169}
8170
8171fn utf8_path(bytes: &[u8]) -> Result<String, FormatError> {
8172 std::str::from_utf8(bytes)
8173 .map(|path| path.to_owned())
8174 .map_err(|_| FormatError::UnsafeArchivePath)
8175}
8176
8177fn manifest_footer_global_pre_hmac_bytes(manifest_footer: &ManifestFooter) -> [u8; 104] {
8178 let mut bytes = [0u8; 104];
8179 bytes.copy_from_slice(&manifest_footer.to_bytes()[..104]);
8180 bytes[36..40].fill(0);
8181 bytes
8182}
8183
8184fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
8185 let digest = Sha256::digest(bytes);
8186 let mut out = [0u8; 32];
8187 out.copy_from_slice(&digest);
8188 out
8189}
8190
8191fn slice<'b>(
8192 bytes: &'b [u8],
8193 offset: usize,
8194 len: usize,
8195 structure: &'static str,
8196) -> Result<&'b [u8], FormatError> {
8197 let end = checked_add(offset, len, structure)?;
8198 bytes.get(offset..end).ok_or(FormatError::InvalidLength {
8199 structure,
8200 expected: end,
8201 actual: bytes.len(),
8202 })
8203}
8204
8205fn read_at_vec(
8206 reader: &dyn ArchiveReadAt,
8207 offset: u64,
8208 len: usize,
8209 structure: &'static str,
8210) -> Result<Vec<u8>, FormatError> {
8211 let expected_end = offset
8212 .checked_add(len as u64)
8213 .ok_or(FormatError::InvalidArchive("archive read range overflow"))?;
8214 let observed_len = reader.len()?;
8215 if expected_end > observed_len {
8216 return Err(FormatError::InvalidLength {
8217 structure,
8218 expected: to_usize(expected_end, structure)?,
8219 actual: to_usize(observed_len, structure)?,
8220 });
8221 }
8222 let mut out = vec![0u8; len];
8223 reader.read_exact_at(offset, &mut out)?;
8224 Ok(out)
8225}
8226
8227fn parallel_map_ref<T, U, F>(items: &[T], jobs: usize, f: F) -> Result<Vec<U>, FormatError>
8228where
8229 T: Sync,
8230 U: Send,
8231 F: Fn(&T) -> Result<U, FormatError> + Sync,
8232{
8233 if jobs <= 1 || items.len() <= 1 {
8234 return items.iter().map(f).collect();
8235 }
8236 let worker_count = jobs.min(items.len());
8237 let chunk_size = items.len().div_ceil(worker_count);
8238 let mut out = Vec::with_capacity(items.len());
8239 thread::scope(|scope| {
8240 let handles = items
8241 .chunks(chunk_size)
8242 .map(|chunk| scope.spawn(|| chunk.iter().map(&f).collect::<Result<Vec<_>, _>>()))
8243 .collect::<Vec<_>>();
8244 for handle in handles {
8245 let mut chunk = handle
8246 .join()
8247 .map_err(|_| FormatError::InvalidArchive("reader worker panicked"))??;
8248 out.append(&mut chunk);
8249 }
8250 Ok(out)
8251 })
8252}
8253
8254fn checked_add(lhs: usize, rhs: usize, structure: &'static str) -> Result<usize, FormatError> {
8255 lhs.checked_add(rhs)
8256 .ok_or(FormatError::InvalidArchive(structure))
8257}
8258
8259fn checked_u64_add(lhs: u64, rhs: u64, structure: &'static str) -> Result<u64, FormatError> {
8260 lhs.checked_add(rhs)
8261 .ok_or(FormatError::InvalidArchive(structure))
8262}
8263
8264fn to_usize(value: u64, structure: &'static str) -> Result<usize, FormatError> {
8265 usize::try_from(value).map_err(|_| FormatError::InvalidArchive(structure))
8266}
8267
8268#[cfg(test)]
8269mod tests {
8270 use std::fs;
8271
8272 use super::*;
8273 use crate::compression::compress_zstd_frame;
8274 use crate::crypto::{compute_hmac, encrypt_padded_aead_object};
8275 use crate::fec::encode_parity_gf16;
8276 use crate::format::{
8277 AeadAlgo, CompressionAlgo, FecAlgo, KdfAlgo, CRYPTO_HEADER_FIXED_LEN, FORMAT_VERSION,
8278 VOLUME_FORMAT_REV,
8279 };
8280 use crate::metadata::{
8281 DirectoryHintEntry, DirectoryHintTableHeader, IndexRootHeader, IndexShardHeader,
8282 ENVELOPE_ENTRY_LEN, FILE_ENTRY_LEN, FRAME_ENTRY_LEN, INDEX_SHARD_HEADER_LEN,
8283 };
8284 use crate::non_seekable_reader::{
8285 extract_non_seekable_stream_to_dir, list_non_seekable_stream, verify_non_seekable_stream,
8286 verify_non_seekable_stream_with_bootstrap_sidecar, verify_non_seekable_stream_with_options,
8287 NonSeekableReaderOptions, SequentialRootAuthStatus,
8288 };
8289 use crate::writer::{
8290 write_archive, write_archive_with_dictionary, write_archive_with_kdf,
8291 write_archive_with_root_auth, RegularFile, RootAuthSigningRequest, RootAuthWriterConfig,
8292 WriterOptions,
8293 };
8294
8295 fn master_key() -> MasterKey {
8296 MasterKey::from_raw_key(&[0x42; 32]).unwrap()
8297 }
8298
8299 #[test]
8300 fn reader_defaults_use_available_parallelism_jobs() {
8301 let options = ReaderOptions::default();
8302
8303 assert_eq!(options.jobs, default_jobs());
8304 assert!(options.jobs >= 1);
8305 }
8306
8307 #[test]
8308 fn reader_options_reject_zero_jobs() {
8309 let err = OpenedArchive::open_with_options(
8310 &[],
8311 &master_key(),
8312 ReaderOptions {
8313 jobs: 0,
8314 ..ReaderOptions::default()
8315 },
8316 )
8317 .unwrap_err();
8318
8319 assert_eq!(
8320 err,
8321 FormatError::ReaderUnsupported("jobs must be at least 1")
8322 );
8323 }
8324
8325 const TEST_ROOT_AUTH_ID: u16 = 0xe001;
8326 const TEST_ROOT_AUTH_VALUE_LEN: u32 = 32;
8327
8328 fn test_root_auth_config() -> RootAuthWriterConfig<'static> {
8329 RootAuthWriterConfig {
8330 authenticator_id: TEST_ROOT_AUTH_ID,
8331 signer_identity_type: 0,
8332 signer_identity: &[],
8333 authenticator_value_length: TEST_ROOT_AUTH_VALUE_LEN,
8334 }
8335 }
8336
8337 fn test_root_auth_value(request: &RootAuthSigningRequest) -> Vec<u8> {
8338 request.archive_root.to_vec()
8339 }
8340
8341 fn test_root_auth_verifies(footer: &RootAuthFooterV1, archive_root: &[u8; 32]) -> bool {
8342 footer.authenticator_id == TEST_ROOT_AUTH_ID
8343 && footer.signer_identity_type == 0
8344 && footer.signer_identity_bytes.is_empty()
8345 && footer.authenticator_value.as_slice() == archive_root
8346 }
8347
8348 fn dictionary() -> &'static [u8] {
8349 b"dir/dict.txt common words common words common words dictionary payload"
8350 }
8351
8352 #[derive(Clone)]
8353 struct CountingReadAt {
8354 bytes: std::sync::Arc<Vec<u8>>,
8355 reads: std::sync::Arc<std::sync::Mutex<Vec<(u64, u64)>>>,
8356 denied_ranges: std::sync::Arc<Vec<(u64, u64)>>,
8357 }
8358
8359 impl CountingReadAt {
8360 fn new(bytes: Vec<u8>, denied_ranges: Vec<(u64, u64)>) -> Self {
8361 Self {
8362 bytes: std::sync::Arc::new(bytes),
8363 reads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
8364 denied_ranges: std::sync::Arc::new(denied_ranges),
8365 }
8366 }
8367
8368 fn reads(&self) -> Vec<(u64, u64)> {
8369 self.reads.lock().unwrap().clone()
8370 }
8371 }
8372
8373 impl ArchiveReadAt for CountingReadAt {
8374 fn len(&self) -> Result<u64, FormatError> {
8375 u64::try_from(self.bytes.as_ref().len())
8376 .map_err(|_| FormatError::InvalidArchive("archive length overflow"))
8377 }
8378
8379 fn read_exact_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), FormatError> {
8380 let end = checked_u64_add(offset, buf.len() as u64, "archive read range overflow")?;
8381 self.reads.lock().unwrap().push((offset, end));
8382 if self
8383 .denied_ranges
8384 .iter()
8385 .any(|(start, limit)| ranges_overlap(offset, end, *start, *limit))
8386 {
8387 return Err(FormatError::InvalidArchive("denied test read"));
8388 }
8389 let start = to_usize(offset, "archive")?;
8390 let end_usize = checked_add(start, buf.len(), "archive")?;
8391 let source = self
8392 .bytes
8393 .get(start..end_usize)
8394 .ok_or(FormatError::InvalidLength {
8395 structure: "archive",
8396 expected: end_usize,
8397 actual: self.bytes.as_ref().len(),
8398 })?;
8399 buf.copy_from_slice(source);
8400 Ok(())
8401 }
8402 }
8403
8404 fn ranges_overlap(left_start: u64, left_end: u64, right_start: u64, right_end: u64) -> bool {
8405 left_start < right_end && right_start < left_end
8406 }
8407
8408 fn single_stream_options() -> WriterOptions {
8409 WriterOptions {
8410 stripe_width: 1,
8411 volume_loss_tolerance: 0,
8412 ..WriterOptions::default()
8413 }
8414 }
8415
8416 struct ChunkedReader {
8417 bytes: Vec<u8>,
8418 cursor: usize,
8419 max_chunk: usize,
8420 }
8421
8422 impl ChunkedReader {
8423 fn new(bytes: Vec<u8>, max_chunk: usize) -> Self {
8424 Self {
8425 bytes,
8426 cursor: 0,
8427 max_chunk,
8428 }
8429 }
8430 }
8431
8432 impl std::io::Read for ChunkedReader {
8433 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
8434 if self.cursor >= self.bytes.len() {
8435 return Ok(0);
8436 }
8437 let available = self.bytes.len() - self.cursor;
8438 let len = available.min(buf.len()).min(self.max_chunk);
8439 buf[..len].copy_from_slice(&self.bytes[self.cursor..self.cursor + len]);
8440 self.cursor += len;
8441 Ok(len)
8442 }
8443 }
8444
8445 #[test]
8446 fn global_file_table_key_step_rejects_distinct_path_regression() {
8447 let previous = ([1u8; 8], b"b.txt".to_vec(), 0);
8448 let current = ([1u8; 8], b"a.txt".to_vec(), 0);
8449
8450 assert_eq!(
8451 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
8452 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8453 );
8454 }
8455
8456 #[test]
8457 fn global_file_table_key_step_rejects_duplicate_full_key() {
8458 let previous = ([1u8; 8], b"a.txt".to_vec(), 7);
8459 let current = ([1u8; 8], b"a.txt".to_vec(), 7);
8460
8461 assert_eq!(
8462 validate_global_file_table_key_step(Some(&previous), ¤t).unwrap_err(),
8463 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
8464 );
8465 }
8466
8467 fn small_block_recovery_options() -> WriterOptions {
8468 WriterOptions {
8469 block_size: 4096,
8470 chunk_size: 32 * 1024,
8471 envelope_target_size: 32 * 1024,
8472 stripe_width: 1,
8473 volume_loss_tolerance: 0,
8474 bit_rot_buffer_pct: 1,
8475 fec_data_shards: 16,
8476 fec_parity_shards: 1,
8477 index_fec_data_shards: 4,
8478 index_fec_parity_shards: 1,
8479 index_root_fec_data_shards: 16,
8480 index_root_fec_parity_shards: 1,
8481 ..WriterOptions::default()
8482 }
8483 }
8484
8485 fn parity_rich_recovery_options() -> WriterOptions {
8486 WriterOptions {
8487 block_size: 4096,
8488 chunk_size: 32 * 1024,
8489 envelope_target_size: 32 * 1024,
8490 stripe_width: 1,
8491 volume_loss_tolerance: 0,
8492 bit_rot_buffer_pct: 40,
8493 fec_data_shards: 16,
8494 fec_parity_shards: 16,
8495 index_fec_data_shards: 4,
8496 index_fec_parity_shards: 4,
8497 index_root_fec_data_shards: 16,
8498 index_root_fec_parity_shards: 16,
8499 ..WriterOptions::default()
8500 }
8501 }
8502
8503 fn pseudo_random_bytes(len: usize) -> Vec<u8> {
8504 let mut state = 0x1234_5678u32;
8505 (0..len)
8506 .map(|_| {
8507 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
8508 (state >> 24) as u8
8509 })
8510 .collect()
8511 }
8512
8513 #[test]
8514 fn opens_lists_verifies_and_extracts_one_file_archive() {
8515 let archive = write_archive(
8516 &[RegularFile::new("dir/hello.txt", b"hello m7")],
8517 &master_key(),
8518 single_stream_options(),
8519 )
8520 .unwrap();
8521 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8522
8523 assert_eq!(
8524 opened.list_files().unwrap(),
8525 vec![ArchiveEntry {
8526 path: "dir/hello.txt".to_string(),
8527 file_data_size: 8,
8528 kind: TarEntryKind::Regular,
8529 mode: 0o644,
8530 mtime: 0,
8531 diagnostics: Vec::new(),
8532 }]
8533 );
8534 opened.verify().unwrap();
8535 assert_eq!(
8536 opened.extract_file("dir/hello.txt").unwrap(),
8537 Some(b"hello m7".to_vec())
8538 );
8539 assert_eq!(opened.extract_file("missing.txt").unwrap(), None);
8540 }
8541
8542 #[test]
8543 fn root_auth_archive_round_trips_and_verifies_with_callback() {
8544 let archive = write_archive_with_root_auth(
8545 &[RegularFile::new("signed.txt", b"root-auth payload")],
8546 &master_key(),
8547 single_stream_options(),
8548 RootAuthWriterConfig {
8549 authenticator_id: 0x7777,
8550 signer_identity_type: 1,
8551 signer_identity: b"test signer",
8552 authenticator_value_length: 32,
8553 },
8554 |request| Ok(request.archive_root.to_vec()),
8555 )
8556 .unwrap();
8557
8558 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8559 opened.verify().unwrap();
8560 let verified = opened
8561 .verify_root_auth_with(|footer, archive_root| {
8562 Ok(footer.authenticator_value == archive_root.as_slice())
8563 })
8564 .unwrap();
8565
8566 assert_eq!(verified.authenticator_id, 0x7777);
8567 assert_eq!(verified.signer_identity_type, 1);
8568 assert_eq!(verified.signer_identity_bytes, b"test signer");
8569 assert_eq!(
8570 verified.archive_root,
8571 opened.root_auth_footer.as_ref().unwrap().archive_root
8572 );
8573 assert_eq!(
8574 verified.diagnostics,
8575 vec![
8576 RootAuthDiagnostic::RootAuthContentVerified,
8577 RootAuthDiagnostic::AuthenticatedMetadataNotRootSigned,
8578 RootAuthDiagnostic::RecoveryMarginNotRootAuthenticated,
8579 RootAuthDiagnostic::RecoveryMarginUnchecked,
8580 ]
8581 );
8582 }
8583
8584 #[test]
8585 fn root_auth_verification_requires_authenticator_success() {
8586 let archive = write_archive_with_root_auth(
8587 &[RegularFile::new("signed.txt", b"root-auth payload")],
8588 &master_key(),
8589 single_stream_options(),
8590 RootAuthWriterConfig {
8591 authenticator_id: 9,
8592 signer_identity_type: 1,
8593 signer_identity: b"test signer",
8594 authenticator_value_length: 32,
8595 },
8596 |request| Ok(request.archive_root.to_vec()),
8597 )
8598 .unwrap();
8599 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
8600
8601 assert_eq!(
8602 opened.verify_root_auth_with(|_, _| Ok(false)).unwrap_err(),
8603 FormatError::InvalidArchive("root-auth authenticator verification failed")
8604 );
8605 }
8606
8607 #[test]
8608 fn public_no_key_verifies_encrypted_data_block_commitment_with_callback() {
8609 let archive = write_archive_with_root_auth(
8610 &[RegularFile::new("public.txt", b"public commitment")],
8611 &master_key(),
8612 single_stream_options(),
8613 RootAuthWriterConfig {
8614 authenticator_id: 0x2222,
8615 signer_identity_type: 1,
8616 signer_identity: b"public verifier",
8617 authenticator_value_length: 32,
8618 },
8619 |request| Ok(request.archive_root.to_vec()),
8620 )
8621 .unwrap();
8622
8623 let verified = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
8624 Ok(footer.authenticator_value == archive_root.as_slice())
8625 })
8626 .unwrap();
8627
8628 assert_eq!(verified.authenticator_id, 0x2222);
8629 assert_eq!(verified.signer_identity_bytes, b"public verifier");
8630 assert!(verified.total_data_block_count > 0);
8631 }
8632
8633 #[test]
8634 fn public_no_key_ignores_untrusted_manifest_and_trailer_block_count_fields() {
8635 let archive = write_archive_with_root_auth(
8636 &[RegularFile::new(
8637 "public-fields.txt",
8638 b"public source authority",
8639 )],
8640 &master_key(),
8641 single_stream_options(),
8642 RootAuthWriterConfig {
8643 authenticator_id: 0x2222,
8644 signer_identity_type: 1,
8645 signer_identity: b"public verifier",
8646 authenticator_value_length: 32,
8647 },
8648 |request| Ok(request.archive_root.to_vec()),
8649 )
8650 .unwrap();
8651 let mut bytes = archive.bytes.clone();
8652
8653 rewrite_public_cmra_image(&mut bytes, |image| {
8654 let manifest_region = image
8655 .regions
8656 .iter_mut()
8657 .find(|region| region.region_type == 3)
8658 .unwrap();
8659 manifest_region.bytes[44..48].copy_from_slice(&99u32.to_le_bytes());
8660
8661 let trailer_region = image
8662 .regions
8663 .iter_mut()
8664 .find(|region| region.region_type == 5)
8665 .unwrap();
8666 let mut trailer = VolumeTrailer::parse(&trailer_region.bytes).unwrap();
8667 trailer.block_count += 7;
8668 trailer_region.bytes = trailer.to_bytes().to_vec();
8669 });
8670
8671 public_no_key_verify_archive_with(&bytes, |footer, archive_root| {
8672 Ok(footer.authenticator_value == archive_root.as_slice())
8673 })
8674 .unwrap();
8675 }
8676
8677 #[test]
8678 fn public_no_key_compares_only_public_crypto_profile_across_volumes() {
8679 let archive = write_archive_with_root_auth(
8680 &[RegularFile::new(
8681 "public-crypto.txt",
8682 b"cross-volume public profile",
8683 )],
8684 &master_key(),
8685 WriterOptions {
8686 stripe_width: 2,
8687 volume_loss_tolerance: 0,
8688 ..WriterOptions::default()
8689 },
8690 RootAuthWriterConfig {
8691 authenticator_id: 0x3333,
8692 signer_identity_type: 1,
8693 signer_identity: b"public verifier",
8694 authenticator_value_length: 32,
8695 },
8696 |request| Ok(request.archive_root.to_vec()),
8697 )
8698 .unwrap();
8699 let mut volumes = archive.volumes.clone();
8700 let volume_header = VolumeHeader::parse(&volumes[1][..VOLUME_HEADER_LEN]).unwrap();
8701 let crypto_offset = volume_header.crypto_header_offset as usize;
8702 let expected_volume_size = 123_456_789u64;
8703 volumes[1][crypto_offset + 52..crypto_offset + 60]
8704 .copy_from_slice(&expected_volume_size.to_le_bytes());
8705 rewrite_public_cmra_image(&mut volumes[1], |image| {
8706 let crypto_region = image
8707 .regions
8708 .iter_mut()
8709 .find(|region| region.region_type == 2)
8710 .unwrap();
8711 crypto_region.bytes[52..60].copy_from_slice(&expected_volume_size.to_le_bytes());
8712 });
8713
8714 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
8715 public_no_key_verify_volumes_with(&volume_refs, |footer, archive_root| {
8716 Ok(footer.authenticator_value == archive_root.as_slice())
8717 })
8718 .unwrap();
8719 }
8720
8721 #[test]
8722 fn locator_based_cmra_recovery_treats_header_damage_as_recoverable() {
8723 let archive = write_archive_with_root_auth(
8724 &[RegularFile::new("cmra-header.txt", b"header fallback")],
8725 &master_key(),
8726 single_stream_options(),
8727 RootAuthWriterConfig {
8728 authenticator_id: 0x4444,
8729 signer_identity_type: 1,
8730 signer_identity: b"public verifier",
8731 authenticator_value_length: 32,
8732 },
8733 |request| Ok(request.archive_root.to_vec()),
8734 )
8735 .unwrap();
8736 let final_locator = final_recovery_locator(&archive.bytes);
8737
8738 let mut bad_crc = archive.bytes.clone();
8739 let crc_offset =
8740 final_locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN - 1;
8741 bad_crc[crc_offset] ^= 0x55;
8742 public_no_key_verify_archive_with(&bad_crc, |footer, archive_root| {
8743 Ok(footer.authenticator_value == archive_root.as_slice())
8744 })
8745 .unwrap();
8746
8747 let mut bad_magic = archive.bytes.clone();
8748 bad_magic[final_locator.cmra_offset as usize] ^= 0x55;
8749 public_no_key_verify_archive_with(&bad_magic, |footer, archive_root| {
8750 Ok(footer.authenticator_value == archive_root.as_slice())
8751 })
8752 .unwrap();
8753
8754 let mut bad_hint = archive.bytes.clone();
8755 bad_hint[crc_offset] ^= 0xAA;
8756 for offset in [
8757 bad_hint.len() - LOCATOR_PAIR_LEN,
8758 bad_hint.len() - CRITICAL_RECOVERY_LOCATOR_LEN,
8759 ] {
8760 let mut locator = CriticalRecoveryLocator::parse(
8761 &bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8762 )
8763 .unwrap();
8764 locator.volume_index_hint += 1;
8765 bad_hint[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN]
8766 .copy_from_slice(&locator.to_bytes());
8767 }
8768 assert_eq!(
8769 public_no_key_verify_archive_with(&bad_hint, |_, _| Ok(true)).unwrap_err(),
8770 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
8771 );
8772 }
8773
8774 #[test]
8775 fn recovers_physical_volume_header_magic_from_cmra_authority() {
8776 let payload = b"front header authority".to_vec();
8777 let archive = write_archive(
8778 &[RegularFile::new("volume-header.txt", &payload)],
8779 &master_key(),
8780 small_block_recovery_options(),
8781 )
8782 .unwrap();
8783
8784 let mut corrupted = archive.bytes;
8785 corrupted[0] ^= 0x55;
8786
8787 let opened = open_archive(&corrupted, &master_key()).unwrap();
8788 assert_eq!(
8789 opened.extract_file("volume-header.txt").unwrap(),
8790 Some(payload)
8791 );
8792 opened.verify().unwrap();
8793 }
8794
8795 #[test]
8796 fn recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8797 let payload = b"crc-valid wrong volume index".to_vec();
8798 let mut options = small_block_recovery_options();
8799 options.stripe_width = 2;
8800 options.volume_loss_tolerance = 0;
8801 let archive = write_archive(
8802 &[RegularFile::new("volume-index.txt", &payload)],
8803 &master_key(),
8804 options,
8805 )
8806 .unwrap();
8807
8808 let mut corrupted = archive.volumes[0].clone();
8809 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8810 assert_eq!(header.volume_index, 0);
8811 header.volume_index = 1;
8812 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8813 assert_eq!(
8814 VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN])
8815 .unwrap()
8816 .volume_index,
8817 1
8818 );
8819
8820 let opened = open_archive_volumes(
8821 &[corrupted.as_slice(), archive.volumes[1].as_slice()],
8822 &master_key(),
8823 )
8824 .unwrap();
8825 assert_eq!(opened.volume_header.volume_index, 0);
8826 assert_eq!(
8827 opened.extract_file("volume-index.txt").unwrap(),
8828 Some(payload)
8829 );
8830 opened.verify().unwrap();
8831 }
8832
8833 #[test]
8834 fn recovers_physical_crypto_header_magic_from_cmra_authority() {
8835 let payload = b"crypto header authority".to_vec();
8836 let archive = write_archive(
8837 &[RegularFile::new("crypto-header.txt", &payload)],
8838 &master_key(),
8839 small_block_recovery_options(),
8840 )
8841 .unwrap();
8842 let crypto_offset = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN])
8843 .unwrap()
8844 .crypto_header_offset;
8845
8846 let mut corrupted = archive.bytes;
8847 corrupted[crypto_offset as usize] ^= 0x55;
8848
8849 let opened = open_archive(&corrupted, &master_key()).unwrap();
8850 assert_eq!(
8851 opened.extract_file("crypto-header.txt").unwrap(),
8852 Some(payload)
8853 );
8854 opened.verify().unwrap();
8855 }
8856
8857 #[test]
8858 fn read_at_api_recovers_physical_header_magic_from_cmra_authority() {
8859 let payload = b"read-at header authority".to_vec();
8860 let archive = write_archive(
8861 &[RegularFile::new("read-at-header.txt", &payload)],
8862 &master_key(),
8863 small_block_recovery_options(),
8864 )
8865 .unwrap();
8866
8867 let mut corrupted = archive.bytes;
8868 corrupted[0] ^= 0x55;
8869
8870 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
8871 assert_eq!(
8872 opened.extract_file("read-at-header.txt").unwrap(),
8873 Some(payload)
8874 );
8875 opened.verify().unwrap();
8876 }
8877
8878 #[test]
8879 fn read_at_api_recovers_crc_valid_physical_volume_index_from_cmra_authority() {
8880 let payload = b"read-at crc-valid wrong volume index".to_vec();
8881 let mut options = small_block_recovery_options();
8882 options.stripe_width = 2;
8883 options.volume_loss_tolerance = 0;
8884 let archive = write_archive(
8885 &[RegularFile::new("read-at-volume-index.txt", &payload)],
8886 &master_key(),
8887 options,
8888 )
8889 .unwrap();
8890
8891 let mut corrupted = archive.volumes[0].clone();
8892 let mut header = VolumeHeader::parse(&corrupted[..VOLUME_HEADER_LEN]).unwrap();
8893 assert_eq!(header.volume_index, 0);
8894 header.volume_index = 1;
8895 corrupted[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
8896
8897 let opened = open_seekable_archive_volumes(
8898 vec![corrupted, archive.volumes[1].clone()],
8899 &master_key(),
8900 )
8901 .unwrap();
8902 assert_eq!(opened.volume_header.volume_index, 0);
8903 assert_eq!(
8904 opened.extract_file("read-at-volume-index.txt").unwrap(),
8905 Some(payload)
8906 );
8907 opened.verify().unwrap();
8908 }
8909
8910 #[test]
8911 fn recovers_cmra_header_magic_from_locator_tuple() {
8912 let payload = b"cmra header authority".to_vec();
8913 let archive = write_archive(
8914 &[RegularFile::new("cmra-header-magic.txt", &payload)],
8915 &master_key(),
8916 small_block_recovery_options(),
8917 )
8918 .unwrap();
8919 let locator = final_recovery_locator(&archive.bytes);
8920
8921 let mut corrupted = archive.bytes;
8922 corrupted[locator.cmra_offset as usize] ^= 0x55;
8923
8924 let opened = open_archive(&corrupted, &master_key()).unwrap();
8925 assert_eq!(
8926 opened.extract_file("cmra-header-magic.txt").unwrap(),
8927 Some(payload)
8928 );
8929 opened.verify().unwrap();
8930 }
8931
8932 #[test]
8933 fn recovers_cmra_shard_magic_as_erasure() {
8934 let payload = b"cmra shard authority".to_vec();
8935 let archive = write_archive(
8936 &[RegularFile::new("cmra-shard-magic.txt", &payload)],
8937 &master_key(),
8938 small_block_recovery_options(),
8939 )
8940 .unwrap();
8941 let locator = final_recovery_locator(&archive.bytes);
8942 let first_shard_offset =
8943 locator.cmra_offset as usize + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
8944
8945 let mut corrupted = archive.bytes;
8946 corrupted[first_shard_offset] ^= 0x55;
8947
8948 let opened = open_archive(&corrupted, &master_key()).unwrap();
8949 assert_eq!(
8950 opened.extract_file("cmra-shard-magic.txt").unwrap(),
8951 Some(payload)
8952 );
8953 opened.verify().unwrap();
8954 }
8955
8956 #[test]
8957 fn key_holding_rejects_cmra_below_authenticated_parity_floor() {
8958 let archive = write_archive(
8959 &[RegularFile::new(
8960 "cmra-floor.txt",
8961 b"authenticated CMRA floor",
8962 )],
8963 &master_key(),
8964 single_stream_options(),
8965 )
8966 .unwrap();
8967 let malformed = rewrite_cmra_parity_count(&archive.bytes, 1);
8968 let final_offset = malformed.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
8969 let locator = CriticalRecoveryLocator::parse(
8970 &malformed[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
8971 )
8972 .unwrap();
8973 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
8974 let crypto_start = volume_header.crypto_header_offset as usize;
8975 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
8976 let crypto_header = CryptoHeader::parse(
8977 &malformed[crypto_start..crypto_end],
8978 volume_header.crypto_header_length,
8979 )
8980 .unwrap();
8981 let subkeys = Subkeys::derive(
8982 &master_key(),
8983 &volume_header.archive_uuid,
8984 &volume_header.session_id,
8985 )
8986 .unwrap();
8987
8988 assert_eq!(
8989 parse_locator_cmra_candidate(
8990 &malformed,
8991 final_offset,
8992 locator,
8993 KeyHoldingTerminalContext {
8994 subkeys: &subkeys,
8995 volume_header: &volume_header,
8996 crypto_header: &crypto_header.fixed,
8997 crypto_header_bytes: &malformed[crypto_start..crypto_end],
8998 },
8999 )
9000 .unwrap_err(),
9001 FormatError::InvalidArchive(
9002 "CMRA parity shard count is below authenticated bit-rot lower bound"
9003 )
9004 );
9005 assert!(open_archive(&malformed, &master_key()).is_err());
9006 }
9007
9008 #[test]
9009 fn locator_tuple_bounds_are_checked_before_locator_position_fields() {
9010 let archive = write_archive(
9011 &[RegularFile::new(
9012 "locator-order.txt",
9013 b"locator tuple first",
9014 )],
9015 &master_key(),
9016 single_stream_options(),
9017 )
9018 .unwrap();
9019 let final_offset = archive.bytes.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
9020 let mut locator = final_recovery_locator(&archive.bytes);
9021 locator.cmra_shard_size = 513;
9022 locator.body_bytes_before_cmra = locator.cmra_offset + 1;
9023 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9024 let crypto_start = volume_header.crypto_header_offset as usize;
9025 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
9026 let crypto_header = CryptoHeader::parse(
9027 &archive.bytes[crypto_start..crypto_end],
9028 volume_header.crypto_header_length,
9029 )
9030 .unwrap();
9031 let subkeys = Subkeys::derive(
9032 &master_key(),
9033 &volume_header.archive_uuid,
9034 &volume_header.session_id,
9035 )
9036 .unwrap();
9037
9038 assert_eq!(
9039 parse_locator_cmra_candidate(
9040 &archive.bytes,
9041 final_offset,
9042 locator,
9043 KeyHoldingTerminalContext {
9044 subkeys: &subkeys,
9045 volume_header: &volume_header,
9046 crypto_header: &crypto_header.fixed,
9047 crypto_header_bytes: &archive.bytes[crypto_start..crypto_end],
9048 },
9049 )
9050 .unwrap_err(),
9051 FormatError::InvalidArchive("CMRA shard_size is invalid")
9052 );
9053 }
9054
9055 #[test]
9056 fn sequential_extract_rejects_bytes_after_terminal_locator() {
9057 let archive = write_archive(
9058 &[RegularFile::new("seq.txt", b"sequential EOF")],
9059 &master_key(),
9060 single_stream_options(),
9061 )
9062 .unwrap();
9063 let mut appended = archive.bytes.clone();
9064 appended.extend_from_slice(&[0xAA; 32]);
9065
9066 assert_eq!(
9067 sequential_extract_tar_stream(&appended, &master_key()).unwrap_err(),
9068 FormatError::InvalidArchive("sequential terminal does not end at EOF")
9069 );
9070 }
9071
9072 #[test]
9073 fn global_file_table_order_rejects_cross_shard_duplicate_reversal() {
9074 let first = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 2048);
9075 let second = (hash_prefix(b"dup.txt"), b"dup.txt".to_vec(), 1024);
9076
9077 assert_eq!(
9078 validate_global_file_table_key_step(Some(&first), &second).unwrap_err(),
9079 FormatError::InvalidArchive("global FileEntry rows are not sorted and unique")
9080 );
9081 }
9082
9083 #[test]
9084 fn root_auth_verifies_key_holding_and_public_no_key_modes() {
9085 let archive = write_archive_with_root_auth(
9086 &[RegularFile::new("signed.txt", b"ed25519 payload")],
9087 &master_key(),
9088 single_stream_options(),
9089 test_root_auth_config(),
9090 |request| Ok(test_root_auth_value(request)),
9091 )
9092 .unwrap();
9093
9094 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9095 let root_auth = opened
9096 .verify_root_auth_with(|footer, archive_root| {
9097 Ok(test_root_auth_verifies(footer, archive_root))
9098 })
9099 .unwrap();
9100 assert_eq!(
9101 root_auth.archive_root,
9102 opened.root_auth_footer.as_ref().unwrap().archive_root
9103 );
9104
9105 let public = public_no_key_verify_archive_with(&archive.bytes, |footer, archive_root| {
9106 Ok(test_root_auth_verifies(footer, archive_root))
9107 })
9108 .unwrap();
9109 assert_eq!(public.archive_root, root_auth.archive_root);
9110 assert_eq!(
9111 public.diagnostics,
9112 vec![
9113 PublicNoKeyDiagnostic::PublicDataBlockCommitmentVerified,
9114 PublicNoKeyDiagnostic::PublicPhysicalCompletenessUnverified,
9115 PublicNoKeyDiagnostic::PublicRecoveryMarginUnchecked,
9116 ]
9117 );
9118 }
9119
9120 #[test]
9121 fn root_auth_verifies_with_tolerated_missing_volume_after_fec_repair() {
9122 let options = WriterOptions {
9123 block_size: 4096,
9124 chunk_size: 16 * 1024,
9125 envelope_target_size: 16 * 1024,
9126 stripe_width: 2,
9127 volume_loss_tolerance: 1,
9128 bit_rot_buffer_pct: 0,
9129 fec_data_shards: 16,
9130 fec_parity_shards: 1,
9131 index_fec_data_shards: 4,
9132 index_fec_parity_shards: 1,
9133 index_root_fec_data_shards: 16,
9134 index_root_fec_parity_shards: 1,
9135 ..WriterOptions::default()
9136 };
9137 let archive = write_archive_with_root_auth(
9138 &[RegularFile::new("missing-volume.txt", b"recover me")],
9139 &master_key(),
9140 options,
9141 test_root_auth_config(),
9142 |request| Ok(test_root_auth_value(request)),
9143 )
9144 .unwrap();
9145
9146 let opened = open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap();
9147 let root_auth = opened
9148 .verify_root_auth_with(|footer, archive_root| {
9149 Ok(test_root_auth_verifies(footer, archive_root))
9150 })
9151 .unwrap();
9152 assert!(root_auth
9153 .diagnostics
9154 .contains(&RootAuthDiagnostic::ReplicatedGlobalCopyUncheckedDueToVolumeLoss));
9155 }
9156
9157 #[test]
9158 fn public_no_key_rejects_unsigned_archives() {
9159 let archive = write_archive(
9160 &[RegularFile::new("plain.txt", b"unsigned")],
9161 &master_key(),
9162 single_stream_options(),
9163 )
9164 .unwrap();
9165
9166 assert_eq!(
9167 public_no_key_verify_archive_with(&archive.bytes, |_, _| Ok(true)).unwrap_err(),
9168 FormatError::InvalidArchive("no valid v41 public CMRA candidate found")
9169 );
9170 }
9171
9172 #[test]
9173 fn unsigned_archive_reports_root_auth_absent() {
9174 let archive = write_archive(
9175 &[RegularFile::new("plain.txt", b"unsigned")],
9176 &master_key(),
9177 single_stream_options(),
9178 )
9179 .unwrap();
9180 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9181
9182 assert_eq!(
9183 opened.verify_root_auth_with(|_, _| Ok(true)).unwrap_err(),
9184 FormatError::ReaderUnsupported("root-auth footer is absent")
9185 );
9186 }
9187
9188 #[test]
9189 fn safe_extract_writes_regular_file_under_root() {
9190 let archive = write_archive(
9191 &[RegularFile::new("dir/hello.txt", b"safe m8")],
9192 &master_key(),
9193 single_stream_options(),
9194 )
9195 .unwrap();
9196 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9197 let tmp = tempfile::tempdir().unwrap();
9198
9199 opened
9200 .extract_file_to(
9201 "dir/hello.txt",
9202 tmp.path(),
9203 SafeExtractionOptions::default(),
9204 )
9205 .unwrap()
9206 .unwrap();
9207
9208 assert_eq!(
9209 std::fs::read(tmp.path().join("dir").join("hello.txt")).unwrap(),
9210 b"safe m8"
9211 );
9212 }
9213
9214 #[test]
9215 fn seekable_extract_all_to_streams_unique_archive() {
9216 let archive = write_archive(
9217 &[
9218 RegularFile::new("alpha.txt", b"alpha"),
9219 RegularFile::new("dir/beta.txt", b"beta"),
9220 ],
9221 &master_key(),
9222 single_stream_options(),
9223 )
9224 .unwrap();
9225 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9226 let tmp = tempfile::tempdir().unwrap();
9227
9228 let diagnostics = opened
9229 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9230 .unwrap();
9231
9232 assert_eq!(diagnostics.len(), 2);
9233 assert_eq!(fs::read(tmp.path().join("alpha.txt")).unwrap(), b"alpha");
9234 assert_eq!(
9235 fs::read(tmp.path().join("dir").join("beta.txt")).unwrap(),
9236 b"beta"
9237 );
9238 }
9239
9240 #[test]
9241 fn seekable_extract_all_to_rejects_duplicate_paths_for_cli_fallback() {
9242 let archive = write_archive(
9243 &[
9244 RegularFile::new("same.txt", b"old"),
9245 RegularFile::new("same.txt", b"new"),
9246 ],
9247 &master_key(),
9248 single_stream_options(),
9249 )
9250 .unwrap();
9251 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9252 let tmp = tempfile::tempdir().unwrap();
9253
9254 assert_eq!(
9255 opened
9256 .extract_all_to(tmp.path(), SafeExtractionOptions::default())
9257 .unwrap_err(),
9258 FormatError::ReaderUnsupported("fast full extract requires unique archive paths")
9259 );
9260 }
9261
9262 #[test]
9263 fn seekable_extract_indexed_files_to_restores_final_duplicate_winner() {
9264 let archive = write_archive(
9265 &[
9266 RegularFile::new("same.txt", b"old"),
9267 RegularFile::new("same.txt", b"new"),
9268 ],
9269 &master_key(),
9270 single_stream_options(),
9271 )
9272 .unwrap();
9273 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9274 let tmp = tempfile::tempdir().unwrap();
9275
9276 let diagnostics = opened
9277 .extract_indexed_files_to(tmp.path(), SafeExtractionOptions::default(), 2)
9278 .unwrap();
9279
9280 assert_eq!(diagnostics.len(), 1);
9281 assert_eq!(diagnostics[0].0, "same.txt");
9282 assert_eq!(fs::read(tmp.path().join("same.txt")).unwrap(), b"new");
9283 }
9284
9285 #[test]
9286 fn safe_extract_rejects_overwriting_existing_file_by_default() {
9287 let archive = write_archive(
9288 &[RegularFile::new("hello.txt", b"new")],
9289 &master_key(),
9290 single_stream_options(),
9291 )
9292 .unwrap();
9293 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9294 let tmp = tempfile::tempdir().unwrap();
9295 std::fs::write(tmp.path().join("hello.txt"), b"old").unwrap();
9296
9297 assert_eq!(
9298 opened
9299 .extract_file_to("hello.txt", tmp.path(), SafeExtractionOptions::default())
9300 .unwrap_err(),
9301 FormatError::UnsafeOverwrite
9302 );
9303 assert_eq!(std::fs::read(tmp.path().join("hello.txt")).unwrap(), b"old");
9304 }
9305
9306 #[test]
9307 fn opens_and_verifies_empty_archive() {
9308 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9309 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9310
9311 assert!(opened.list_files().unwrap().is_empty());
9312 opened.verify().unwrap();
9313 }
9314
9315 #[test]
9316 fn default_reader_options_allow_v36_trailing_garbage_scan() {
9317 let archive = write_archive(
9318 &[RegularFile::new("garbage-tolerant.txt", b"still intact")],
9319 &master_key(),
9320 single_stream_options(),
9321 )
9322 .unwrap();
9323 let mut with_trailing_garbage = archive.bytes.clone();
9324 with_trailing_garbage.extend_from_slice(b"ignored trailing bytes");
9325
9326 let opened = open_archive(&with_trailing_garbage, &master_key()).unwrap();
9327 assert_eq!(
9328 opened.extract_file("garbage-tolerant.txt").unwrap(),
9329 Some(b"still intact".to_vec())
9330 );
9331 }
9332
9333 #[test]
9334 fn seekable_open_rejects_too_small_and_unavailable_header_crypto_bytes() {
9335 assert_eq!(
9336 open_archive(
9337 &[0u8; VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1],
9338 &master_key()
9339 )
9340 .unwrap_err(),
9341 FormatError::InvalidLength {
9342 structure: "archive",
9343 expected: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9344 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN - 1,
9345 }
9346 );
9347
9348 let mut header = test_volume_header();
9349 header.crypto_header_length = 512;
9350 let mut unavailable_crypto = header.to_bytes().to_vec();
9351 unavailable_crypto.resize(VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN, 0);
9352
9353 assert_eq!(
9354 open_archive(&unavailable_crypto, &master_key()).unwrap_err(),
9355 FormatError::InvalidLength {
9356 structure: "CryptoHeader",
9357 expected: VOLUME_HEADER_LEN + 512,
9358 actual: VOLUME_HEADER_LEN + VOLUME_TRAILER_LEN,
9359 }
9360 );
9361 }
9362
9363 #[test]
9364 fn seekable_open_recovers_physical_noncanonical_crypto_header_offset() {
9365 let archive = write_archive(
9366 &[RegularFile::new("offset.txt", b"offset")],
9367 &master_key(),
9368 small_block_recovery_options(),
9369 )
9370 .unwrap();
9371 let mut mutated = archive.bytes;
9372 let mut header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
9373 header.crypto_header_offset = VOLUME_HEADER_LEN as u32 + 1;
9374 mutated[..VOLUME_HEADER_LEN].copy_from_slice(&header.to_bytes());
9375
9376 let opened = open_archive(&mutated, &master_key()).unwrap();
9377 assert_eq!(
9378 opened.volume_header.crypto_header_offset,
9379 VOLUME_HEADER_LEN as u32
9380 );
9381 assert_eq!(
9382 opened.extract_file("offset.txt").unwrap(),
9383 Some(b"offset".to_vec())
9384 );
9385 opened.verify().unwrap();
9386 }
9387
9388 #[test]
9389 fn rejects_wrong_key_before_metadata_release() {
9390 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
9391 let wrong = MasterKey::from_raw_key(&[0x43; 32]).unwrap();
9392
9393 assert_eq!(
9394 open_archive(&archive.bytes, &wrong).unwrap_err(),
9395 FormatError::HmacMismatch {
9396 structure: "CryptoHeader"
9397 }
9398 );
9399 }
9400
9401 #[test]
9402 fn rejects_payload_tamper_even_with_recomputed_block_crc() {
9403 let mut archive = write_archive(
9404 &[RegularFile::new("file.txt", b"authenticated")],
9405 &master_key(),
9406 single_stream_options(),
9407 )
9408 .unwrap()
9409 .bytes;
9410 let volume = VolumeHeader::parse(&archive[..VOLUME_HEADER_LEN]).unwrap();
9411 let crypto_end = VOLUME_HEADER_LEN + usize::try_from(volume.crypto_header_length).unwrap();
9412 let crypto = CryptoHeader::parse(
9413 &archive[VOLUME_HEADER_LEN..crypto_end],
9414 volume.crypto_header_length,
9415 )
9416 .unwrap();
9417 let block_size = crypto.fixed.block_size as usize;
9418 archive[crypto_end + 16] ^= 1;
9419 let crc_offset = crypto_end + 16 + block_size;
9420 let crc = crc32c::crc32c(&archive[crypto_end..crc_offset]);
9421 archive[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9422
9423 let opened = open_archive(&archive, &master_key()).unwrap();
9424 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9425 }
9426
9427 #[test]
9428 fn list_and_extract_use_final_view_for_duplicate_paths() {
9429 let archive = write_archive(
9430 &[
9431 RegularFile::new("same.txt", b"old"),
9432 RegularFile::new("same.txt", b"newer"),
9433 ],
9434 &master_key(),
9435 single_stream_options(),
9436 )
9437 .unwrap();
9438 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9439
9440 assert_eq!(
9441 opened.list_index_entries().unwrap(),
9442 vec![ArchiveIndexEntry {
9443 path: "same.txt".to_string(),
9444 file_data_size: 5,
9445 }]
9446 );
9447 assert_eq!(
9448 opened.lookup_index_entry("same.txt").unwrap(),
9449 Some(ArchiveIndexEntry {
9450 path: "same.txt".to_string(),
9451 file_data_size: 5,
9452 })
9453 );
9454 assert_eq!(opened.lookup_index_entry("missing.txt").unwrap(), None);
9455 assert_eq!(
9456 opened.list_files().unwrap(),
9457 vec![ArchiveEntry {
9458 path: "same.txt".to_string(),
9459 file_data_size: 5,
9460 kind: TarEntryKind::Regular,
9461 mode: 0o644,
9462 mtime: 0,
9463 diagnostics: Vec::new(),
9464 }]
9465 );
9466 assert_eq!(
9467 opened.extract_file("same.txt").unwrap(),
9468 Some(b"newer".to_vec())
9469 );
9470 opened.verify().unwrap();
9471 }
9472
9473 #[test]
9474 fn index_entries_do_not_decrypt_payload_envelopes() {
9475 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9476 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9477
9478 assert_eq!(
9479 opened.list_index_entries().unwrap(),
9480 vec![
9481 ArchiveIndexEntry {
9482 path: "broken.txt".to_string(),
9483 file_data_size: b"broken payload\n".len() as u64,
9484 },
9485 ArchiveIndexEntry {
9486 path: "healthy.txt".to_string(),
9487 file_data_size: b"healthy payload\n".len() as u64,
9488 },
9489 ]
9490 );
9491 assert_eq!(
9492 opened.lookup_index_entry("broken.txt").unwrap(),
9493 Some(ArchiveIndexEntry {
9494 path: "broken.txt".to_string(),
9495 file_data_size: b"broken payload\n".len() as u64,
9496 })
9497 );
9498 assert_eq!(opened.list_files().unwrap_err(), FormatError::AeadFailure);
9499 }
9500
9501 #[test]
9502 fn extract_file_does_not_decrypt_unselected_payload_envelope() {
9503 let (mut opened, broken_payload_block) = multi_envelope_reader_fixture();
9506 corrupt_payload_record(&mut opened.blocks, broken_payload_block);
9507
9508 assert_eq!(
9509 opened.extract_file("healthy.txt").unwrap(),
9510 Some(b"healthy payload\n".to_vec())
9511 );
9512 assert_eq!(
9513 opened.extract_file("broken.txt").unwrap_err(),
9514 FormatError::AeadFailure
9515 );
9516 assert_eq!(opened.verify().unwrap_err(), FormatError::AeadFailure);
9517 }
9518
9519 #[test]
9520 fn seekable_extract_does_not_read_unselected_payload_envelope() {
9521 let healthy = pseudo_random_bytes(64 * 1024);
9522 let broken = pseudo_random_bytes(64 * 1024);
9523 let options = WriterOptions {
9524 block_size: 4096,
9525 chunk_size: 4096,
9526 envelope_target_size: 8192,
9527 stripe_width: 1,
9528 volume_loss_tolerance: 0,
9529 bit_rot_buffer_pct: 0,
9530 fec_data_shards: 4,
9531 fec_parity_shards: 0,
9532 index_fec_data_shards: 4,
9533 index_fec_parity_shards: 0,
9534 index_root_fec_data_shards: 4,
9535 index_root_fec_parity_shards: 0,
9536 ..WriterOptions::default()
9537 };
9538 let archive = write_archive(
9539 &[
9540 RegularFile::new("healthy.bin", &healthy),
9541 RegularFile::new("broken.bin", &broken),
9542 ],
9543 &master_key(),
9544 options,
9545 )
9546 .unwrap();
9547 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9548 let healthy_envelopes = envelope_indices_for_path(&eager, "healthy.bin");
9549 let broken_envelopes = envelope_entries_for_path(&eager, "broken.bin");
9550 let denied_block_indices = broken_envelopes
9551 .iter()
9552 .filter(|envelope| !healthy_envelopes.contains(&envelope.envelope_index))
9553 .flat_map(|envelope| {
9554 let block_count =
9555 envelope.data_block_count as u64 + envelope.parity_block_count as u64;
9556 envelope.first_block_index..envelope.first_block_index + block_count
9557 })
9558 .collect::<BTreeSet<_>>();
9559 assert!(
9560 !denied_block_indices.is_empty(),
9561 "fixture must place broken.bin in at least one unshared envelope"
9562 );
9563 let denied_ranges = block_record_slots(&archive.bytes)
9564 .into_iter()
9565 .filter_map(|(offset, len, record)| {
9566 denied_block_indices
9567 .contains(&record.block_index)
9568 .then_some((offset as u64, (offset + len) as u64))
9569 })
9570 .collect::<Vec<_>>();
9571 assert!(!denied_ranges.is_empty());
9572
9573 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9574 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9575
9576 assert_eq!(opened.extract_file("healthy.bin").unwrap(), Some(healthy));
9577 for (read_start, read_end) in reader.reads() {
9578 assert!(
9579 denied_ranges
9580 .iter()
9581 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9582 "targeted extract read an unrelated payload BlockRecord range"
9583 );
9584 }
9585 assert_eq!(
9586 opened.extract_file("broken.bin").unwrap_err(),
9587 FormatError::InvalidArchive("denied test read")
9588 );
9589 }
9590
9591 #[test]
9592 fn extract_file_to_writer_streams_before_reading_later_envelopes() {
9593 struct FailOnFirstWrite;
9594
9595 impl std::io::Write for FailOnFirstWrite {
9596 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
9597 Err(std::io::Error::other("sink stopped"))
9598 }
9599
9600 fn flush(&mut self) -> std::io::Result<()> {
9601 Ok(())
9602 }
9603 }
9604
9605 let payload = pseudo_random_bytes(128 * 1024);
9606 let options = WriterOptions {
9607 block_size: 4096,
9608 chunk_size: 4096,
9609 envelope_target_size: 8192,
9610 stripe_width: 1,
9611 volume_loss_tolerance: 0,
9612 bit_rot_buffer_pct: 0,
9613 fec_data_shards: 4,
9614 fec_parity_shards: 0,
9615 index_fec_data_shards: 4,
9616 index_fec_parity_shards: 0,
9617 index_root_fec_data_shards: 4,
9618 index_root_fec_parity_shards: 0,
9619 ..WriterOptions::default()
9620 };
9621 let archive = write_archive(
9622 &[RegularFile::new("large.bin", &payload)],
9623 &master_key(),
9624 options,
9625 )
9626 .unwrap();
9627 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9628 let envelopes = envelope_entries_for_path(&eager, "large.bin");
9629 let first_envelope = envelopes
9630 .first()
9631 .expect("large fixture should have at least one envelope")
9632 .envelope_index;
9633 let later_envelope_blocks = envelopes
9634 .iter()
9635 .filter(|entry| entry.envelope_index != first_envelope)
9636 .flat_map(|entry| {
9637 let block_count = entry.data_block_count as u64 + entry.parity_block_count as u64;
9638 entry.first_block_index..entry.first_block_index + block_count
9639 })
9640 .collect::<BTreeSet<_>>();
9641 assert!(
9642 !later_envelope_blocks.is_empty(),
9643 "fixture must span more than one payload envelope"
9644 );
9645 let denied_ranges = block_record_slots(&archive.bytes)
9646 .into_iter()
9647 .filter_map(|(offset, len, record)| {
9648 later_envelope_blocks
9649 .contains(&record.block_index)
9650 .then_some((offset as u64, (offset + len) as u64))
9651 })
9652 .collect::<Vec<_>>();
9653 assert!(!denied_ranges.is_empty());
9654
9655 let reader = CountingReadAt::new(archive.bytes, denied_ranges.clone());
9656 let opened = open_seekable_archive(reader.clone(), &master_key()).unwrap();
9657 let mut writer = FailOnFirstWrite;
9658
9659 let err = opened
9660 .extract_file_to_writer("large.bin", &mut writer)
9661 .unwrap_err();
9662 assert_eq!(err.to_string(), "extraction output write failed");
9663 for (read_start, read_end) in reader.reads() {
9664 assert!(
9665 denied_ranges
9666 .iter()
9667 .all(|(start, end)| !ranges_overlap(read_start, read_end, *start, *end)),
9668 "streaming writer read a later payload envelope before surfacing writer failure"
9669 );
9670 }
9671 }
9672
9673 #[test]
9674 fn extract_file_to_writer_writes_bounded_chunks() {
9675 struct ChunkRecorder {
9676 total: usize,
9677 max_write: usize,
9678 writes: usize,
9679 }
9680
9681 impl std::io::Write for ChunkRecorder {
9682 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
9683 self.total += buf.len();
9684 self.max_write = self.max_write.max(buf.len());
9685 self.writes += 1;
9686 Ok(buf.len())
9687 }
9688
9689 fn flush(&mut self) -> std::io::Result<()> {
9690 Ok(())
9691 }
9692 }
9693
9694 let payload = pseudo_random_bytes(128 * 1024);
9695 let options = WriterOptions {
9696 block_size: 4096,
9697 chunk_size: 4096,
9698 envelope_target_size: 8192,
9699 stripe_width: 1,
9700 volume_loss_tolerance: 0,
9701 bit_rot_buffer_pct: 0,
9702 fec_data_shards: 4,
9703 fec_parity_shards: 0,
9704 index_fec_data_shards: 4,
9705 index_fec_parity_shards: 0,
9706 index_root_fec_data_shards: 4,
9707 index_root_fec_parity_shards: 0,
9708 ..WriterOptions::default()
9709 };
9710 let archive = write_archive(
9711 &[RegularFile::new("large.bin", &payload)],
9712 &master_key(),
9713 options,
9714 )
9715 .unwrap();
9716 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9717 let mut writer = ChunkRecorder {
9718 total: 0,
9719 max_write: 0,
9720 writes: 0,
9721 };
9722
9723 opened
9724 .extract_file_to_writer("large.bin", &mut writer)
9725 .unwrap()
9726 .unwrap();
9727
9728 assert_eq!(writer.total, payload.len());
9729 assert!(writer.writes > 1);
9730 assert!(
9731 writer.max_write <= options.chunk_size as usize,
9732 "writer saw a {} byte chunk, larger than the {} byte frame target",
9733 writer.max_write,
9734 options.chunk_size
9735 );
9736 }
9737
9738 #[test]
9739 fn extract_file_to_writer_with_progress_reports_payload_bytes() {
9740 struct ChunkRecorder {
9741 total: usize,
9742 }
9743
9744 impl std::io::Write for ChunkRecorder {
9745 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
9746 self.total += buf.len();
9747 Ok(buf.len())
9748 }
9749
9750 fn flush(&mut self) -> std::io::Result<()> {
9751 Ok(())
9752 }
9753 }
9754
9755 let payload = pseudo_random_bytes(128 * 1024);
9756 let options = WriterOptions {
9757 block_size: 4096,
9758 chunk_size: 4096,
9759 envelope_target_size: 8192,
9760 stripe_width: 1,
9761 volume_loss_tolerance: 0,
9762 bit_rot_buffer_pct: 0,
9763 fec_data_shards: 4,
9764 fec_parity_shards: 0,
9765 index_fec_data_shards: 4,
9766 index_fec_parity_shards: 0,
9767 index_root_fec_data_shards: 4,
9768 index_root_fec_parity_shards: 0,
9769 ..WriterOptions::default()
9770 };
9771 let archive = write_archive(
9772 &[RegularFile::new("large.bin", &payload)],
9773 &master_key(),
9774 options,
9775 )
9776 .unwrap();
9777 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9778 let mut writer = ChunkRecorder { total: 0 };
9779 let mut progress_events = Vec::new();
9780 let mut progress = |archive_path: &str, bytes: u64| {
9781 progress_events.push((archive_path.to_owned(), bytes));
9782 };
9783
9784 opened
9785 .extract_file_to_writer_with_progress("large.bin", &mut writer, &mut progress)
9786 .unwrap()
9787 .unwrap();
9788
9789 let reported_bytes = progress_events.iter().map(|(_, bytes)| *bytes).sum::<u64>();
9790 assert_eq!(writer.total, payload.len());
9791 assert_eq!(reported_bytes, payload.len() as u64);
9792 assert!(progress_events.len() > 1);
9793 assert!(progress_events.iter().all(|(path, _)| path == "large.bin"));
9794 }
9795
9796 #[test]
9797 fn streaming_filesystem_extract_does_not_publish_partial_file_on_late_payload_error() {
9798 let payload = pseudo_random_bytes(128 * 1024);
9799 let options = WriterOptions {
9800 block_size: 4096,
9801 chunk_size: 4096,
9802 envelope_target_size: 8192,
9803 stripe_width: 1,
9804 volume_loss_tolerance: 0,
9805 bit_rot_buffer_pct: 0,
9806 fec_data_shards: 4,
9807 fec_parity_shards: 0,
9808 index_fec_data_shards: 4,
9809 index_fec_parity_shards: 0,
9810 index_root_fec_data_shards: 4,
9811 index_root_fec_parity_shards: 0,
9812 ..WriterOptions::default()
9813 };
9814 let archive = write_archive(
9815 &[RegularFile::new("large.bin", &payload)],
9816 &master_key(),
9817 options,
9818 )
9819 .unwrap();
9820 let eager = open_archive(&archive.bytes, &master_key()).unwrap();
9821 let envelopes = envelope_entries_for_path(&eager, "large.bin");
9822 let last_envelope = envelopes
9823 .last()
9824 .expect("large fixture should have at least one envelope");
9825 assert_ne!(
9826 envelopes.first().unwrap().envelope_index,
9827 last_envelope.envelope_index,
9828 "fixture must span more than one payload envelope"
9829 );
9830 let corrupt_slot = block_record_slots(&archive.bytes)
9831 .into_iter()
9832 .enumerate()
9833 .find_map(|(slot, (_, _, record))| {
9834 (record.block_index == last_envelope.first_block_index).then_some(slot)
9835 })
9836 .unwrap();
9837 let mut corrupted = archive.bytes;
9838 corrupt_block_record_payload_at_slot(&mut corrupted, corrupt_slot);
9839 let opened = open_seekable_archive(corrupted, &master_key()).unwrap();
9840 let tmp = tempfile::tempdir().unwrap();
9841
9842 assert!(matches!(
9843 opened
9844 .extract_file_to("large.bin", tmp.path(), SafeExtractionOptions::default())
9845 .unwrap_err(),
9846 FormatError::AeadFailure | FormatError::FecTooFewAvailableShards
9847 ));
9848 assert!(!tmp.path().join("large.bin").exists());
9849 assert_eq!(std::fs::read_dir(tmp.path()).unwrap().count(), 0);
9850 }
9851
9852 #[test]
9853 fn bootstrap_sidecar_opens_lists_verifies_and_extracts() {
9854 let archive = write_archive(
9855 &[RegularFile::new("dir/sidecar.txt", b"hello sidecar")],
9856 &master_key(),
9857 single_stream_options(),
9858 )
9859 .unwrap();
9860 let opened = open_archive_with_bootstrap_sidecar(
9861 &archive.bytes,
9862 &archive.bootstrap_sidecar,
9863 &master_key(),
9864 )
9865 .unwrap();
9866
9867 assert_eq!(
9868 opened.list_files().unwrap(),
9869 vec![ArchiveEntry {
9870 path: "dir/sidecar.txt".to_string(),
9871 file_data_size: 13,
9872 kind: TarEntryKind::Regular,
9873 mode: 0o644,
9874 mtime: 0,
9875 diagnostics: Vec::new(),
9876 }]
9877 );
9878 assert_eq!(
9879 opened.extract_file("dir/sidecar.txt").unwrap(),
9880 Some(b"hello sidecar".to_vec())
9881 );
9882 opened.verify().unwrap();
9883 }
9884
9885 #[test]
9886 fn dictionary_archive_opens_lists_verifies_and_extracts_seekable() {
9887 let archive = write_archive_with_dictionary(
9888 &[RegularFile::new(
9889 "dir/dict.txt",
9890 b"common words common words dictionary payload",
9891 )],
9892 &master_key(),
9893 single_stream_options(),
9894 dictionary(),
9895 )
9896 .unwrap();
9897 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9898
9899 assert_eq!(opened.crypto_header.has_dictionary, 1);
9900 assert!(opened.index_root.header.dictionary_data_block_count > 0);
9901 assert_eq!(
9902 opened.list_files().unwrap(),
9903 vec![ArchiveEntry {
9904 path: "dir/dict.txt".to_string(),
9905 file_data_size: 44,
9906 kind: TarEntryKind::Regular,
9907 mode: 0o644,
9908 mtime: 0,
9909 diagnostics: Vec::new(),
9910 }]
9911 );
9912 assert_eq!(
9913 opened.extract_file("dir/dict.txt").unwrap(),
9914 Some(b"common words common words dictionary payload".to_vec())
9915 );
9916 opened.verify().unwrap();
9917 }
9918
9919 #[test]
9920 fn dictionary_object_tamper_fails_before_payload_decompression() {
9921 let archive = write_archive_with_dictionary(
9922 &[RegularFile::new(
9923 "dir/dict.txt",
9924 b"common words common words dictionary payload",
9925 )],
9926 &master_key(),
9927 single_stream_options(),
9928 dictionary(),
9929 )
9930 .unwrap();
9931 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
9932 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
9933 let crypto_end = VOLUME_HEADER_LEN + volume_header.crypto_header_length as usize;
9934 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
9935 let dictionary_offset =
9936 crypto_end + opened.index_root.header.dictionary_first_block as usize * record_len;
9937
9938 let mut tampered = archive.bytes.clone();
9939 tampered[dictionary_offset + 16] ^= 0x01;
9940 let crc_offset = dictionary_offset + 16 + opened.crypto_header.block_size as usize;
9941 let crc = crc32c::crc32c(&tampered[dictionary_offset..crc_offset]);
9942 tampered[crc_offset..crc_offset + 4].copy_from_slice(&crc.to_le_bytes());
9943
9944 assert_eq!(
9945 open_archive(&tampered, &master_key()).unwrap_err(),
9946 FormatError::AeadFailure
9947 );
9948 }
9949
9950 #[test]
9951 fn dictionary_archive_bootstraps_from_sidecar_for_non_seekable_open() {
9952 let archive = write_archive_with_dictionary(
9953 &[RegularFile::new(
9954 "dict-sidecar.txt",
9955 b"common words common words sidecar payload",
9956 )],
9957 &master_key(),
9958 single_stream_options(),
9959 dictionary(),
9960 )
9961 .unwrap();
9962 let opened = open_non_seekable_archive(
9963 &archive.bytes,
9964 &master_key(),
9965 Some(&archive.bootstrap_sidecar),
9966 )
9967 .unwrap();
9968
9969 assert_eq!(
9970 opened.extract_file("dict-sidecar.txt").unwrap(),
9971 Some(b"common words common words sidecar payload".to_vec())
9972 );
9973 opened.verify().unwrap();
9974 }
9975
9976 #[test]
9977 fn non_seekable_full_sidecar_bootstraps_when_terminal_trailer_is_corrupt() {
9978 let archive = write_archive(
9979 &[RegularFile::new(
9980 "sidecar-terminal.txt",
9981 b"sidecar authority",
9982 )],
9983 &master_key(),
9984 single_stream_options(),
9985 )
9986 .unwrap();
9987 let mut corrupted = archive.bytes.clone();
9988 corrupt_v41_terminal_recovery(&mut corrupted);
9989 assert!(open_archive(&corrupted, &master_key()).is_err());
9990
9991 let opened =
9992 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
9993 .unwrap();
9994
9995 assert!(opened.volume_trailer.is_none());
9996 assert_eq!(
9997 opened.extract_file("sidecar-terminal.txt").unwrap(),
9998 Some(b"sidecar authority".to_vec())
9999 );
10000 opened.verify().unwrap();
10001 }
10002
10003 #[test]
10004 fn dictionary_full_sidecar_bootstraps_when_terminal_material_is_absent() {
10005 let archive = write_archive_with_dictionary(
10006 &[RegularFile::new(
10007 "dict-no-terminal.txt",
10008 b"common words common words without terminal",
10009 )],
10010 &master_key(),
10011 single_stream_options(),
10012 dictionary(),
10013 )
10014 .unwrap();
10015 let terminal_offset = terminal_material_offset(&archive.bytes);
10016 let truncated = archive.bytes[..terminal_offset].to_vec();
10017 assert!(open_archive(&truncated, &master_key()).is_err());
10018
10019 let opened =
10020 open_non_seekable_archive(&truncated, &master_key(), Some(&archive.bootstrap_sidecar))
10021 .unwrap();
10022
10023 assert!(opened.volume_trailer.is_none());
10024 assert_eq!(
10025 opened.extract_file("dict-no-terminal.txt").unwrap(),
10026 Some(b"common words common words without terminal".to_vec())
10027 );
10028 opened.verify().unwrap();
10029 }
10030
10031 #[test]
10032 fn bootstrap_sidecar_treats_crc_failed_payload_block_as_erasure() {
10033 let archive = write_archive(
10034 &[RegularFile::new(
10035 "sidecar-erasure.txt",
10036 b"repair through sidecar",
10037 )],
10038 &master_key(),
10039 single_stream_options(),
10040 )
10041 .unwrap();
10042 let mut corrupted = archive.bytes.clone();
10043 corrupt_first_block_record_payload(&mut corrupted);
10044
10045 let opened = open_archive_with_bootstrap_sidecar(
10046 &corrupted,
10047 &archive.bootstrap_sidecar,
10048 &master_key(),
10049 )
10050 .unwrap();
10051 assert_eq!(
10052 opened.extract_file("sidecar-erasure.txt").unwrap(),
10053 Some(b"repair through sidecar".to_vec())
10054 );
10055 }
10056
10057 #[test]
10058 fn extraction_rejects_logical_payload_above_total_size_cap() {
10059 let archive = write_archive(
10060 &[RegularFile::new("cap.txt", b"payload")],
10061 &master_key(),
10062 single_stream_options(),
10063 )
10064 .unwrap();
10065 let options = ReaderOptions {
10066 max_total_extraction_size: 3,
10067 ..ReaderOptions::default()
10068 };
10069 let opened =
10070 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
10071
10072 assert_eq!(
10073 opened.extract_file("cap.txt").unwrap_err(),
10074 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10075 );
10076 }
10077
10078 #[test]
10079 fn verify_does_not_apply_extraction_payload_cap() {
10080 let archive = write_archive(
10081 &[RegularFile::new("verify-cap.txt", b"payload")],
10082 &master_key(),
10083 single_stream_options(),
10084 )
10085 .unwrap();
10086 let options = ReaderOptions {
10087 max_total_extraction_size: 3,
10088 ..ReaderOptions::default()
10089 };
10090 let opened =
10091 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
10092
10093 opened.verify().unwrap();
10094 assert_eq!(
10095 opened.extract_file("verify-cap.txt").unwrap_err(),
10096 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10097 );
10098 }
10099
10100 #[test]
10101 fn verify_streams_past_legacy_in_memory_tar_cap() {
10102 let data = vec![0x5a; 4096];
10103 let archive = write_archive(
10104 &[RegularFile::new("verify-large.txt", &data)],
10105 &master_key(),
10106 single_stream_options(),
10107 )
10108 .unwrap();
10109 let options = ReaderOptions {
10110 max_verify_tar_size: 1,
10111 ..ReaderOptions::default()
10112 };
10113 let opened =
10114 OpenedArchive::open_with_options(&archive.bytes, &master_key(), options).unwrap();
10115
10116 opened.verify().unwrap();
10117 }
10118
10119 #[test]
10120 fn dictionary_sidecar_requires_dictionary_record_section() {
10121 let archive = write_archive_with_dictionary(
10122 &[RegularFile::new("dict-missing.txt", b"common words")],
10123 &master_key(),
10124 single_stream_options(),
10125 dictionary(),
10126 )
10127 .unwrap();
10128 let header = BootstrapSidecarHeader::parse(
10129 &archive.bootstrap_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN],
10130 )
10131 .unwrap();
10132 let mut missing_dictionary =
10133 archive.bootstrap_sidecar[..header.dictionary_records_offset as usize].to_vec();
10134 rewrite_sidecar_header(&mut missing_dictionary, &master_key(), |header| {
10135 header.flags &= !0x04;
10136 header.dictionary_records_offset = 0;
10137 header.dictionary_records_length = 0;
10138 });
10139
10140 assert_eq!(
10141 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&missing_dictionary))
10142 .unwrap_err(),
10143 FormatError::ReaderUnsupported("dictionary bootstrap required")
10144 );
10145 }
10146
10147 #[test]
10148 fn dictionary_sidecar_records_are_validated_against_dictionary_extent() {
10149 let archive = write_archive_with_dictionary(
10150 &[RegularFile::new("dict-sidecar-kind.txt", b"common words")],
10151 &master_key(),
10152 single_stream_options(),
10153 dictionary(),
10154 )
10155 .unwrap();
10156
10157 let mut wrong_kind = archive.bootstrap_sidecar.clone();
10158 mutate_sidecar_dictionary_record(&mut wrong_kind, 0, |record| {
10159 record.kind = BlockKind::IndexRootData;
10160 });
10161 assert_eq!(
10162 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
10163 .unwrap_err(),
10164 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
10165 );
10166
10167 let mut wrong_last = archive.bootstrap_sidecar.clone();
10168 mutate_sidecar_dictionary_record(&mut wrong_last, 0, |record| {
10169 record.flags = 0;
10170 });
10171 assert_eq!(
10172 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
10173 .unwrap_err(),
10174 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
10175 );
10176 }
10177
10178 #[test]
10179 fn non_seekable_random_access_requires_sidecar() {
10180 let archive = write_archive(
10181 &[RegularFile::new("file.txt", b"payload")],
10182 &master_key(),
10183 single_stream_options(),
10184 )
10185 .unwrap();
10186
10187 assert_eq!(
10188 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10189 FormatError::ReaderUnsupported(
10190 "non-seekable random access requires a bootstrap sidecar"
10191 )
10192 );
10193 assert!(open_non_seekable_archive(
10194 &archive.bytes,
10195 &master_key(),
10196 Some(&archive.bootstrap_sidecar)
10197 )
10198 .is_ok());
10199 }
10200
10201 #[test]
10202 fn non_seekable_bootstrap_rejects_index_root_only_sidecar() {
10203 let archive = write_archive(
10204 &[RegularFile::new("sparse.txt", b"sparse sidecar")],
10205 &master_key(),
10206 single_stream_options(),
10207 )
10208 .unwrap();
10209 let index_root_only = sparse_bootstrap_sidecar(
10210 &archive.bootstrap_sidecar,
10211 &master_key(),
10212 false,
10213 true,
10214 false,
10215 );
10216
10217 assert_eq!(
10218 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&index_root_only))
10219 .unwrap_err(),
10220 FormatError::ReaderUnsupported(
10221 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10222 )
10223 );
10224 }
10225
10226 #[test]
10227 fn seekable_sidecar_uses_index_root_records_after_terminal_manifest_authority() {
10228 let archive = write_archive(
10229 &[RegularFile::new("sparse-index.txt", b"recover index root")],
10230 &master_key(),
10231 single_stream_options(),
10232 )
10233 .unwrap();
10234 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10235 let mut corrupted = archive.bytes.clone();
10236 corrupt_object_extent_records(
10237 &mut corrupted,
10238 index_root_extent_from_manifest(&opened.manifest_footer),
10239 );
10240 assert!(open_archive(&corrupted, &master_key()).is_err());
10241
10242 let index_root_only = sparse_bootstrap_sidecar(
10243 &archive.bootstrap_sidecar,
10244 &master_key(),
10245 false,
10246 true,
10247 false,
10248 );
10249 let recovered =
10250 open_archive_with_bootstrap_sidecar(&corrupted, &index_root_only, &master_key())
10251 .unwrap();
10252
10253 assert_eq!(
10254 recovered.extract_file("sparse-index.txt").unwrap(),
10255 Some(b"recover index root".to_vec())
10256 );
10257 recovered.verify().unwrap();
10258 }
10259
10260 #[test]
10261 fn seekable_sidecar_uses_dictionary_records_after_index_root_authority() {
10262 let archive = write_archive_with_dictionary(
10263 &[RegularFile::new(
10264 "sparse-dict.txt",
10265 b"common words common words sparse dictionary",
10266 )],
10267 &master_key(),
10268 single_stream_options(),
10269 dictionary(),
10270 )
10271 .unwrap();
10272 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
10273 let mut corrupted = archive.bytes.clone();
10274 corrupt_object_extent_records(
10275 &mut corrupted,
10276 dictionary_extent_from_index_root(&opened.index_root).unwrap(),
10277 );
10278 assert!(open_archive(&corrupted, &master_key()).is_err());
10279
10280 let dictionary_only = sparse_bootstrap_sidecar(
10281 &archive.bootstrap_sidecar,
10282 &master_key(),
10283 false,
10284 false,
10285 true,
10286 );
10287 assert_eq!(
10288 open_non_seekable_archive(&archive.bytes, &master_key(), Some(&dictionary_only))
10289 .unwrap_err(),
10290 FormatError::ReaderUnsupported(
10291 "non-seekable bootstrap sidecar requires ManifestFooter and IndexRoot sections"
10292 )
10293 );
10294
10295 let recovered =
10296 open_archive_with_bootstrap_sidecar(&corrupted, &dictionary_only, &master_key())
10297 .unwrap();
10298 assert_eq!(
10299 recovered.extract_file("sparse-dict.txt").unwrap(),
10300 Some(b"common words common words sparse dictionary".to_vec())
10301 );
10302 recovered.verify().unwrap();
10303 }
10304
10305 #[test]
10306 fn sequential_extracts_dictionary_free_tar_stream() {
10307 let archive = write_archive(
10308 &[RegularFile::new("seq.txt", b"streaming")],
10309 &master_key(),
10310 single_stream_options(),
10311 )
10312 .unwrap();
10313
10314 let tar_stream = sequential_extract_tar_stream(&archive.bytes, &master_key()).unwrap();
10315 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10316 assert_eq!(member.path, b"seq.txt");
10317 assert_eq!(member.data, b"streaming");
10318 }
10319
10320 #[test]
10321 fn sequential_rejects_logical_payload_above_total_size_cap() {
10322 let archive = write_archive(
10323 &[RegularFile::new("seq-cap.txt", b"payload")],
10324 &master_key(),
10325 single_stream_options(),
10326 )
10327 .unwrap();
10328 let options = ReaderOptions {
10329 max_total_extraction_size: 3,
10330 ..ReaderOptions::default()
10331 };
10332
10333 assert_eq!(
10334 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10335 .unwrap_err(),
10336 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10337 );
10338 }
10339
10340 #[test]
10341 fn sequential_rejects_tar_stream_above_buffer_cap_during_decode() {
10342 let archive = write_archive(
10343 &[RegularFile::new("seq-buffer-cap.txt", b"payload")],
10344 &master_key(),
10345 single_stream_options(),
10346 )
10347 .unwrap();
10348 let options = ReaderOptions {
10349 max_verify_tar_size: 512,
10350 ..ReaderOptions::default()
10351 };
10352
10353 assert_eq!(
10354 sequential_extract_tar_stream_with_options(&archive.bytes, &master_key(), options)
10355 .unwrap_err(),
10356 FormatError::ReaderUnsupported(
10357 "sequential tar stream exceeds configured verification cap"
10358 )
10359 );
10360 }
10361
10362 #[test]
10363 fn sequential_repairs_crc_failed_payload_data_when_parity_is_guaranteed() {
10364 let archive = write_archive(
10365 &[RegularFile::new("seq-erasure.txt", b"stream repair")],
10366 &master_key(),
10367 single_stream_options(),
10368 )
10369 .unwrap();
10370 let mut corrupted = archive.bytes;
10371 corrupt_first_block_record_payload(&mut corrupted);
10372
10373 let tar_stream = sequential_extract_tar_stream(&corrupted, &master_key()).unwrap();
10374 let member = parse_tar_member_group(&tar_stream, 4096).unwrap();
10375 assert_eq!(member.path, b"seq-erasure.txt");
10376 assert_eq!(member.data, b"stream repair");
10377 }
10378
10379 #[test]
10380 fn sequential_rejects_crc_failed_payload_data_without_guaranteed_parity() {
10381 let archive = write_archive(
10382 &[RegularFile::new("seq-no-parity.txt", b"no repair")],
10383 &master_key(),
10384 WriterOptions {
10385 bit_rot_buffer_pct: 0,
10386 fec_parity_shards: 0,
10387 index_fec_parity_shards: 0,
10388 index_root_fec_parity_shards: 0,
10389 ..single_stream_options()
10390 },
10391 )
10392 .unwrap();
10393 let mut corrupted = archive.bytes;
10394 corrupt_first_block_record_payload(&mut corrupted);
10395
10396 assert_eq!(
10397 sequential_extract_tar_stream(&corrupted, &master_key()).unwrap_err(),
10398 FormatError::BadCrc {
10399 structure: "BlockRecord"
10400 }
10401 );
10402 }
10403
10404 #[test]
10405 fn sequential_rejects_when_terminal_authentication_fails_without_returning_bytes() {
10406 let archive = write_archive(
10407 &[RegularFile::new(
10408 "seq.txt",
10409 b"payload must not be returned after terminal auth failure",
10410 )],
10411 &master_key(),
10412 single_stream_options(),
10413 )
10414 .unwrap();
10415 let mut corrupted = archive.bytes;
10416 corrupt_v41_terminal_recovery(&mut corrupted);
10417
10418 match sequential_extract_tar_stream(&corrupted, &master_key()) {
10419 Ok(bytes) => panic!(
10420 "sequential helper returned {} decoded byte(s) despite terminal HMAC failure",
10421 bytes.len()
10422 ),
10423 Err(err) => assert_eq!(
10424 err,
10425 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10426 ),
10427 }
10428 }
10429
10430 #[test]
10431 fn sequential_rejects_dictionary_archive_without_bootstrap_before_payload_release() {
10432 let archive = write_archive_with_dictionary(
10433 &[RegularFile::new(
10434 "seq-dict.txt",
10435 b"common words common words dictionary payload",
10436 )],
10437 &master_key(),
10438 single_stream_options(),
10439 b"common words dictionary",
10440 )
10441 .unwrap();
10442
10443 match sequential_extract_tar_stream(&archive.bytes, &master_key()) {
10444 Ok(bytes) => panic!(
10445 "sequential helper returned {} decoded byte(s) for dictionary archive without bootstrap",
10446 bytes.len()
10447 ),
10448 Err(err) => assert_eq!(
10449 err,
10450 FormatError::ReaderUnsupported(
10451 "dictionary bootstrap required for non-seekable sequential extraction"
10452 )
10453 ),
10454 }
10455 }
10456
10457 #[test]
10458 fn non_seekable_dictionary_error_keeps_missing_bootstrap_wording() {
10459 let archive = write_archive_with_dictionary(
10460 &[RegularFile::new(
10461 "seq-dict-open.txt",
10462 b"common words common words bootstrap required",
10463 )],
10464 &master_key(),
10465 single_stream_options(),
10466 b"common words bootstrap",
10467 )
10468 .unwrap();
10469
10470 assert_eq!(
10471 open_non_seekable_archive(&archive.bytes, &master_key(), None).unwrap_err(),
10472 FormatError::ReaderUnsupported(
10473 "non-seekable random access requires a bootstrap sidecar"
10474 )
10475 );
10476 }
10477
10478 #[test]
10479 fn sequential_zstd_stream_rejects_skippable_frame_segments() {
10480 let skippable = [0x50, 0x2a, 0x4d, 0x18, 0, 0, 0, 0];
10481 let mut output = Vec::new();
10482
10483 assert_eq!(
10484 decode_concatenated_zstd_frames_with_cap(
10485 &skippable,
10486 None,
10487 &mut output,
10488 usize::MAX,
10489 None,
10490 )
10491 .unwrap_err(),
10492 FormatError::NotStandardZstdFrame
10493 );
10494 assert!(output.is_empty());
10495 }
10496
10497 #[test]
10498 fn live_non_seekable_verify_stream_accepts_single_volume_archive() {
10499 let archive = write_archive(
10500 &[RegularFile::new("live.txt", b"stream verify")],
10501 &master_key(),
10502 single_stream_options(),
10503 )
10504 .unwrap();
10505
10506 let report =
10507 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10508
10509 assert_eq!(report.file_count, 1);
10510 assert_eq!(report.total_volumes, 1);
10511 assert_eq!(report.root_auth, SequentialRootAuthStatus::Absent);
10512 assert!(report.payload_block_count > 0);
10513 }
10514
10515 #[test]
10516 fn live_non_seekable_verify_stream_accepts_tiny_read_chunks() {
10517 let archive = write_archive(
10518 &[RegularFile::new("tiny-chunks.txt", b"one byte at a time")],
10519 &master_key(),
10520 single_stream_options(),
10521 )
10522 .unwrap();
10523
10524 let report =
10525 verify_non_seekable_stream(ChunkedReader::new(archive.bytes, 1), &master_key())
10526 .unwrap();
10527
10528 assert_eq!(report.file_count, 1);
10529 assert_eq!(report.tar_total_size % 512, 0);
10530 }
10531
10532 #[test]
10533 fn live_non_seekable_verify_stream_accepts_empty_archive() {
10534 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10535
10536 let report =
10537 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10538
10539 assert_eq!(report.file_count, 0);
10540 assert_eq!(report.payload_block_count, 0);
10541 assert_eq!(report.tar_total_size, 0);
10542 }
10543
10544 #[test]
10545 fn live_non_seekable_verify_rejects_dictionary_archive_without_bootstrap() {
10546 let archive = write_archive_with_dictionary(
10547 &[RegularFile::new(
10548 "live-dict.txt",
10549 b"common words common words dictionary payload",
10550 )],
10551 &master_key(),
10552 single_stream_options(),
10553 b"common words dictionary",
10554 )
10555 .unwrap();
10556
10557 assert_eq!(
10558 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key())
10559 .unwrap_err(),
10560 FormatError::ReaderUnsupported(
10561 "dictionary bootstrap required for non-seekable sequential verification"
10562 )
10563 );
10564 }
10565
10566 #[test]
10567 fn live_non_seekable_verify_accepts_dictionary_archive_with_bootstrap() {
10568 let archive = write_archive_with_dictionary(
10569 &[RegularFile::new(
10570 "live-dict-sidecar.txt",
10571 b"common words common words dictionary payload",
10572 )],
10573 &master_key(),
10574 single_stream_options(),
10575 b"common words dictionary",
10576 )
10577 .unwrap();
10578
10579 let report = verify_non_seekable_stream_with_bootstrap_sidecar(
10580 std::io::Cursor::new(archive.bytes),
10581 &archive.bootstrap_sidecar,
10582 &master_key(),
10583 NonSeekableReaderOptions::default(),
10584 )
10585 .unwrap();
10586
10587 assert_eq!(report.file_count, 1);
10588 assert_eq!(report.total_volumes, 1);
10589 }
10590
10591 #[test]
10592 fn live_non_seekable_verify_rejects_terminal_tail_above_cap() {
10593 let archive = write_archive(
10594 &[RegularFile::new("tail-cap.txt", b"payload")],
10595 &master_key(),
10596 single_stream_options(),
10597 )
10598 .unwrap();
10599 let options = NonSeekableReaderOptions {
10600 max_terminal_tail_size: 8,
10601 ..NonSeekableReaderOptions::default()
10602 };
10603
10604 assert_eq!(
10605 verify_non_seekable_stream_with_options(
10606 std::io::Cursor::new(archive.bytes),
10607 &master_key(),
10608 options
10609 )
10610 .unwrap_err(),
10611 FormatError::ReaderUnsupported("terminal tail exceeds configured cap")
10612 );
10613 }
10614
10615 #[test]
10616 fn live_non_seekable_verify_rejects_metadata_above_retention_cap() {
10617 let archive = write_archive(
10618 &[RegularFile::new("metadata-cap.txt", b"payload")],
10619 &master_key(),
10620 single_stream_options(),
10621 )
10622 .unwrap();
10623 let options = NonSeekableReaderOptions {
10624 max_retained_metadata_bytes: 1,
10625 ..NonSeekableReaderOptions::default()
10626 };
10627
10628 assert_eq!(
10629 verify_non_seekable_stream_with_options(
10630 std::io::Cursor::new(archive.bytes),
10631 &master_key(),
10632 options
10633 )
10634 .unwrap_err(),
10635 FormatError::ReaderUnsupported("retained metadata exceeds configured streaming cap")
10636 );
10637 }
10638
10639 #[test]
10640 fn live_non_seekable_verify_repairs_crc_failed_metadata_block() {
10641 let archive = write_archive(
10642 &[RegularFile::new("metadata-erasure.txt", b"payload")],
10643 &master_key(),
10644 single_stream_options(),
10645 )
10646 .unwrap();
10647 let mut corrupted = archive.bytes;
10648 let slot = first_block_record_slot_with_kind(&corrupted, BlockKind::IndexRootData).unwrap();
10649 corrupt_block_record_payload_at_slot(&mut corrupted, slot);
10650
10651 let report =
10652 verify_non_seekable_stream(std::io::Cursor::new(corrupted), &master_key()).unwrap();
10653
10654 assert_eq!(report.file_count, 1);
10655 }
10656
10657 #[test]
10658 fn live_non_seekable_verify_rejects_member_count_above_cap() {
10659 let archive = write_archive(
10660 &[RegularFile::new("member-cap.txt", b"payload")],
10661 &master_key(),
10662 single_stream_options(),
10663 )
10664 .unwrap();
10665 let options = NonSeekableReaderOptions {
10666 max_streamed_member_count: 0,
10667 ..NonSeekableReaderOptions::default()
10668 };
10669
10670 assert_eq!(
10671 verify_non_seekable_stream_with_options(
10672 std::io::Cursor::new(archive.bytes),
10673 &master_key(),
10674 options
10675 )
10676 .unwrap_err(),
10677 FormatError::ReaderUnsupported("tar member count exceeds configured streaming cap")
10678 );
10679 }
10680
10681 #[test]
10682 fn live_non_seekable_verify_rejects_total_extraction_cap_during_decode() {
10683 let archive = write_archive(
10684 &[RegularFile::new("live-total-cap.txt", b"payload")],
10685 &master_key(),
10686 single_stream_options(),
10687 )
10688 .unwrap();
10689 let mut options = NonSeekableReaderOptions::default();
10690 options.reader.max_total_extraction_size = 3;
10691
10692 assert_eq!(
10693 verify_non_seekable_stream_with_options(
10694 std::io::Cursor::new(archive.bytes),
10695 &master_key(),
10696 options
10697 )
10698 .unwrap_err(),
10699 FormatError::ReaderUnsupported("total extraction size exceeds configured cap")
10700 );
10701 }
10702
10703 #[test]
10704 fn live_non_seekable_verify_reports_root_auth_wire_only() {
10705 let archive = write_archive_with_root_auth(
10706 &[RegularFile::new("signed-live.txt", b"root-auth stream")],
10707 &master_key(),
10708 single_stream_options(),
10709 test_root_auth_config(),
10710 |request| Ok(test_root_auth_value(request)),
10711 )
10712 .unwrap();
10713
10714 let report =
10715 verify_non_seekable_stream(std::io::Cursor::new(archive.bytes), &master_key()).unwrap();
10716
10717 assert_eq!(report.root_auth, SequentialRootAuthStatus::WireValidOnly);
10718 }
10719
10720 #[test]
10721 fn live_non_seekable_extract_stream_commits_after_terminal_verify() {
10722 let archive = write_archive(
10723 &[
10724 RegularFile::new("alpha.txt", b"alpha"),
10725 RegularFile::new("nested/beta.txt", b"beta"),
10726 ],
10727 &master_key(),
10728 single_stream_options(),
10729 )
10730 .unwrap();
10731 let tmp = tempfile::tempdir().unwrap();
10732 let out = tmp.path().join("out");
10733
10734 let report = extract_non_seekable_stream_to_dir(
10735 std::io::Cursor::new(archive.bytes),
10736 &master_key(),
10737 &out,
10738 NonSeekableReaderOptions::default(),
10739 SafeExtractionOptions::default(),
10740 )
10741 .unwrap();
10742
10743 assert_eq!(report.verification.file_count, 2);
10744 assert_eq!(report.extracted_member_count, 2);
10745 assert_eq!(fs::read(out.join("alpha.txt")).unwrap(), b"alpha");
10746 assert_eq!(fs::read(out.join("nested/beta.txt")).unwrap(), b"beta");
10747 }
10748
10749 #[test]
10750 fn live_non_seekable_extract_stream_accepts_tiny_read_chunks() {
10751 let archive = write_archive(
10752 &[RegularFile::new("tiny-extract.txt", b"chunked extraction")],
10753 &master_key(),
10754 single_stream_options(),
10755 )
10756 .unwrap();
10757 let tmp = tempfile::tempdir().unwrap();
10758 let out = tmp.path().join("out");
10759
10760 extract_non_seekable_stream_to_dir(
10761 ChunkedReader::new(archive.bytes, 1),
10762 &master_key(),
10763 &out,
10764 NonSeekableReaderOptions::default(),
10765 SafeExtractionOptions::default(),
10766 )
10767 .unwrap();
10768
10769 assert_eq!(
10770 fs::read(out.join("tiny-extract.txt")).unwrap(),
10771 b"chunked extraction"
10772 );
10773 }
10774
10775 #[test]
10776 fn live_non_seekable_extract_stream_terminal_failure_leaves_no_final_output() {
10777 let archive = write_archive(
10778 &[RegularFile::new("late-fail.txt", b"must remain staged")],
10779 &master_key(),
10780 single_stream_options(),
10781 )
10782 .unwrap();
10783 let mut corrupted = archive.bytes;
10784 corrupt_v41_terminal_recovery(&mut corrupted);
10785 let tmp = tempfile::tempdir().unwrap();
10786 let out = tmp.path().join("out");
10787
10788 match extract_non_seekable_stream_to_dir(
10789 std::io::Cursor::new(corrupted),
10790 &master_key(),
10791 &out,
10792 NonSeekableReaderOptions::default(),
10793 SafeExtractionOptions::default(),
10794 )
10795 .unwrap_err()
10796 {
10797 ExtractError::Format(err) => assert_eq!(
10798 err,
10799 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
10800 ),
10801 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10802 }
10803 assert!(!out.exists());
10804 }
10805
10806 #[test]
10807 fn live_non_seekable_extract_stream_existing_destination_obeys_overwrite_policy() {
10808 let archive = write_archive(
10809 &[RegularFile::new("same.txt", b"new")],
10810 &master_key(),
10811 single_stream_options(),
10812 )
10813 .unwrap();
10814 let tmp = tempfile::tempdir().unwrap();
10815 let out = tmp.path().join("out");
10816 fs::create_dir(&out).unwrap();
10817 fs::write(out.join("same.txt"), b"old").unwrap();
10818
10819 match extract_non_seekable_stream_to_dir(
10820 std::io::Cursor::new(archive.bytes.clone()),
10821 &master_key(),
10822 &out,
10823 NonSeekableReaderOptions::default(),
10824 SafeExtractionOptions::default(),
10825 )
10826 .unwrap_err()
10827 {
10828 ExtractError::Format(err) => assert_eq!(err, FormatError::UnsafeOverwrite),
10829 ExtractError::Output(err) => panic!("unexpected output error: {err}"),
10830 }
10831 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"old");
10832
10833 extract_non_seekable_stream_to_dir(
10834 std::io::Cursor::new(archive.bytes),
10835 &master_key(),
10836 &out,
10837 NonSeekableReaderOptions::default(),
10838 SafeExtractionOptions {
10839 overwrite_existing: true,
10840 },
10841 )
10842 .unwrap();
10843 assert_eq!(fs::read(out.join("same.txt")).unwrap(), b"new");
10844 }
10845
10846 #[test]
10847 fn live_non_seekable_list_stream_matches_seekable_final_view() {
10848 let archive = write_archive(
10849 &[
10850 RegularFile::new("a.txt", b"a"),
10851 RegularFile::new("b.txt", b"bb"),
10852 ],
10853 &master_key(),
10854 single_stream_options(),
10855 )
10856 .unwrap();
10857 let seekable = open_archive(&archive.bytes, &master_key()).unwrap();
10858 let expected = seekable.list_files().unwrap();
10859
10860 let report = list_non_seekable_stream(
10861 std::io::Cursor::new(archive.bytes),
10862 &master_key(),
10863 NonSeekableReaderOptions::default(),
10864 )
10865 .unwrap();
10866
10867 assert_eq!(report.verification.file_count, 2);
10868 assert_eq!(report.entries, expected);
10869 }
10870
10871 #[test]
10872 fn bootstrap_sidecar_rejects_bad_flags_and_trailing_bytes() {
10873 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10874 let mut bad_flags = archive.bootstrap_sidecar.clone();
10875 rewrite_sidecar_header(&mut bad_flags, &master_key(), |header| {
10876 header.flags |= 0x08;
10877 });
10878 assert_eq!(
10879 open_archive_with_bootstrap_sidecar(&archive.bytes, &bad_flags, &master_key())
10880 .unwrap_err(),
10881 FormatError::UnknownBootstrapSidecarFlags(0x0b)
10882 );
10883
10884 let mut trailing = archive.bootstrap_sidecar.clone();
10885 trailing.push(0);
10886 assert_eq!(
10887 open_archive_with_bootstrap_sidecar(&archive.bytes, &trailing, &master_key())
10888 .unwrap_err(),
10889 FormatError::NonCanonicalBootstrapSidecarLayout
10890 );
10891 }
10892
10893 #[test]
10894 fn bootstrap_sidecar_rejects_bad_manifest_footer_semantics() {
10895 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10896 let mut wrong_volume = archive.bootstrap_sidecar.clone();
10897 mutate_sidecar_manifest(&mut wrong_volume, &master_key(), |footer| {
10898 footer.volume_index = 1;
10899 });
10900 assert_eq!(
10901 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_volume, &master_key())
10902 .unwrap_err(),
10903 FormatError::InvalidArchive("sidecar ManifestFooter volume_index must be zero")
10904 );
10905
10906 let mut non_authoritative = archive.bootstrap_sidecar.clone();
10907 mutate_sidecar_manifest(&mut non_authoritative, &master_key(), |footer| {
10908 footer.is_authoritative = 0;
10909 });
10910 assert_eq!(
10911 open_archive_with_bootstrap_sidecar(&archive.bytes, &non_authoritative, &master_key())
10912 .unwrap_err(),
10913 FormatError::InvalidArchive("sidecar ManifestFooter is not authoritative")
10914 );
10915 }
10916
10917 #[test]
10918 fn sidecar_manifest_validation_does_not_compare_opened_volume_index() {
10919 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10920 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
10921 let crypto_start = volume_header.crypto_header_offset as usize;
10922 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
10923 let crypto_header = CryptoHeader::parse(
10924 &archive.bytes[crypto_start..crypto_end],
10925 volume_header.crypto_header_length,
10926 )
10927 .unwrap();
10928 let subkeys = Subkeys::derive(
10929 &master_key(),
10930 &volume_header.archive_uuid,
10931 &volume_header.session_id,
10932 )
10933 .unwrap();
10934 let mut opened_header = volume_header;
10935 opened_header.volume_index = 1;
10936
10937 let parsed = parse_bootstrap_sidecar(
10938 &archive.bootstrap_sidecar,
10939 &opened_header,
10940 &crypto_header.fixed,
10941 &subkeys,
10942 )
10943 .unwrap();
10944
10945 assert_eq!(parsed.manifest_footer.unwrap().volume_index, 0);
10946 }
10947
10948 #[test]
10949 fn bootstrap_sidecar_rejects_conflicting_manifest_bootstrap_fields() {
10950 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
10951 let mut conflicting = archive.bootstrap_sidecar.clone();
10952 mutate_sidecar_manifest(&mut conflicting, &master_key(), |footer| {
10953 footer.index_root_first_block += 1;
10954 });
10955
10956 assert_eq!(
10957 open_archive_with_bootstrap_sidecar(&archive.bytes, &conflicting, &master_key())
10958 .unwrap_err(),
10959 FormatError::InvalidArchive("bootstrap sidecar conflicts with terminal ManifestFooter")
10960 );
10961 }
10962
10963 #[test]
10964 fn sidecar_size_cap_counts_only_present_sparse_sections() {
10965 let mut crypto_header = test_crypto_header();
10966 crypto_header.has_dictionary = 1;
10967 crypto_header.index_root_fec_data_shards = 1;
10968 crypto_header.index_root_fec_parity_shards = 0;
10969 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
10970 let header = BootstrapSidecarHeader {
10971 archive_uuid: [0x31; 16],
10972 session_id: [0x42; 16],
10973 flags: 0x04,
10974 manifest_footer_offset: 0,
10975 manifest_footer_length: 0,
10976 index_root_records_offset: 0,
10977 index_root_records_length: 0,
10978 dictionary_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
10979 dictionary_records_length: record_len,
10980 sidecar_hmac: [0u8; 32],
10981 header_crc32c: 0,
10982 };
10983
10984 validate_sidecar_size_cap(
10985 &header,
10986 &crypto_header,
10987 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len,
10988 )
10989 .unwrap();
10990 assert_eq!(
10991 validate_sidecar_size_cap(
10992 &header,
10993 &crypto_header,
10994 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len + 1,
10995 )
10996 .unwrap_err(),
10997 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
10998 );
10999 }
11000
11001 #[test]
11002 fn sidecar_size_cap_rejects_sparse_section_above_class_max() {
11003 let mut crypto_header = test_crypto_header();
11004 crypto_header.index_root_fec_data_shards = 1;
11005 crypto_header.index_root_fec_parity_shards = 0;
11006 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
11007 let header = BootstrapSidecarHeader {
11008 archive_uuid: [0x31; 16],
11009 session_id: [0x42; 16],
11010 flags: 0x02,
11011 manifest_footer_offset: 0,
11012 manifest_footer_length: 0,
11013 index_root_records_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
11014 index_root_records_length: record_len * 2,
11015 dictionary_records_offset: 0,
11016 dictionary_records_length: 0,
11017 sidecar_hmac: [0u8; 32],
11018 header_crc32c: 0,
11019 };
11020
11021 assert_eq!(
11022 validate_sidecar_size_cap(
11023 &header,
11024 &crypto_header,
11025 BOOTSTRAP_SIDECAR_HEADER_LEN as u64 + record_len * 2,
11026 )
11027 .unwrap_err(),
11028 FormatError::InvalidArchive("bootstrap sidecar IndexRoot records exceed resource cap")
11029 );
11030 }
11031
11032 #[test]
11033 fn sidecar_size_cap_uses_wide_arithmetic_for_large_record_classes() {
11034 let mut crypto_header = test_crypto_header();
11035 crypto_header.block_size = u32::MAX;
11036 crypto_header.index_root_fec_data_shards = u16::MAX;
11037 crypto_header.index_root_fec_parity_shards = u16::MAX;
11038 let record_len = crypto_header.block_size as u64 + BLOCK_RECORD_FRAMING_LEN as u64;
11039 let max_records = crypto_header.index_root_fec_data_shards as u64
11040 + crypto_header.index_root_fec_parity_shards as u64;
11041 let max_section_len = max_records * record_len;
11042 let cap = BOOTSTRAP_SIDECAR_HEADER_LEN as u64
11043 + MANIFEST_FOOTER_LEN as u64
11044 + max_section_len
11045 + max_section_len;
11046 let header = BootstrapSidecarHeader {
11047 archive_uuid: [0x31; 16],
11048 session_id: [0x42; 16],
11049 flags: 0x01 | 0x02 | 0x04,
11050 manifest_footer_offset: BOOTSTRAP_SIDECAR_HEADER_LEN as u64,
11051 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
11052 index_root_records_offset: 0,
11053 index_root_records_length: max_section_len,
11054 dictionary_records_offset: 0,
11055 dictionary_records_length: max_section_len,
11056 sidecar_hmac: [0u8; 32],
11057 header_crc32c: 0,
11058 };
11059
11060 validate_sidecar_size_cap(&header, &crypto_header, cap).unwrap();
11061 assert_eq!(
11062 validate_sidecar_size_cap(&header, &crypto_header, cap + 1).unwrap_err(),
11063 FormatError::InvalidArchive("bootstrap sidecar exceeds resource cap")
11064 );
11065 }
11066
11067 #[test]
11068 fn bootstrap_sidecar_rejects_dictionary_section_for_no_dictionary_archive() {
11069 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11070 let mut with_dictionary = archive.bootstrap_sidecar.clone();
11071 let header =
11072 BootstrapSidecarHeader::parse(&with_dictionary[..BOOTSTRAP_SIDECAR_HEADER_LEN])
11073 .unwrap();
11074 let record_len = sidecar_record_len(&with_dictionary);
11075 let first_record = header.index_root_records_offset as usize;
11076 let copied_record = with_dictionary[first_record..first_record + record_len].to_vec();
11077 let dictionary_offset = with_dictionary.len() as u64;
11078 with_dictionary.extend_from_slice(&copied_record);
11079 rewrite_sidecar_header(&mut with_dictionary, &master_key(), |header| {
11080 header.flags |= 0x04;
11081 header.dictionary_records_offset = dictionary_offset;
11082 header.dictionary_records_length = record_len as u64;
11083 });
11084
11085 assert_eq!(
11086 open_archive_with_bootstrap_sidecar(&archive.bytes, &with_dictionary, &master_key())
11087 .unwrap_err(),
11088 FormatError::InvalidArchive(
11089 "bootstrap sidecar has dictionary records while has_dictionary is false"
11090 )
11091 );
11092 }
11093
11094 #[test]
11095 fn bootstrap_sidecar_rejects_missing_duplicate_wrong_kind_and_wrong_last_flag() {
11096 let archive = write_archive(&[], &master_key(), single_stream_options()).unwrap();
11097 let mut missing = archive.bootstrap_sidecar.clone();
11098 let record_len = sidecar_record_len(&missing);
11099 let new_len = missing.len() - record_len;
11100 missing.truncate(new_len);
11101 rewrite_sidecar_header(&mut missing, &master_key(), |header| {
11102 header.index_root_records_length -= record_len as u64;
11103 });
11104 assert_eq!(
11105 open_archive_with_bootstrap_sidecar(&archive.bytes, &missing, &master_key())
11106 .unwrap_err(),
11107 FormatError::InvalidArchive(
11108 "sidecar BlockRecord section does not match declared extent"
11109 )
11110 );
11111
11112 let mut duplicate = archive.bootstrap_sidecar.clone();
11113 mutate_sidecar_index_record(&mut duplicate, 1, |record| {
11114 record.block_index -= 1;
11115 });
11116 assert_eq!(
11117 open_archive_with_bootstrap_sidecar(&archive.bytes, &duplicate, &master_key())
11118 .unwrap_err(),
11119 FormatError::InvalidArchive(
11120 "sidecar BlockRecord section has missing or duplicate blocks"
11121 )
11122 );
11123
11124 let mut misordered = archive.bootstrap_sidecar.clone();
11125 swap_sidecar_index_records(&mut misordered, 0, 1);
11126 assert_eq!(
11127 open_archive_with_bootstrap_sidecar(&archive.bytes, &misordered, &master_key())
11128 .unwrap_err(),
11129 FormatError::InvalidArchive(
11130 "sidecar BlockRecord section has missing or duplicate blocks"
11131 )
11132 );
11133
11134 let mut wrong_kind = archive.bootstrap_sidecar.clone();
11135 mutate_sidecar_index_record(&mut wrong_kind, 0, |record| {
11136 record.kind = BlockKind::PayloadData;
11137 });
11138 assert_eq!(
11139 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_kind, &master_key())
11140 .unwrap_err(),
11141 FormatError::InvalidArchive("sidecar BlockRecord section has wrong kind")
11142 );
11143
11144 let mut wrong_last = archive.bootstrap_sidecar.clone();
11145 mutate_sidecar_index_record(&mut wrong_last, 0, |record| {
11146 record.flags = 0;
11147 });
11148 assert_eq!(
11149 open_archive_with_bootstrap_sidecar(&archive.bytes, &wrong_last, &master_key())
11150 .unwrap_err(),
11151 FormatError::InvalidArchive("sidecar BlockRecord section has wrong last-data flag")
11152 );
11153 }
11154
11155 #[test]
11156 fn verify_helper_rejects_envelope_frame_coverage_gap() {
11157 let frames = BTreeMap::from([(
11158 0,
11159 FrameEntry {
11160 frame_index: 0,
11161 envelope_index: 0,
11162 offset_in_envelope: 0,
11163 compressed_size: 10,
11164 decompressed_size: 512,
11165 flags: 0,
11166 tar_stream_offset: 0,
11167 },
11168 )]);
11169 let envelopes = BTreeMap::from([(
11170 0,
11171 EnvelopeEntry {
11172 envelope_index: 0,
11173 first_block_index: 0,
11174 data_block_count: 1,
11175 parity_block_count: 1,
11176 encrypted_size: 4096,
11177 plaintext_size: 11,
11178 first_frame_index: 0,
11179 frame_count: 1,
11180 },
11181 )]);
11182
11183 assert_eq!(
11184 validate_envelope_frame_coverage(&frames, &envelopes).unwrap_err(),
11185 FormatError::InvalidArchive("EnvelopeEntry frame coverage has a gap or overlap")
11186 );
11187 }
11188
11189 #[test]
11190 fn verify_helper_rejects_file_extent_gaps_and_overlaps() {
11191 assert!(validate_file_extent_coverage_ranges(&[(512, 512), (0, 512)], 1024).is_ok());
11192 assert_eq!(
11193 validate_file_extent_coverage_ranges(&[(0, 512), (1024, 512)], 1536).unwrap_err(),
11194 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11195 );
11196 assert_eq!(
11197 validate_file_extent_coverage_ranges(&[(0, 1024), (512, 512)], 1024).unwrap_err(),
11198 FormatError::InvalidArchive("FileEntry extents do not cover tar stream exactly")
11199 );
11200 }
11201
11202 #[test]
11203 fn verify_rejects_authenticated_content_hash_mismatch() {
11204 let options = WriterOptions {
11205 index_root_fec_parity_shards: 0,
11206 ..single_stream_options()
11207 };
11208 let archive = write_archive(
11209 &[RegularFile::new("content-hash.txt", b"hash covered")],
11210 &master_key(),
11211 options,
11212 )
11213 .unwrap();
11214 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11215
11216 let mut root = opened.index_root.clone();
11217 root.header.content_sha256 = [0xa5; 32];
11218 let root_plaintext = root.to_bytes();
11219 IndexRoot::parse(
11220 &root_plaintext,
11221 false,
11222 metadata_limits(&opened.crypto_header),
11223 )
11224 .unwrap();
11225 assert_eq!(
11226 root_plaintext.len() as u32,
11227 opened.manifest_footer.index_root_decompressed_size
11228 );
11229
11230 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11231 let mut next_block_index = opened.manifest_footer.index_root_first_block;
11232 let replacement = encrypt_test_object(
11233 &compressed_root,
11234 &opened.subkeys.index_root_key,
11235 &opened.subkeys.index_nonce_seed,
11236 b"idxroot",
11237 0,
11238 BlockKind::IndexRootData,
11239 &mut next_block_index,
11240 &opened.crypto_header,
11241 &opened.volume_header,
11242 );
11243 assert_eq!(
11244 replacement.extent.data_block_count,
11245 opened.manifest_footer.index_root_data_block_count
11246 );
11247 assert_eq!(
11248 replacement.extent.encrypted_size,
11249 opened.manifest_footer.index_root_encrypted_size
11250 );
11251
11252 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11253 let crypto_end = volume_header.crypto_header_offset as usize
11254 + volume_header.crypto_header_length as usize;
11255 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11256 let mut malformed = archive.bytes.clone();
11257 for record in replacement.records {
11258 let offset = crypto_end + record.block_index as usize * record_len;
11259 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11260 }
11261
11262 let reopened = open_archive(&malformed, &master_key()).unwrap();
11263 assert_eq!(
11264 reopened.verify().unwrap_err(),
11265 FormatError::InvalidArchive(
11266 "IndexRoot content_sha256 does not match decoded tar stream"
11267 )
11268 );
11269 }
11270
11271 #[test]
11272 fn verify_rejects_file_entry_tar_path_and_size_mismatches() {
11273 let (mut path_mismatch, _) = multi_envelope_reader_fixture();
11274 rewrite_as_single_healthy_file(&mut path_mismatch, |_file, path| {
11275 path[0] = b'x';
11276 });
11277 assert_eq!(
11278 path_mismatch.verify().unwrap_err(),
11279 FormatError::InvalidArchive("tar member path does not match FileEntry path")
11280 );
11281
11282 let (mut size_mismatch, _) = multi_envelope_reader_fixture();
11283 rewrite_as_single_healthy_file(&mut size_mismatch, |file, _path| {
11284 file.file_data_size += 1;
11285 });
11286 assert_eq!(
11287 size_mismatch.verify().unwrap_err(),
11288 FormatError::InvalidArchive("tar member size does not match FileEntry file_data_size")
11289 );
11290 }
11291
11292 #[test]
11293 fn verify_rejects_inconsistent_duplicate_local_frame_rows_across_shards() {
11294 let (mut opened, _) = multi_envelope_reader_fixture();
11295 let locating = opened.index_root.shards[0].clone();
11296 let mut duplicate = opened.load_index_shard(&locating).unwrap();
11297 duplicate.header.shard_index = 1;
11298 duplicate.frames[0].flags ^= 0x0000_0001;
11299 let duplicate_plaintext = duplicate.to_bytes();
11300 let mut next_block_index = opened
11301 .blocks
11302 .keys()
11303 .last()
11304 .copied()
11305 .map(|index| index + 1)
11306 .unwrap_or(0);
11307 let duplicate_object = encrypt_test_object(
11308 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11309 &opened.subkeys.index_shard_key,
11310 &opened.subkeys.index_nonce_seed,
11311 b"idxshard",
11312 1,
11313 BlockKind::IndexShardData,
11314 &mut next_block_index,
11315 &opened.crypto_header,
11316 &opened.volume_header,
11317 );
11318 insert_records(&mut opened.blocks, &duplicate_object.records);
11319 opened.index_root.shards.push(ShardEntry {
11320 shard_index: 1,
11321 first_block_index: duplicate_object.extent.first_block_index,
11322 data_block_count: duplicate_object.extent.data_block_count,
11323 parity_block_count: 0,
11324 encrypted_size: duplicate_object.extent.encrypted_size,
11325 decompressed_size: duplicate_plaintext.len() as u32,
11326 file_count: locating.file_count,
11327 first_path_hash: locating.first_path_hash,
11328 last_path_hash: locating.last_path_hash,
11329 });
11330 opened.index_root.header.file_count += locating.file_count as u64;
11331
11332 assert_eq!(
11333 opened.verify().unwrap_err(),
11334 FormatError::InvalidArchive("duplicate FrameEntry rows do not match")
11335 );
11336 }
11337
11338 #[test]
11339 fn verify_rejects_inconsistent_duplicate_local_envelope_rows_across_shards() {
11340 let (mut opened, _) = multi_envelope_reader_fixture();
11341 let locating = opened.index_root.shards[0].clone();
11342 let mut duplicate = opened.load_index_shard(&locating).unwrap();
11343 duplicate.header.shard_index = 1;
11344 duplicate.envelopes[0].first_block_index += 1;
11345 let duplicate_plaintext = duplicate.to_bytes();
11346 let mut next_block_index = opened
11347 .blocks
11348 .keys()
11349 .last()
11350 .copied()
11351 .map(|index| index + 1)
11352 .unwrap_or(0);
11353 let duplicate_object = encrypt_test_object(
11354 &compress_zstd_frame(&duplicate_plaintext, 1).unwrap(),
11355 &opened.subkeys.index_shard_key,
11356 &opened.subkeys.index_nonce_seed,
11357 b"idxshard",
11358 1,
11359 BlockKind::IndexShardData,
11360 &mut next_block_index,
11361 &opened.crypto_header,
11362 &opened.volume_header,
11363 );
11364 insert_records(&mut opened.blocks, &duplicate_object.records);
11365 opened.index_root.shards.push(ShardEntry {
11366 shard_index: 1,
11367 first_block_index: duplicate_object.extent.first_block_index,
11368 data_block_count: duplicate_object.extent.data_block_count,
11369 parity_block_count: 0,
11370 encrypted_size: duplicate_object.extent.encrypted_size,
11371 decompressed_size: duplicate_plaintext.len() as u32,
11372 file_count: locating.file_count,
11373 first_path_hash: locating.first_path_hash,
11374 last_path_hash: locating.last_path_hash,
11375 });
11376 opened.index_root.header.file_count += locating.file_count as u64;
11377
11378 assert_eq!(
11379 opened.verify().unwrap_err(),
11380 FormatError::InvalidArchive("duplicate EnvelopeEntry rows do not match")
11381 );
11382 }
11383
11384 #[test]
11385 fn verify_rejects_non_contiguous_global_envelope_indexes() {
11386 let (mut opened, _) = multi_envelope_reader_fixture();
11387 replace_first_index_shard(&mut opened, |shard| {
11388 let frame = shard
11389 .frames
11390 .iter_mut()
11391 .find(|entry| entry.frame_index == 1)
11392 .unwrap();
11393 frame.envelope_index = 2;
11394
11395 let envelope = shard
11396 .envelopes
11397 .iter_mut()
11398 .find(|entry| entry.envelope_index == 1)
11399 .unwrap();
11400 envelope.envelope_index = 2;
11401 });
11402
11403 assert_eq!(
11404 opened.verify().unwrap_err(),
11405 FormatError::InvalidMetadata {
11406 structure: "EnvelopeEntry",
11407 reason: "global index coverage has a gap",
11408 }
11409 );
11410 }
11411
11412 #[test]
11413 fn verify_rejects_payload_object_extent_overlap() {
11414 let (mut opened, _) = multi_envelope_reader_fixture();
11415 replace_first_index_shard(&mut opened, |shard| {
11416 let first_block_index = shard.envelopes[0].first_block_index;
11417 shard.envelopes[1].first_block_index = first_block_index;
11418 });
11419
11420 assert_eq!(
11421 opened.verify().unwrap_err(),
11422 FormatError::InvalidArchive("encrypted object block ranges overlap")
11423 );
11424 }
11425
11426 #[test]
11427 fn verify_accepts_cross_shard_shared_envelope_frame_union() {
11428 let volume_header = test_volume_header();
11429 let crypto_header = test_crypto_header();
11430 let subkeys = Subkeys::derive(
11431 &master_key(),
11432 &volume_header.archive_uuid,
11433 &volume_header.session_id,
11434 )
11435 .unwrap();
11436 let mut next_block_index = 0u64;
11437 let mut blocks = BTreeMap::new();
11438
11439 let alpha = test_member(b"alpha.txt", b"alpha cross shard\n");
11440 let beta = test_member(b"beta.txt", b"beta cross shard\n");
11441 let tar_stream = [alpha.as_slice(), beta.as_slice()].concat();
11442 let frame0_plaintext = compress_zstd_frame(&alpha, 1).unwrap();
11443 let frame1_plaintext = compress_zstd_frame(&beta, 1).unwrap();
11444 let envelope_plaintext =
11445 [frame0_plaintext.as_slice(), frame1_plaintext.as_slice()].concat();
11446 let payload = encrypt_test_object(
11447 &envelope_plaintext,
11448 &subkeys.enc_key,
11449 &subkeys.nonce_seed,
11450 b"envelope",
11451 0,
11452 BlockKind::PayloadData,
11453 &mut next_block_index,
11454 &crypto_header,
11455 &volume_header,
11456 );
11457 insert_records(&mut blocks, &payload.records);
11458
11459 let envelope = EnvelopeEntry {
11460 envelope_index: 0,
11461 first_block_index: payload.extent.first_block_index,
11462 data_block_count: payload.extent.data_block_count,
11463 parity_block_count: 0,
11464 encrypted_size: payload.extent.encrypted_size,
11465 plaintext_size: envelope_plaintext.len() as u32,
11466 first_frame_index: 0,
11467 frame_count: 2,
11468 };
11469 let frame0 = FrameEntry {
11470 frame_index: 0,
11471 envelope_index: 0,
11472 offset_in_envelope: 0,
11473 compressed_size: frame0_plaintext.len() as u32,
11474 decompressed_size: alpha.len() as u32,
11475 flags: 0x0000_0003,
11476 tar_stream_offset: 0,
11477 };
11478 let frame1 = FrameEntry {
11479 frame_index: 1,
11480 envelope_index: 0,
11481 offset_in_envelope: frame0_plaintext.len() as u32,
11482 compressed_size: frame1_plaintext.len() as u32,
11483 decompressed_size: beta.len() as u32,
11484 flags: 0x0000_0003,
11485 tar_stream_offset: alpha.len() as u64,
11486 };
11487
11488 let (shard0_plaintext, first0, last0) = build_test_index_shard(
11489 &[TestFileMeta {
11490 path: b"alpha.txt".to_vec(),
11491 frame_index: 0,
11492 tar_stream_offset: 0,
11493 member_group_size: alpha.len() as u64,
11494 file_data_size: b"alpha cross shard\n".len() as u64,
11495 }],
11496 &[frame0],
11497 std::slice::from_ref(&envelope),
11498 );
11499 let (mut shard1_plaintext, first1, last1) = build_test_index_shard(
11500 &[TestFileMeta {
11501 path: b"beta.txt".to_vec(),
11502 frame_index: 1,
11503 tar_stream_offset: alpha.len() as u64,
11504 member_group_size: beta.len() as u64,
11505 file_data_size: b"beta cross shard\n".len() as u64,
11506 }],
11507 &[frame1],
11508 std::slice::from_ref(&envelope),
11509 );
11510 shard1_plaintext[8..16].copy_from_slice(&1u64.to_le_bytes());
11511
11512 let shard0 = encrypt_test_object(
11513 &compress_zstd_frame(&shard0_plaintext, 1).unwrap(),
11514 &subkeys.index_shard_key,
11515 &subkeys.index_nonce_seed,
11516 b"idxshard",
11517 0,
11518 BlockKind::IndexShardData,
11519 &mut next_block_index,
11520 &crypto_header,
11521 &volume_header,
11522 );
11523 let shard1 = encrypt_test_object(
11524 &compress_zstd_frame(&shard1_plaintext, 1).unwrap(),
11525 &subkeys.index_shard_key,
11526 &subkeys.index_nonce_seed,
11527 b"idxshard",
11528 1,
11529 BlockKind::IndexShardData,
11530 &mut next_block_index,
11531 &crypto_header,
11532 &volume_header,
11533 );
11534 insert_records(&mut blocks, &shard0.records);
11535 insert_records(&mut blocks, &shard1.records);
11536
11537 let index_root = IndexRoot {
11538 header: IndexRootHeader {
11539 frame_count: 2,
11540 envelope_count: 1,
11541 file_count: 2,
11542 payload_block_count: payload.extent.data_block_count as u64,
11543 tar_total_size: tar_stream.len() as u64,
11544 content_sha256: sha256_bytes(&tar_stream),
11545 ..IndexRootHeader::empty()
11546 },
11547 shards: vec![
11548 ShardEntry {
11549 shard_index: 0,
11550 first_block_index: shard0.extent.first_block_index,
11551 data_block_count: shard0.extent.data_block_count,
11552 parity_block_count: 0,
11553 encrypted_size: shard0.extent.encrypted_size,
11554 decompressed_size: shard0_plaintext.len() as u32,
11555 file_count: 1,
11556 first_path_hash: first0,
11557 last_path_hash: last0,
11558 },
11559 ShardEntry {
11560 shard_index: 1,
11561 first_block_index: shard1.extent.first_block_index,
11562 data_block_count: shard1.extent.data_block_count,
11563 parity_block_count: 0,
11564 encrypted_size: shard1.extent.encrypted_size,
11565 decompressed_size: shard1_plaintext.len() as u32,
11566 file_count: 1,
11567 first_path_hash: first1,
11568 last_path_hash: last1,
11569 },
11570 ],
11571 directory_hint_shards: Vec::new(),
11572 };
11573
11574 let index_root_plaintext = index_root.to_bytes();
11575 let index_root_object = encrypt_test_object(
11576 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
11577 &subkeys.index_root_key,
11578 &subkeys.index_nonce_seed,
11579 b"idxroot",
11580 0,
11581 BlockKind::IndexRootData,
11582 &mut next_block_index,
11583 &crypto_header,
11584 &volume_header,
11585 );
11586 insert_records(&mut blocks, &index_root_object.records);
11587
11588 let archive_uuid = volume_header.archive_uuid;
11589 let session_id = volume_header.session_id;
11590 let opened = OpenedArchive {
11591 options: ReaderOptions::default(),
11592 observed_archive_bytes: 1_000_000,
11593 observed_volume_count: 1,
11594 subkeys,
11595 blocks,
11596 lazy_blocks: None,
11597 crypto_header_bytes: Vec::new(),
11598 volume_header,
11599 crypto_header,
11600 manifest_footer: ManifestFooter {
11601 archive_uuid,
11602 session_id,
11603 volume_index: 0,
11604 is_authoritative: 1,
11605 total_volumes: 1,
11606 index_root_first_block: index_root_object.extent.first_block_index,
11607 index_root_data_block_count: index_root_object.extent.data_block_count,
11608 index_root_parity_block_count: 0,
11609 index_root_encrypted_size: index_root_object.extent.encrypted_size,
11610 index_root_decompressed_size: index_root_plaintext.len() as u32,
11611 manifest_hmac: [0u8; 32],
11612 },
11613 volume_trailer: Some(VolumeTrailer {
11614 archive_uuid,
11615 session_id,
11616 volume_index: 0,
11617 block_count: next_block_index,
11618 bytes_written: 0,
11619 manifest_footer_offset: 0,
11620 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
11621 closed_at_ns: 0,
11622 root_auth_footer_offset: 0,
11623 root_auth_footer_length: 0,
11624 root_auth_flags: 0,
11625 trailer_hmac: [0u8; 32],
11626 }),
11627 root_auth_footer: None,
11628 index_root,
11629 payload_dictionary: None,
11630 };
11631
11632 opened.verify().unwrap();
11633 }
11634
11635 #[test]
11636 fn verify_rejects_authenticated_archive_missing_required_directory_hints() {
11637 let options = WriterOptions {
11638 index_root_fec_parity_shards: 0,
11639 ..single_stream_options()
11640 };
11641 let archive = write_archive(
11642 &[RegularFile::new("only.txt", b"only payload")],
11643 &master_key(),
11644 options,
11645 )
11646 .unwrap();
11647 let opened = open_archive(&archive.bytes, &master_key()).unwrap();
11648 assert!(opened.index_root.directory_hint_shards.is_empty());
11649
11650 let mut root = opened.index_root.clone();
11651 root.header.file_count = DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1;
11652 root.shards[0].file_count = (DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1) as u32;
11653 let root_plaintext = root.to_bytes();
11654 IndexRoot::parse(
11655 &root_plaintext,
11656 false,
11657 metadata_limits(&opened.crypto_header),
11658 )
11659 .unwrap();
11660 assert_eq!(
11661 root_plaintext.len() as u32,
11662 opened.manifest_footer.index_root_decompressed_size
11663 );
11664
11665 let compressed_root = compress_zstd_frame(&root_plaintext, options.zstd_level).unwrap();
11666 let mut next_block_index = opened.manifest_footer.index_root_first_block;
11667 let replacement = encrypt_test_object(
11668 &compressed_root,
11669 &opened.subkeys.index_root_key,
11670 &opened.subkeys.index_nonce_seed,
11671 b"idxroot",
11672 0,
11673 BlockKind::IndexRootData,
11674 &mut next_block_index,
11675 &opened.crypto_header,
11676 &opened.volume_header,
11677 );
11678 assert_eq!(
11679 replacement.extent.first_block_index,
11680 opened.manifest_footer.index_root_first_block
11681 );
11682 assert_eq!(
11683 replacement.extent.data_block_count,
11684 opened.manifest_footer.index_root_data_block_count
11685 );
11686 assert_eq!(
11687 replacement.extent.encrypted_size,
11688 opened.manifest_footer.index_root_encrypted_size
11689 );
11690
11691 let volume_header = VolumeHeader::parse(&archive.bytes[..VOLUME_HEADER_LEN]).unwrap();
11692 let crypto_end = volume_header.crypto_header_offset as usize
11693 + volume_header.crypto_header_length as usize;
11694 let record_len = opened.crypto_header.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
11695 let mut malformed = archive.bytes.clone();
11696 for record in replacement.records {
11697 let offset = crypto_end + record.block_index as usize * record_len;
11698 malformed[offset..offset + record_len].copy_from_slice(&record.to_bytes());
11699 }
11700
11701 let reopened = open_archive(&malformed, &master_key()).unwrap();
11702 assert_eq!(
11703 reopened.index_root.header.file_count,
11704 DIRECTORY_HINT_REQUIRED_FILE_COUNT + 1
11705 );
11706 assert!(reopened.index_root.directory_hint_shards.is_empty());
11707
11708 assert_eq!(
11709 reopened.verify().unwrap_err(),
11710 FormatError::InvalidArchive("IndexRoot file_count requires directory hints")
11711 );
11712 }
11713
11714 #[test]
11715 fn expected_directory_hint_rows_include_ancestors_and_directory_entries() {
11716 let mut map = DirectoryHintMap::new();
11717 add_expected_directory_hint_rows(&mut map, 2, b"foo/bar/baz.txt", TarEntryKind::Regular);
11718 add_expected_directory_hint_rows(&mut map, 4, b"foo/bar", TarEntryKind::Directory);
11719
11720 assert_eq!(map.get(&Vec::new()), Some(&BTreeSet::from([2, 4])));
11721 assert_eq!(map.get(b"foo".as_slice()), Some(&BTreeSet::from([2, 4])));
11722 assert_eq!(
11723 map.get(b"foo/bar".as_slice()),
11724 Some(&BTreeSet::from([2, 4]))
11725 );
11726 assert!(!map.contains_key(b"foo/bar/baz.txt".as_slice()));
11727 assert!(!map.contains_key(b"foobar".as_slice()));
11728 }
11729
11730 #[test]
11731 fn directory_hint_validation_requires_exact_global_map() {
11732 let mut expected = DirectoryHintMap::new();
11733 add_expected_directory_hint_rows(&mut expected, 0, b"foo/bar.txt", TarEntryKind::Regular);
11734 add_expected_directory_hint_rows(&mut expected, 1, b"foo", TarEntryKind::Directory);
11735 let rows = sorted_directory_hint_rows(&expected);
11736 let table = directory_hint_table_from_rows(7, &rows, 2);
11737
11738 validate_directory_hint_tables_against_expected(std::slice::from_ref(&table), &expected)
11739 .unwrap();
11740
11741 let mut missing_root = expected.clone();
11742 missing_root.remove(&Vec::new());
11743 let missing_root_rows = sorted_directory_hint_rows(&missing_root);
11744 let missing_root_table = directory_hint_table_from_rows(8, &missing_root_rows, 2);
11745 assert_eq!(
11746 validate_directory_hint_tables_against_expected(&[missing_root_table], &expected)
11747 .unwrap_err(),
11748 FormatError::InvalidArchive("directory hint map does not match decoded files")
11749 );
11750
11751 let mut expected_missing_directory_entry = expected.clone();
11752 expected_missing_directory_entry
11753 .get_mut(b"foo".as_slice())
11754 .unwrap()
11755 .remove(&1);
11756 assert_eq!(
11757 validate_directory_hint_tables_against_expected(
11758 std::slice::from_ref(&table),
11759 &expected_missing_directory_entry,
11760 )
11761 .unwrap_err(),
11762 FormatError::InvalidArchive("directory hint map does not match decoded files")
11763 );
11764
11765 let mut extra = expected.clone();
11766 extra.insert(b"foo/extra".to_vec(), BTreeSet::from([0]));
11767 let extra_rows = sorted_directory_hint_rows(&extra);
11768 let extra_table = directory_hint_table_from_rows(9, &extra_rows, 2);
11769 assert_eq!(
11770 validate_directory_hint_tables_against_expected(&[extra_table], &expected).unwrap_err(),
11771 FormatError::InvalidArchive("directory hint map does not match decoded files")
11772 );
11773 }
11774
11775 #[test]
11776 fn directory_hint_validation_rejects_global_order_mismatch() {
11777 let mut expected = DirectoryHintMap::new();
11778 expected.insert(Vec::new(), BTreeSet::from([0]));
11779 expected.insert(b"alpha".to_vec(), BTreeSet::from([0]));
11780 let rows = sorted_directory_hint_rows(&expected);
11781 let first = directory_hint_table_from_rows(8, &rows[..1], 1);
11782 let second = directory_hint_table_from_rows(9, &rows[1..], 1);
11783
11784 assert_eq!(
11785 validate_directory_hint_tables_against_expected(&[second, first], &expected)
11786 .unwrap_err(),
11787 FormatError::InvalidArchive("DirectoryHintEntry rows are not globally sorted")
11788 );
11789 }
11790
11791 #[test]
11792 fn object_extent_rejects_parity_above_class_cap() {
11793 let crypto_header = CryptoHeaderFixed {
11794 length: 0,
11795 compression_algo: CompressionAlgo::ZstdFramed,
11796 aead_algo: AeadAlgo::AesGcmSiv256,
11797 fec_algo: FecAlgo::ReedSolomonGF16,
11798 kdf_algo: KdfAlgo::Raw,
11799 chunk_size: 1024,
11800 envelope_target_size: 4096,
11801 block_size: 4096,
11802 fec_data_shards: 1,
11803 fec_parity_shards: 1,
11804 index_fec_data_shards: 1,
11805 index_fec_parity_shards: 1,
11806 index_root_fec_data_shards: 1,
11807 index_root_fec_parity_shards: 1,
11808 stripe_width: 1,
11809 volume_loss_tolerance: 0,
11810 bit_rot_buffer_pct: 0,
11811 has_dictionary: 0,
11812 max_path_length: 4096,
11813 expected_volume_size: 0,
11814 };
11815 let extent = ObjectExtent {
11816 first_block_index: 0,
11817 data_block_count: 1,
11818 parity_block_count: 2,
11819 encrypted_size: 4096,
11820 };
11821
11822 assert_eq!(
11823 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11824 FormatError::InvalidArchive("encrypted object exceeds its class parity-shard maximum")
11825 );
11826 }
11827
11828 #[test]
11829 fn object_extent_rejects_parity_below_recoverability_requirement() {
11830 let crypto_header = CryptoHeaderFixed {
11831 length: 0,
11832 compression_algo: CompressionAlgo::ZstdFramed,
11833 aead_algo: AeadAlgo::AesGcmSiv256,
11834 fec_algo: FecAlgo::ReedSolomonGF16,
11835 kdf_algo: KdfAlgo::Raw,
11836 chunk_size: 1024,
11837 envelope_target_size: 4096,
11838 block_size: 4096,
11839 fec_data_shards: 1,
11840 fec_parity_shards: 1,
11841 index_fec_data_shards: 1,
11842 index_fec_parity_shards: 1,
11843 index_root_fec_data_shards: 1,
11844 index_root_fec_parity_shards: 1,
11845 stripe_width: 2,
11846 volume_loss_tolerance: 1,
11847 bit_rot_buffer_pct: 0,
11848 has_dictionary: 0,
11849 max_path_length: 4096,
11850 expected_volume_size: 0,
11851 };
11852 let extent = ObjectExtent {
11853 first_block_index: 0,
11854 data_block_count: 1,
11855 parity_block_count: 0,
11856 encrypted_size: 4096,
11857 };
11858
11859 assert_eq!(
11860 validate_object_extent(extent, &crypto_header, 1, 1).unwrap_err(),
11861 FormatError::InvalidArchive(
11862 "encrypted object parity does not match v41 compute_parity"
11863 )
11864 );
11865 }
11866
11867 #[test]
11868 fn encrypted_object_extent_matrix_rejects_overlaps() {
11869 let (opened, _) = multi_envelope_reader_fixture();
11870 let loaded_shard = opened
11871 .load_index_shard(&opened.index_root.shards[0])
11872 .unwrap();
11873 let base_envelopes = loaded_shard
11874 .envelopes
11875 .iter()
11876 .map(|entry| (entry.envelope_index, entry.clone()))
11877 .collect::<BTreeMap<_, _>>();
11878 let payload_start = loaded_shard.envelopes[0].first_block_index;
11879 let overlap = FormatError::InvalidArchive("encrypted object block ranges overlap");
11880
11881 let mut payload_overlap = base_envelopes.clone();
11882 payload_overlap
11883 .get_mut(&loaded_shard.envelopes[1].envelope_index)
11884 .unwrap()
11885 .first_block_index = payload_start;
11886 assert_eq!(
11887 opened
11888 .validate_encrypted_object_block_ranges(&payload_overlap)
11889 .unwrap_err(),
11890 overlap
11891 );
11892
11893 let mut shard_overlap = opened.clone();
11894 let shard = shard_overlap.index_root.shards[0].clone();
11895 shard_overlap.index_root.shards.push(ShardEntry {
11896 shard_index: 1,
11897 ..shard
11898 });
11899 assert_eq!(
11900 shard_overlap
11901 .validate_encrypted_object_block_ranges(&base_envelopes)
11902 .unwrap_err(),
11903 overlap
11904 );
11905
11906 let mut dictionary_overlap = opened.clone();
11907 dictionary_overlap.crypto_header.has_dictionary = 1;
11908 dictionary_overlap.index_root.header.dictionary_first_block = payload_start;
11909 dictionary_overlap
11910 .index_root
11911 .header
11912 .dictionary_data_block_count = 1;
11913 dictionary_overlap
11914 .index_root
11915 .header
11916 .dictionary_parity_block_count = 0;
11917 dictionary_overlap
11918 .index_root
11919 .header
11920 .dictionary_encrypted_size = 4096;
11921 dictionary_overlap
11922 .index_root
11923 .header
11924 .dictionary_decompressed_size = 128;
11925 assert_eq!(
11926 dictionary_overlap
11927 .validate_encrypted_object_block_ranges(&base_envelopes)
11928 .unwrap_err(),
11929 overlap
11930 );
11931
11932 let mut hint_overlap = opened.clone();
11933 hint_overlap
11934 .index_root
11935 .directory_hint_shards
11936 .push(DirectoryHintShardEntry {
11937 hint_shard_index: 0,
11938 first_dir_hash: [0; 8],
11939 last_dir_hash: [0; 8],
11940 first_block_index: payload_start,
11941 data_block_count: 1,
11942 parity_block_count: 0,
11943 encrypted_size: 4096,
11944 decompressed_size: 128,
11945 entry_count: 1,
11946 });
11947 assert_eq!(
11948 hint_overlap
11949 .validate_encrypted_object_block_ranges(&base_envelopes)
11950 .unwrap_err(),
11951 overlap
11952 );
11953 }
11954
11955 #[test]
11956 fn load_metadata_object_rejects_per_object_zstd_frame_exactness_mutations() {
11957 let volume_header = test_volume_header();
11958 let crypto_header = test_crypto_header();
11959 let subkeys = Subkeys::derive(
11960 &master_key(),
11961 &volume_header.archive_uuid,
11962 &volume_header.session_id,
11963 )
11964 .unwrap();
11965 let mut next_block_index = 0u64;
11966
11967 let index_root_payload = b"index root metadata object";
11968 let index_root_compressed = compress_zstd_frame(index_root_payload, 1).unwrap();
11969 assert_metadata_object_from_compressed(
11970 &{
11971 let mut bytes = index_root_compressed.clone();
11972 bytes.push(0);
11973 bytes
11974 },
11975 index_root_payload.len(),
11976 &subkeys,
11977 &volume_header,
11978 &crypto_header,
11979 &subkeys.index_root_key,
11980 &subkeys.index_nonce_seed,
11981 b"idxroot",
11982 0,
11983 BlockKind::IndexRootData,
11984 BlockKind::IndexRootParity,
11985 crypto_header.index_root_fec_data_shards,
11986 crypto_header.index_root_fec_parity_shards,
11987 &mut next_block_index,
11988 FormatError::TrailingBytesAfterZstdFrame,
11989 );
11990 assert_metadata_object_from_compressed(
11991 &index_root_compressed,
11992 index_root_payload.len() + 1,
11993 &subkeys,
11994 &volume_header,
11995 &crypto_header,
11996 &subkeys.index_root_key,
11997 &subkeys.index_nonce_seed,
11998 b"idxroot",
11999 0,
12000 BlockKind::IndexRootData,
12001 BlockKind::IndexRootParity,
12002 crypto_header.index_root_fec_data_shards,
12003 crypto_header.index_root_fec_parity_shards,
12004 &mut next_block_index,
12005 FormatError::ZstdDecompressedSizeMismatch {
12006 expected: index_root_payload.len() + 1,
12007 actual: index_root_payload.len(),
12008 },
12009 );
12010
12011 let index_shard_payload = b"index shard metadata object";
12012 let index_shard_compressed = compress_zstd_frame(index_shard_payload, 1).unwrap();
12013 assert_metadata_object_from_compressed(
12014 &{
12015 let mut bytes = index_shard_compressed.clone();
12016 bytes.push(0);
12017 bytes
12018 },
12019 index_shard_payload.len(),
12020 &subkeys,
12021 &volume_header,
12022 &crypto_header,
12023 &subkeys.index_shard_key,
12024 &subkeys.index_nonce_seed,
12025 b"idxshard",
12026 1,
12027 BlockKind::IndexShardData,
12028 BlockKind::IndexShardParity,
12029 crypto_header.index_fec_data_shards,
12030 crypto_header.index_fec_parity_shards,
12031 &mut next_block_index,
12032 FormatError::TrailingBytesAfterZstdFrame,
12033 );
12034 assert_metadata_object_from_compressed(
12035 &index_shard_compressed,
12036 index_shard_payload.len() + 1,
12037 &subkeys,
12038 &volume_header,
12039 &crypto_header,
12040 &subkeys.index_shard_key,
12041 &subkeys.index_nonce_seed,
12042 b"idxshard",
12043 1,
12044 BlockKind::IndexShardData,
12045 BlockKind::IndexShardParity,
12046 crypto_header.index_fec_data_shards,
12047 crypto_header.index_fec_parity_shards,
12048 &mut next_block_index,
12049 FormatError::ZstdDecompressedSizeMismatch {
12050 expected: index_shard_payload.len() + 1,
12051 actual: index_shard_payload.len(),
12052 },
12053 );
12054
12055 let directory_hint_payload = b"directory hint metadata object";
12056 let directory_hint_compressed = compress_zstd_frame(directory_hint_payload, 1).unwrap();
12057 assert_metadata_object_from_compressed(
12058 &{
12059 let mut bytes = directory_hint_compressed.clone();
12060 bytes.push(0);
12061 bytes
12062 },
12063 directory_hint_payload.len(),
12064 &subkeys,
12065 &volume_header,
12066 &crypto_header,
12067 &subkeys.dir_hint_key,
12068 &subkeys.index_nonce_seed,
12069 b"dirhint",
12070 0,
12071 BlockKind::DirectoryHintData,
12072 BlockKind::DirectoryHintParity,
12073 crypto_header.index_fec_data_shards,
12074 crypto_header.index_fec_parity_shards,
12075 &mut next_block_index,
12076 FormatError::TrailingBytesAfterZstdFrame,
12077 );
12078 assert_metadata_object_from_compressed(
12079 &directory_hint_compressed,
12080 directory_hint_payload.len() + 1,
12081 &subkeys,
12082 &volume_header,
12083 &crypto_header,
12084 &subkeys.dir_hint_key,
12085 &subkeys.index_nonce_seed,
12086 b"dirhint",
12087 0,
12088 BlockKind::DirectoryHintData,
12089 BlockKind::DirectoryHintParity,
12090 crypto_header.index_fec_data_shards,
12091 crypto_header.index_fec_parity_shards,
12092 &mut next_block_index,
12093 FormatError::ZstdDecompressedSizeMismatch {
12094 expected: directory_hint_payload.len() + 1,
12095 actual: directory_hint_payload.len(),
12096 },
12097 );
12098
12099 let dictionary_payload = b"dictionary metadata object";
12100 let dictionary_compressed = compress_zstd_frame(dictionary_payload, 1).unwrap();
12101 assert_metadata_object_from_compressed(
12102 &{
12103 let mut bytes = dictionary_compressed.clone();
12104 bytes.push(0);
12105 bytes
12106 },
12107 dictionary_payload.len(),
12108 &subkeys,
12109 &volume_header,
12110 &crypto_header,
12111 &subkeys.dictionary_key,
12112 &subkeys.index_nonce_seed,
12113 b"dict",
12114 0,
12115 BlockKind::DictionaryData,
12116 BlockKind::DictionaryParity,
12117 crypto_header.index_root_fec_data_shards,
12118 crypto_header.index_root_fec_parity_shards,
12119 &mut next_block_index,
12120 FormatError::TrailingBytesAfterZstdFrame,
12121 );
12122 assert_metadata_object_from_compressed(
12123 &dictionary_compressed,
12124 dictionary_payload.len() + 1,
12125 &subkeys,
12126 &volume_header,
12127 &crypto_header,
12128 &subkeys.dictionary_key,
12129 &subkeys.index_nonce_seed,
12130 b"dict",
12131 0,
12132 BlockKind::DictionaryData,
12133 BlockKind::DictionaryParity,
12134 crypto_header.index_root_fec_data_shards,
12135 crypto_header.index_root_fec_parity_shards,
12136 &mut next_block_index,
12137 FormatError::ZstdDecompressedSizeMismatch {
12138 expected: dictionary_payload.len() + 1,
12139 actual: dictionary_payload.len(),
12140 },
12141 );
12142 }
12143
12144 #[test]
12145 fn load_metadata_object_extent_rejects_encrypted_size_not_data_block_count_times_block_size() {
12146 let volume_header = test_volume_header();
12147 let crypto_header = test_crypto_header();
12148 let subkeys = Subkeys::derive(
12149 &master_key(),
12150 &volume_header.archive_uuid,
12151 &volume_header.session_id,
12152 )
12153 .unwrap();
12154 let mut next_block_index = 0u64;
12155
12156 let index_root_payload = b"index root metadata object";
12157 let (index_root_extent, index_root_records) = build_metadata_object_from_payload(
12158 index_root_payload,
12159 &subkeys,
12160 &volume_header,
12161 &crypto_header,
12162 &subkeys.index_root_key,
12163 &subkeys.index_nonce_seed,
12164 b"idxroot",
12165 0,
12166 BlockKind::IndexRootData,
12167 &mut next_block_index,
12168 );
12169 let mut index_root_extent = index_root_extent;
12170 index_root_extent.encrypted_size = index_root_extent
12171 .encrypted_size
12172 .saturating_add(crypto_header.block_size);
12173 assert_eq!(
12174 load_metadata_object_from_parts(
12175 &index_root_records,
12176 ObjectLoadContext::index_root(
12177 &volume_header,
12178 &crypto_header,
12179 &subkeys,
12180 index_root_extent,
12181 ),
12182 index_root_payload.len() as u32,
12183 )
12184 .unwrap_err(),
12185 FormatError::InvalidArchive(
12186 "encrypted object size is not data_block_count * block_size"
12187 )
12188 );
12189
12190 let index_shard_payload = b"index shard metadata object";
12191 let (index_shard_extent, index_shard_records) = build_metadata_object_from_payload(
12192 index_shard_payload,
12193 &subkeys,
12194 &volume_header,
12195 &crypto_header,
12196 &subkeys.index_shard_key,
12197 &subkeys.index_nonce_seed,
12198 b"idxshard",
12199 1,
12200 BlockKind::IndexShardData,
12201 &mut next_block_index,
12202 );
12203 let mut index_shard_extent = index_shard_extent;
12204 index_shard_extent.encrypted_size = index_shard_extent
12205 .encrypted_size
12206 .saturating_add(crypto_header.block_size);
12207 assert_eq!(
12208 load_metadata_object_from_parts(
12209 &index_shard_records,
12210 ObjectLoadContext {
12211 volume_header: &volume_header,
12212 crypto_header: &crypto_header,
12213 extent: index_shard_extent,
12214 data_kind: BlockKind::IndexShardData,
12215 parity_kind: BlockKind::IndexShardParity,
12216 key: &subkeys.index_shard_key,
12217 nonce_seed: &subkeys.index_nonce_seed,
12218 domain: b"idxshard",
12219 counter: 1,
12220 class_data_shard_max: crypto_header.index_fec_data_shards,
12221 class_parity_shard_max: crypto_header.index_fec_parity_shards,
12222 },
12223 index_shard_payload.len() as u32,
12224 )
12225 .unwrap_err(),
12226 FormatError::InvalidArchive(
12227 "encrypted object size is not data_block_count * block_size"
12228 )
12229 );
12230
12231 let directory_hint_payload = b"directory hint metadata object";
12232 let (directory_hint_extent, directory_hint_records) = build_metadata_object_from_payload(
12233 directory_hint_payload,
12234 &subkeys,
12235 &volume_header,
12236 &crypto_header,
12237 &subkeys.dir_hint_key,
12238 &subkeys.index_nonce_seed,
12239 b"dirhint",
12240 0,
12241 BlockKind::DirectoryHintData,
12242 &mut next_block_index,
12243 );
12244 let mut directory_hint_extent = directory_hint_extent;
12245 directory_hint_extent.encrypted_size = directory_hint_extent
12246 .encrypted_size
12247 .saturating_add(crypto_header.block_size);
12248 assert_eq!(
12249 load_metadata_object_from_parts(
12250 &directory_hint_records,
12251 ObjectLoadContext {
12252 volume_header: &volume_header,
12253 crypto_header: &crypto_header,
12254 extent: directory_hint_extent,
12255 data_kind: BlockKind::DirectoryHintData,
12256 parity_kind: BlockKind::DirectoryHintParity,
12257 key: &subkeys.dir_hint_key,
12258 nonce_seed: &subkeys.index_nonce_seed,
12259 domain: b"dirhint",
12260 counter: 0,
12261 class_data_shard_max: crypto_header.index_fec_data_shards,
12262 class_parity_shard_max: crypto_header.index_fec_parity_shards,
12263 },
12264 directory_hint_payload.len() as u32,
12265 )
12266 .unwrap_err(),
12267 FormatError::InvalidArchive(
12268 "encrypted object size is not data_block_count * block_size"
12269 )
12270 );
12271
12272 let dictionary_payload = b"dictionary metadata object";
12273 let (dictionary_extent, dictionary_records) = build_metadata_object_from_payload(
12274 dictionary_payload,
12275 &subkeys,
12276 &volume_header,
12277 &crypto_header,
12278 &subkeys.dictionary_key,
12279 &subkeys.index_nonce_seed,
12280 b"dict",
12281 0,
12282 BlockKind::DictionaryData,
12283 &mut next_block_index,
12284 );
12285 let mut dictionary_extent = dictionary_extent;
12286 dictionary_extent.encrypted_size = dictionary_extent
12287 .encrypted_size
12288 .saturating_add(crypto_header.block_size);
12289 assert_eq!(
12290 load_metadata_object_from_parts(
12291 &dictionary_records,
12292 ObjectLoadContext {
12293 volume_header: &volume_header,
12294 crypto_header: &crypto_header,
12295 extent: dictionary_extent,
12296 data_kind: BlockKind::DictionaryData,
12297 parity_kind: BlockKind::DictionaryParity,
12298 key: &subkeys.dictionary_key,
12299 nonce_seed: &subkeys.index_nonce_seed,
12300 domain: b"dict",
12301 counter: 0,
12302 class_data_shard_max: crypto_header.index_root_fec_data_shards,
12303 class_parity_shard_max: crypto_header.index_root_fec_parity_shards,
12304 },
12305 dictionary_payload.len() as u32,
12306 )
12307 .unwrap_err(),
12308 FormatError::InvalidArchive(
12309 "encrypted object size is not data_block_count * block_size"
12310 )
12311 );
12312 }
12313
12314 #[test]
12315 fn opens_complete_multi_volume_archive() {
12316 let files = [RegularFile::new("alpha.txt", b"hello from volume stripes")];
12317 let archive = write_archive(
12318 &files,
12319 &master_key(),
12320 WriterOptions {
12321 stripe_width: 2,
12322 volume_loss_tolerance: 1,
12323 ..single_stream_options()
12324 },
12325 )
12326 .unwrap();
12327 assert_eq!(archive.volumes.len(), 2);
12328
12329 let volume_refs = archive
12330 .volumes
12331 .iter()
12332 .map(Vec::as_slice)
12333 .collect::<Vec<_>>();
12334 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12335
12336 assert_eq!(opened.volume_header.stripe_width, 2);
12337 assert_eq!(opened.list_files().unwrap()[0].path, "alpha.txt");
12338 assert_eq!(
12339 opened.extract_file("alpha.txt").unwrap(),
12340 Some(b"hello from volume stripes".to_vec())
12341 );
12342 opened.verify().unwrap();
12343 }
12344
12345 #[test]
12346 fn recovers_from_one_missing_volume_when_parity_allows() {
12347 let files = [RegularFile::new("alpha.txt", b"recover me")];
12348 let archive = write_archive(
12349 &files,
12350 &master_key(),
12351 WriterOptions {
12352 stripe_width: 2,
12353 volume_loss_tolerance: 1,
12354 ..single_stream_options()
12355 },
12356 )
12357 .unwrap();
12358
12359 let recovered =
12360 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap();
12361 assert_eq!(
12362 recovered.extract_file("alpha.txt").unwrap(),
12363 Some(b"recover me".to_vec())
12364 );
12365 recovered.verify().unwrap();
12366 }
12367
12368 #[test]
12369 fn recovers_from_crc_corrupted_block_when_parity_allows() {
12370 let files = [RegularFile::new("alpha.txt", b"repair corrupt block")];
12371 let archive = write_archive(
12372 &files,
12373 &master_key(),
12374 WriterOptions {
12375 stripe_width: 2,
12376 volume_loss_tolerance: 1,
12377 ..single_stream_options()
12378 },
12379 )
12380 .unwrap();
12381 let mut volumes = archive.volumes.clone();
12382 corrupt_first_block_record_payload(&mut volumes[0]);
12383
12384 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12385 let recovered = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12386
12387 assert_eq!(
12388 recovered.extract_file("alpha.txt").unwrap(),
12389 Some(b"repair corrupt block".to_vec())
12390 );
12391 recovered.verify().unwrap();
12392 }
12393
12394 #[test]
12395 fn rejects_multi_volume_count_mismatch_without_tolerance() {
12396 let files = [RegularFile::new("alpha.txt", b"count check")];
12397 let archive = write_archive(
12398 &files,
12399 &master_key(),
12400 WriterOptions {
12401 stripe_width: 3,
12402 volume_loss_tolerance: 0,
12403 ..single_stream_options()
12404 },
12405 )
12406 .unwrap();
12407
12408 assert_eq!(
12409 open_archive_volumes(&[archive.volumes[0].as_slice()], &master_key()).unwrap_err(),
12410 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12411 );
12412 }
12413
12414 #[test]
12415 fn rejects_multi_volume_manifest_bootstrap_field_mismatch() {
12416 let files = [RegularFile::new("alpha.txt", b"footer mismatch")];
12417 let archive = write_archive(
12418 &files,
12419 &master_key(),
12420 WriterOptions {
12421 stripe_width: 2,
12422 volume_loss_tolerance: 1,
12423 ..single_stream_options()
12424 },
12425 )
12426 .unwrap();
12427
12428 let mut bad_first = archive.volumes[0].clone();
12429 rewrite_manifest_footer(&mut bad_first, &master_key(), |footer| {
12430 footer.index_root_first_block = footer.index_root_first_block.wrapping_add(1);
12431 });
12432
12433 open_archive_volumes(
12434 &[bad_first.as_slice(), archive.volumes[1].as_slice()],
12435 &master_key(),
12436 )
12437 .unwrap();
12438 }
12439
12440 #[test]
12441 fn repairs_corrupted_index_root_block_in_multi_volume_archive() {
12442 let files = [RegularFile::new("alpha.txt", b"repair meta root")];
12443 let archive = write_archive(
12444 &files,
12445 &master_key(),
12446 WriterOptions {
12447 stripe_width: 2,
12448 volume_loss_tolerance: 1,
12449 ..single_stream_options()
12450 },
12451 )
12452 .unwrap();
12453 let mut volumes = archive.volumes.clone();
12454
12455 let mut corrupted = false;
12456 for volume in &mut volumes {
12457 if let Some(slot) =
12458 block_record_slots_with_kind(volume, BlockKind::IndexRootData).first()
12459 {
12460 corrupt_block_record_payload_at_slot(volume, *slot);
12461 corrupted = true;
12462 break;
12463 }
12464 }
12465 assert!(corrupted, "expected an IndexRootData record");
12466
12467 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12468 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12469 assert_eq!(
12470 opened.extract_file("alpha.txt").unwrap(),
12471 Some(b"repair meta root".to_vec())
12472 );
12473 opened.verify().unwrap();
12474 }
12475
12476 #[test]
12477 fn repairs_corrupted_index_shard_block_in_multi_volume_archive() {
12478 let files = [RegularFile::new("alpha.txt", b"repair meta shard")];
12479 let archive = write_archive(
12480 &files,
12481 &master_key(),
12482 WriterOptions {
12483 stripe_width: 2,
12484 volume_loss_tolerance: 1,
12485 ..single_stream_options()
12486 },
12487 )
12488 .unwrap();
12489 let mut volumes = archive.volumes.clone();
12490
12491 let mut corrupted = false;
12492 for volume in &mut volumes {
12493 if let Some(slot) =
12494 block_record_slots_with_kind(volume, BlockKind::IndexShardData).first()
12495 {
12496 corrupt_block_record_payload_at_slot(volume, *slot);
12497 corrupted = true;
12498 break;
12499 }
12500 }
12501 assert!(corrupted, "expected an IndexShardData record");
12502
12503 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12504 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12505 assert_eq!(
12506 opened.extract_file("alpha.txt").unwrap(),
12507 Some(b"repair meta shard".to_vec())
12508 );
12509 opened.verify().unwrap();
12510 }
12511
12512 #[test]
12513 fn rejects_missing_volume_when_loss_tolerance_zero_even_with_bitrot_parity() {
12514 let files = [RegularFile::new(
12515 "alpha.txt",
12516 b"bitrot parity is not volume loss",
12517 )];
12518 let archive = write_archive(
12519 &files,
12520 &master_key(),
12521 WriterOptions {
12522 stripe_width: 2,
12523 volume_loss_tolerance: 0,
12524 bit_rot_buffer_pct: 1,
12525 ..single_stream_options()
12526 },
12527 )
12528 .unwrap();
12529
12530 assert_eq!(
12531 open_archive_volumes(&[archive.volumes[1].as_slice()], &master_key()).unwrap_err(),
12532 FormatError::InvalidArchive("missing volume count exceeds volume_loss_tolerance")
12533 );
12534 }
12535
12536 #[test]
12537 fn repairs_crc_erasure_only_within_parity_budget() {
12538 let payload = pseudo_random_bytes(12_000);
12539 let archive = write_archive(
12540 &[RegularFile::new("rot.bin", &payload)],
12541 &master_key(),
12542 small_block_recovery_options(),
12543 )
12544 .unwrap();
12545 let payload_slots = first_payload_data_run_slots(&archive.bytes);
12546 assert!(
12547 payload_slots.len() >= 2,
12548 "fixture must contain a multi-block payload object"
12549 );
12550
12551 let mut one_erasure = archive.bytes.clone();
12552 corrupt_block_record_payload_at_slot(&mut one_erasure, payload_slots[0]);
12553 let repaired = open_archive(&one_erasure, &master_key()).unwrap();
12554 assert_eq!(
12555 repaired.extract_file("rot.bin").unwrap(),
12556 Some(payload.clone())
12557 );
12558
12559 let mut two_erasures = archive.bytes.clone();
12560 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[0]);
12561 corrupt_block_record_payload_at_slot(&mut two_erasures, payload_slots[1]);
12562 let unrepaired = open_archive(&two_erasures, &master_key()).unwrap();
12563 assert_eq!(
12564 unrepaired.extract_file("rot.bin").unwrap_err(),
12565 FormatError::FecTooFewAvailableShards
12566 );
12567 }
12568
12569 #[test]
12570 fn verify_rejects_missing_required_object_block_extent() {
12571 let (mut opened, missing_block) = multi_envelope_reader_fixture();
12572 assert!(opened.blocks.remove(&missing_block).is_some());
12573
12574 assert_eq!(
12575 opened.verify().unwrap_err(),
12576 FormatError::FecTooFewAvailableShards
12577 );
12578 }
12579
12580 #[test]
12581 fn parity_crc_erasure_does_not_hide_authenticated_data() {
12582 let payload = pseudo_random_bytes(12_000);
12583 let archive = write_archive(
12584 &[RegularFile::new("parity-erasure.bin", &payload)],
12585 &master_key(),
12586 parity_rich_recovery_options(),
12587 )
12588 .unwrap();
12589 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12590 let parity_slots = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity);
12591 assert!(
12592 parity_slots.len() >= 2,
12593 "fixture must contain redundant parity shards"
12594 );
12595 let mut corrupted = archive.bytes;
12596 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12597 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slots[0]);
12598
12599 let opened = open_archive(&corrupted, &master_key()).unwrap();
12600 assert_eq!(
12601 opened.extract_file("parity-erasure.bin").unwrap(),
12602 Some(payload)
12603 );
12604 opened.verify().unwrap();
12605 }
12606
12607 #[test]
12608 fn repair_patches_restore_crc_erased_payload_block() {
12609 let payload = pseudo_random_bytes(12_000);
12610 let archive = write_archive(
12611 &[RegularFile::new("rot.bin", &payload)],
12612 &master_key(),
12613 small_block_recovery_options(),
12614 )
12615 .unwrap();
12616 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12617 let mut corrupted = archive.bytes.clone();
12618 corrupt_block_record_payload_at_slot(&mut corrupted, payload_slot);
12619
12620 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12621 opened.verify().unwrap();
12622 let patches = opened.repair_patches().unwrap();
12623 assert_eq!(patches.len(), 1);
12624 apply_repair_patches(&mut corrupted, &patches);
12625
12626 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12627 repaired.verify().unwrap();
12628 assert!(repaired.repair_patches().unwrap().is_empty());
12629 }
12630
12631 #[test]
12632 fn repair_patches_restore_crc_erased_payload_parity_block() {
12633 let payload = pseudo_random_bytes(12_000);
12634 let archive = write_archive(
12635 &[RegularFile::new("parity-erasure.bin", &payload)],
12636 &master_key(),
12637 parity_rich_recovery_options(),
12638 )
12639 .unwrap();
12640 let parity_slot = block_record_slots_with_kind(&archive.bytes, BlockKind::PayloadParity)[0];
12641 let mut corrupted = archive.bytes.clone();
12642 corrupt_block_record_payload_at_slot(&mut corrupted, parity_slot);
12643
12644 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12645 opened.verify().unwrap();
12646 let patches = opened.repair_patches().unwrap();
12647 assert_eq!(patches.len(), 1);
12648 apply_repair_patches(&mut corrupted, &patches);
12649
12650 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12651 repaired.verify().unwrap();
12652 assert!(repaired.repair_patches().unwrap().is_empty());
12653 }
12654
12655 #[test]
12656 fn recovers_physical_odd_block_size_from_cmra_authority() {
12657 let archive = write_archive(
12658 &[RegularFile::new("odd-block.txt", b"payload")],
12659 &master_key(),
12660 small_block_recovery_options(),
12661 )
12662 .unwrap();
12663 let mut malformed = archive.bytes;
12664 let volume_header = VolumeHeader::parse(&malformed[..VOLUME_HEADER_LEN]).unwrap();
12665 let block_size_offset = volume_header.crypto_header_offset as usize + 24;
12666 malformed[block_size_offset..block_size_offset + 4].copy_from_slice(&4097u32.to_le_bytes());
12667
12668 let opened = open_archive(&malformed, &master_key()).unwrap();
12669 assert_ne!(opened.crypto_header.block_size, 4097);
12670 assert_eq!(
12671 opened.extract_file("odd-block.txt").unwrap(),
12672 Some(b"payload".to_vec())
12673 );
12674 opened.verify().unwrap();
12675 }
12676
12677 #[test]
12678 fn repairs_structurally_malformed_payload_block_slots() {
12679 let payload = pseudo_random_bytes(12_000);
12680 let archive = write_archive(
12681 &[RegularFile::new("structural-block.bin", &payload)],
12682 &master_key(),
12683 small_block_recovery_options(),
12684 )
12685 .unwrap();
12686 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12687
12688 let mut bad_magic = archive.bytes.clone();
12689 corrupt_block_record_magic_at_slot(&mut bad_magic, payload_slot);
12690 assert_eq!(
12691 open_archive(&bad_magic, &master_key())
12692 .unwrap()
12693 .extract_file("structural-block.bin")
12694 .unwrap(),
12695 Some(payload.clone())
12696 );
12697
12698 let mut bad_reserved = archive.bytes;
12699 corrupt_block_record_reserved_at_slot(&mut bad_reserved, payload_slot);
12700 assert_eq!(
12701 open_archive(&bad_reserved, &master_key())
12702 .unwrap()
12703 .extract_file("structural-block.bin")
12704 .unwrap(),
12705 Some(payload)
12706 );
12707 }
12708
12709 #[test]
12710 fn repair_patches_restore_structurally_malformed_payload_block_slot() {
12711 let payload = pseudo_random_bytes(12_000);
12712 let archive = write_archive(
12713 &[RegularFile::new("structural-patch.bin", &payload)],
12714 &master_key(),
12715 small_block_recovery_options(),
12716 )
12717 .unwrap();
12718 let payload_slot = first_payload_data_run_slots(&archive.bytes)[0];
12719 let mut corrupted = archive.bytes.clone();
12720 corrupt_block_record_magic_at_slot(&mut corrupted, payload_slot);
12721
12722 let opened = open_seekable_archive(corrupted.clone(), &master_key()).unwrap();
12723 opened.verify().unwrap();
12724 assert_eq!(
12725 opened.extract_file("structural-patch.bin").unwrap(),
12726 Some(payload)
12727 );
12728 let patches = opened.repair_patches().unwrap();
12729 assert_eq!(patches.len(), 1);
12730 apply_repair_patches(&mut corrupted, &patches);
12731
12732 let repaired = open_seekable_archive(corrupted, &master_key()).unwrap();
12733 repaired.verify().unwrap();
12734 assert!(repaired.repair_patches().unwrap().is_empty());
12735 }
12736
12737 #[test]
12738 fn repairs_structurally_malformed_index_root_block_slot() {
12739 let archive = write_archive(
12740 &[RegularFile::new(
12741 "structural-index-root.txt",
12742 b"metadata repair",
12743 )],
12744 &master_key(),
12745 small_block_recovery_options(),
12746 )
12747 .unwrap();
12748 let index_root_slot =
12749 first_block_record_slot_with_kind(&archive.bytes, BlockKind::IndexRootData).unwrap();
12750 let mut corrupted = archive.bytes;
12751 corrupt_block_record_magic_at_slot(&mut corrupted, index_root_slot);
12752
12753 let opened = open_archive(&corrupted, &master_key()).unwrap();
12754 assert_eq!(
12755 opened.extract_file("structural-index-root.txt").unwrap(),
12756 Some(b"metadata repair".to_vec())
12757 );
12758 opened.verify().unwrap();
12759 }
12760
12761 #[test]
12762 fn rejects_parity_block_with_last_data_flag() {
12763 let archive = write_archive(
12764 &[RegularFile::new("parity-flag.txt", b"payload")],
12765 &master_key(),
12766 small_block_recovery_options(),
12767 )
12768 .unwrap();
12769 let parity_slot =
12770 first_block_record_slot_with_kind(&archive.bytes, BlockKind::PayloadParity).unwrap();
12771 let mut malformed = archive.bytes;
12772 mutate_block_record_at_slot(&mut malformed, parity_slot, |record| {
12773 record.flags = 0x01;
12774 });
12775
12776 assert_eq!(
12777 open_archive(&malformed, &master_key()).unwrap_err(),
12778 FormatError::ParityBlockHasLastDataFlag
12779 );
12780 }
12781
12782 #[test]
12783 fn rejects_missing_and_duplicate_payload_last_data_flags() {
12784 let payload = pseudo_random_bytes(12_000);
12785 let archive = write_archive(
12786 &[RegularFile::new("flags.bin", &payload)],
12787 &master_key(),
12788 small_block_recovery_options(),
12789 )
12790 .unwrap();
12791 let payload_slots = first_payload_data_run_slots(&archive.bytes);
12792 assert!(
12793 payload_slots.len() >= 2,
12794 "fixture must contain a multi-block payload object"
12795 );
12796
12797 let mut duplicate_last = archive.bytes.clone();
12798 mutate_block_record_at_slot(&mut duplicate_last, payload_slots[0], |record| {
12799 record.flags = 0x01;
12800 });
12801 let opened = open_archive(&duplicate_last, &master_key()).unwrap();
12802 assert_eq!(
12803 opened.extract_file("flags.bin").unwrap_err(),
12804 FormatError::InvalidArchive("object last-data flag is not on the final data block")
12805 );
12806
12807 let mut missing_last = archive.bytes;
12808 mutate_block_record_at_slot(
12809 &mut missing_last,
12810 *payload_slots.last().unwrap(),
12811 |record| {
12812 record.flags = 0;
12813 },
12814 );
12815 let opened = open_archive(&missing_last, &master_key()).unwrap();
12816 assert_eq!(
12817 opened.extract_file("flags.bin").unwrap_err(),
12818 FormatError::InvalidArchive("object last-data flag is not on the final data block")
12819 );
12820 }
12821
12822 #[test]
12823 fn recovers_from_one_corrupt_manifest_footer_copy_when_another_volume_authenticates() {
12824 let files = [RegularFile::new(
12825 "footer-copy.txt",
12826 b"survives one bad footer",
12827 )];
12828 let archive = write_archive(
12829 &files,
12830 &master_key(),
12831 WriterOptions {
12832 stripe_width: 2,
12833 volume_loss_tolerance: 1,
12834 ..single_stream_options()
12835 },
12836 )
12837 .unwrap();
12838 let mut volumes = archive.volumes.clone();
12839 corrupt_manifest_footer_hmac(&mut volumes[0]);
12840
12841 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
12842 let opened = open_archive_volumes(&volume_refs, &master_key()).unwrap();
12843 assert_eq!(opened.manifest_footer.volume_index, 0);
12844 assert_eq!(opened.volume_header.volume_index, 0);
12845 assert_eq!(opened.volume_trailer.as_ref().unwrap().volume_index, 0);
12846 assert_eq!(
12847 opened.extract_file("footer-copy.txt").unwrap(),
12848 Some(b"survives one bad footer".to_vec())
12849 );
12850 opened.verify().unwrap();
12851 }
12852
12853 #[test]
12854 fn manifest_footer_corruption_requires_trusted_sidecar() {
12855 let archive = write_archive(
12856 &[RegularFile::new("footer.txt", b"sidecar authority")],
12857 &master_key(),
12858 single_stream_options(),
12859 )
12860 .unwrap();
12861 let manifest_offset = terminal_material_offset(&archive.bytes);
12862 let mut corrupted = archive.bytes.clone();
12863 corrupted[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
12864 corrupt_v41_terminal_recovery(&mut corrupted);
12865
12866 assert!(open_archive(&corrupted, &master_key()).is_err());
12867
12868 let opened =
12869 open_non_seekable_archive(&corrupted, &master_key(), Some(&archive.bootstrap_sidecar))
12870 .unwrap();
12871 assert!(opened.volume_trailer.is_none());
12872 assert_eq!(
12873 opened.extract_file("footer.txt").unwrap(),
12874 Some(b"sidecar authority".to_vec())
12875 );
12876 opened.verify().unwrap();
12877 }
12878
12879 #[test]
12880 fn authenticated_footer_trailer_and_sidecar_hmac_boundaries_are_enforced() {
12881 let archive = write_archive(
12882 &[RegularFile::new("hmac-boundary.txt", b"boundary bytes")],
12883 &master_key(),
12884 single_stream_options(),
12885 )
12886 .unwrap();
12887 let strict_options = ReaderOptions {
12888 max_trailing_garbage_scan: 0,
12889 ..ReaderOptions::default()
12890 };
12891
12892 let manifest_offset = terminal_material_offset(&archive.bytes);
12893 for offset in [
12894 manifest_offset + 71,
12895 manifest_offset + MANIFEST_HMAC_COVERED_LEN,
12896 ] {
12897 let mut corrupted = archive.bytes.clone();
12898 corrupted[offset] ^= 0x01;
12899 open_archive(&corrupted, &master_key()).unwrap();
12900 }
12901
12902 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
12903 for offset in [
12904 trailer_offset + 75,
12905 trailer_offset + TRAILER_HMAC_COVERED_LEN,
12906 ] {
12907 let mut corrupted = archive.bytes.clone();
12908 corrupted[offset] ^= 0x01;
12909 OpenedArchive::open_with_options(&corrupted, &master_key(), strict_options).unwrap();
12910 }
12911
12912 let mut covered_sidecar = archive.bootstrap_sidecar.clone();
12913 let mut header =
12914 BootstrapSidecarHeader::parse(&covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12915 .unwrap();
12916 header.manifest_footer_offset += 1;
12917 covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12918 assert_eq!(
12919 open_archive_with_bootstrap_sidecar(&archive.bytes, &covered_sidecar, &master_key())
12920 .unwrap_err(),
12921 FormatError::HmacMismatch {
12922 structure: "BootstrapSidecarHeader"
12923 }
12924 );
12925
12926 let mut tag_sidecar = archive.bootstrap_sidecar.clone();
12927 let mut header =
12928 BootstrapSidecarHeader::parse(&tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
12929 header.sidecar_hmac[0] ^= 1;
12930 tag_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header.to_bytes());
12931 assert_eq!(
12932 open_archive_with_bootstrap_sidecar(&archive.bytes, &tag_sidecar, &master_key())
12933 .unwrap_err(),
12934 FormatError::HmacMismatch {
12935 structure: "BootstrapSidecarHeader"
12936 }
12937 );
12938
12939 let mut non_covered_sidecar = archive.bootstrap_sidecar.clone();
12940 let header =
12941 BootstrapSidecarHeader::parse(&non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN])
12942 .unwrap();
12943 let mut header_bytes = header.to_bytes();
12944 header_bytes[124] ^= 0x01;
12945 let crc = crc32c::crc32c(&header_bytes[..124]);
12946 header_bytes[124..128].copy_from_slice(&crc.to_le_bytes());
12947 non_covered_sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
12948 let opened = open_archive_with_bootstrap_sidecar(
12949 &archive.bytes,
12950 &non_covered_sidecar,
12951 &master_key(),
12952 )
12953 .unwrap();
12954 assert_eq!(
12955 opened.extract_file("hmac-boundary.txt").unwrap(),
12956 Some(b"boundary bytes".to_vec())
12957 );
12958 }
12959
12960 #[test]
12961 fn rejects_authenticated_footer_and_trailer_volume_index_mismatches() {
12962 let archive = write_archive(
12963 &[RegularFile::new("volume-index.txt", b"identity")],
12964 &master_key(),
12965 single_stream_options(),
12966 )
12967 .unwrap();
12968
12969 let mut bad_trailer = archive.bytes.clone();
12970 rewrite_volume_trailer(&mut bad_trailer, &master_key(), |trailer| {
12971 trailer.volume_index = 1;
12972 });
12973 open_archive(&bad_trailer, &master_key()).unwrap();
12974
12975 let mut bad_manifest = archive.bytes;
12976 rewrite_manifest_footer(&mut bad_manifest, &master_key(), |footer| {
12977 footer.volume_index = 1;
12978 });
12979 open_archive(&bad_manifest, &master_key()).unwrap();
12980 }
12981
12982 #[test]
12983 fn rejects_same_key_header_terminal_material_splice() {
12984 let first = write_archive(
12985 &[RegularFile::new("splice.txt", b"same shape")],
12986 &master_key(),
12987 single_stream_options(),
12988 )
12989 .unwrap();
12990 let second = write_archive(
12991 &[RegularFile::new("splice.txt", b"same shape")],
12992 &master_key(),
12993 single_stream_options(),
12994 )
12995 .unwrap();
12996 assert_ne!(first.archive_uuid, second.archive_uuid);
12997 assert_eq!(
12998 terminal_material_offset(&first.bytes),
12999 terminal_material_offset(&second.bytes)
13000 );
13001 assert_eq!(first.bytes.len(), second.bytes.len());
13002
13003 let terminal_offset = terminal_material_offset(&first.bytes);
13004 let mut spliced = first.bytes.clone();
13005 spliced[terminal_offset..].copy_from_slice(&second.bytes[terminal_offset..]);
13006
13007 assert_eq!(
13008 open_archive(&spliced, &master_key()).unwrap_err(),
13009 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13010 );
13011 }
13012
13013 #[test]
13014 fn rejects_cmra_crypto_header_pre_hmac_mismatch() {
13015 let kdf_params = crate::crypto::KdfParams::Argon2id {
13016 t_cost: 1,
13017 m_cost_kib: 8,
13018 parallelism: 1,
13019 salt: b"0123456789abcdef".to_vec(),
13020 };
13021 let archive = write_archive_with_kdf(
13022 &[RegularFile::new("cmra-crypto.txt", b"same fixed header")],
13023 &master_key(),
13024 single_stream_options(),
13025 &kdf_params,
13026 )
13027 .unwrap();
13028 let mut mutated = archive.bytes.clone();
13029 let volume_header = VolumeHeader::parse(&mutated[..VOLUME_HEADER_LEN]).unwrap();
13030 let subkeys = Subkeys::derive(
13031 &master_key(),
13032 &volume_header.archive_uuid,
13033 &volume_header.session_id,
13034 )
13035 .unwrap();
13036
13037 rewrite_cmra_image(&mut mutated, CmraRecoveryMode::KeyHolding, |image| {
13038 let crypto_region = image
13039 .regions
13040 .iter_mut()
13041 .find(|region| region.region_type == 2)
13042 .unwrap();
13043 let hmac_offset = crypto_region.bytes.len() - CRYPTO_HEADER_HMAC_LEN;
13044 let salt_start = CRYPTO_HEADER_FIXED_LEN + 16;
13045 crypto_region.bytes[salt_start] ^= 0x01;
13046 let hmac = compute_hmac(
13047 HmacDomain::CryptoHeader,
13048 &subkeys.mac_key,
13049 &volume_header.archive_uuid,
13050 &volume_header.session_id,
13051 &crypto_region.bytes[..hmac_offset],
13052 );
13053 crypto_region.bytes[hmac_offset..].copy_from_slice(&hmac);
13054 });
13055
13056 let final_offset = mutated.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13057 let locator = final_recovery_locator(&mutated);
13058 let crypto_start = volume_header.crypto_header_offset as usize;
13059 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13060 let parsed_crypto = CryptoHeader::parse(
13061 &mutated[crypto_start..crypto_end],
13062 volume_header.crypto_header_length,
13063 )
13064 .unwrap();
13065 assert_eq!(
13066 parse_locator_cmra_candidate(
13067 &mutated,
13068 final_offset,
13069 locator,
13070 KeyHoldingTerminalContext {
13071 subkeys: &subkeys,
13072 volume_header: &volume_header,
13073 crypto_header: &parsed_crypto.fixed,
13074 crypto_header_bytes: &mutated[crypto_start..crypto_end],
13075 },
13076 )
13077 .unwrap_err(),
13078 FormatError::InvalidArchive("CMRA CryptoHeader differs from parsed CryptoHeader")
13079 );
13080 assert!(open_archive(&mutated, &master_key()).is_err());
13081 }
13082
13083 #[test]
13084 fn recovers_physical_crypto_header_splice_from_cmra_authority() {
13085 let base = WriterOptions {
13086 archive_uuid: Some([0x11; 16]),
13087 session_id: Some([0x22; 16]),
13088 ..small_block_recovery_options()
13089 };
13090 let same_archive = WriterOptions {
13091 archive_uuid: Some([0x11; 16]),
13092 session_id: Some([0x33; 16]),
13093 ..small_block_recovery_options()
13094 };
13095
13096 let first = write_archive(
13097 &[RegularFile::new("splice.txt", b"same shape")],
13098 &master_key(),
13099 base,
13100 )
13101 .unwrap();
13102 let second = write_archive(
13103 &[RegularFile::new("splice.txt", b"same shape")],
13104 &master_key(),
13105 same_archive,
13106 )
13107 .unwrap();
13108
13109 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
13110 let second_volume_header = VolumeHeader::parse(&second.bytes[..VOLUME_HEADER_LEN]).unwrap();
13111 let crypto_start = volume_header.crypto_header_offset as usize;
13112 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13113 let second_crypto_end = second_volume_header.crypto_header_offset as usize
13114 + second_volume_header.crypto_header_length as usize;
13115 assert_eq!(crypto_end, second_crypto_end);
13116
13117 let mut spliced = first.bytes.clone();
13118 spliced[crypto_start..crypto_end].copy_from_slice(&second.bytes[crypto_start..crypto_end]);
13119
13120 let opened = open_archive(&spliced, &master_key()).unwrap();
13121 assert_eq!(
13122 opened.crypto_header_bytes,
13123 first.bytes[crypto_start..crypto_end].to_vec()
13124 );
13125 assert_eq!(
13126 opened.extract_file("splice.txt").unwrap(),
13127 Some(b"same shape".to_vec())
13128 );
13129 opened.verify().unwrap();
13130 }
13131
13132 #[test]
13133 fn rejects_same_key_object_splice_with_session_mismatch() {
13134 let first = write_archive(
13135 &[RegularFile::new("splice.txt", b"same shape")],
13136 &master_key(),
13137 WriterOptions {
13138 archive_uuid: Some([0x11; 16]),
13139 session_id: Some([0x22; 16]),
13140 ..single_stream_options()
13141 },
13142 )
13143 .unwrap();
13144 let second = write_archive(
13145 &[RegularFile::new("splice.txt", b"same shape")],
13146 &master_key(),
13147 WriterOptions {
13148 archive_uuid: Some([0x11; 16]),
13149 session_id: Some([0x33; 16]),
13150 ..single_stream_options()
13151 },
13152 )
13153 .unwrap();
13154
13155 let volume_header = VolumeHeader::parse(&first.bytes[..VOLUME_HEADER_LEN]).unwrap();
13156 let crypto_end = volume_header.crypto_header_offset as usize
13157 + volume_header.crypto_header_length as usize;
13158 let terminal_offset = terminal_material_offset(&first.bytes);
13159 let second_terminal_offset = terminal_material_offset(&second.bytes);
13160 assert_eq!(terminal_offset, second_terminal_offset);
13161
13162 let mut spliced = first.bytes.clone();
13163 spliced[crypto_end..terminal_offset]
13164 .copy_from_slice(&second.bytes[crypto_end..terminal_offset]);
13165
13166 assert_eq!(
13167 open_archive(&spliced, &master_key()).unwrap_err(),
13168 FormatError::AeadFailure
13169 );
13170 }
13171
13172 #[test]
13173 fn rejects_authenticated_trailer_pointer_and_count_mutations() {
13174 let archive = write_archive(
13175 &[RegularFile::new(
13176 "trailer-range.txt",
13177 b"authenticated ranges",
13178 )],
13179 &master_key(),
13180 single_stream_options(),
13181 )
13182 .unwrap();
13183 let strict_options = ReaderOptions {
13184 max_trailing_garbage_scan: 0,
13185 ..ReaderOptions::default()
13186 };
13187 let bytes = archive.bytes;
13188 let manifest_offset = terminal_material_offset(&bytes);
13189 let trailer_offset = manifest_offset + MANIFEST_FOOTER_LEN;
13190
13191 let mut wrong_footer_length = bytes.clone();
13192 rewrite_volume_trailer(&mut wrong_footer_length, &master_key(), |trailer| {
13193 trailer.manifest_footer_length = 42;
13194 });
13195 OpenedArchive::open_with_options(&wrong_footer_length, &master_key(), strict_options)
13196 .unwrap();
13197
13198 for (label, offset) in [
13199 (
13200 "offset before trailer by 1",
13201 manifest_offset.saturating_sub(1),
13202 ),
13203 ("offset after trailer", manifest_offset + 1),
13204 ("offset at stream start", 0),
13205 ("offset at trailer", trailer_offset),
13206 ("offset beyond trailer", trailer_offset + 4),
13207 ] {
13208 let mut wrong_footer_offset = bytes.clone();
13209 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13210 trailer.manifest_footer_offset = offset as u64;
13211 });
13212 open_archive(&wrong_footer_offset, &master_key())
13213 .unwrap_or_else(|err| panic!("manifest offset case {label}: {err:?}"));
13214 }
13215
13216 let mut wrong_bytes_written = bytes.clone();
13217 rewrite_volume_trailer(&mut wrong_bytes_written, &master_key(), |trailer| {
13218 trailer.bytes_written += 1;
13219 });
13220 open_archive(&wrong_bytes_written, &master_key()).unwrap();
13221
13222 let mut wrong_block_count = bytes.clone();
13223 rewrite_volume_trailer(&mut wrong_block_count, &master_key(), |trailer| {
13224 trailer.block_count += 1;
13225 });
13226 open_archive(&wrong_block_count, &master_key()).unwrap();
13227
13228 let mut wrong_footer_offset = bytes.clone();
13229 rewrite_volume_trailer(&mut wrong_footer_offset, &master_key(), |trailer| {
13230 trailer.manifest_footer_offset = bytes.len() as u64 + 1024;
13231 });
13232 open_archive(&wrong_footer_offset, &master_key()).unwrap();
13233 }
13234
13235 #[test]
13236 fn rejects_authenticated_trailer_outside_trailing_scan_cap() {
13237 let archive = write_archive(
13238 &[RegularFile::new(
13239 "trailer-trailing-scan.txt",
13240 b"trailer scan boundaries",
13241 )],
13242 &master_key(),
13243 single_stream_options(),
13244 )
13245 .unwrap();
13246 let options = ReaderOptions {
13247 max_trailing_garbage_scan: 8,
13248 ..ReaderOptions::default()
13249 };
13250
13251 let mut within_scan = archive.bytes.clone();
13252 within_scan.resize(within_scan.len() + options.max_trailing_garbage_scan, 0xAA);
13253 let opened =
13254 OpenedArchive::open_with_options(&within_scan, &master_key(), options).unwrap();
13255 assert_eq!(
13256 opened.extract_file("trailer-trailing-scan.txt").unwrap(),
13257 Some(b"trailer scan boundaries".to_vec())
13258 );
13259
13260 let mut beyond_scan = archive.bytes.clone();
13261 beyond_scan.resize(
13262 beyond_scan.len() + max_critical_recovery_scan(options).unwrap() + 1,
13263 0xAA,
13264 );
13265 assert_eq!(
13266 OpenedArchive::open_with_options(&beyond_scan, &master_key(), options).unwrap_err(),
13267 FormatError::InvalidArchive("no valid v41 CMRA candidate found")
13268 );
13269 }
13270
13271 #[test]
13272 fn rejects_authenticated_index_root_extent_size_mismatch_at_open() {
13273 let archive = write_archive(
13274 &[RegularFile::new("index-root-size.txt", b"extent size")],
13275 &master_key(),
13276 single_stream_options(),
13277 )
13278 .unwrap();
13279 let mut malformed = archive.bytes;
13280 let slot = first_block_record_slot_with_kind(&malformed, BlockKind::IndexRootData)
13281 .expect("archive should contain IndexRootData");
13282 mutate_block_record_at_slot(&mut malformed, slot, |record| {
13283 record.payload[0] ^= 0x55;
13284 });
13285
13286 assert_eq!(
13287 open_archive(&malformed, &master_key()).unwrap_err(),
13288 FormatError::AeadFailure
13289 );
13290 }
13291
13292 #[test]
13293 fn rejects_block_record_at_wrong_stripe_position() {
13294 let files = [RegularFile::new("alpha.txt", b"wrong stripe")];
13295 let archive = write_archive(
13296 &files,
13297 &master_key(),
13298 WriterOptions {
13299 stripe_width: 2,
13300 volume_loss_tolerance: 1,
13301 ..single_stream_options()
13302 },
13303 )
13304 .unwrap();
13305 let mut volumes = archive.volumes.clone();
13306 mutate_first_block_record(&mut volumes[0], |record| {
13307 record.block_index += 2;
13308 });
13309
13310 let volume_refs = volumes.iter().map(Vec::as_slice).collect::<Vec<_>>();
13311 assert_eq!(
13312 open_archive_volumes(&volume_refs, &master_key()).unwrap_err(),
13313 FormatError::InvalidArchive("BlockRecord index does not match volume position")
13314 );
13315 }
13316
13317 #[test]
13318 fn rejects_decreasing_block_record_index_in_required_region() {
13319 let archive = write_archive(
13320 &[RegularFile::new("alpha.txt", b"decreasing block index")],
13321 &master_key(),
13322 single_stream_options(),
13323 )
13324 .unwrap();
13325 assert!(block_record_slots(&archive.bytes).len() >= 2);
13326
13327 let mut malformed = archive.bytes;
13328 mutate_block_record_at_slot(&mut malformed, 1, |record| {
13329 record.block_index = 0;
13330 });
13331
13332 assert_eq!(
13333 open_archive(&malformed, &master_key()).unwrap_err(),
13334 FormatError::InvalidArchive("BlockRecord index does not match volume position")
13335 );
13336 }
13337
13338 #[test]
13339 fn rejects_duplicate_authenticated_volume_indexes() {
13340 let files = [RegularFile::new("alpha.txt", b"duplicates")];
13341 let archive = write_archive(
13342 &files,
13343 &master_key(),
13344 WriterOptions {
13345 stripe_width: 2,
13346 volume_loss_tolerance: 1,
13347 ..single_stream_options()
13348 },
13349 )
13350 .unwrap();
13351
13352 assert_eq!(
13353 open_archive_volumes(
13354 &[archive.volumes[0].as_slice(), archive.volumes[0].as_slice()],
13355 &master_key()
13356 )
13357 .unwrap_err(),
13358 FormatError::InvalidArchive("duplicate authenticated volume index")
13359 );
13360 }
13361
13362 #[test]
13363 fn rejects_conflicting_duplicate_authenticated_volume_indexes_by_default() {
13364 let files = [RegularFile::new("alpha.txt", b"conflicting duplicates")];
13365 let archive = write_archive(
13366 &files,
13367 &master_key(),
13368 WriterOptions {
13369 stripe_width: 2,
13370 volume_loss_tolerance: 1,
13371 ..single_stream_options()
13372 },
13373 )
13374 .unwrap();
13375 let mut conflicting = archive.volumes[0].clone();
13376 corrupt_first_block_record_payload(&mut conflicting);
13377
13378 assert_eq!(
13379 open_archive_volumes(
13380 &[archive.volumes[0].as_slice(), conflicting.as_slice()],
13381 &master_key()
13382 )
13383 .unwrap_err(),
13384 FormatError::InvalidArchive("duplicate authenticated volume index")
13385 );
13386 }
13387
13388 fn directory_hint_table_from_rows(
13389 hint_shard_index: u64,
13390 rows: &[(Vec<u8>, Vec<u32>)],
13391 shard_count: u32,
13392 ) -> DirectoryHintTable {
13393 let mut entries = Vec::new();
13394 let mut shard_row_indexes = Vec::new();
13395 let mut string_pool = Vec::new();
13396
13397 for (path, rows) in rows {
13398 let path_offset = if path.is_empty() {
13399 0
13400 } else {
13401 let offset = string_pool.len() as u64;
13402 string_pool.extend_from_slice(path);
13403 offset
13404 };
13405 let shard_list_start_index = shard_row_indexes.len() as u32;
13406 shard_row_indexes.extend_from_slice(rows);
13407 entries.push(DirectoryHintEntry {
13408 dir_hash: hash_prefix(path),
13409 path_offset,
13410 path_length: path.len() as u32,
13411 shard_list_start_index,
13412 shard_count: rows.len() as u32,
13413 });
13414 }
13415
13416 let table_bytes =
13417 directory_hint_table_bytes(hint_shard_index, entries, shard_row_indexes, string_pool);
13418 let locating = DirectoryHintShardEntry {
13419 hint_shard_index,
13420 first_dir_hash: hash_prefix(&rows.first().unwrap().0),
13421 last_dir_hash: hash_prefix(&rows.last().unwrap().0),
13422 first_block_index: 0,
13423 data_block_count: 1,
13424 parity_block_count: 0,
13425 encrypted_size: 4096,
13426 decompressed_size: table_bytes.len() as u32,
13427 entry_count: rows.len() as u64,
13428 };
13429 DirectoryHintTable::parse(
13430 &table_bytes,
13431 &locating,
13432 shard_count,
13433 MetadataLimits::default(),
13434 )
13435 .unwrap()
13436 }
13437
13438 fn directory_hint_table_bytes(
13439 hint_shard_index: u64,
13440 entries: Vec<DirectoryHintEntry>,
13441 shard_row_indexes: Vec<u32>,
13442 string_pool: Vec<u8>,
13443 ) -> Vec<u8> {
13444 let header_len = DirectoryHintTableHeader {
13445 version: 1,
13446 hint_shard_index,
13447 entry_count: 0,
13448 entry_table_offset: 0,
13449 shard_list_offset: 0,
13450 string_pool_offset: 0,
13451 string_pool_size: 0,
13452 }
13453 .to_bytes()
13454 .len();
13455 let entry_len = entries
13456 .first()
13457 .map(|entry| entry.to_bytes().len())
13458 .unwrap_or(0);
13459 let shard_list_offset = if entries.is_empty() {
13460 0
13461 } else {
13462 header_len + entries.len() * entry_len
13463 };
13464 let string_pool_offset = if string_pool.is_empty() {
13465 0
13466 } else {
13467 shard_list_offset + shard_row_indexes.len() * 4
13468 };
13469
13470 let header = DirectoryHintTableHeader {
13471 version: 1,
13472 hint_shard_index,
13473 entry_count: entries.len() as u64,
13474 entry_table_offset: if entries.is_empty() {
13475 0
13476 } else {
13477 header_len as u64
13478 },
13479 shard_list_offset: shard_list_offset as u64,
13480 string_pool_offset: string_pool_offset as u64,
13481 string_pool_size: string_pool.len() as u64,
13482 };
13483
13484 let mut out = Vec::new();
13485 out.extend_from_slice(&header.to_bytes());
13486 for entry in entries {
13487 out.extend_from_slice(&entry.to_bytes());
13488 }
13489 for row in shard_row_indexes {
13490 out.extend_from_slice(&row.to_le_bytes());
13491 }
13492 out.extend_from_slice(&string_pool);
13493 out
13494 }
13495
13496 fn corrupt_first_block_record_payload(volume: &mut [u8]) {
13497 let (record_offset, _) = first_block_record(volume);
13498 volume[record_offset + 16] ^= 0x55;
13499 }
13500
13501 fn corrupt_block_record_payload_at_slot(volume: &mut [u8], slot: usize) {
13502 let (record_offset, _) = block_record_at_slot(volume, slot);
13503 volume[record_offset + 16] ^= 0x55;
13504 }
13505
13506 fn apply_repair_patches(volume: &mut [u8], patches: &[ArchiveRepairPatch]) {
13507 for patch in patches {
13508 let offset = patch.record_offset as usize;
13509 let end = offset + patch.record_bytes.len();
13510 volume[offset..end].copy_from_slice(&patch.record_bytes);
13511 }
13512 }
13513
13514 fn corrupt_block_record_magic_at_slot(volume: &mut [u8], slot: usize) {
13515 let (record_offset, _) = block_record_at_slot(volume, slot);
13516 volume[record_offset] ^= 0x55;
13517 }
13518
13519 fn corrupt_block_record_reserved_at_slot(volume: &mut [u8], slot: usize) {
13520 let (record_offset, _) = block_record_at_slot(volume, slot);
13521 volume[record_offset + 14] = 0x01;
13522 }
13523
13524 fn corrupt_manifest_footer_hmac(volume: &mut [u8]) {
13525 let manifest_offset = terminal_material_offset(volume);
13526 volume[manifest_offset + MANIFEST_HMAC_COVERED_LEN] ^= 0x01;
13527 }
13528
13529 fn final_recovery_locator(volume: &[u8]) -> CriticalRecoveryLocator {
13530 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13531 CriticalRecoveryLocator::parse(
13532 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13533 )
13534 .unwrap()
13535 }
13536
13537 fn rewrite_cmra_parity_count(volume: &[u8], parity_shard_count: u16) -> Vec<u8> {
13538 let locator = final_recovery_locator(volume);
13539 let tuple = CmraDecoderTuple::from(locator);
13540 assert!(parity_shard_count < tuple.parity_shard_count);
13541 let cmra_offset = locator.cmra_offset as usize;
13542 let shard_size = tuple.shard_size as usize;
13543 let row_len = CRITICAL_METADATA_RECOVERY_SHARD_HEADER_LEN + shard_size;
13544 let kept_rows = tuple.data_shard_count as usize + parity_shard_count as usize;
13545 let mut header = CriticalMetadataRecoveryHeader::parse(
13546 &volume[cmra_offset..cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN],
13547 )
13548 .unwrap();
13549 header.parity_shard_count = parity_shard_count;
13550
13551 let mut cmra =
13552 Vec::with_capacity(CRITICAL_METADATA_RECOVERY_HEADER_LEN + kept_rows * row_len);
13553 cmra.extend_from_slice(&header.to_bytes());
13554 let rows_start = cmra_offset + CRITICAL_METADATA_RECOVERY_HEADER_LEN;
13555 for row in 0..kept_rows {
13556 let start = rows_start + row * row_len;
13557 cmra.extend_from_slice(&volume[start..start + row_len]);
13558 }
13559
13560 let mut out = Vec::with_capacity(cmra_offset + cmra.len() + LOCATOR_PAIR_LEN);
13561 out.extend_from_slice(&volume[..cmra_offset]);
13562 out.extend_from_slice(&cmra);
13563 let mut mirror = locator;
13564 mirror.locator_sequence = 1;
13565 mirror.cmra_length = cmra.len() as u32;
13566 mirror.cmra_parity_shard_count = parity_shard_count;
13567 out.extend_from_slice(&mirror.to_bytes());
13568 let final_locator = CriticalRecoveryLocator {
13569 locator_sequence: 0,
13570 ..mirror
13571 };
13572 out.extend_from_slice(&final_locator.to_bytes());
13573 out
13574 }
13575
13576 fn rewrite_public_cmra_image(
13577 volume: &mut [u8],
13578 mutate: impl FnOnce(&mut CriticalMetadataImage),
13579 ) {
13580 rewrite_cmra_image(volume, CmraRecoveryMode::PublicNoKey, mutate);
13581 }
13582
13583 fn rewrite_cmra_image(
13584 volume: &mut [u8],
13585 mode: CmraRecoveryMode,
13586 mutate: impl FnOnce(&mut CriticalMetadataImage),
13587 ) {
13588 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13589 let locator = final_recovery_locator(volume);
13590 let tuple = CmraDecoderTuple::from(locator);
13591 let recovered = recover_cmra(volume, locator.cmra_offset, Some(tuple), mode).unwrap();
13592 let mut image = recovered.image;
13593 mutate(&mut image);
13594 refresh_critical_image_region_digests(&mut image);
13595 let image_bytes = image.to_bytes().unwrap();
13596 assert_eq!(image_bytes.len(), tuple.image_length as usize);
13597
13598 let shard_size = tuple.shard_size as usize;
13599 let data_shard_count = tuple.data_shard_count as usize;
13600 let parity_shard_count = tuple.parity_shard_count as usize;
13601 assert!(image_bytes.len() <= data_shard_count * shard_size);
13602
13603 let mut data_shards = Vec::with_capacity(data_shard_count);
13604 for idx in 0..data_shard_count {
13605 let start = idx * shard_size;
13606 let end = (start + shard_size).min(image_bytes.len());
13607 let mut payload = vec![0u8; shard_size];
13608 if start < image_bytes.len() {
13609 payload[..end - start].copy_from_slice(&image_bytes[start..end]);
13610 }
13611 data_shards.push(payload);
13612 }
13613 let parity_shards = encode_parity_gf16(&data_shards, parity_shard_count).unwrap();
13614 let image_sha256 = sha256_bytes(&image_bytes);
13615
13616 let header = CriticalMetadataRecoveryHeader {
13617 shard_size: tuple.shard_size,
13618 data_shard_count: tuple.data_shard_count,
13619 parity_shard_count: tuple.parity_shard_count,
13620 image_length: tuple.image_length,
13621 archive_uuid_hint: locator.archive_uuid_hint,
13622 session_id_hint: locator.session_id_hint,
13623 volume_index_hint: locator.volume_index_hint,
13624 image_sha256,
13625 header_crc32c: 0,
13626 };
13627 let mut cmra = Vec::new();
13628 cmra.extend_from_slice(&header.to_bytes());
13629 for (idx, payload) in data_shards.into_iter().enumerate() {
13630 let payload_len = if idx + 1 == data_shard_count {
13631 image_bytes.len() - idx * shard_size
13632 } else {
13633 shard_size
13634 };
13635 cmra.extend_from_slice(
13636 &CriticalMetadataRecoveryShard {
13637 shard_index: idx as u16,
13638 shard_role: 0,
13639 shard_payload_length: payload_len as u32,
13640 payload,
13641 shard_crc32c: 0,
13642 }
13643 .to_bytes(shard_size)
13644 .unwrap(),
13645 );
13646 }
13647 for (idx, payload) in parity_shards.into_iter().enumerate() {
13648 cmra.extend_from_slice(
13649 &CriticalMetadataRecoveryShard {
13650 shard_index: (data_shard_count + idx) as u16,
13651 shard_role: 1,
13652 shard_payload_length: shard_size as u32,
13653 payload,
13654 shard_crc32c: 0,
13655 }
13656 .to_bytes(shard_size)
13657 .unwrap(),
13658 );
13659 }
13660 assert_eq!(cmra.len() as u64, recovered.cmra_length);
13661 let cmra_offset = locator.cmra_offset as usize;
13662 volume[cmra_offset..cmra_offset + cmra.len()].copy_from_slice(&cmra);
13663
13664 rewrite_locator_image_sha(volume, final_offset, image_sha256);
13665 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13666 rewrite_locator_image_sha(volume, mirror_offset, image_sha256);
13667 }
13668
13669 fn refresh_critical_image_region_digests(image: &mut CriticalMetadataImage) {
13670 image.volume_header_sha256 = sha256_bytes(
13671 &image
13672 .regions
13673 .iter()
13674 .find(|region| region.region_type == 1)
13675 .unwrap()
13676 .bytes,
13677 );
13678 image.crypto_header_sha256 = sha256_bytes(
13679 &image
13680 .regions
13681 .iter()
13682 .find(|region| region.region_type == 2)
13683 .unwrap()
13684 .bytes,
13685 );
13686 image.manifest_footer_sha256 = sha256_bytes(
13687 &image
13688 .regions
13689 .iter()
13690 .find(|region| region.region_type == 3)
13691 .unwrap()
13692 .bytes,
13693 );
13694 image.root_auth_footer_sha256 = image
13695 .regions
13696 .iter()
13697 .find(|region| region.region_type == 4)
13698 .map(|region| sha256_bytes(®ion.bytes))
13699 .unwrap_or([0u8; 32]);
13700 image.volume_trailer_sha256 = sha256_bytes(
13701 &image
13702 .regions
13703 .iter()
13704 .find(|region| region.region_type == 5)
13705 .unwrap()
13706 .bytes,
13707 );
13708 }
13709
13710 fn rewrite_locator_image_sha(volume: &mut [u8], offset: usize, image_sha256: [u8; 32]) {
13711 let mut locator =
13712 CriticalRecoveryLocator::parse(&volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN])
13713 .unwrap();
13714 locator.cmra_image_sha256 = image_sha256;
13715 volume[offset..offset + CRITICAL_RECOVERY_LOCATOR_LEN].copy_from_slice(&locator.to_bytes());
13716 }
13717
13718 fn corrupt_v41_terminal_recovery(volume: &mut [u8]) {
13719 let final_offset = volume.len() - CRITICAL_RECOVERY_LOCATOR_LEN;
13720 let final_locator = CriticalRecoveryLocator::parse(
13721 &volume[final_offset..final_offset + CRITICAL_RECOVERY_LOCATOR_LEN],
13722 )
13723 .unwrap();
13724 let mirror_offset = final_offset - CRITICAL_RECOVERY_LOCATOR_LEN;
13725 volume[final_locator.cmra_offset as usize] ^= 0x55;
13726 volume[mirror_offset] ^= 0x55;
13727 volume[final_offset] ^= 0x55;
13728 }
13729
13730 fn mutate_first_block_record(volume: &mut [u8], mutate: impl FnOnce(&mut BlockRecord)) {
13731 let (record_offset, record_len) = first_block_record(volume);
13732 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13733 let mut record = BlockRecord::parse(
13734 &volume[record_offset..record_offset + record_len],
13735 block_size,
13736 )
13737 .unwrap();
13738 mutate(&mut record);
13739 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13740 }
13741
13742 fn mutate_block_record_at_slot(
13743 volume: &mut [u8],
13744 slot: usize,
13745 mutate: impl FnOnce(&mut BlockRecord),
13746 ) {
13747 let (record_offset, record_len) = block_record_at_slot(volume, slot);
13748 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
13749 let mut record = BlockRecord::parse(
13750 &volume[record_offset..record_offset + record_len],
13751 block_size,
13752 )
13753 .unwrap();
13754 mutate(&mut record);
13755 volume[record_offset..record_offset + record_len].copy_from_slice(&record.to_bytes());
13756 }
13757
13758 fn first_block_record(volume: &[u8]) -> (usize, usize) {
13759 block_record_at_slot(volume, 0)
13760 }
13761
13762 fn block_record_at_slot(volume: &[u8], slot: usize) -> (usize, usize) {
13763 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13764 let crypto_start = volume_header.crypto_header_offset as usize;
13765 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13766 let crypto_header = CryptoHeader::parse(
13767 &volume[crypto_start..crypto_end],
13768 volume_header.crypto_header_length,
13769 )
13770 .unwrap();
13771 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13772 let record_offset = crypto_end + slot * record_len;
13773 assert!(volume.len() >= record_offset + record_len);
13774 (record_offset, record_len)
13775 }
13776
13777 fn first_block_record_slot_with_kind(volume: &[u8], kind: BlockKind) -> Option<usize> {
13778 block_record_slots(volume)
13779 .into_iter()
13780 .enumerate()
13781 .find_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13782 }
13783
13784 fn block_record_slots_with_kind(volume: &[u8], kind: BlockKind) -> Vec<usize> {
13785 block_record_slots(volume)
13786 .into_iter()
13787 .enumerate()
13788 .filter_map(|(slot, (_, _, record))| (record.kind == kind).then_some(slot))
13789 .collect()
13790 }
13791
13792 fn first_payload_data_run_slots(volume: &[u8]) -> Vec<usize> {
13793 let mut slots = Vec::new();
13794 for (slot, (_, _, record)) in block_record_slots(volume).into_iter().enumerate() {
13795 if record.kind == BlockKind::PayloadData {
13796 slots.push(slot);
13797 } else if !slots.is_empty() {
13798 break;
13799 }
13800 }
13801 slots
13802 }
13803
13804 fn envelope_indices_for_path(opened: &OpenedArchive, path: &str) -> BTreeSet<u64> {
13805 envelope_entries_for_path(opened, path)
13806 .into_iter()
13807 .map(|entry| entry.envelope_index)
13808 .collect()
13809 }
13810
13811 fn envelope_entries_for_path(opened: &OpenedArchive, path: &str) -> Vec<EnvelopeEntry> {
13812 let normalized =
13813 normalize_lookup_file_path(path, opened.crypto_header.max_path_length).unwrap();
13814 let located = opened.locate_index_file(&normalized).unwrap().unwrap();
13815 let file = &located.shard.files[located.file_index];
13816 frame_range_for_file(&located.shard, file)
13817 .unwrap()
13818 .into_iter()
13819 .map(|frame| {
13820 located
13821 .shard
13822 .envelopes
13823 .iter()
13824 .find(|entry| entry.envelope_index == frame.envelope_index)
13825 .unwrap()
13826 .clone()
13827 })
13828 .collect()
13829 }
13830
13831 fn block_record_slots(volume: &[u8]) -> Vec<(usize, usize, BlockRecord)> {
13832 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13833 let crypto_start = volume_header.crypto_header_offset as usize;
13834 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
13835 let crypto_header = CryptoHeader::parse(
13836 &volume[crypto_start..crypto_end],
13837 volume_header.crypto_header_length,
13838 )
13839 .unwrap();
13840 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
13841 let manifest_offset = terminal_material_offset(volume);
13842 assert_eq!((manifest_offset - crypto_end) % record_len, 0);
13843 let record_count = (manifest_offset - crypto_end) / record_len;
13844 (0..record_count)
13845 .map(|slot| {
13846 let offset = crypto_end + slot * record_len;
13847 let record = BlockRecord::parse(
13848 &volume[offset..offset + record_len],
13849 record_len - BLOCK_RECORD_FRAMING_LEN,
13850 )
13851 .unwrap();
13852 (offset, record_len, record)
13853 })
13854 .collect()
13855 }
13856
13857 fn rewrite_manifest_footer(
13858 volume: &mut [u8],
13859 master_key: &MasterKey,
13860 mutate: impl FnOnce(&mut ManifestFooter),
13861 ) {
13862 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13863 let offset = terminal_material_offset(volume);
13864 let mut footer =
13865 ManifestFooter::parse(&volume[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
13866 mutate(&mut footer);
13867 footer.manifest_hmac = [0u8; 32];
13868 let mut footer_bytes = footer.to_bytes();
13869 let subkeys = Subkeys::derive(
13870 master_key,
13871 &volume_header.archive_uuid,
13872 &volume_header.session_id,
13873 )
13874 .unwrap();
13875 footer.manifest_hmac = compute_hmac(
13876 HmacDomain::ManifestFooter,
13877 &subkeys.mac_key,
13878 &volume_header.archive_uuid,
13879 &volume_header.session_id,
13880 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
13881 );
13882 footer_bytes = footer.to_bytes();
13883 volume[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
13884 }
13885
13886 fn rewrite_volume_trailer(
13887 volume: &mut [u8],
13888 master_key: &MasterKey,
13889 mutate: impl FnOnce(&mut VolumeTrailer),
13890 ) {
13891 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
13892 let offset = terminal_material_offset(volume) + MANIFEST_FOOTER_LEN;
13893 let mut trailer =
13894 VolumeTrailer::parse(&volume[offset..offset + VOLUME_TRAILER_LEN]).unwrap();
13895 mutate(&mut trailer);
13896 trailer.trailer_hmac = [0u8; 32];
13897 let mut trailer_bytes = trailer.to_bytes();
13898 let subkeys = Subkeys::derive(
13899 master_key,
13900 &volume_header.archive_uuid,
13901 &volume_header.session_id,
13902 )
13903 .unwrap();
13904 trailer.trailer_hmac = compute_hmac(
13905 HmacDomain::VolumeTrailer,
13906 &subkeys.mac_key,
13907 &volume_header.archive_uuid,
13908 &volume_header.session_id,
13909 &trailer_bytes[..TRAILER_HMAC_COVERED_LEN],
13910 );
13911 trailer_bytes = trailer.to_bytes();
13912 volume[offset..offset + VOLUME_TRAILER_LEN].copy_from_slice(&trailer_bytes);
13913 }
13914
13915 fn rewrite_sidecar_header(
13916 sidecar: &mut [u8],
13917 master_key: &MasterKey,
13918 mutate: impl FnOnce(&mut BootstrapSidecarHeader),
13919 ) {
13920 let mut header =
13921 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13922 mutate(&mut header);
13923 write_signed_sidecar_header(sidecar, master_key, &mut header);
13924 }
13925
13926 fn write_signed_sidecar_header(
13927 sidecar: &mut [u8],
13928 master_key: &MasterKey,
13929 header: &mut BootstrapSidecarHeader,
13930 ) {
13931 header.sidecar_hmac = [0u8; 32];
13932 let mut header_bytes = header.to_bytes();
13933 let subkeys =
13934 Subkeys::derive(master_key, &header.archive_uuid, &header.session_id).unwrap();
13935 header.sidecar_hmac = compute_hmac(
13936 HmacDomain::BootstrapSidecar,
13937 &subkeys.mac_key,
13938 &header.archive_uuid,
13939 &header.session_id,
13940 &header_bytes[..SIDECAR_HMAC_COVERED_LEN],
13941 );
13942 header_bytes = header.to_bytes();
13943 sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN].copy_from_slice(&header_bytes);
13944 }
13945
13946 fn sparse_bootstrap_sidecar(
13947 source: &[u8],
13948 master_key: &MasterKey,
13949 include_manifest: bool,
13950 include_index_root: bool,
13951 include_dictionary: bool,
13952 ) -> Vec<u8> {
13953 let source_header =
13954 BootstrapSidecarHeader::parse(&source[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
13955 let mut sidecar = vec![0u8; BOOTSTRAP_SIDECAR_HEADER_LEN];
13956 let mut header = BootstrapSidecarHeader {
13957 archive_uuid: source_header.archive_uuid,
13958 session_id: source_header.session_id,
13959 flags: 0,
13960 manifest_footer_offset: 0,
13961 manifest_footer_length: 0,
13962 index_root_records_offset: 0,
13963 index_root_records_length: 0,
13964 dictionary_records_offset: 0,
13965 dictionary_records_length: 0,
13966 sidecar_hmac: [0u8; 32],
13967 header_crc32c: 0,
13968 };
13969
13970 if include_manifest {
13971 assert!(source_header.has_manifest_footer());
13972 let (offset, length) = append_sidecar_section(
13973 source,
13974 &mut sidecar,
13975 source_header.manifest_footer_offset,
13976 source_header.manifest_footer_length as u64,
13977 );
13978 header.flags |= 0x01;
13979 header.manifest_footer_offset = offset;
13980 header.manifest_footer_length = length as u32;
13981 }
13982 if include_index_root {
13983 assert!(source_header.has_index_root_records());
13984 let (offset, length) = append_sidecar_section(
13985 source,
13986 &mut sidecar,
13987 source_header.index_root_records_offset,
13988 source_header.index_root_records_length,
13989 );
13990 header.flags |= 0x02;
13991 header.index_root_records_offset = offset;
13992 header.index_root_records_length = length;
13993 }
13994 if include_dictionary {
13995 assert!(source_header.has_dictionary_records());
13996 let (offset, length) = append_sidecar_section(
13997 source,
13998 &mut sidecar,
13999 source_header.dictionary_records_offset,
14000 source_header.dictionary_records_length,
14001 );
14002 header.flags |= 0x04;
14003 header.dictionary_records_offset = offset;
14004 header.dictionary_records_length = length;
14005 }
14006
14007 write_signed_sidecar_header(&mut sidecar, master_key, &mut header);
14008 sidecar
14009 }
14010
14011 fn append_sidecar_section(
14012 source: &[u8],
14013 sidecar: &mut Vec<u8>,
14014 source_offset: u64,
14015 length: u64,
14016 ) -> (u64, u64) {
14017 let source_offset = source_offset as usize;
14018 let length = length as usize;
14019 let offset = sidecar.len() as u64;
14020 sidecar.extend_from_slice(&source[source_offset..source_offset + length]);
14021 (offset, length as u64)
14022 }
14023
14024 fn mutate_sidecar_manifest(
14025 sidecar: &mut [u8],
14026 master_key: &MasterKey,
14027 mutate: impl FnOnce(&mut ManifestFooter),
14028 ) {
14029 let header =
14030 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14031 let offset = header.manifest_footer_offset as usize;
14032 let mut footer =
14033 ManifestFooter::parse(&sidecar[offset..offset + MANIFEST_FOOTER_LEN]).unwrap();
14034 mutate(&mut footer);
14035 footer.manifest_hmac = [0u8; 32];
14036 let mut footer_bytes = footer.to_bytes();
14037 let subkeys =
14038 Subkeys::derive(master_key, &footer.archive_uuid, &footer.session_id).unwrap();
14039 footer.manifest_hmac = compute_hmac(
14040 HmacDomain::ManifestFooter,
14041 &subkeys.mac_key,
14042 &footer.archive_uuid,
14043 &footer.session_id,
14044 &footer_bytes[..MANIFEST_HMAC_COVERED_LEN],
14045 );
14046 footer_bytes = footer.to_bytes();
14047 sidecar[offset..offset + MANIFEST_FOOTER_LEN].copy_from_slice(&footer_bytes);
14048 }
14049
14050 fn mutate_sidecar_index_record(
14051 sidecar: &mut [u8],
14052 record_index: usize,
14053 mutate: impl FnOnce(&mut BlockRecord),
14054 ) {
14055 let header =
14056 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14057 let record_len = sidecar_record_len(sidecar);
14058 let offset = header.index_root_records_offset as usize + record_index * record_len;
14059 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
14060 let mut record =
14061 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
14062 mutate(&mut record);
14063 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14064 }
14065
14066 fn mutate_sidecar_dictionary_record(
14067 sidecar: &mut [u8],
14068 record_index: usize,
14069 mutate: impl FnOnce(&mut BlockRecord),
14070 ) {
14071 let header =
14072 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14073 let record_len = sidecar_record_len(sidecar);
14074 let offset = header.dictionary_records_offset as usize + record_index * record_len;
14075 let block_size = record_len - BLOCK_RECORD_FRAMING_LEN;
14076 let mut record =
14077 BlockRecord::parse(&sidecar[offset..offset + record_len], block_size).unwrap();
14078 mutate(&mut record);
14079 sidecar[offset..offset + record_len].copy_from_slice(&record.to_bytes());
14080 }
14081
14082 fn swap_sidecar_index_records(sidecar: &mut [u8], left: usize, right: usize) {
14083 let header =
14084 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14085 let record_len = sidecar_record_len(sidecar);
14086 let left_offset = header.index_root_records_offset as usize + left * record_len;
14087 let right_offset = header.index_root_records_offset as usize + right * record_len;
14088 for idx in 0..record_len {
14089 sidecar.swap(left_offset + idx, right_offset + idx);
14090 }
14091 }
14092
14093 fn sidecar_record_len(sidecar: &[u8]) -> usize {
14094 let header =
14095 BootstrapSidecarHeader::parse(&sidecar[..BOOTSTRAP_SIDECAR_HEADER_LEN]).unwrap();
14096 let footer_offset = header.manifest_footer_offset as usize;
14097 let footer =
14098 ManifestFooter::parse(&sidecar[footer_offset..footer_offset + MANIFEST_FOOTER_LEN])
14099 .unwrap();
14100 let index_record_count = footer.index_root_data_block_count as usize
14101 + footer.index_root_parity_block_count as usize;
14102 header.index_root_records_length as usize / index_record_count
14103 }
14104
14105 fn corrupt_object_extent_records(volume: &mut [u8], extent: ObjectExtent) {
14106 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
14107 assert_eq!(volume_header.volume_index, 0);
14108 assert_eq!(volume_header.stripe_width, 1);
14109 let crypto_start = volume_header.crypto_header_offset as usize;
14110 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14111 let crypto_header = CryptoHeader::parse(
14112 &volume[crypto_start..crypto_end],
14113 volume_header.crypto_header_length,
14114 )
14115 .unwrap();
14116 let record_len = crypto_header.fixed.block_size as usize + BLOCK_RECORD_FRAMING_LEN;
14117 let record_count = extent.data_block_count as u64 + extent.parity_block_count as u64;
14118 for offset in 0..record_count {
14119 let block_index = extent.first_block_index + offset;
14120 let record_offset = crypto_end + block_index as usize * record_len;
14121 volume[record_offset + 16] ^= 0x55;
14122 }
14123 }
14124
14125 fn terminal_material_offset(volume: &[u8]) -> usize {
14126 let volume_header = VolumeHeader::parse(&volume[..VOLUME_HEADER_LEN]).unwrap();
14127 let crypto_start = volume_header.crypto_header_offset as usize;
14128 let crypto_end = crypto_start + volume_header.crypto_header_length as usize;
14129 let crypto_header = CryptoHeader::parse(
14130 &volume[crypto_start..crypto_end],
14131 volume_header.crypto_header_length,
14132 )
14133 .unwrap();
14134 let (_, offset, _) = parse_stream_block_prefix(
14135 volume,
14136 crypto_end,
14137 crypto_header.fixed.block_size as usize,
14138 &volume_header,
14139 )
14140 .unwrap();
14141 offset
14142 }
14143
14144 #[derive(Debug)]
14145 struct TestObject {
14146 extent: ObjectExtent,
14147 records: Vec<BlockRecord>,
14148 }
14149
14150 #[derive(Debug)]
14151 struct TestFileMeta {
14152 path: Vec<u8>,
14153 frame_index: u64,
14154 tar_stream_offset: u64,
14155 member_group_size: u64,
14156 file_data_size: u64,
14157 }
14158
14159 fn multi_envelope_reader_fixture() -> (OpenedArchive, u64) {
14160 let volume_header = test_volume_header();
14161 let crypto_header = test_crypto_header();
14162 let subkeys = Subkeys::derive(
14163 &master_key(),
14164 &volume_header.archive_uuid,
14165 &volume_header.session_id,
14166 )
14167 .unwrap();
14168 let mut next_block_index = 0u64;
14169 let mut blocks = BTreeMap::new();
14170
14171 let healthy = test_member(b"healthy.txt", b"healthy payload\n");
14172 let broken = test_member(b"broken.txt", b"broken payload\n");
14173 let tar_stream = [healthy.as_slice(), broken.as_slice()].concat();
14174
14175 let healthy_frame = compress_zstd_frame(&healthy, 1).unwrap();
14176 let broken_frame = compress_zstd_frame(&broken, 1).unwrap();
14177
14178 let healthy_payload = encrypt_test_object(
14179 &healthy_frame,
14180 &subkeys.enc_key,
14181 &subkeys.nonce_seed,
14182 b"envelope",
14183 0,
14184 BlockKind::PayloadData,
14185 &mut next_block_index,
14186 &crypto_header,
14187 &volume_header,
14188 );
14189 let broken_payload = encrypt_test_object(
14190 &broken_frame,
14191 &subkeys.enc_key,
14192 &subkeys.nonce_seed,
14193 b"envelope",
14194 1,
14195 BlockKind::PayloadData,
14196 &mut next_block_index,
14197 &crypto_header,
14198 &volume_header,
14199 );
14200 let broken_payload_block = broken_payload.extent.first_block_index;
14201 insert_records(&mut blocks, &healthy_payload.records);
14202 insert_records(&mut blocks, &broken_payload.records);
14203
14204 let frames = vec![
14205 FrameEntry {
14206 frame_index: 0,
14207 envelope_index: 0,
14208 offset_in_envelope: 0,
14209 compressed_size: healthy_frame.len() as u32,
14210 decompressed_size: healthy.len() as u32,
14211 flags: 0x0000_0003,
14212 tar_stream_offset: 0,
14213 },
14214 FrameEntry {
14215 frame_index: 1,
14216 envelope_index: 1,
14217 offset_in_envelope: 0,
14218 compressed_size: broken_frame.len() as u32,
14219 decompressed_size: broken.len() as u32,
14220 flags: 0x0000_0003,
14221 tar_stream_offset: healthy.len() as u64,
14222 },
14223 ];
14224 let envelopes = vec![
14225 EnvelopeEntry {
14226 envelope_index: 0,
14227 first_block_index: healthy_payload.extent.first_block_index,
14228 data_block_count: healthy_payload.extent.data_block_count,
14229 parity_block_count: 0,
14230 encrypted_size: healthy_payload.extent.encrypted_size,
14231 plaintext_size: healthy_frame.len() as u32,
14232 first_frame_index: 0,
14233 frame_count: 1,
14234 },
14235 EnvelopeEntry {
14236 envelope_index: 1,
14237 first_block_index: broken_payload.extent.first_block_index,
14238 data_block_count: broken_payload.extent.data_block_count,
14239 parity_block_count: 0,
14240 encrypted_size: broken_payload.extent.encrypted_size,
14241 plaintext_size: broken_frame.len() as u32,
14242 first_frame_index: 1,
14243 frame_count: 1,
14244 },
14245 ];
14246 let files = vec![
14247 TestFileMeta {
14248 path: b"healthy.txt".to_vec(),
14249 frame_index: 0,
14250 tar_stream_offset: 0,
14251 member_group_size: healthy.len() as u64,
14252 file_data_size: b"healthy payload\n".len() as u64,
14253 },
14254 TestFileMeta {
14255 path: b"broken.txt".to_vec(),
14256 frame_index: 1,
14257 tar_stream_offset: healthy.len() as u64,
14258 member_group_size: broken.len() as u64,
14259 file_data_size: b"broken payload\n".len() as u64,
14260 },
14261 ];
14262
14263 let (index_shard_plaintext, first_path_hash, last_path_hash) =
14264 build_test_index_shard(&files, &frames, &envelopes);
14265 let index_shard = encrypt_test_object(
14266 &compress_zstd_frame(&index_shard_plaintext, 1).unwrap(),
14267 &subkeys.index_shard_key,
14268 &subkeys.index_nonce_seed,
14269 b"idxshard",
14270 0,
14271 BlockKind::IndexShardData,
14272 &mut next_block_index,
14273 &crypto_header,
14274 &volume_header,
14275 );
14276 insert_records(&mut blocks, &index_shard.records);
14277
14278 let shard_entry = ShardEntry {
14279 shard_index: 0,
14280 first_block_index: index_shard.extent.first_block_index,
14281 data_block_count: index_shard.extent.data_block_count,
14282 parity_block_count: 0,
14283 encrypted_size: index_shard.extent.encrypted_size,
14284 decompressed_size: index_shard_plaintext.len() as u32,
14285 file_count: files.len() as u32,
14286 first_path_hash,
14287 last_path_hash,
14288 };
14289 let mut root_header = IndexRootHeader::empty();
14290 root_header.frame_count = frames.len() as u64;
14291 root_header.envelope_count = envelopes.len() as u64;
14292 root_header.file_count = files.len() as u64;
14293 root_header.payload_block_count = healthy_payload.extent.data_block_count as u64
14294 + broken_payload.extent.data_block_count as u64;
14295 root_header.tar_total_size = tar_stream.len() as u64;
14296 root_header.content_sha256 = sha256_bytes(&tar_stream);
14297 let index_root = IndexRoot {
14298 header: root_header,
14299 shards: vec![shard_entry],
14300 directory_hint_shards: Vec::new(),
14301 };
14302
14303 let index_root_plaintext = index_root.to_bytes();
14304 let index_root_object = encrypt_test_object(
14305 &compress_zstd_frame(&index_root_plaintext, 1).unwrap(),
14306 &subkeys.index_root_key,
14307 &subkeys.index_nonce_seed,
14308 b"idxroot",
14309 0,
14310 BlockKind::IndexRootData,
14311 &mut next_block_index,
14312 &crypto_header,
14313 &volume_header,
14314 );
14315 insert_records(&mut blocks, &index_root_object.records);
14316
14317 let archive_uuid = volume_header.archive_uuid;
14318 let session_id = volume_header.session_id;
14319 let opened = OpenedArchive {
14320 options: ReaderOptions::default(),
14321 observed_archive_bytes: 1_000_000,
14322 observed_volume_count: 1,
14323 subkeys,
14324 blocks,
14325 lazy_blocks: None,
14326 crypto_header_bytes: Vec::new(),
14327 volume_header,
14328 crypto_header,
14329 manifest_footer: ManifestFooter {
14330 archive_uuid,
14331 session_id,
14332 volume_index: 0,
14333 is_authoritative: 1,
14334 total_volumes: 1,
14335 index_root_first_block: index_root_object.extent.first_block_index,
14336 index_root_data_block_count: index_root_object.extent.data_block_count,
14337 index_root_parity_block_count: 0,
14338 index_root_encrypted_size: index_root_object.extent.encrypted_size,
14339 index_root_decompressed_size: index_root_plaintext.len() as u32,
14340 manifest_hmac: [0u8; 32],
14341 },
14342 volume_trailer: Some(VolumeTrailer {
14343 archive_uuid,
14344 session_id,
14345 volume_index: 0,
14346 block_count: next_block_index,
14347 bytes_written: 0,
14348 manifest_footer_offset: 0,
14349 manifest_footer_length: MANIFEST_FOOTER_LEN as u32,
14350 closed_at_ns: 0,
14351 root_auth_footer_offset: 0,
14352 root_auth_footer_length: 0,
14353 root_auth_flags: 0,
14354 trailer_hmac: [0u8; 32],
14355 }),
14356 root_auth_footer: None,
14357 index_root,
14358 payload_dictionary: None,
14359 };
14360 (opened, broken_payload_block)
14361 }
14362
14363 fn replace_first_index_shard(opened: &mut OpenedArchive, mutate: impl FnOnce(&mut IndexShard)) {
14364 let locating = opened.index_root.shards[0].clone();
14365 let mut shard = opened.load_index_shard(&locating).unwrap();
14366 mutate(&mut shard);
14367 let plaintext = shard.to_bytes();
14368 let mut next_block_index = opened
14369 .blocks
14370 .keys()
14371 .last()
14372 .copied()
14373 .map(|index| index + 1)
14374 .unwrap_or(0);
14375 let replacement = encrypt_test_object(
14376 &compress_zstd_frame(&plaintext, 1).unwrap(),
14377 &opened.subkeys.index_shard_key,
14378 &opened.subkeys.index_nonce_seed,
14379 b"idxshard",
14380 locating.shard_index,
14381 BlockKind::IndexShardData,
14382 &mut next_block_index,
14383 &opened.crypto_header,
14384 &opened.volume_header,
14385 );
14386 insert_records(&mut opened.blocks, &replacement.records);
14387 opened.index_root.shards[0] = ShardEntry {
14388 shard_index: locating.shard_index,
14389 first_block_index: replacement.extent.first_block_index,
14390 data_block_count: replacement.extent.data_block_count,
14391 parity_block_count: 0,
14392 encrypted_size: replacement.extent.encrypted_size,
14393 decompressed_size: plaintext.len() as u32,
14394 file_count: shard.files.len() as u32,
14395 first_path_hash: shard.files.first().unwrap().path_hash,
14396 last_path_hash: shard.files.last().unwrap().path_hash,
14397 };
14398 }
14399
14400 fn rewrite_as_single_healthy_file(
14401 opened: &mut OpenedArchive,
14402 mutate: impl FnOnce(&mut FileEntry, &mut Vec<u8>),
14403 ) {
14404 let healthy_path = b"healthy.txt";
14405 let healthy_payload = b"healthy payload\n";
14406 let healthy_member = test_member(healthy_path, healthy_payload);
14407 replace_first_index_shard(opened, |shard| {
14408 let file_index = (0..shard.files.len())
14409 .find(|idx| shard.file_path(*idx) == Some(healthy_path.as_slice()))
14410 .unwrap();
14411 let mut file = shard.files[file_index].clone();
14412 let frame = shard
14413 .frames
14414 .iter()
14415 .find(|entry| entry.frame_index == 0)
14416 .unwrap()
14417 .clone();
14418 let envelope = shard
14419 .envelopes
14420 .iter()
14421 .find(|entry| entry.envelope_index == 0)
14422 .unwrap()
14423 .clone();
14424 let mut path = healthy_path.to_vec();
14425
14426 file.path_offset = 0;
14427 file.path_length = path.len() as u32;
14428 file.first_frame_index = 0;
14429 file.frame_count = 1;
14430 file.offset_in_first_frame_plaintext = 0;
14431 file.tar_member_group_size = healthy_member.len() as u64;
14432 file.file_data_size = healthy_payload.len() as u64;
14433 file.flags = 0;
14434 mutate(&mut file, &mut path);
14435 file.path_offset = 0;
14436 file.path_length = path.len() as u32;
14437 file.path_hash = hash_prefix(&path);
14438
14439 shard.files = vec![file];
14440 shard.frames = vec![frame];
14441 shard.envelopes = vec![envelope];
14442 shard.string_pool = path;
14443 });
14444
14445 opened.index_root.header.file_count = 1;
14446 opened.index_root.header.frame_count = 1;
14447 opened.index_root.header.envelope_count = 1;
14448 opened.index_root.header.payload_block_count = 1;
14449 opened.index_root.header.tar_total_size = healthy_member.len() as u64;
14450 opened.index_root.header.content_sha256 = sha256_bytes(&healthy_member);
14451 }
14452
14453 fn test_volume_header() -> VolumeHeader {
14454 VolumeHeader {
14455 format_version: FORMAT_VERSION,
14456 volume_format_rev: VOLUME_FORMAT_REV,
14457 volume_index: 0,
14458 stripe_width: 1,
14459 archive_uuid: [0x31; 16],
14460 session_id: [0x42; 16],
14461 crypto_header_offset: VOLUME_HEADER_LEN as u32,
14462 crypto_header_length: CRYPTO_HEADER_FIXED_LEN as u32,
14463 header_crc32c: 0,
14464 }
14465 }
14466
14467 fn test_crypto_header() -> CryptoHeaderFixed {
14468 CryptoHeaderFixed {
14469 length: CRYPTO_HEADER_FIXED_LEN as u32,
14470 compression_algo: CompressionAlgo::ZstdFramed,
14471 aead_algo: AeadAlgo::AesGcmSiv256,
14472 fec_algo: FecAlgo::ReedSolomonGF16,
14473 kdf_algo: KdfAlgo::Raw,
14474 chunk_size: 4096,
14475 envelope_target_size: 8192,
14476 block_size: 4096,
14477 fec_data_shards: 4,
14478 fec_parity_shards: 0,
14479 index_fec_data_shards: 4,
14480 index_fec_parity_shards: 0,
14481 index_root_fec_data_shards: 4,
14482 index_root_fec_parity_shards: 0,
14483 stripe_width: 1,
14484 volume_loss_tolerance: 0,
14485 bit_rot_buffer_pct: 0,
14486 has_dictionary: 0,
14487 max_path_length: 4096,
14488 expected_volume_size: 0,
14489 }
14490 }
14491
14492 #[allow(clippy::too_many_arguments)]
14493 fn encrypt_test_object(
14494 plaintext: &[u8],
14495 key: &[u8; 32],
14496 nonce_seed: &[u8; 32],
14497 domain: &[u8],
14498 counter: u64,
14499 data_kind: BlockKind,
14500 next_block_index: &mut u64,
14501 crypto_header: &CryptoHeaderFixed,
14502 volume_header: &VolumeHeader,
14503 ) -> TestObject {
14504 let block_size = crypto_header.block_size as usize;
14505 let encrypted = encrypt_padded_aead_object(
14506 AeadObjectContext {
14507 algo: crypto_header.aead_algo,
14508 key,
14509 nonce_seed,
14510 domain,
14511 archive_uuid: &volume_header.archive_uuid,
14512 session_id: &volume_header.session_id,
14513 counter,
14514 },
14515 block_size,
14516 plaintext,
14517 )
14518 .unwrap();
14519 assert_eq!(encrypted.len() % block_size, 0);
14520
14521 let first_block_index = *next_block_index;
14522 let data_block_count = encrypted.len() / block_size;
14523 let records = encrypted
14524 .chunks(block_size)
14525 .enumerate()
14526 .map(|(index, payload)| BlockRecord {
14527 block_index: first_block_index + index as u64,
14528 kind: data_kind,
14529 flags: if index + 1 == data_block_count {
14530 0x01
14531 } else {
14532 0
14533 },
14534 payload: payload.to_vec(),
14535 record_crc32c: 0,
14536 })
14537 .collect::<Vec<_>>();
14538 *next_block_index += data_block_count as u64;
14539
14540 TestObject {
14541 extent: ObjectExtent {
14542 first_block_index,
14543 data_block_count: data_block_count as u32,
14544 parity_block_count: 0,
14545 encrypted_size: encrypted.len() as u32,
14546 },
14547 records,
14548 }
14549 }
14550
14551 fn insert_records(blocks: &mut BTreeMap<u64, BlockRecord>, records: &[BlockRecord]) {
14552 for record in records {
14553 assert!(blocks.insert(record.block_index, record.clone()).is_none());
14554 }
14555 }
14556
14557 #[allow(clippy::too_many_arguments)]
14558 fn build_metadata_object_from_payload(
14559 payload: &[u8],
14560 _subkeys: &Subkeys,
14561 volume_header: &VolumeHeader,
14562 crypto_header: &CryptoHeaderFixed,
14563 key: &[u8; 32],
14564 nonce_seed: &[u8; 32],
14565 domain: &[u8],
14566 counter: u64,
14567 data_kind: BlockKind,
14568 next_block_index: &mut u64,
14569 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14570 let compressed = compress_zstd_frame(payload, 1).unwrap();
14571 build_metadata_object_from_compressed(
14572 &compressed,
14573 key,
14574 nonce_seed,
14575 domain,
14576 counter,
14577 data_kind,
14578 next_block_index,
14579 crypto_header,
14580 volume_header,
14581 )
14582 }
14583
14584 #[allow(clippy::too_many_arguments)]
14585 fn build_metadata_object_from_compressed(
14586 compressed: &[u8],
14587 key: &[u8; 32],
14588 nonce_seed: &[u8; 32],
14589 domain: &[u8],
14590 counter: u64,
14591 data_kind: BlockKind,
14592 next_block_index: &mut u64,
14593 crypto_header: &CryptoHeaderFixed,
14594 volume_header: &VolumeHeader,
14595 ) -> (ObjectExtent, BTreeMap<u64, BlockRecord>) {
14596 let object = encrypt_test_object(
14597 compressed,
14598 key,
14599 nonce_seed,
14600 domain,
14601 counter,
14602 data_kind,
14603 next_block_index,
14604 crypto_header,
14605 volume_header,
14606 );
14607
14608 let mut blocks = BTreeMap::new();
14609 for record in object.records {
14610 blocks.insert(record.block_index, record);
14611 }
14612 (object.extent, blocks)
14613 }
14614
14615 #[allow(clippy::too_many_arguments)]
14616 fn assert_metadata_object_from_compressed(
14617 compressed: &[u8],
14618 decompressed_size: usize,
14619 _subkeys: &Subkeys,
14620 volume_header: &VolumeHeader,
14621 crypto_header: &CryptoHeaderFixed,
14622 key: &[u8; 32],
14623 nonce_seed: &[u8; 32],
14624 domain: &[u8],
14625 counter: u64,
14626 data_kind: BlockKind,
14627 parity_kind: BlockKind,
14628 class_data_shards: u16,
14629 class_parity_shards: u16,
14630 next_block_index: &mut u64,
14631 expected: FormatError,
14632 ) {
14633 let (extent, blocks) = build_metadata_object_from_compressed(
14634 compressed,
14635 key,
14636 nonce_seed,
14637 domain,
14638 counter,
14639 data_kind,
14640 next_block_index,
14641 crypto_header,
14642 volume_header,
14643 );
14644 let error = load_metadata_object_from_parts(
14645 &blocks,
14646 ObjectLoadContext {
14647 volume_header,
14648 crypto_header,
14649 extent,
14650 data_kind,
14651 parity_kind,
14652 key,
14653 nonce_seed,
14654 domain,
14655 counter,
14656 class_data_shard_max: class_data_shards,
14657 class_parity_shard_max: class_parity_shards,
14658 },
14659 decompressed_size as u32,
14660 )
14661 .unwrap_err();
14662 assert_eq!(error, expected);
14663 }
14664
14665 fn corrupt_payload_record(blocks: &mut BTreeMap<u64, BlockRecord>, block_index: u64) {
14666 let record = blocks.get_mut(&block_index).unwrap();
14667 assert_eq!(record.kind, BlockKind::PayloadData);
14668 record.payload[0] ^= 0x55;
14669 }
14670
14671 fn build_test_index_shard(
14672 files: &[TestFileMeta],
14673 frames: &[FrameEntry],
14674 envelopes: &[EnvelopeEntry],
14675 ) -> (Vec<u8>, [u8; 8], [u8; 8]) {
14676 let mut sorted = files
14677 .iter()
14678 .map(|file| (hash_prefix(&file.path), file))
14679 .collect::<Vec<_>>();
14680 sorted.sort_by(|left, right| {
14681 (left.0, left.1.path.as_slice(), left.1.tar_stream_offset).cmp(&(
14682 right.0,
14683 right.1.path.as_slice(),
14684 right.1.tar_stream_offset,
14685 ))
14686 });
14687
14688 let mut string_pool = Vec::new();
14689 let mut file_entries = Vec::with_capacity(sorted.len());
14690 for (path_hash, file) in &sorted {
14691 let path_offset = string_pool.len() as u32;
14692 string_pool.extend_from_slice(&file.path);
14693 file_entries.push(FileEntry {
14694 path_hash: *path_hash,
14695 path_offset,
14696 path_length: file.path.len() as u32,
14697 first_frame_index: file.frame_index,
14698 frame_count: 1,
14699 offset_in_first_frame_plaintext: 0,
14700 tar_member_group_size: file.member_group_size,
14701 file_data_size: file.file_data_size,
14702 flags: 0,
14703 });
14704 }
14705
14706 let header = IndexShardHeader {
14707 version: 1,
14708 shard_index: 0,
14709 file_count: file_entries.len() as u32,
14710 frame_count: frames.len() as u32,
14711 envelope_count: envelopes.len() as u32,
14712 file_table_offset: INDEX_SHARD_HEADER_LEN as u32,
14713 frame_table_offset: (INDEX_SHARD_HEADER_LEN + file_entries.len() * FILE_ENTRY_LEN)
14714 as u32,
14715 envelope_table_offset: (INDEX_SHARD_HEADER_LEN
14716 + file_entries.len() * FILE_ENTRY_LEN
14717 + frames.len() * FRAME_ENTRY_LEN) as u32,
14718 string_pool_offset: (INDEX_SHARD_HEADER_LEN
14719 + file_entries.len() * FILE_ENTRY_LEN
14720 + frames.len() * FRAME_ENTRY_LEN
14721 + envelopes.len() * ENVELOPE_ENTRY_LEN) as u32,
14722 string_pool_size: string_pool.len() as u32,
14723 };
14724
14725 let mut bytes = Vec::new();
14726 bytes.extend_from_slice(&header.to_bytes());
14727 for entry in &file_entries {
14728 bytes.extend_from_slice(&entry.to_bytes());
14729 }
14730 for entry in frames {
14731 bytes.extend_from_slice(&entry.to_bytes());
14732 }
14733 for entry in envelopes {
14734 bytes.extend_from_slice(&entry.to_bytes());
14735 }
14736 bytes.extend_from_slice(&string_pool);
14737
14738 (bytes, sorted.first().unwrap().0, sorted.last().unwrap().0)
14739 }
14740
14741 fn test_member(path: &[u8], data: &[u8]) -> Vec<u8> {
14742 let mut out = Vec::new();
14743 out.extend_from_slice(&test_tar_header(path, data.len() as u64));
14744 out.extend_from_slice(data);
14745 out.resize(out.len() + padding_to_512(data.len()), 0);
14746 out
14747 }
14748
14749 fn test_tar_header(path: &[u8], size: u64) -> [u8; 512] {
14750 let mut header = [0u8; 512];
14751 header[..path.len()].copy_from_slice(path);
14752 write_test_tar_octal(&mut header[100..108], 0o644);
14753 write_test_tar_octal(&mut header[108..116], 0);
14754 write_test_tar_octal(&mut header[116..124], 0);
14755 write_test_tar_octal(&mut header[124..136], size);
14756 write_test_tar_octal(&mut header[136..148], 0);
14757 header[148..156].fill(b' ');
14758 header[156] = b'0';
14759 header[257..263].copy_from_slice(b"ustar\0");
14760 header[263..265].copy_from_slice(b"00");
14761 let checksum = header.iter().map(|byte| *byte as u64).sum::<u64>();
14762 write_test_tar_checksum(&mut header[148..156], checksum);
14763 header
14764 }
14765
14766 fn write_test_tar_octal(field: &mut [u8], value: u64) {
14767 let digits = format!("{value:o}");
14768 field.fill(0);
14769 let start = field.len() - 1 - digits.len();
14770 field[..start].fill(b'0');
14771 field[start..start + digits.len()].copy_from_slice(digits.as_bytes());
14772 }
14773
14774 fn write_test_tar_checksum(field: &mut [u8], value: u64) {
14775 let digits = format!("{value:06o}");
14776 field[0..6].copy_from_slice(digits.as_bytes());
14777 field[6] = 0;
14778 field[7] = b' ';
14779 }
14780
14781 fn padding_to_512(len: usize) -> usize {
14782 let remainder = len % 512;
14783 if remainder == 0 {
14784 0
14785 } else {
14786 512 - remainder
14787 }
14788 }
14789}